< Summary

Information
Class: Services.LabelServices.OzonExemplarHelper
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/LabelServices/OzonExemplarHelper.cs
Line coverage
97%
Covered lines: 85
Uncovered lines: 2
Coverable lines: 87
Total lines: 203
Line coverage: 97.7%
Branch coverage
74%
Covered branches: 77
Total branches: 104
Branch coverage: 74%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
ParseRequirements(...)82%505094.73%
IsShipAvailable(...)64.28%4242100%
AddIfFailed(...)75%1212100%

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{
 11    public long ProductId { get; init; }
 12
 13    public long ExemplarId { get; init; }
 14
 15    /// <summary>Порядковый номер экземпляра в рамках товара (1..quantity) — для подписи в UI.</summary>
 16    public int Index { get; init; }
 17
 18    public int Quantity { get; init; }
 19
 20    public bool NeedsMandatoryMark { get; init; }
 21
 22    public bool NeedsGtd { get; init; }
 23
 24    public bool NeedsRnpt { get; init; }
 25
 26    /// <summary>Требования, которые форма не закрывает: вес, IMEI, ювелирный УИН.</summary>
 27    public IReadOnlyList<string> Unsupported { get; init; } = [];
 28
 29    public bool HasAnyRequirement =>
 30        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
 150    private static readonly Dictionary<string, string> UnsupportedRequirementNames =
 151        new(StringComparer.OrdinalIgnoreCase)
 152        {
 153            ["weight"] = "фактический вес экземпляра",
 154            ["imei"] = "IMEI",
 155            ["jw_uin"] = "УИН ювелирного изделия"
 156        };
 57
 158    private static readonly Dictionary<string, string> CheckStatusNames =
 159        new(StringComparer.OrdinalIgnoreCase)
 160        {
 161            ["processing"] = "проверяется",
 162            ["failed"] = "проверка не пройдена",
 163            ["passed"] = "проверка пройдена"
 164        };
 65
 66    /// <summary>
 67    /// Разворачивает ответ /v6/fbs/posting/product/exemplar/create-or-get в плоский список
 68    /// требований по экземплярам. Возвращает только экземпляры, по которым что-то нужно заполнить.
 69    /// </summary>
 70    public static List<OzonExemplarRequirement> ParseRequirements(JObject? createOrGetResponse)
 71    {
 572        var result = new List<OzonExemplarRequirement>();
 573        if (createOrGetResponse?["products"] is not JArray products)
 74        {
 175            return result;
 76        }
 77
 1478        foreach (var product in products.OfType<JObject>())
 79        {
 380            var productId = product["product_id"]?.Value<long>() ?? 0;
 381            if (productId <= 0)
 82            {
 83                continue;
 84            }
 85
 386            var needsMandatoryMark = product["is_mandatory_mark_needed"]?.Value<bool>() ?? false;
 387            var needsGtd = product["is_gtd_needed"]?.Value<bool>() ?? false;
 388            var needsRnpt = product["is_rnpt_needed"]?.Value<bool>() ?? false;
 89
 390            var unsupported = new List<string>();
 391            if (product["is_weight_needed"]?.Value<bool>() ?? false)
 192                unsupported.Add(UnsupportedRequirementNames["weight"]);
 393            if (product["is_jw_uin_needed"]?.Value<bool>() ?? false)
 094                unsupported.Add(UnsupportedRequirementNames["jw_uin"]);
 395            if (product["has_imei"]?.Value<bool>() ?? false)
 196                unsupported.Add(UnsupportedRequirementNames["imei"]);
 97
 398            if (!needsMandatoryMark && !needsGtd && !needsRnpt && unsupported.Count == 0)
 99            {
 100                continue;
 101            }
 102
 2103            var exemplarIds = (product["exemplars"] as JArray)?
 2104                .OfType<JObject>()
 3105                .Select(exemplar => exemplar["exemplar_id"]?.Value<long>() ?? 0)
 3106                .Where(id => id > 0)
 2107                .ToList() ?? [];
 108
 2109            var quantity = product["quantity"]?.Value<int>() ?? exemplarIds.Count;
 2110            if (quantity <= 0)
 111            {
 0112                quantity = Math.Max(exemplarIds.Count, 1);
 113            }
 114
 10115            for (var index = 0; index < Math.Max(quantity, exemplarIds.Count); index++)
 116            {
 3117                result.Add(new OzonExemplarRequirement
 3118                {
 3119                    ProductId = productId,
 3120                    ExemplarId = index < exemplarIds.Count ? exemplarIds[index] : 0,
 3121                    Index = index + 1,
 3122                    Quantity = quantity,
 3123                    NeedsMandatoryMark = needsMandatoryMark,
 3124                    NeedsGtd = needsGtd,
 3125                    NeedsRnpt = needsRnpt,
 3126                    Unsupported = unsupported
 3127                });
 128            }
 129        }
 130
 4131        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    {
 4140        var status = statusResponse?["status"]?.Value<string>() ?? string.Empty;
 4141        if (status.Equals(ShipAvailableStatus, StringComparison.OrdinalIgnoreCase))
 142        {
 1143            reasons = [];
 1144            return true;
 145        }
 146
 3147        var collected = new List<string>();
 148
 8149        foreach (var product in (statusResponse?["products"] as JArray)?.OfType<JObject>() ?? [])
 150        {
 1151            var productId = product["product_id"]?.Value<long>() ?? 0;
 152
 4153            foreach (var exemplar in (product["exemplars"] as JArray)?.OfType<JObject>() ?? [])
 154            {
 4155                foreach (var mark in (exemplar["marks"] as JArray)?.OfType<JObject>() ?? [])
 156                {
 1157                    AddIfFailed(collected, productId, "код маркировки",
 1158                        mark["check_status"]?.Value<string>(), mark["error_codes"] as JArray);
 159                }
 160
 1161                AddIfFailed(collected, productId, "ГТД",
 1162                    exemplar["gtd_check_status"]?.Value<string>(), exemplar["gtd_error_codes"] as JArray);
 1163                AddIfFailed(collected, productId, "РНПТ",
 1164                    exemplar["rnpt_check_status"]?.Value<string>(), exemplar["rnpt_error_codes"] as JArray);
 1165                AddIfFailed(collected, productId, "вес",
 1166                    exemplar["weight_check_status"]?.Value<string>(), exemplar["weight_error_codes"] as JArray);
 167            }
 168        }
 169
 3170        if (collected.Count == 0)
 171        {
 2172            collected.Add(status.Equals(ValidationInProgressStatus, StringComparison.OrdinalIgnoreCase)
 2173                ? "Ozon ещё проверяет данные экземпляров. Подождите несколько секунд и повторите печать."
 2174                : "Ozon не подтвердил готовность отправления к сборке. Проверьте данные экземпляров.");
 175        }
 176
 3177        reasons = collected;
 3178        return false;
 179    }
 180
 181    private static void AddIfFailed(List<string> target, long productId, string fieldName,
 182        string? checkStatus, JArray? errorCodes)
 183    {
 4184        if (string.IsNullOrWhiteSpace(checkStatus) ||
 4185            checkStatus.Equals("passed", StringComparison.OrdinalIgnoreCase))
 186        {
 3187            return;
 188        }
 189
 1190        var statusName = CheckStatusNames.TryGetValue(checkStatus, out var friendly) ? friendly : checkStatus;
 1191        var codes = errorCodes?.Values<string>()
 1192            .Where(code => !string.IsNullOrWhiteSpace(code))
 1193            .ToList() ?? [];
 194
 1195        var message = $"Товар {productId}, {fieldName}: {statusName}";
 1196        if (codes.Count > 0)
 197        {
 1198            message += $" ({string.Join(", ", codes)})";
 199        }
 200
 1201        target.Add(message + ".");
 1202    }
 203}