< Summary

Information
Class: Services.LabelServices.OzonExemplarRequirement
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/LabelServices/OzonExemplarHelper.cs
Line coverage
88%
Covered lines: 8
Uncovered lines: 1
Coverable lines: 9
Total lines: 203
Line coverage: 88.8%
Branch coverage
0%
Covered branches: 0
Total branches: 6
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ProductId()100%11100%
get_ExemplarId()100%11100%
get_Index()100%11100%
get_Quantity()100%11100%
get_NeedsMandatoryMark()100%11100%
get_NeedsGtd()100%11100%
get_NeedsRnpt()100%11100%
get_Unsupported()100%11100%
get_HasAnyRequirement()0%4260%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/LabelServices/OzonExemplarHelper.cs

#LineLine coverage
 1using Newtonsoft.Json.Linq;
 2
 3namespace Services.LabelServices;
 4
 5/// <summary>
 6/// Одно поле-требование Ozon по конкретному экземпляру товара в отправлении.
 7/// Экземпляры разворачиваются по <c>quantity</c>: у каждого свой код маркировки.
 8/// </summary>
 9public sealed class OzonExemplarRequirement
 10{
 511    public long ProductId { get; init; }
 12
 513    public long ExemplarId { get; init; }
 14
 15    /// <summary>Порядковый номер экземпляра в рамках товара (1..quantity) — для подписи в UI.</summary>
 516    public int Index { get; init; }
 17
 318    public int Quantity { get; init; }
 19
 620    public bool NeedsMandatoryMark { get; init; }
 21
 522    public bool NeedsGtd { get; init; }
 23
 524    public bool NeedsRnpt { get; init; }
 25
 26    /// <summary>Требования, которые форма не закрывает: вес, IMEI, ювелирный УИН.</summary>
 1127    public IReadOnlyList<string> Unsupported { get; init; } = [];
 28
 29    public bool HasAnyRequirement =>
 030        NeedsMandatoryMark || NeedsGtd || NeedsRnpt || Unsupported.Count > 0;
 31}
 32
 33/// <summary>Итог сохранения данных экземпляров: готово ли отправление к сборке.</summary>
 34public sealed class OzonExemplarSaveResult
 35{
 36    public bool ShipAvailable { get; init; }
 37
 38    public IReadOnlyList<string> Reasons { get; init; } = [];
 39}
 40
 41/// <summary>
 42/// Разбор ответов Ozon exemplar API (v6 create-or-get / v6 set / v5 status).
 43/// Чистые функции без обращений к сети — покрываются юнит-тестами.
 44/// </summary>
 45public static class OzonExemplarHelper
 46{
 47    public const string ShipAvailableStatus = "ship_available";
 48    public const string ValidationInProgressStatus = "validation_in_progress";
 49
 50    private static readonly Dictionary<string, string> UnsupportedRequirementNames =
 51        new(StringComparer.OrdinalIgnoreCase)
 52        {
 53            ["weight"] = "фактический вес экземпляра",
 54            ["imei"] = "IMEI",
 55            ["jw_uin"] = "УИН ювелирного изделия"
 56        };
 57
 58    private static readonly Dictionary<string, string> CheckStatusNames =
 59        new(StringComparer.OrdinalIgnoreCase)
 60        {
 61            ["processing"] = "проверяется",
 62            ["failed"] = "проверка не пройдена",
 63            ["passed"] = "проверка пройдена"
 64        };
 65
 66    /// <summary>
 67    /// Разворачивает ответ /v6/fbs/posting/product/exemplar/create-or-get в плоский список
 68    /// требований по экземплярам. Возвращает только экземпляры, по которым что-то нужно заполнить.
 69    /// </summary>
 70    public static List<OzonExemplarRequirement> ParseRequirements(JObject? createOrGetResponse)
 71    {
 72        var result = new List<OzonExemplarRequirement>();
 73        if (createOrGetResponse?["products"] is not JArray products)
 74        {
 75            return result;
 76        }
 77
 78        foreach (var product in products.OfType<JObject>())
 79        {
 80            var productId = product["product_id"]?.Value<long>() ?? 0;
 81            if (productId <= 0)
 82            {
 83                continue;
 84            }
 85
 86            var needsMandatoryMark = product["is_mandatory_mark_needed"]?.Value<bool>() ?? false;
 87            var needsGtd = product["is_gtd_needed"]?.Value<bool>() ?? false;
 88            var needsRnpt = product["is_rnpt_needed"]?.Value<bool>() ?? false;
 89
 90            var unsupported = new List<string>();
 91            if (product["is_weight_needed"]?.Value<bool>() ?? false)
 92                unsupported.Add(UnsupportedRequirementNames["weight"]);
 93            if (product["is_jw_uin_needed"]?.Value<bool>() ?? false)
 94                unsupported.Add(UnsupportedRequirementNames["jw_uin"]);
 95            if (product["has_imei"]?.Value<bool>() ?? false)
 96                unsupported.Add(UnsupportedRequirementNames["imei"]);
 97
 98            if (!needsMandatoryMark && !needsGtd && !needsRnpt && unsupported.Count == 0)
 99            {
 100                continue;
 101            }
 102
 103            var exemplarIds = (product["exemplars"] as JArray)?
 104                .OfType<JObject>()
 105                .Select(exemplar => exemplar["exemplar_id"]?.Value<long>() ?? 0)
 106                .Where(id => id > 0)
 107                .ToList() ?? [];
 108
 109            var quantity = product["quantity"]?.Value<int>() ?? exemplarIds.Count;
 110            if (quantity <= 0)
 111            {
 112                quantity = Math.Max(exemplarIds.Count, 1);
 113            }
 114
 115            for (var index = 0; index < Math.Max(quantity, exemplarIds.Count); index++)
 116            {
 117                result.Add(new OzonExemplarRequirement
 118                {
 119                    ProductId = productId,
 120                    ExemplarId = index < exemplarIds.Count ? exemplarIds[index] : 0,
 121                    Index = index + 1,
 122                    Quantity = quantity,
 123                    NeedsMandatoryMark = needsMandatoryMark,
 124                    NeedsGtd = needsGtd,
 125                    NeedsRnpt = needsRnpt,
 126                    Unsupported = unsupported
 127                });
 128            }
 129        }
 130
 131        return result;
 132    }
 133
 134    /// <summary>
 135    /// Разбирает ответ /v5/fbs/posting/product/exemplar/status. Готовность к сборке —
 136    /// <c>status = ship_available</c>; иначе собирает причины отказа по каждому экземпляру.
 137    /// </summary>
 138    public static bool IsShipAvailable(JObject? statusResponse, out IReadOnlyList<string> reasons)
 139    {
 140        var status = statusResponse?["status"]?.Value<string>() ?? string.Empty;
 141        if (status.Equals(ShipAvailableStatus, StringComparison.OrdinalIgnoreCase))
 142        {
 143            reasons = [];
 144            return true;
 145        }
 146
 147        var collected = new List<string>();
 148
 149        foreach (var product in (statusResponse?["products"] as JArray)?.OfType<JObject>() ?? [])
 150        {
 151            var productId = product["product_id"]?.Value<long>() ?? 0;
 152
 153            foreach (var exemplar in (product["exemplars"] as JArray)?.OfType<JObject>() ?? [])
 154            {
 155                foreach (var mark in (exemplar["marks"] as JArray)?.OfType<JObject>() ?? [])
 156                {
 157                    AddIfFailed(collected, productId, "код маркировки",
 158                        mark["check_status"]?.Value<string>(), mark["error_codes"] as JArray);
 159                }
 160
 161                AddIfFailed(collected, productId, "ГТД",
 162                    exemplar["gtd_check_status"]?.Value<string>(), exemplar["gtd_error_codes"] as JArray);
 163                AddIfFailed(collected, productId, "РНПТ",
 164                    exemplar["rnpt_check_status"]?.Value<string>(), exemplar["rnpt_error_codes"] as JArray);
 165                AddIfFailed(collected, productId, "вес",
 166                    exemplar["weight_check_status"]?.Value<string>(), exemplar["weight_error_codes"] as JArray);
 167            }
 168        }
 169
 170        if (collected.Count == 0)
 171        {
 172            collected.Add(status.Equals(ValidationInProgressStatus, StringComparison.OrdinalIgnoreCase)
 173                ? "Ozon ещё проверяет данные экземпляров. Подождите несколько секунд и повторите печать."
 174                : "Ozon не подтвердил готовность отправления к сборке. Проверьте данные экземпляров.");
 175        }
 176
 177        reasons = collected;
 178        return false;
 179    }
 180
 181    private static void AddIfFailed(List<string> target, long productId, string fieldName,
 182        string? checkStatus, JArray? errorCodes)
 183    {
 184        if (string.IsNullOrWhiteSpace(checkStatus) ||
 185            checkStatus.Equals("passed", StringComparison.OrdinalIgnoreCase))
 186        {
 187            return;
 188        }
 189
 190        var statusName = CheckStatusNames.TryGetValue(checkStatus, out var friendly) ? friendly : checkStatus;
 191        var codes = errorCodes?.Values<string>()
 192            .Where(code => !string.IsNullOrWhiteSpace(code))
 193            .ToList() ?? [];
 194
 195        var message = $"Товар {productId}, {fieldName}: {statusName}";
 196        if (codes.Count > 0)
 197        {
 198            message += $" ({string.Join(", ", codes)})";
 199        }
 200
 201        target.Add(message + ".");
 202    }
 203}