< Summary

Information
Class: Services.ApiServices.ProfitLigaApi.ProfitLigaQuotationDestination
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/ProfitLigaApi/ProfitLigaPriceMapper.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 187
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_DisplayName()100%11100%

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>
 911public 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    {
 31        var wantedArticle = Normalize(article);
 32        var wantedBrand = Normalize(brand);
 33
 34        var items = new List<PreOrderItem>();
 35        foreach (var card in ProfitLigaJson.Objects(response))
 36        {
 37            var cardArticle = ProfitLigaJson.String(card, "article");
 38            var cardBrand = ProfitLigaJson.String(card, "brand");
 39            if (Normalize(cardArticle) != wantedArticle) continue;
 40            if (wantedBrand.Length > 0 && Normalize(cardBrand) != wantedBrand) continue;
 41
 42            foreach (var (key, offer) in ProfitLigaJson.KeyedObjects(card["products"]))
 43            {
 44                var mapped = MapOffer(offer, key, cardArticle, cardBrand,
 45                    ProfitLigaJson.String(card, "description"),
 46                    preferredSupplierId, preferredSupplierCurrencyCode, destination);
 47                if (mapped is not null) items.Add(mapped);
 48            }
 49        }
 50
 51        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    {
 58        var price = ProfitLigaJson.Decimal(offer, "price");
 59        if (price is null || price.Value <= 0m) return null;
 60
 61        var quantity = ProfitLigaJson.Decimal(offer, "quantity") ?? 0m;
 62        var articleId = ProfitLigaJson.String(offer, "article_id") ?? string.Empty;
 63        var warehouseId = ProfitLigaJson.String(offer, "warehouse_id") ?? string.Empty;
 64        var productCode = ProfitLigaJson.String(offer, "product_code");
 65        var warehouseName = ProfitLigaJson.String(offer, "custom_warehouse_name");
 66        var multi = ProfitLigaJson.Int(offer, "multi") ?? 0;
 67        var days = ToDeliveryDays(ProfitLigaJson.Int(offer, "delivery_time"));
 68        var article = ProfitLigaJson.String(offer, "article") ?? cardArticle ?? string.Empty;
 69        var brand = ProfitLigaJson.String(offer, "brand") ?? cardBrand;
 70
 71        return new PreOrderItem
 72        {
 73            // В прайсовой строке поставщика наименование искажено (1/4" приходит как 1 4&amp;quot,.),
 74            // поэтому основным берём описание карточки.
 75            Caption = DecodeHtml(cardDescription) ?? DecodeHtml(ProfitLigaJson.String(offer, "description")) ?? article,
 76            Producer = brand,
 77            Code = article,
 78            Rest = quantity.ToString("0.##", CultureInfo.InvariantCulture),
 79            MinQuantity = multi,
 80            DeliveryDays = days.ToString(CultureInfo.InvariantCulture),
 81            DeliveryDaysMin = days,
 82            DeliveryDaysMax = days,
 83            Price = price.Value.ToString("0.##", CultureInfo.InvariantCulture),
 84            CostPrice = price.Value.ToString("0.##", CultureInfo.InvariantCulture),
 85            CostPriceFormatted = $"{price.Value.ToString("0.##", CultureInfo.InvariantCulture)} RUB",
 86            CurrencyCode = "RUB",
 87            SourcePrice = price.Value,
 88            SourceCurrencyCode = "RUB",
 89            Direction = string.IsNullOrWhiteSpace(warehouseName) ? warehouseId : warehouseName,
 90            Title = SupplierTitle,
 91            SiteUrl = SupplierSiteUrl,
 92            ItemId = ProfitLigaJson.BuildCartKey(articleId, warehouseId),
 93            // Ключ словаря products — устойчивый хеш предложения на стороне поставщика.
 94            ItemHash = string.IsNullOrWhiteSpace(offerKey)
 95                ? ProfitLigaJson.BuildCartKey(articleId, warehouseId)
 96                : offerKey,
 97            Description = BuildDescription(offer, multi, warehouseName),
 98            PriceDescription = $"Цена Профит-Лиги: {price.Value.ToString("0.##", CultureInfo.InvariantCulture)} RUB",
 99            PreferredSupplierId = preferredSupplierId,
 100            PreferredSupplierCurrencyCode = preferredSupplierCurrencyCode,
 101            ProviderData = new Dictionary<string, string?>
 102            {
 103                // Без providerKey фронт не свяжет предложение с корзиной поставщика.
 104                ["providerKey"] = ProviderKey,
 105                ["warehouseName"] = warehouseName,
 106                [ArticleIdKey] = articleId,
 107                [WarehouseIdKey] = warehouseId,
 108                [ProductCodeKey] = productCode,
 109                [MultiKey] = multi > 0 ? multi.ToString(CultureInfo.InvariantCulture) : null,
 110                // Колонка «Точка доставки» в таблице проценки — общие для всех поставщиков ключи.
 111                ["deliveryDestination"] = destination?.DisplayName,
 112                ["deliveryMappingAddress"] = destination?.DisplayName,
 113                ["deliveryMappingLinked"] = destination?.DisplayName is null ? "false" : "true",
 114                ["deliveryMappingSettingsUrl"] = destination?.SettingsUrl
 115            }
 116        };
 117    }
 118
 119    /// <summary>API отдаёт срок в часах; общая таблица проценки оперирует днями,
 120    /// а часовую надбавку поставщика добавляет SupplierDeliverySurchargeFilter.</summary>
 121    public static int ToDeliveryDays(int? deliveryHours) =>
 122        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    {
 126        var parts = new List<string>();
 127        if (!string.IsNullOrWhiteSpace(warehouseName)) parts.Add($"Склад: {warehouseName}");
 128        if (multi > 1) parts.Add($"Кратность: {multi}");
 129
 130        var showDate = ProfitLigaJson.String(offer, "show_date");
 131        if (!string.IsNullOrWhiteSpace(showDate)) parts.Add($"Поставка на склад: {showDate}");
 132
 133        var deliveryDate = FormatDeliveryDate(ProfitLigaJson.String(offer, "delivery_date"));
 134        if (deliveryDate is not null)
 135            // Дата считается от точки, выставленной в аккаунте поставщика (POST /cart/point), а её
 136            // в проценке не трогаем — иначе параллельные запросы перебивали бы точку друг у друга.
 137            parts.Add($"Доставка до точки: {deliveryDate} (для точки по умолчанию в аккаунте)");
 138
 139        var probability = ProfitLigaJson.Decimal(offer, "delivery_probability");
 140        if (probability is not null) parts.Add($"Вероятность поставки: {probability.Value.ToString("0.#", CultureInfo.In
 141
 142        if (ProfitLigaJson.Bool(offer, "allow_return"))
 143        {
 144            var returnDays = ProfitLigaJson.Int(offer, "return_days") ?? 0;
 145            parts.Add(returnDays > 0 ? $"Возврат: до {returnDays} дн." : "Возврат: разрешён");
 146        }
 147
 148        var waitings = ProfitLigaJson.Decimal(offer, "waitings");
 149        if (waitings is not null && waitings.Value > 0m)
 150            parts.Add($"Ожидается на склад: {waitings.Value.ToString("0.##", CultureInfo.InvariantCulture)}");
 151
 152        if (ProfitLigaJson.Bool(offer, "sale"))
 153        {
 154            var reason = ProfitLigaJson.String(offer, "comment");
 155            parts.Add(string.IsNullOrWhiteSpace(reason) ? "Уценённый товар" : $"Уценка: {reason}");
 156        }
 157
 158        return string.Join("; ", parts);
 159    }
 160
 161    /// <summary>API отдаёт дату строкой «2026-08-22 08:00:00»; в подсказке нужен привычный формат.</summary>
 162    private static string? FormatDeliveryDate(string? value)
 163    {
 164        if (string.IsNullOrWhiteSpace(value)) return null;
 165        return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
 166            ? parsed.ToString("dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture)
 167            : value.Trim();
 168    }
 169
 170    /// <summary>Наименования приходят с HTML-сущностями, а в строках прайса — с двойным
 171    /// экранированием (&amp;amp;quot;). Двух проходов достаточно для обоих случаев.</summary>
 172    private static string? DecodeHtml(string? value)
 173    {
 174        if (string.IsNullOrWhiteSpace(value)) return null;
 175
 176        var decoded = System.Net.WebUtility.HtmlDecode(value);
 177        if (decoded.Contains('&') && decoded.Contains(';'))
 178            decoded = System.Net.WebUtility.HtmlDecode(decoded);
 179        return decoded.Trim();
 180    }
 181
 182    /// <summary>Артикулы у поставщика и в заказе различаются разделителями и регистром.</summary>
 183    private static string Normalize(string? value) =>
 184        string.IsNullOrWhiteSpace(value)
 185            ? string.Empty
 186            : new string(value.Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
 187}

Methods/Properties

get_DisplayName()