| | | 1 | | using System.Globalization; |
| | | 2 | | using Newtonsoft.Json.Linq; |
| | | 3 | | using OzonDomains.Models; |
| | | 4 | | using Services.ApiServices.TradesoftApi.Models.Response; |
| | | 5 | | |
| | | 6 | | namespace Services.ApiServices.ZZapApi; |
| | | 7 | | |
| | | 8 | | /// <summary>Разворачивает ответ /search/light в строки общей таблицы проценки. ZZap — агрегатор: |
| | | 9 | | /// каждая строка ответа — предложение отдельного продавца, поэтому «направление» здесь — |
| | | 10 | | /// продавец и его точка выдачи, а не склад поставщика.</summary> |
| | | 11 | | public 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 | | { |
| | 36 | 47 | | var wantedArticle = Normalize(article); |
| | 36 | 48 | | var data = response?["result"]?["data"] as JArray; |
| | 40 | 49 | | if (data is null) return []; |
| | | 50 | | |
| | 32 | 51 | | var items = new List<PreOrderItem>(); |
| | 202 | 52 | | foreach (var token in data) |
| | | 53 | | { |
| | 69 | 54 | | if (token is not JObject offer) continue; |
| | | 55 | | // Артикул сверяем сами: ZZap иногда отдаёт «OC 90» на запрос «OC90», а /search/light |
| | | 56 | | // по контракту не возвращает замены, поэтому чужой номер здесь — это шум, а не аналог. |
| | 67 | 57 | | if (Normalize(Text(offer, "partnumber")) != wantedArticle) continue; |
| | 65 | 58 | | var mapped = MapOffer(offer, article, brand, settings, codeRegion); |
| | 120 | 59 | | if (mapped is not null) items.Add(mapped); |
| | | 60 | | } |
| | | 61 | | |
| | 32 | 62 | | return items |
| | 55 | 63 | | .OrderBy(x => x.SourcePrice ?? decimal.MaxValue) |
| | 55 | 64 | | .ThenBy(x => x.DeliveryDaysMin) |
| | 32 | 65 | | .Take(MaxOffers) |
| | 32 | 66 | | .ToList(); |
| | | 67 | | } |
| | | 68 | | |
| | | 69 | | private static PreOrderItem? MapOffer(JObject offer, string article, string? brand, ApiSupplierSettings settings, in |
| | | 70 | | { |
| | 65 | 71 | | var price = offer.Value<decimal?>("price_v2"); |
| | 68 | 72 | | if (price is null || price.Value <= 0m) return null; |
| | | 73 | | // Б/у и уценка отсеяны type_request=5, но страхуемся: в проценку Studio2 идут только новые детали. |
| | 63 | 74 | | if (offer.Value<bool?>("used_v2") == true) return null; |
| | | 75 | | |
| | 61 | 76 | | var deliveryDays = offer.Value<int?>("delivery_days") ?? 0; |
| | 64 | 77 | | if (deliveryDays >= UnknownDeliveryDaysThreshold) return null; |
| | | 78 | | |
| | 58 | 79 | | var quantity = offer.Value<int?>("qty_v2") ?? 0; |
| | 58 | 80 | | var quantityText = Text(offer, "descr_qty_v2"); |
| | 58 | 81 | | var seller = Text(offer, "class_user") ?? "Продавец ZZap"; |
| | 58 | 82 | | var location = Text(offer, "location") ?? Text(offer, "descr_address"); |
| | 58 | 83 | | var codeDocB = Text(offer, "code_doc_b") ?? string.Empty; |
| | 61 | 84 | | if (codeDocB.Length == 0) return null; |
| | 55 | 85 | | var pack = Math.Max(1, offer.Value<int?>("pack") ?? 1); |
| | 55 | 86 | | var qtyMax = offer.Value<int?>("qty_max") ?? 0; |
| | 57 | 87 | | if (qtyMax >= UnlimitedQtyMax || qtyMax < 0) qtyMax = 0; |
| | 55 | 88 | | var minSumOrder = offer.Value<decimal?>("min_sum_order") ?? 0m; |
| | 55 | 89 | | var typePrice = Text(offer, "type_price") ?? string.Empty; |
| | 55 | 90 | | var priceTypeText = Text(offer, "descr_type_price"); |
| | | 91 | | // W в type_price — прайс для юр. лиц и ИП; descr_type_price дублирует это текстом. |
| | 55 | 92 | | var legalOnly = typePrice.Contains('W') || !string.IsNullOrWhiteSpace(priceTypeText); |
| | 55 | 93 | | var priceText = price.Value.ToString("0.##", CultureInfo.InvariantCulture); |
| | | 94 | | |
| | | 95 | | // Условия продажи и выдачи продавца — в общий ключ deliveryConditions: таблица покажет значок у направления. |
| | 55 | 96 | | var conditions = string.Join("\n", new[] |
| | 55 | 97 | | { |
| | 55 | 98 | | Text(offer, "apply"), |
| | 55 | 99 | | Text(offer, "shipment"), |
| | 55 | 100 | | minSumOrder > 0m ? $"Минимальная сумма заказа у продавца: {minSumOrder:0.##} ₽" : null, |
| | 55 | 101 | | pack > 1 ? $"Кратность упаковки: {pack} шт." : null, |
| | 55 | 102 | | qtyMax > 0 ? $"Максимум к заказу: {qtyMax} шт." : null, |
| | 55 | 103 | | legalOnly ? (priceTypeText ?? "Цена только для юр. лиц и ИП") : null, |
| | 55 | 104 | | offer.Value<bool?>("courier") == true ? "Есть курьерская доставка" : null, |
| | 55 | 105 | | Text(offer, "descr_price_date") is { } priceAge ? $"Прайс обновлён: {priceAge}" : null |
| | 495 | 106 | | }.Where(x => !string.IsNullOrWhiteSpace(x))); |
| | | 107 | | |
| | 55 | 108 | | var description = string.Join(" · ", new[] |
| | 55 | 109 | | { |
| | 55 | 110 | | Text(offer, "descr_delivery"), |
| | 55 | 111 | | conditions.Replace("\n", " · "), |
| | 55 | 112 | | Attribution |
| | 220 | 113 | | }.Where(x => !string.IsNullOrWhiteSpace(x))); |
| | | 114 | | |
| | 55 | 115 | | return new PreOrderItem |
| | 55 | 116 | | { |
| | 55 | 117 | | Caption = Text(offer, "class_cat") ?? article, |
| | 55 | 118 | | Producer = Text(offer, "class_man") ?? brand, |
| | 55 | 119 | | Code = Text(offer, "partnumber") ?? article, |
| | 55 | 120 | | // qty_v2 < 0 — продавец подтверждает наличие, не раскрывая количество. |
| | 55 | 121 | | Rest = quantity < 0 ? (quantityText ?? "есть") : quantity.ToString(CultureInfo.InvariantCulture), |
| | 55 | 122 | | MinQuantity = pack, |
| | 55 | 123 | | DeliveryDays = deliveryDays.ToString(CultureInfo.InvariantCulture), |
| | 55 | 124 | | DeliveryDaysMin = deliveryDays, |
| | 55 | 125 | | DeliveryDaysMax = deliveryDays, |
| | 55 | 126 | | Price = priceText, |
| | 55 | 127 | | CostPrice = priceText, |
| | 55 | 128 | | CostPriceFormatted = priceText, |
| | 55 | 129 | | CurrencyCode = "RUB", |
| | 55 | 130 | | SourcePrice = price, |
| | 55 | 131 | | SourceCurrencyCode = "RUB", |
| | 55 | 132 | | Direction = string.IsNullOrWhiteSpace(location) ? seller : $"{seller} · {location}", |
| | 55 | 133 | | ItemId = codeDocB, |
| | 55 | 134 | | ItemHash = codeDocB, |
| | 55 | 135 | | Title = SupplierTitle, |
| | 55 | 136 | | SiteUrl = SupplierSiteUrl, |
| | 55 | 137 | | Description = description, |
| | 55 | 138 | | PriceDescription = priceTypeText, |
| | 55 | 139 | | PreferredSupplierId = settings.SupplierId, |
| | 55 | 140 | | PreferredSupplierCurrencyCode = settings.Supplier?.CurrencyCode.ToString(), |
| | 55 | 141 | | ProviderData = new Dictionary<string, string?> |
| | 55 | 142 | | { |
| | 55 | 143 | | ["providerKey"] = ApiSupplierSettings.ZZapMoscowProviderKey, |
| | 55 | 144 | | [CodeDocBKey] = codeDocB, |
| | 55 | 145 | | [SellerUserKeyKey] = Text(offer, "user_key"), |
| | 55 | 146 | | [SellerNameKey] = seller, |
| | 55 | 147 | | [SellerLocationKey] = location, |
| | 55 | 148 | | [CodeRegionKey] = codeRegion.ToString(CultureInfo.InvariantCulture), |
| | 55 | 149 | | [MinSumOrderKey] = minSumOrder.ToString("0.##", CultureInfo.InvariantCulture), |
| | 55 | 150 | | [PackKey] = pack.ToString(CultureInfo.InvariantCulture), |
| | 55 | 151 | | [QtyMaxKey] = qtyMax > 0 ? qtyMax.ToString(CultureInfo.InvariantCulture) : null, |
| | 55 | 152 | | [LegalOnlyKey] = legalOnly ? "true" : "false", |
| | 55 | 153 | | [TypePriceKey] = typePrice, |
| | 55 | 154 | | [DeliveryConditionsKey] = conditions.Length > 0 ? conditions : null, |
| | 55 | 155 | | ["zzapWholesale"] = offer.Value<bool?>("wholesale_v2") == true ? "true" : "false", |
| | 55 | 156 | | ["zzapPriceDate"] = Text(offer, "price_date") |
| | 55 | 157 | | } |
| | 55 | 158 | | }; |
| | | 159 | | } |
| | | 160 | | |
| | | 161 | | private static string? Text(JObject offer, string name) |
| | | 162 | | { |
| | 907 | 163 | | var value = offer[name]; |
| | 1391 | 164 | | if (value is null || value.Type == JTokenType.Null) return null; |
| | 423 | 165 | | var text = value.Type == JTokenType.String ? value.Value<string>() : value.ToString(); |
| | 423 | 166 | | return string.IsNullOrWhiteSpace(text) ? null : text.Trim(); |
| | | 167 | | } |
| | | 168 | | |
| | | 169 | | private static string Normalize(string? value) => |
| | 103 | 170 | | new string((value ?? string.Empty).Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant(); |
| | | 171 | | } |