< Summary

Information
Class: OzonOrdersWeb.Api.Partner.PartnerOrdersController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Api/Partner/PartnerOrdersController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 52
Coverable lines: 52
Total lines: 196
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 41
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_PartnerId()0%620%
List()100%210%
Filters()100%210%
Suggestions()100%210%
Get()0%620%
Label()0%156120%
OzonRequirements()0%620%
OzonCountries()0%620%
SetOzonCountries()0%2040%
SetOzonExemplars()0%2040%
Ship()0%620%
PrintLabel()0%4260%
Failure(...)0%3050%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Api/Partner/PartnerOrdersController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using OzonDomains.Models.Partners;
 4using OzonOrdersWeb.Authorization;
 5using OzonOrdersWeb.Extensions;
 6using OzonOrdersWeb.WebServices.Partners;
 7using Services.LabelServices;
 8using Services.PartnerServices;
 9
 10namespace OzonOrdersWeb.Api.Partner;
 11
 12/// <summary>Заказы партнёра. Видны только заказы, подходящие под правила, которые задал владелец.</summary>
 13[ApiController]
 14[ApiExplorerSettings(GroupName = PartnerOfficeExtensions.SwaggerDocName)]
 15[ServiceEndpoint]
 16[Authorize(AuthenticationSchemes = PartnerOfficeExtensions.PartnerCookieScheme)]
 17[Route("partner-api/orders")]
 018public sealed class PartnerOrdersController(
 019    IPartnerOrdersService orders,
 020    IPartnerPermissionService permissions,
 021    IPartnerAuditService audit,
 022    IPartnerOrderActionsService actions) : ControllerBase
 23{
 024    private long PartnerId => User.TryGetPartnerId(out var id) ? id : 0;
 25
 26    /// <summary>Список заказов с фильтрами и постраничной выдачей, новые сверху.</summary>
 27    /// <param name="search">Подстрока: номер отправления, артикул, название, трек-номер.</param>
 28    /// <param name="statusId">Статус заказа.</param>
 29    /// <param name="ozonStatus">Отображаемый статус Ozon; точное совпадение.</param>
 30    /// <param name="warehouseId">Склад отгрузки.</param>
 31    /// <param name="clientId">Клиент (кабинет Ozon).</param>
 32    /// <param name="from">Дата принятия заказа, с (включительно).</param>
 33    /// <param name="to">Дата принятия заказа, по (включительно).</param>
 34    /// <param name="page">Номер страницы с 1.</param>
 35    /// <param name="pageSize">Размер страницы, до 100.</param>
 36    [HttpGet]
 37    [ProducesResponseType(typeof(PartnerPage<PartnerOrderListItemDto>), StatusCodes.Status200OK)]
 38    public async Task<IActionResult> List(
 39        [FromQuery] string? search, [FromQuery] int? statusId, [FromQuery] string? ozonStatus, [FromQuery] int? warehous
 40        [FromQuery] int? clientId, [FromQuery] DateTime? from, [FromQuery] DateTime? to,
 41        [FromQuery] int page = 1, [FromQuery] int pageSize = 50, CancellationToken ct = default) =>
 042        Ok(await orders.ListAsync(PartnerId,
 043            new PartnerOrderQuery(search, statusId, warehouseId, clientId, from, to, page, pageSize, ozonStatus), ct));
 44
 45    /// <summary>Значения для фильтров: статусы, склады и клиенты среди видимых партнёру заказов.</summary>
 46    [HttpGet("filters")]
 47    [ProducesResponseType(typeof(PartnerOrderFilters), StatusCodes.Status200OK)]
 48    public async Task<IActionResult> Filters(CancellationToken ct) =>
 049        Ok(await orders.GetFiltersAsync(PartnerId, ct));
 50
 51    /// <summary>До 10 подсказок по заказам в рамках видимости партнёра. Для производительности запрос короче 2 символов
 52    [HttpGet("suggestions")]
 53    [ProducesResponseType(typeof(IReadOnlyList<PartnerOrderSuggestion>), StatusCodes.Status200OK)]
 54    public async Task<IActionResult> Suggestions([FromQuery] string? q, [FromQuery] int limit = PartnerOrdersService.Max
 055        Ok(await orders.SuggestAsync(PartnerId, q, limit, ct));
 56
 57    /// <summary>Карточка заказа. Заказ вне правил партнёра отдаёт 404, как будто его нет.</summary>
 58    [HttpGet("{id:int}")]
 59    [ProducesResponseType(typeof(PartnerOrderDetailsDto), StatusCodes.Status200OK)]
 60    [ProducesResponseType(StatusCodes.Status404NotFound)]
 61    public async Task<IActionResult> Get(int id, CancellationToken ct)
 62    {
 063        var order = await orders.GetAsync(PartnerId, id, ct);
 064        return order is null ? NotFound() : Ok(order);
 065    }
 66
 67    /// <summary>
 68    /// Скачать готовую этикетку (PDF). Нужна возможность «печать этикетки».
 69    /// Новую этикетку не создаёт и заказ в Ozon не отгружает: это отдельное действие.
 70    /// </summary>
 71    [HttpGet("{id:int}/label")]
 72    [Produces("application/pdf", "application/json")]
 73    [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
 74    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 75    [ProducesResponseType(StatusCodes.Status404NotFound)]
 76    public async Task<IActionResult> Label(int id, CancellationToken ct)
 77    {
 078        if (!await permissions.HasAsync(PartnerId, PartnerPermissionCode.LabelPrint, ct))
 079            return StatusCode(StatusCodes.Status403Forbidden, new { error = "Печать этикеток не разрешена." });
 80
 081        var label = await orders.GetExistingLabelAsync(PartnerId, id, ct);
 082        if (label is null) return NotFound(new { error = "Этикетка не найдена." });
 83
 084        var details = await orders.GetAsync(PartnerId, id, ct);
 085        await audit.WriteAsync("label_download", PartnerActorKind.Partner, User.Identity?.Name ?? "partner", PartnerId,
 086            orderNumber: details?.Order.ShipmentNumber, ipAddress: HttpContext.Connection.RemoteIpAddress?.ToString(), c
 087        return File(label.Content, "application/pdf", label.FileName);
 088    }
 89
 90    // ---------------------------------------------------------------------------------------------
 91    // Действия над заказом Ozon. Работают через те же сервисы Studio2, что кнопки в Studio2:
 92    // данные экземпляров -> отгрузка (/v4/posting/fbs/ship) -> печать этикетки.
 93    // ---------------------------------------------------------------------------------------------
 94
 95    /// <summary>
 96    /// Что Ozon просит заполнить перед отгрузкой: страны-изготовители и данные экземпляров (Честный ЗНАК, ГТД, РНПТ).
 97    /// Нужна возможность «Сборка и отгрузка» или «Печать этикетки».
 98    /// </summary>
 99    [HttpGet("{id:int}/ozon/requirements")]
 100    [ProducesResponseType(typeof(PartnerShipRequirements), StatusCodes.Status200OK)]
 101    public async Task<IActionResult> OzonRequirements(int id, CancellationToken ct)
 102    {
 0103        var r = await actions.GetRequirementsAsync(PartnerId, id, ct);
 0104        return r.Ok ? Ok(r.Value) : Failure(r.Result);
 0105    }
 106
 107    /// <summary>Справочник стран-изготовителей Ozon для заказа (поиск по названию).</summary>
 108    [HttpGet("{id:int}/ozon/countries")]
 109    [ProducesResponseType(typeof(IReadOnlyList<PartnerCountryOption>), StatusCodes.Status200OK)]
 110    public async Task<IActionResult> OzonCountries(int id, [FromQuery] string? search, CancellationToken ct)
 111    {
 0112        var r = await actions.GetCountriesAsync(PartnerId, id, search, ct);
 0113        return r.Ok ? Ok(r.Value) : Failure(r.Result);
 0114    }
 115
 116    /// <summary>Сохраняет в Ozon страны-изготовители товаров заказа.</summary>
 117    [HttpPut("{id:int}/ozon/countries")]
 118    [ProducesResponseType(typeof(PartnerActionResult), StatusCodes.Status200OK)]
 119    public async Task<IActionResult> SetOzonCountries(int id, [FromBody] SetCountriesBody body, CancellationToken ct)
 120    {
 0121        var r = await actions.SetCountriesAsync(PartnerId, id, body.Selections ?? [], ct);
 0122        return r.Success ? Ok(r) : Failure(r);
 0123    }
 124
 125    /// <summary>
 126    /// Сохраняет в Ozon данные экземпляров и ждёт вердикта о готовности отправления к сборке.
 127    /// Без этого отгрузка отвечает EXEMPLAR_INFO_NOT_FILLED_COMPLETELY.
 128    /// </summary>
 129    [HttpPut("{id:int}/ozon/exemplars")]
 130    [ProducesResponseType(typeof(PartnerActionResult), StatusCodes.Status200OK)]
 131    public async Task<IActionResult> SetOzonExemplars(int id, [FromBody] SetExemplarsBody body, CancellationToken ct)
 132    {
 0133        var r = await actions.SetExemplarsAsync(PartnerId, id, body.Exemplars ?? [], ct);
 0134        return r.Success ? Ok(r) : Failure(r);
 0135    }
 136
 137    /// <summary>
 138    /// Отгрузка: вызов Ozon API по заказу, как «Отгрузить» в Studio2. Нужна возможность «Сборка и отгрузка».
 139    /// Studio-части одного отправления (posting/1, posting/2) уходят в Ozon одной отгрузкой.
 140    /// </summary>
 141    [HttpPost("{id:int}/ship")]
 142    [ProducesResponseType(typeof(PartnerActionResult), StatusCodes.Status200OK)]
 143    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 144    [ProducesResponseType(StatusCodes.Status404NotFound)]
 145    [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
 146    public async Task<IActionResult> Ship(int id, CancellationToken ct)
 147    {
 0148        var r = await actions.ShipAsync(PartnerId, id, ct);
 0149        return r.Success ? Ok(r) : Failure(r);
 0150    }
 151
 152    /// <summary>
 153    /// Печать этикетки как в Studio2. Сначала берётся готовая из хранилища Studio2; если её нет, этикетка
 154    /// запрашивается у Ozon API и сохраняется в хранилище Studio2 (дальше её видят и в Studio2). Если отправление
 155    /// ещё не собрано, перед печатью его нужно отгрузить: для этого нужна и возможность «Сборка и отгрузка».
 156    /// Нужна возможность «Печать этикетки». Ответ: PDF; заголовки <c>X-Label-Generated</c> и <c>X-Auto-Shipped</c>.
 157    /// </summary>
 158    [HttpPost("{id:int}/label")]
 159    [Produces("application/pdf", "application/json")]
 160    [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
 161    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 162    [ProducesResponseType(StatusCodes.Status404NotFound)]
 163    [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
 164    public async Task<IActionResult> PrintLabel(int id, CancellationToken ct)
 165    {
 0166        var r = await actions.PrintLabelAsync(PartnerId, id, ct);
 0167        if (!r.Ok) return Failure(r.Result);
 168
 0169        Response.Headers["X-Label-Generated"] = r.Value!.Generated ? "true" : "false";
 0170        Response.Headers["X-Auto-Shipped"] = r.Value.AutoShipped ? "true" : "false";
 0171        return File(r.Value.Content, "application/pdf", r.Value.FileName);
 0172    }
 173
 174    private IActionResult Failure(PartnerActionResult r)
 175    {
 0176        var body = new { error = r.Message, reasons = r.Reasons };
 0177        return r.Error switch
 0178        {
 0179            PartnerActionError.NotFound => NotFound(body),
 0180            PartnerActionError.Forbidden => StatusCode(StatusCodes.Status403Forbidden, body),
 0181            PartnerActionError.NotOzonOrder => BadRequest(body),
 0182            PartnerActionError.NoOzonClient => StatusCode(StatusCodes.Status409Conflict, body),
 0183            _ => UnprocessableEntity(body)
 0184        };
 185    }
 186}
 187
 188public sealed class SetCountriesBody
 189{
 190    public List<PartnerCountrySelection>? Selections { get; set; }
 191}
 192
 193public sealed class SetExemplarsBody
 194{
 195    public List<OzonExemplarInput>? Exemplars { get; set; }
 196}