< Summary

Information
Class: Services.ApiServices.ProfitLigaApi.ProfitLigaPriceMapper
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/ProfitLigaApi/ProfitLigaPriceMapper.cs
Line coverage
96%
Covered lines: 100
Uncovered lines: 4
Coverable lines: 104
Total lines: 187
Line coverage: 96.1%
Branch coverage
82%
Covered branches: 71
Total branches: 86
Branch coverage: 82.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
MapOriginals(...)100%1212100%
MapOffer(...)82.35%3434100%
ToDeliveryDays(...)100%66100%
BuildDescription(...)77.27%242285%
FormatDeliveryDate(...)75%44100%
DecodeHtml(...)66.66%6680%
Normalize(...)50%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/ProfitLigaApi/ProfitLigaPriceMapper.cs

#LineLine coverage
 1using System.Globalization;
 2using Newtonsoft.Json.Linq;
 3using Services.ApiServices.TradesoftApi.Models.Response;
 4
 5namespace Services.ApiServices.ProfitLigaApi;
 6
 7/// <summary>Разворачивает ответ поиска Профит-Лиги в строки общей таблицы проценки.
 8/// Цены уже в рублях, поэтому конвертация не нужна — исходная цена дублируется в SourcePrice.</summary>
 9/// <summary>Торговая точка, в которую уедет заказ по этому предложению: связь «склад отгрузки → точка»
 10/// из настроек API-поставщика. SettingsUrl ведёт на строку этой связи.</summary>
 11public sealed record ProfitLigaQuotationDestination(string? DisplayName, string? SettingsUrl);
 12
 13public static class ProfitLigaPriceMapper
 14{
 15    public const string ProviderKey = "ProfitLiga";
 16    public const string SupplierTitle = "Профит-Лига";
 17    public const string SupplierSiteUrl = "https://pr-lg.ru/";
 18
 19    /// <summary>Ключи ProviderData, без которых позицию не положить в корзину поставщика.</summary>
 20    public const string ArticleIdKey = "profitLigaArticleId";
 21    public const string WarehouseIdKey = "profitLigaWarehouseId";
 22    public const string ProductCodeKey = "profitLigaProductCode";
 23    public const string MultiKey = "profitLigaMulti";
 24
 25    /// <summary>Предложения только по запрошенному артикулу и бренду: контракт проценки запрещает
 26    /// отдавать замены, а карточка ответа может относиться к другому товару.</summary>
 27    public static IReadOnlyList<PreOrderItem> MapOriginals(JToken? response, string article, string? brand,
 28        int? preferredSupplierId, string? preferredSupplierCurrencyCode,
 29        ProfitLigaQuotationDestination? destination = null)
 30    {
 2131        var wantedArticle = Normalize(article);
 2132        var wantedBrand = Normalize(brand);
 33
 2134        var items = new List<PreOrderItem>();
 8235        foreach (var card in ProfitLigaJson.Objects(response))
 36        {
 2037            var cardArticle = ProfitLigaJson.String(card, "article");
 2038            var cardBrand = ProfitLigaJson.String(card, "brand");
 2039            if (Normalize(cardArticle) != wantedArticle) continue;
 1940            if (wantedBrand.Length > 0 && Normalize(cardBrand) != wantedBrand) continue;
 41
 8842            foreach (var (key, offer) in ProfitLigaJson.KeyedObjects(card["products"]))
 43            {
 2644                var mapped = MapOffer(offer, key, cardArticle, cardBrand,
 2645                    ProfitLigaJson.String(card, "description"),
 2646                    preferredSupplierId, preferredSupplierCurrencyCode, destination);
 5247                if (mapped is not null) items.Add(mapped);
 48            }
 49        }
 50
 2151        return items;
 52    }
 53
 54    private static PreOrderItem? MapOffer(JObject offer, string? offerKey, string? cardArticle, string? cardBrand,
 55        string? cardDescription, int? preferredSupplierId, string? preferredSupplierCurrencyCode,
 56        ProfitLigaQuotationDestination? destination)
 57    {
 2658        var price = ProfitLigaJson.Decimal(offer, "price");
 2659        if (price is null || price.Value <= 0m) return null;
 60
 2661        var quantity = ProfitLigaJson.Decimal(offer, "quantity") ?? 0m;
 2662        var articleId = ProfitLigaJson.String(offer, "article_id") ?? string.Empty;
 2663        var warehouseId = ProfitLigaJson.String(offer, "warehouse_id") ?? string.Empty;
 2664        var productCode = ProfitLigaJson.String(offer, "product_code");
 2665        var warehouseName = ProfitLigaJson.String(offer, "custom_warehouse_name");
 2666        var multi = ProfitLigaJson.Int(offer, "multi") ?? 0;
 2667        var days = ToDeliveryDays(ProfitLigaJson.Int(offer, "delivery_time"));
 2668        var article = ProfitLigaJson.String(offer, "article") ?? cardArticle ?? string.Empty;
 2669        var brand = ProfitLigaJson.String(offer, "brand") ?? cardBrand;
 70
 2671        return new PreOrderItem
 2672        {
 2673            // В прайсовой строке поставщика наименование искажено (1/4" приходит как 1 4&amp;quot,.),
 2674            // поэтому основным берём описание карточки.
 2675            Caption = DecodeHtml(cardDescription) ?? DecodeHtml(ProfitLigaJson.String(offer, "description")) ?? article,
 2676            Producer = brand,
 2677            Code = article,
 2678            Rest = quantity.ToString("0.##", CultureInfo.InvariantCulture),
 2679            MinQuantity = multi,
 2680            DeliveryDays = days.ToString(CultureInfo.InvariantCulture),
 2681            DeliveryDaysMin = days,
 2682            DeliveryDaysMax = days,
 2683            Price = price.Value.ToString("0.##", CultureInfo.InvariantCulture),
 2684            CostPrice = price.Value.ToString("0.##", CultureInfo.InvariantCulture),
 2685            CostPriceFormatted = $"{price.Value.ToString("0.##", CultureInfo.InvariantCulture)} RUB",
 2686            CurrencyCode = "RUB",
 2687            SourcePrice = price.Value,
 2688            SourceCurrencyCode = "RUB",
 2689            Direction = string.IsNullOrWhiteSpace(warehouseName) ? warehouseId : warehouseName,
 2690            Title = SupplierTitle,
 2691            SiteUrl = SupplierSiteUrl,
 2692            ItemId = ProfitLigaJson.BuildCartKey(articleId, warehouseId),
 2693            // Ключ словаря products — устойчивый хеш предложения на стороне поставщика.
 2694            ItemHash = string.IsNullOrWhiteSpace(offerKey)
 2695                ? ProfitLigaJson.BuildCartKey(articleId, warehouseId)
 2696                : offerKey,
 2697            Description = BuildDescription(offer, multi, warehouseName),
 2698            PriceDescription = $"Цена Профит-Лиги: {price.Value.ToString("0.##", CultureInfo.InvariantCulture)} RUB",
 2699            PreferredSupplierId = preferredSupplierId,
 26100            PreferredSupplierCurrencyCode = preferredSupplierCurrencyCode,
 26101            ProviderData = new Dictionary<string, string?>
 26102            {
 26103                // Без providerKey фронт не свяжет предложение с корзиной поставщика.
 26104                ["providerKey"] = ProviderKey,
 26105                ["warehouseName"] = warehouseName,
 26106                [ArticleIdKey] = articleId,
 26107                [WarehouseIdKey] = warehouseId,
 26108                [ProductCodeKey] = productCode,
 26109                [MultiKey] = multi > 0 ? multi.ToString(CultureInfo.InvariantCulture) : null,
 26110                // Колонка «Точка доставки» в таблице проценки — общие для всех поставщиков ключи.
 26111                ["deliveryDestination"] = destination?.DisplayName,
 26112                ["deliveryMappingAddress"] = destination?.DisplayName,
 26113                ["deliveryMappingLinked"] = destination?.DisplayName is null ? "false" : "true",
 26114                ["deliveryMappingSettingsUrl"] = destination?.SettingsUrl
 26115            }
 26116        };
 117    }
 118
 119    /// <summary>API отдаёт срок в часах; общая таблица проценки оперирует днями,
 120    /// а часовую надбавку поставщика добавляет SupplierDeliverySurchargeFilter.</summary>
 121    public static int ToDeliveryDays(int? deliveryHours) =>
 32122        deliveryHours is null or <= 0 ? 0 : (int)Math.Ceiling(deliveryHours.Value / 24m);
 123
 124    private static string BuildDescription(JObject offer, int multi, string? warehouseName)
 125    {
 26126        var parts = new List<string>();
 49127        if (!string.IsNullOrWhiteSpace(warehouseName)) parts.Add($"Склад: {warehouseName}");
 36128        if (multi > 1) parts.Add($"Кратность: {multi}");
 129
 26130        var showDate = ProfitLigaJson.String(offer, "show_date");
 35131        if (!string.IsNullOrWhiteSpace(showDate)) parts.Add($"Поставка на склад: {showDate}");
 132
 26133        var deliveryDate = FormatDeliveryDate(ProfitLigaJson.String(offer, "delivery_date"));
 26134        if (deliveryDate is not null)
 135            // Дата считается от точки, выставленной в аккаунте поставщика (POST /cart/point), а её
 136            // в проценке не трогаем — иначе параллельные запросы перебивали бы точку друг у друга.
 1137            parts.Add($"Доставка до точки: {deliveryDate} (для точки по умолчанию в аккаунте)");
 138
 26139        var probability = ProfitLigaJson.Decimal(offer, "delivery_probability");
 44140        if (probability is not null) parts.Add($"Вероятность поставки: {probability.Value.ToString("0.#", CultureInfo.In
 141
 26142        if (ProfitLigaJson.Bool(offer, "allow_return"))
 143        {
 18144            var returnDays = ProfitLigaJson.Int(offer, "return_days") ?? 0;
 18145            parts.Add(returnDays > 0 ? $"Возврат: до {returnDays} дн." : "Возврат: разрешён");
 146        }
 147
 26148        var waitings = ProfitLigaJson.Decimal(offer, "waitings");
 26149        if (waitings is not null && waitings.Value > 0m)
 0150            parts.Add($"Ожидается на склад: {waitings.Value.ToString("0.##", CultureInfo.InvariantCulture)}");
 151
 26152        if (ProfitLigaJson.Bool(offer, "sale"))
 153        {
 0154            var reason = ProfitLigaJson.String(offer, "comment");
 0155            parts.Add(string.IsNullOrWhiteSpace(reason) ? "Уценённый товар" : $"Уценка: {reason}");
 156        }
 157
 26158        return string.Join("; ", parts);
 159    }
 160
 161    /// <summary>API отдаёт дату строкой «2026-08-22 08:00:00»; в подсказке нужен привычный формат.</summary>
 162    private static string? FormatDeliveryDate(string? value)
 163    {
 51164        if (string.IsNullOrWhiteSpace(value)) return null;
 1165        return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
 1166            ? parsed.ToString("dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture)
 1167            : value.Trim();
 168    }
 169
 170    /// <summary>Наименования приходят с HTML-сущностями, а в строках прайса — с двойным
 171    /// экранированием (&amp;amp;quot;). Двух проходов достаточно для обоих случаев.</summary>
 172    private static string? DecodeHtml(string? value)
 173    {
 29174        if (string.IsNullOrWhiteSpace(value)) return null;
 175
 25176        var decoded = System.Net.WebUtility.HtmlDecode(value);
 25177        if (decoded.Contains('&') && decoded.Contains(';'))
 0178            decoded = System.Net.WebUtility.HtmlDecode(decoded);
 25179        return decoded.Trim();
 180    }
 181
 182    /// <summary>Артикулы у поставщика и в заказе различаются разделителями и регистром.</summary>
 183    private static string Normalize(string? value) =>
 81184        string.IsNullOrWhiteSpace(value)
 81185            ? string.Empty
 81186            : new string(value.Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
 187}