< Summary

Information
Class: Services.ApiServices.BergApi.BergPriceMapper
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/BergApi/BergPriceMapper.cs
Line coverage
99%
Covered lines: 100
Uncovered lines: 1
Coverable lines: 101
Total lines: 153
Line coverage: 99%
Branch coverage
84%
Covered branches: 73
Total branches: 86
Branch coverage: 84.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Map(...)81.66%6060100%
BrandMatches(...)87.5%8887.5%
.cctor()100%11100%
Transliterate(...)100%88100%
ResolveDeliveryDays(...)90%1010100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/BergApi/BergPriceMapper.cs

#LineLine coverage
 1using System.Globalization;
 2using Newtonsoft.Json.Linq;
 3using OzonDomains;
 4using Services.ApiServices.TradesoftApi.Models.Response;
 5using Services.ProducerAliases;
 6
 7namespace Services.ApiServices.BergApi;
 8
 9public static class BergPriceMapper
 10{
 11    public static IReadOnlyList<PreOrderItem> Map(JToken response, int? preferredSupplierId, CurrencyCode? preferredSupp
 12        DateTime? today = null, string? deliveryDestination = null, string? requestedBrand = null,
 13        ProducerBrandAliasMap? brandAliases = null, bool deliveryMappingLinked = false,
 14        string? deliverySettingsUrl = null)
 15    {
 916        var aliases = brandAliases ?? ProducerBrandAliasMap.Empty;
 917        var items = new List<PreOrderItem>();
 4018        foreach (var resource in response["resources"]?.OfType<JObject>() ?? Enumerable.Empty<JObject>())
 19        {
 1120            var article = resource.Value<string>("article") ?? string.Empty;
 1121            var producer = resource.SelectToken("brand.name")?.Value<string>() ?? string.Empty;
 1122            if (!string.IsNullOrWhiteSpace(requestedBrand) && !BrandMatches(producer, requestedBrand, aliases))
 23                continue;
 724            var caption = resource.Value<string>("name") ?? article;
 25            // id карточки нужен для заказа; без него предложение остаётся только справочным,
 26            // а ItemId продолжает опираться на артикул, как раньше.
 727            var orderableResourceId = resource.Value<long?>("id");
 728            var resourceId = orderableResourceId?.ToString(CultureInfo.InvariantCulture) ?? article;
 2829            foreach (var offer in resource["offers"]?.OfType<JObject>() ?? Enumerable.Empty<JObject>())
 30            {
 731                var price = offer.Value<decimal?>("price");
 732                if (price is null) continue;
 33
 734                var warehouseName = offer.SelectToken("warehouse.name")?.Value<string>() ?? "Berg";
 735                var warehouseId = offer.SelectToken("warehouse.id")?.Value<string>() ?? string.Empty;
 736                var assuredDays = offer.Value<int?>("assured_period") ?? 0;
 737                var averageDays = offer.Value<int?>("average_period") ?? assuredDays;
 738                var deliveryDays = ResolveDeliveryDays(offer, assuredDays, averageDays, today ?? DateTime.Today);
 739                var quantity = offer.Value<decimal?>("quantity") ?? 0m;
 740                var multiplication = offer.Value<int?>("multiplication_factor") ?? 0;
 741                var itemId = string.Join("|", resourceId, warehouseId, price.Value.ToString(CultureInfo.InvariantCulture
 742                var description = string.Join("; ", new[]
 743                {
 744                    $"Артикул: {article}", $"Бренд: {producer}", $"Склад: {warehouseName}",
 745                    $"Наличие: {quantity.ToString("0.##", CultureInfo.InvariantCulture)}",
 746                    $"Средний срок: {deliveryDays.Min} д.", $"Гарантированный срок: {deliveryDays.Max} д.",
 747                    $"Надёжность: {offer.Value<int?>("reliability")?.ToString(CultureInfo.InvariantCulture) ?? "не указа
 748                    offer.Value<bool?>("is_transit") == true ? "Транзит: да" : "Транзит: нет",
 749                    offer.Value<bool?>("available_more") == true ? "Доступно больше указанного остатка" : null
 7050                }.Where(x => !string.IsNullOrWhiteSpace(x)));
 51
 752                items.Add(new PreOrderItem
 753                {
 754                    Caption = caption,
 755                    Producer = producer,
 756                    Code = article,
 757                    Rest = quantity.ToString("0.##", CultureInfo.InvariantCulture),
 758                    MinQuantity = multiplication,
 759                    DeliveryDays = deliveryDays.Min.ToString(CultureInfo.InvariantCulture),
 760                    DeliveryDaysMin = deliveryDays.Min,
 761                    DeliveryDaysMax = deliveryDays.Max,
 762                    Price = price.Value.ToString("0.##", CultureInfo.InvariantCulture),
 763                    CostPrice = price.Value.ToString("0.##", CultureInfo.InvariantCulture),
 764                    CostPriceFormatted = price.Value.ToString("0.##", CultureInfo.InvariantCulture),
 765                    CurrencyCode = "RUB",
 766                    SourcePrice = price,
 767                    SourceCurrencyCode = "RUB",
 768                    Direction = warehouseName,
 769                    Title = "Берг",
 770                    SiteUrl = "https://berg.ru/",
 771                    ItemId = itemId,
 772                    ItemHash = itemId.GetHashCode(StringComparison.Ordinal).ToString("X", CultureInfo.InvariantCulture),
 773                    Description = description,
 774                    PriceDescription = $"Цена Berg: {price.Value:0.##} RUB",
 775                    PreferredSupplierId = preferredSupplierId,
 776                    PreferredSupplierCurrencyCode = preferredSupplierCurrency?.ToString(),
 777                    // providerKey включает предложение в корзину и заказ через API: фронтенд
 778                    // определяет поставщика строки только по нему. resourceId/warehouseId — адрес
 779                    // предложения для place_order, чтобы JS не разбирал ItemId строкой.
 780                    ProviderData = new Dictionary<string, string?>
 781                    {
 782                        ["providerKey"] = "Berg",
 783                        ["resourceId"] = orderableResourceId?.ToString(CultureInfo.InvariantCulture),
 784                        ["warehouseId"] = string.IsNullOrWhiteSpace(warehouseId) ? null : warehouseId,
 785                        ["deliveryDestination"] = string.IsNullOrWhiteSpace(deliveryDestination) ? null : deliveryDestin
 786                        // Без связи «склад отгрузки → адрес Berg» place_order отправить нельзя,
 787                        // поэтому автоподбор такие предложения пропускает.
 788                        ["deliveryMappingLinked"] = deliveryMappingLinked ? "true" : "false",
 789                        ["deliverySettingsUrl"] = deliverySettingsUrl
 790                    }
 791                });
 92            }
 93        }
 994        return items;
 95    }
 96
 97    // Совпадение бренда проверяется в трёх видах, потому что одно и то же имя приходит по-разному:
 98    // 1) алиасы (TecDoc "Kayaba" против Berg "KYB") — словарь OneCProducerAliases;
 99    // 2) транслитерация (TecDoc "LUKOIL" против Berg "ЛУКОЙЛ");
 100    // 3) простая нормализация (регистр, пробелы, дефисы).
 101    private static bool BrandMatches(string producer, string requestedBrand, ProducerBrandAliasMap aliases)
 102    {
 8103        if (string.Equals(producer, requestedBrand, StringComparison.OrdinalIgnoreCase))
 1104            return true;
 105
 7106        var canonicalRequested = aliases.Canonicalize(requestedBrand);
 7107        if (canonicalRequested.Length == 0)
 0108            return false;
 109
 7110        return aliases.Canonicalize(producer) == canonicalRequested
 7111            || aliases.Canonicalize(Transliterate(producer, iotaAsY: true)) == canonicalRequested
 7112            || aliases.Canonicalize(Transliterate(producer, iotaAsY: false)) == canonicalRequested;
 113    }
 114
 1115    private static readonly Dictionary<char, string> CyrillicToLatin = new()
 1116    {
 1117        ['А'] = "A", ['Б'] = "B", ['В'] = "V", ['Г'] = "G", ['Д'] = "D", ['Е'] = "E", ['Ё'] = "E",
 1118        ['Ж'] = "ZH", ['З'] = "Z", ['И'] = "I", ['К'] = "K", ['Л'] = "L", ['М'] = "M", ['Н'] = "N",
 1119        ['О'] = "O", ['П'] = "P", ['Р'] = "R", ['С'] = "S", ['Т'] = "T", ['У'] = "U", ['Ф'] = "F",
 1120        ['Х'] = "KH", ['Ц'] = "TS", ['Ч'] = "CH", ['Ш'] = "SH", ['Щ'] = "SHCH", ['Ъ'] = "", ['Ы'] = "Y",
 1121        ['Ь'] = "", ['Э'] = "E", ['Ю'] = "YU", ['Я'] = "YA"
 1122    };
 123
 124    /// <summary>Й транслитерируется по-разному ("май" → "may", но "Лукойл" → "Lukoil"), поэтому
 125    /// пробуем оба варианта, а не жёстко фиксируем один.</summary>
 126    private static string Transliterate(string value, bool iotaAsY)
 127    {
 10128        var builder = new System.Text.StringBuilder(value.Length);
 108129        foreach (var ch in value.ToUpperInvariant())
 130        {
 48131            if (ch == 'Й') { builder.Append(iotaAsY ? "Y" : "I"); continue; }
 42132            builder.Append(CyrillicToLatin.TryGetValue(ch, out var mapped) ? mapped : ch.ToString());
 133        }
 10134        return builder.ToString();
 135    }
 136
 137    private static (int Min, int Max) ResolveDeliveryDays(JObject offer, int assuredDays, int averageDays, DateTime toda
 138    {
 7139        var schedule = offer["address_timetable"]?.OfType<JObject>().FirstOrDefault();
 7140        var deliveryFrom = schedule?.Value<string>("delivery_from");
 7141        var deliveryTo = schedule?.Value<string>("delivery_to");
 7142        if (DateTime.TryParse(deliveryFrom, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var from))
 143        {
 1144            var min = Math.Max(0, (from.Date - today.Date).Days);
 1145            var max = DateTime.TryParse(deliveryTo, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var to
 1146                ? Math.Max(min, (to.Date - today.Date).Days)
 1147                : min;
 1148            return (min, max);
 149        }
 150
 6151        return (assuredDays, Math.Max(assuredDays, averageDays));
 152    }
 153}