< Summary

Information
Class: Services.ApiServices.ZZapApi.ZZapPriceMapper
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/ZZapApi/ZZapPriceMapper.cs
Line coverage
100%
Covered lines: 99
Uncovered lines: 0
Coverable lines: 99
Total lines: 171
Line coverage: 100%
Branch coverage
98%
Covered branches: 83
Total branches: 84
Branch coverage: 98.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
MapOriginals(...)100%1414100%
MapOffer(...)98.33%6060100%
Text(...)100%88100%
Normalize(...)100%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/ZZapApi/ZZapPriceMapper.cs

#LineLine coverage
 1using System.Globalization;
 2using Newtonsoft.Json.Linq;
 3using OzonDomains.Models;
 4using Services.ApiServices.TradesoftApi.Models.Response;
 5
 6namespace Services.ApiServices.ZZapApi;
 7
 8/// <summary>Разворачивает ответ /search/light в строки общей таблицы проценки. ZZap — агрегатор:
 9/// каждая строка ответа — предложение отдельного продавца, поэтому «направление» здесь —
 10/// продавец и его точка выдачи, а не склад поставщика.</summary>
 11public static class ZZapPriceMapper
 12{
 13    public const string SupplierTitle = "ZZap Москва";
 14    public const string SupplierSiteUrl = ZZapMoscowSupplierConnector.SupplierSiteUrl;
 15    /// <summary>Правила ZZap требуют явно указывать источник данных при их показе.</summary>
 16    public const string Attribution = "Информация о запчастях предоставлена системой ZZap";
 17
 18    /// <summary>Ключи ProviderData. code_doc_b — единственный идентификатор предложения для корзины и заказа.</summary>
 19    public const string CodeDocBKey = "zzapCodeDocB";
 20    public const string SellerUserKeyKey = "zzapSellerUserKey";
 21    public const string SellerNameKey = "zzapSellerName";
 22    public const string MinSumOrderKey = "zzapMinSumOrder";
 23    public const string SellerLocationKey = "zzapSellerLocation";
 24    /// <summary>Ограничения продавца по позиции: кратность упаковки, максимум к заказу, «только юр. лица».</summary>
 25    public const string PackKey = "zzapPack";
 26    public const string QtyMaxKey = "zzapQtyMax";
 27    public const string LegalOnlyKey = "zzapLegalOnly";
 28    public const string TypePriceKey = "zzapTypePrice";
 29    /// <summary>Общий ключ проценки: по нему таблица показывает значок «условия поставки» у направления.</summary>
 30    public const string DeliveryConditionsKey = "deliveryConditions";
 31    /// <summary>qty_max при скрытом остатке — заглушка «без ограничения».</summary>
 32    private const int UnlimitedQtyMax = 999_999_999;
 33    /// <summary>Регион, под которым получено предложение: тот же уходит в POST /cart и /cart/order.</summary>
 34    public const string CodeRegionKey = "zzapCodeRegion";
 35
 36    /// <summary>Значения delivery_days ≥ этого порога ZZap использует как «срок не указан» (254/255).
 37    /// Такие предложения нельзя показывать как реальный срок поставки.</summary>
 38    public const int UnknownDeliveryDaysThreshold = 100;
 39
 40    /// <summary>Сколько лучших по цене предложений отдаём в таблицу: по ходовому артикулу ZZap возвращает
 41    /// сотню продавцов, и без отсечки они вытесняют остальные источники из поля зрения.</summary>
 42    public const int MaxOffers = 20;
 43
 44    public static IReadOnlyList<PreOrderItem> MapOriginals(JObject? response, string article, string? brand,
 45        ApiSupplierSettings settings, int codeRegion)
 46    {
 3647        var wantedArticle = Normalize(article);
 3648        var data = response?["result"]?["data"] as JArray;
 4049        if (data is null) return [];
 50
 3251        var items = new List<PreOrderItem>();
 20252        foreach (var token in data)
 53        {
 6954            if (token is not JObject offer) continue;
 55            // Артикул сверяем сами: ZZap иногда отдаёт «OC 90» на запрос «OC90», а /search/light
 56            // по контракту не возвращает замены, поэтому чужой номер здесь — это шум, а не аналог.
 6757            if (Normalize(Text(offer, "partnumber")) != wantedArticle) continue;
 6558            var mapped = MapOffer(offer, article, brand, settings, codeRegion);
 12059            if (mapped is not null) items.Add(mapped);
 60        }
 61
 3262        return items
 5563            .OrderBy(x => x.SourcePrice ?? decimal.MaxValue)
 5564            .ThenBy(x => x.DeliveryDaysMin)
 3265            .Take(MaxOffers)
 3266            .ToList();
 67    }
 68
 69    private static PreOrderItem? MapOffer(JObject offer, string article, string? brand, ApiSupplierSettings settings, in
 70    {
 6571        var price = offer.Value<decimal?>("price_v2");
 6872        if (price is null || price.Value <= 0m) return null;
 73        // Б/у и уценка отсеяны type_request=5, но страхуемся: в проценку Studio2 идут только новые детали.
 6374        if (offer.Value<bool?>("used_v2") == true) return null;
 75
 6176        var deliveryDays = offer.Value<int?>("delivery_days") ?? 0;
 6477        if (deliveryDays >= UnknownDeliveryDaysThreshold) return null;
 78
 5879        var quantity = offer.Value<int?>("qty_v2") ?? 0;
 5880        var quantityText = Text(offer, "descr_qty_v2");
 5881        var seller = Text(offer, "class_user") ?? "Продавец ZZap";
 5882        var location = Text(offer, "location") ?? Text(offer, "descr_address");
 5883        var codeDocB = Text(offer, "code_doc_b") ?? string.Empty;
 6184        if (codeDocB.Length == 0) return null;
 5585        var pack = Math.Max(1, offer.Value<int?>("pack") ?? 1);
 5586        var qtyMax = offer.Value<int?>("qty_max") ?? 0;
 5787        if (qtyMax >= UnlimitedQtyMax || qtyMax < 0) qtyMax = 0;
 5588        var minSumOrder = offer.Value<decimal?>("min_sum_order") ?? 0m;
 5589        var typePrice = Text(offer, "type_price") ?? string.Empty;
 5590        var priceTypeText = Text(offer, "descr_type_price");
 91        // W в type_price — прайс для юр. лиц и ИП; descr_type_price дублирует это текстом.
 5592        var legalOnly = typePrice.Contains('W') || !string.IsNullOrWhiteSpace(priceTypeText);
 5593        var priceText = price.Value.ToString("0.##", CultureInfo.InvariantCulture);
 94
 95        // Условия продажи и выдачи продавца — в общий ключ deliveryConditions: таблица покажет значок у направления.
 5596        var conditions = string.Join("\n", new[]
 5597        {
 5598            Text(offer, "apply"),
 5599            Text(offer, "shipment"),
 55100            minSumOrder > 0m ? $"Минимальная сумма заказа у продавца: {minSumOrder:0.##} ₽" : null,
 55101            pack > 1 ? $"Кратность упаковки: {pack} шт." : null,
 55102            qtyMax > 0 ? $"Максимум к заказу: {qtyMax} шт." : null,
 55103            legalOnly ? (priceTypeText ?? "Цена только для юр. лиц и ИП") : null,
 55104            offer.Value<bool?>("courier") == true ? "Есть курьерская доставка" : null,
 55105            Text(offer, "descr_price_date") is { } priceAge ? $"Прайс обновлён: {priceAge}" : null
 495106        }.Where(x => !string.IsNullOrWhiteSpace(x)));
 107
 55108        var description = string.Join(" · ", new[]
 55109        {
 55110            Text(offer, "descr_delivery"),
 55111            conditions.Replace("\n", " · "),
 55112            Attribution
 220113        }.Where(x => !string.IsNullOrWhiteSpace(x)));
 114
 55115        return new PreOrderItem
 55116        {
 55117            Caption = Text(offer, "class_cat") ?? article,
 55118            Producer = Text(offer, "class_man") ?? brand,
 55119            Code = Text(offer, "partnumber") ?? article,
 55120            // qty_v2 < 0 — продавец подтверждает наличие, не раскрывая количество.
 55121            Rest = quantity < 0 ? (quantityText ?? "есть") : quantity.ToString(CultureInfo.InvariantCulture),
 55122            MinQuantity = pack,
 55123            DeliveryDays = deliveryDays.ToString(CultureInfo.InvariantCulture),
 55124            DeliveryDaysMin = deliveryDays,
 55125            DeliveryDaysMax = deliveryDays,
 55126            Price = priceText,
 55127            CostPrice = priceText,
 55128            CostPriceFormatted = priceText,
 55129            CurrencyCode = "RUB",
 55130            SourcePrice = price,
 55131            SourceCurrencyCode = "RUB",
 55132            Direction = string.IsNullOrWhiteSpace(location) ? seller : $"{seller} · {location}",
 55133            ItemId = codeDocB,
 55134            ItemHash = codeDocB,
 55135            Title = SupplierTitle,
 55136            SiteUrl = SupplierSiteUrl,
 55137            Description = description,
 55138            PriceDescription = priceTypeText,
 55139            PreferredSupplierId = settings.SupplierId,
 55140            PreferredSupplierCurrencyCode = settings.Supplier?.CurrencyCode.ToString(),
 55141            ProviderData = new Dictionary<string, string?>
 55142            {
 55143                ["providerKey"] = ApiSupplierSettings.ZZapMoscowProviderKey,
 55144                [CodeDocBKey] = codeDocB,
 55145                [SellerUserKeyKey] = Text(offer, "user_key"),
 55146                [SellerNameKey] = seller,
 55147                [SellerLocationKey] = location,
 55148                [CodeRegionKey] = codeRegion.ToString(CultureInfo.InvariantCulture),
 55149                [MinSumOrderKey] = minSumOrder.ToString("0.##", CultureInfo.InvariantCulture),
 55150                [PackKey] = pack.ToString(CultureInfo.InvariantCulture),
 55151                [QtyMaxKey] = qtyMax > 0 ? qtyMax.ToString(CultureInfo.InvariantCulture) : null,
 55152                [LegalOnlyKey] = legalOnly ? "true" : "false",
 55153                [TypePriceKey] = typePrice,
 55154                [DeliveryConditionsKey] = conditions.Length > 0 ? conditions : null,
 55155                ["zzapWholesale"] = offer.Value<bool?>("wholesale_v2") == true ? "true" : "false",
 55156                ["zzapPriceDate"] = Text(offer, "price_date")
 55157            }
 55158        };
 159    }
 160
 161    private static string? Text(JObject offer, string name)
 162    {
 907163        var value = offer[name];
 1391164        if (value is null || value.Type == JTokenType.Null) return null;
 423165        var text = value.Type == JTokenType.String ? value.Value<string>() : value.ToString();
 423166        return string.IsNullOrWhiteSpace(text) ? null : text.Trim();
 167    }
 168
 169    private static string Normalize(string? value) =>
 103170        new string((value ?? string.Empty).Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
 171}