< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.ArmtekPriceController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/ArmtekPriceController.cs
Line coverage
17%
Covered lines: 51
Uncovered lines: 233
Coverable lines: 284
Total lines: 479
Line coverage: 17.9%
Branch coverage
5%
Covered branches: 11
Total branches: 205
Branch coverage: 5.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/ArmtekPriceController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.Extensions.Caching.Memory;
 4using Services.ApiServices.ArmtekApi;
 5using Services.ApiServices.ArmtekApi.Models.Envelope;
 6using Services.ApiServices.ArmtekApi.Models.Response;
 7using Services.ApiServices.TradesoftApi.Models.Response;
 8using Services.ApiSupplierSettingsServices;
 9using Services.PriceСlculationServices;
 10using OzonDomains;
 11using OzonDomains.Models;
 12using OzonOrdersWeb.Areas.Studio2.ViewModels;
 13using OzonOrdersWeb.Filters;
 14
 15namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 16
 17[Area("Studio2")]
 18[Authorize(Roles = "User,Admin")]
 19[ServiceFilter(typeof(SupplierQuotationHistoryFilter), Order = 0)]
 20[ServiceFilter(typeof(SupplierDeliverySurchargeFilter), Order = 1)]
 21[ServiceFilter(typeof(SupplierWarehouseRestrictionFilter), Order = 2)]
 22public class ArmtekPriceController : Controller
 23{
 24    protected readonly ArmtekDataManager ArmtekDataManager;
 25    protected readonly ApiSupplierSettingsService SettingsService;
 26    protected readonly ICurrencyRateFetcher CurrencyRateFetcher;
 27    private readonly IMemoryCache _cache;
 28    private readonly ArmtekDeliveryAddressMappingService _deliveryAddressMappings;
 29    private readonly ArmtekStoreDirectory _storeDirectory;
 30
 1031    public ArmtekPriceController(ArmtekDataManager armtekDataManager,
 1032        ApiSupplierSettingsService settingsService,
 1033        ICurrencyRateFetcher currencyRateFetcher,
 1034        IMemoryCache cache,
 1035        ArmtekDeliveryAddressMappingService deliveryAddressMappings,
 1036        ArmtekStoreDirectory storeDirectory)
 37    {
 1038        ArmtekDataManager = armtekDataManager;
 1039        SettingsService = settingsService;
 1040        CurrencyRateFetcher = currencyRateFetcher;
 1041        _cache = cache;
 1042        _deliveryAddressMappings = deliveryAddressMappings;
 1043        _storeDirectory = storeDirectory;
 1044    }
 45
 046    protected virtual string ProviderDisplayName => "Армтек";
 1347    protected virtual string ProviderCacheKey => "Armtek";
 48
 49    protected virtual Task<ApiSupplierSettings> GetSettingsAsync(CancellationToken cancellationToken)
 750        => SettingsService.GetArmtekAsync(cancellationToken);
 51
 52    /// <summary>
 53    /// Прогревает соединение и справочники Армтека до первого поиска товара.
 54    /// Поиск артикула намеренно не выполняется: он остаётся точным и актуальным на момент раскрытия строки.
 55    /// </summary>
 56    [HttpGet]
 57    public async Task<IActionResult> Warmup(string? vkorg = null)
 58    {
 59        try
 60        {
 661            var settings = await GetSettingsAsync(HttpContext.RequestAborted);
 562            if (!settings.IsQuotationEnabled)
 163                return NoContent();
 64
 465            ResolveStoreNamesByKeyzak(vkorg);
 466            await GetUserInfoAsync(vkorg);
 67
 068            return NoContent();
 69        }
 270        catch (ArmtekApiException)
 71        {
 72            // Прогрев опционален: при ошибке внешнего API поставщика отдаём 204 NoContent,
 73            // чтобы не порождать ложных сбоев на клиенте и в логах.
 274            return NoContent();
 75        }
 176        catch (HttpRequestException)
 77        {
 78            // Временный сетевой сбой внешнего поставщика при фоновом прогреве не ломает страницу.
 179            return NoContent();
 80        }
 281        catch (OperationCanceledException) when (HttpContext.RequestAborted.IsCancellationRequested)
 82        {
 83            // Прокси или браузер закрыл соединение до ответа; это не ошибка приложения.
 184            return StatusCode(499, new { error = "request_canceled" });
 85        }
 186        catch (OperationCanceledException)
 87        {
 88            // Таймаут обращения к внешнему сервису поставщика во время прогрева.
 189            return NoContent();
 90        }
 691    }
 92
 93    [HttpGet]
 94    public async Task<IActionResult> Search(string code, string? brand, int? orderId = null, string? vkorg = null,
 95        bool includeAnalogs = false, string? zasylWarehouseName = null)
 96    {
 97        try
 98        {
 499            if (string.IsNullOrWhiteSpace(code))
 3100                return Ok(new ArmtekQuotationResponse([], null));
 101
 1102            var settings = await GetSettingsAsync(HttpContext.RequestAborted);
 1103            if (!settings.IsQuotationEnabled)
 0104                return Ok(new ArmtekQuotationResponse([], null));
 105
 106
 1107            var article = code.Trim();
 1108            var producer = string.IsNullOrWhiteSpace(brand) ? null : brand.Trim();
 1109            var searchInput = await ResolveAssortmentSearchInputAsync(article, producer, vkorg);
 110
 1111            var userInfo = await GetUserInfoAsync(vkorg);
 0112            if (userInfo.HasErrors)
 0113                return Ok(new { items = Array.Empty<PreOrderItem>(), error = BuildArmtekErrorMessage(userInfo.Messages) 
 114
 0115            var warehouse = await _deliveryAddressMappings.ResolveWarehouseAsync(orderId, zasylWarehouseName, HttpContex
 0116            var deliveryMapping = await _deliveryAddressMappings.ResolveAsync(ProviderCacheKey, orderId, zasylWarehouseN
 0117            var kunnrRg = deliveryMapping?.BuyerKunnr ?? ResolveKunnrRg(userInfo.Resp);
 0118            if (string.IsNullOrWhiteSpace(kunnrRg))
 0119                return Ok(new { items = Array.Empty<PreOrderItem>(), error = "Армтек getUserInfo не вернул ни одного KUN
 120
 0121            var queryType = includeAnalogs ? 2 : 1;
 122
 123            // Справочник складов грузится в фоне при старте приложения; проценка его не ждёт.
 0124            var storeNameByKeyzak = ResolveStoreNamesByKeyzak(vkorg);
 0125            var searchResponse = await GetSearchResponseAsync(
 0126                kunnrRg: kunnrRg,
 0127                pin: searchInput.Pin,
 0128                brand: searchInput.Brand,
 0129                vkorg: vkorg,
 0130                queryType: queryType,
 0131                kunnrZa: deliveryMapping?.DeliveryAddressKunnr,
 0132                incotermsSelfPickup: deliveryMapping is null ? null : false);
 133
 0134            if (searchResponse.HasErrors)
 0135                return Ok(new { items = Array.Empty<PreOrderItem>(), error = BuildArmtekErrorMessage(searchResponse.Mess
 136
 0137            var items = new List<PreOrderItem>();
 0138            foreach (var offer in searchResponse.Resp ?? new List<ArmtekSearchItem>())
 139            {
 0140                var sourceCurrency = ResolveCurrency(offer.Currency, CurrencyCode.RUB);
 0141                var rateToRub = await GetRateToRubAsync(sourceCurrency);
 0142                var item = MapToPreOrderItem(offer, storeNameByKeyzak, rateToRub, sourceCurrency,
 0143                    settings.SupplierId, settings.Supplier?.CurrencyCode, ProviderDisplayName);
 0144                if (item != null)
 145                {
 0146                    AddDeliveryMappingMetadata(item, deliveryMapping, userInfo.Resp, settings.DeliveryAddress);
 0147                    items.Add(item);
 148                }
 0149            }
 150
 0151            var warning = orderId.HasValue && deliveryMapping is null
 0152                ? warehouse.IsUnknownZasyl
 0153                    ? $"Засыл «{warehouse.WarehouseName}» не найден среди складов Studio2. Проценка Armtek выполнена с н
 0154                    : "Адрес доставки Armtek не связан с эффективным складом заказа. Проценка выполнена с настройками Ar
 0155                : null;
 0156            var settingsUrl = warning is null ? null : Url.Action(
 0157                ProviderCacheKey == ApiSupplierSettings.ArmtekByProviderKey ? "ArmtekBy" : "Armtek",
 0158                "ApiSupplierSettings", new { area = "Studio2" }) +
 0159                (warehouse.WarehouseId.HasValue ? $"#armtek-mapping-{warehouse.WarehouseId.Value}" : string.Empty);
 0160            return Ok(new ArmtekQuotationResponse(items, warning, settingsUrl));
 161        }
 1162        catch (ArmtekApiException ex)
 163        {
 1164            return Ok(new { items = Array.Empty<PreOrderItem>(), error = ex.Message });
 165        }
 0166        catch (HttpRequestException)
 167        {
 0168            return Ok(new { items = Array.Empty<PreOrderItem>(), error = "Армтек временно разорвал соединение. Повторите
 169        }
 0170        catch (Exception ex) when (ex is TimeoutException or TaskCanceledException)
 171        {
 0172            return Ok(new { items = Array.Empty<PreOrderItem>(), error = "Истекло время ожидания ответа от Армтек." });
 173        }
 4174    }
 175
 176    private static string ResolveKunnrRg(ArmtekUserInfoResponse? userInfo)
 177    {
 0178        var buyers = userInfo?.Structure?.RgTab ?? new List<ArmtekBuyerItem>();
 0179        var preferred = buyers.FirstOrDefault(x => x.Default == 1);
 0180        if (!string.IsNullOrWhiteSpace(preferred?.Kunnr))
 0181            return preferred.Kunnr;
 182
 0183        return buyers.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Kunnr))?.Kunnr ?? string.Empty;
 184    }
 185
 186    private void AddDeliveryMappingMetadata(PreOrderItem item, ArmtekDeliveryAddressSelection? mapping,
 187        ArmtekUserInfoResponse? userInfo, string? configuredDeliveryAddress)
 188    {
 0189        item.ProviderData ??= new Dictionary<string, string?>();
 0190        item.ProviderData["deliveryMappingLinked"] = mapping is null ? "false" : "true";
 0191        if (mapping is null)
 192        {
 0193            if (!string.IsNullOrWhiteSpace(configuredDeliveryAddress))
 0194                item.ProviderData["deliveryDestination"] = configuredDeliveryAddress.Trim();
 0195            return;
 196        }
 197
 0198        var buyer = userInfo?.Structure.RgTab.FirstOrDefault(x => x.Kunnr == mapping.BuyerKunnr);
 0199        var address = buyer?.ZaTab.FirstOrDefault(x => x.Kunnr == mapping.DeliveryAddressKunnr);
 0200        var addressName = address is null
 0201            ? mapping.DeliveryAddressKunnr
 0202            : string.Join("; ", new[] { address.ShortName, address.Address }.Where(x => !string.IsNullOrWhiteSpace(x)));
 0203        item.ProviderData["deliveryMappingAddress"] = addressName;
 0204        item.ProviderData["deliveryDestination"] = configuredDeliveryAddress ?? addressName;
 205        // Корзина в окне проведения показывает ту же пару «клиент + адрес», что и страница /SupplierCart.
 0206        item.ProviderData["deliveryMappingBuyerKunnr"] = mapping.BuyerKunnr;
 0207        item.ProviderData["deliveryMappingBuyerName"] = buyer is null
 0208            ? null
 0209            : (string.IsNullOrWhiteSpace(buyer.ShortName) ? buyer.FullName : buyer.ShortName);
 0210        item.ProviderData["deliveryMappingAddressKunnr"] = mapping.DeliveryAddressKunnr;
 0211        item.ProviderData["deliveryMappingSettingsUrl"] = Url.Action(
 0212            ProviderCacheKey == ApiSupplierSettings.ArmtekByProviderKey ? "ArmtekBy" : "Armtek",
 0213            "ApiSupplierSettings", new { area = "Studio2" }) + $"#armtek-mapping-{mapping.ShipmentWarehouseId}";
 0214    }
 215
 216    private static string BuildArmtekErrorMessage(IEnumerable<Services.ApiServices.ArmtekApi.Models.Envelope.ArmtekMessa
 217    {
 0218        var list = (messages ?? Enumerable.Empty<Services.ApiServices.ArmtekApi.Models.Envelope.ArmtekMessage>())
 0219            .Select(m => m?.Text?.Trim())
 0220            .Where(t => !string.IsNullOrWhiteSpace(t))
 0221            .ToList();
 222
 0223        return list.Count > 0
 0224            ? string.Join("; ", list)
 0225            : "Армтек вернул ошибку без текста в MESSAGES.";
 226    }
 227
 228    private IReadOnlyDictionary<string, string> ResolveStoreNamesByKeyzak(string? vkorg)
 229    {
 4230        if (_storeDirectory.TryGet(ProviderCacheKey, vkorg, out var stores))
 0231            return stores;
 232
 233        // Справочник ещё не загружен: подпись склада будет кодом KEYZAK, загрузка идёт в фоне.
 4234        _storeDirectory.RequestLoad(ProviderCacheKey, vkorg);
 4235        return stores;
 236    }
 237
 238    private async Task<Services.ApiServices.ArmtekApi.Models.Envelope.ArmtekResponse<ArmtekUserInfoResponse>> GetUserInf
 239    {
 5240        var cacheKey = $"api-supplier:{ProviderCacheKey}:user-info:{ResolveVkorgCachePart(vkorg)}";
 5241        if (_cache.TryGetValue(cacheKey, out Services.ApiServices.ArmtekApi.Models.Envelope.ArmtekResponse<ArmtekUserInf
 0242            return cachedUserInfo;
 243
 5244        var userInfo = await ArmtekDataManager.GetUserInfoAsync(vkorg: vkorg, structure: true, ct: HttpContext.RequestAb
 0245        if (!userInfo.HasErrors)
 0246            _cache.Set(cacheKey, userInfo, TimeSpan.FromMinutes(5));
 0247        return userInfo;
 0248    }
 249
 250    /// <summary>
 251    /// Portal Armtek умеет нормализовать артикул (например AP1086 → AP 108/6), а ws_search/search — нет всегда.
 252    /// Поэтому перед проценкой подбираем каноническую пару PIN/BRAND через ассортимент, не смешивая бренды-аналогии.
 253    /// </summary>
 254    private async Task<ArmtekSearchInput> ResolveAssortmentSearchInputAsync(string article, string? producer, string? vk
 255    {
 1256        if (string.IsNullOrWhiteSpace(producer))
 1257            return new ArmtekSearchInput(article, producer);
 258
 0259        var cacheKey = $"api-supplier:{ProviderCacheKey}:assortment:{ResolveVkorgCachePart(vkorg)}:{NormalizeForMatch(ar
 0260        if (_cache.TryGetValue(cacheKey, out ArmtekSearchInput? cached) && cached is not null)
 0261            return cached;
 262
 263        try
 264        {
 0265            var apiBrand = await ArmtekDataManager.ResolveBrandAsync(producer, HttpContext.RequestAborted);
 0266            var response = await ArmtekDataManager.AssortmentSearchAsync(article, vkorg, ct: HttpContext.RequestAborted)
 0267            if (response.HasErrors || response.Resp is null)
 0268                return new ArmtekSearchInput(article, producer);
 269
 0270            var candidate = response.Resp.FirstOrDefault(item =>
 0271                !string.IsNullOrWhiteSpace(item.Pin) &&
 0272                !string.IsNullOrWhiteSpace(item.Brand) &&
 0273                (string.Equals(NormalizeForMatch(item.Brand), NormalizeForMatch(apiBrand), StringComparison.Ordinal) ||
 0274                 string.Equals(NormalizeForMatch(item.Brand), NormalizeForMatch(producer), StringComparison.Ordinal)));
 0275            if (candidate is null)
 0276                return new ArmtekSearchInput(article, producer);
 277
 0278            var resolved = new ArmtekSearchInput(candidate.Pin.Trim(), candidate.Brand.Trim());
 0279            _cache.Set(cacheKey, resolved, TimeSpan.FromMinutes(5));
 0280            return resolved;
 281        }
 0282        catch (Exception ex) when (ex is ArmtekApiException or HttpRequestException or TaskCanceledException)
 283        {
 284            // Ассортимент — вспомогательная нормализация. При его недоступности сохраняем прежнюю проценку.
 0285            return new ArmtekSearchInput(article, producer);
 286        }
 1287    }
 288
 289    private async Task<Services.ApiServices.ArmtekApi.Models.Envelope.ArmtekResponse<List<ArmtekSearchItem>>> GetSearchR
 290        string kunnrRg,
 291        string pin,
 292        string? brand,
 293        string? vkorg,
 294        int queryType,
 295        string? kunnrZa,
 296        bool? incotermsSelfPickup)
 297    {
 0298        var cacheKey = $"api-supplier:{ProviderCacheKey}:quotation:{ResolveVkorgCachePart(vkorg)}:{kunnrRg}:{pin.Trim().
 0299        if (_cache.TryGetValue(cacheKey, out Services.ApiServices.ArmtekApi.Models.Envelope.ArmtekResponse<List<ArmtekSe
 0300            return cachedSearch;
 301
 0302        var searchResponse = await ArmtekDataManager.SearchAsync(
 0303            kunnrRg: kunnrRg,
 0304            pin: pin,
 0305            brand: brand,
 0306            vkorg: vkorg,
 0307            queryType: queryType,
 0308            kunnrZa: kunnrZa,
 0309            incotermsSelfPickup: incotermsSelfPickup,
 0310            ct: HttpContext.RequestAborted);
 311
 312        // Цена быстро меняется, поэтому держим только короткий кэш. Ошибки и частичные ответы не кэшируем.
 0313        if (!searchResponse.HasErrors)
 0314            _cache.Set(cacheKey, searchResponse, TimeSpan.FromMinutes(1));
 0315        return searchResponse;
 0316    }
 317
 5318    private static string ResolveVkorgCachePart(string? vkorg) => string.IsNullOrWhiteSpace(vkorg) ? "default" : vkorg.T
 319
 0320    private static string NormalizeForMatch(string? value) => new((value ?? string.Empty)
 0321        .Where(char.IsLetterOrDigit)
 0322        .Select(char.ToUpperInvariant)
 0323        .ToArray());
 324
 1325    private sealed record ArmtekSearchInput(string Pin, string? Brand);
 326
 327    private static PreOrderItem? MapToPreOrderItem(
 328        ArmtekSearchItem? offer,
 329        IReadOnlyDictionary<string, string> storeNameByKeyzak,
 330        decimal rateToRub,
 331        CurrencyCode sourceCurrencyCode,
 332        int? preferredSupplierId,
 333        CurrencyCode? preferredSupplierCurrencyCode,
 334        string providerDisplayName)
 335    {
 0336        if (offer == null)
 0337            return null;
 338
 0339        var supplierPrice = ParseDecimalInvariant(offer.Price);
 0340        if (supplierPrice == null)
 0341            return null;
 342
 0343        var roundedPrice = ArmtekPriceCurrencyConverter.ConvertToRub(supplierPrice.Value, rateToRub);
 0344        var deliveryDays = TryGetDeliveryDays(offer.DeliveryDate);
 0345        var keyzak = offer.Keyzak ?? string.Empty;
 0346        var partnerWarehouseCode = offer.PartnerWarehouseCode ?? string.Empty;
 0347        storeNameByKeyzak.TryGetValue(keyzak, out var keyzakName);
 0348        var direction = BuildDirectionLabel(keyzak, keyzakName, partnerWarehouseCode, providerDisplayName);
 0349        var title = BuildSupplierLabel(keyzak, keyzakName, partnerWarehouseCode, providerDisplayName);
 0350        var itemId = !string.IsNullOrWhiteSpace(offer.ArtId)
 0351            ? offer.ArtId
 0352            : $"{offer.Pin}|{offer.Brand}|{direction}";
 353
 0354        return new PreOrderItem
 0355        {
 0356            Caption = string.IsNullOrWhiteSpace(offer.Name) ? offer.Pin : offer.Name,
 0357            Rest = string.IsNullOrWhiteSpace(offer.Rvalue) ? "0" : offer.Rvalue,
 0358            DeliveryDays = deliveryDays?.ToString() ?? "-",
 0359            DeliveryDaysMin = deliveryDays ?? 0,
 0360            DeliveryDaysMax = deliveryDays ?? 0,
 0361            CurrencyCode = "RUB",
 0362            Producer = offer.Brand ?? string.Empty,
 0363            Code = offer.Pin ?? string.Empty,
 0364            Price = roundedPrice.ToString("0", System.Globalization.CultureInfo.InvariantCulture),
 0365            CostPrice = roundedPrice.ToString("0", System.Globalization.CultureInfo.InvariantCulture),
 0366            CostPriceFormatted = roundedPrice.ToString("0", System.Globalization.CultureInfo.InvariantCulture),
 0367            Direction = string.IsNullOrWhiteSpace(direction) ? providerDisplayName : direction,
 0368            ItemHash = itemId.GetHashCode().ToString("X"),
 0369            ItemId = itemId,
 0370            SiteUrl = "https://ws.armtek.ru/",
 0371            Title = title,
 0372            Description = BuildDescription(offer, deliveryDays, rateToRub),
 0373            PriceDescription = BuildPriceDescription(supplierPrice.Value, sourceCurrencyCode, rateToRub, roundedPrice),
 0374            SourcePrice = supplierPrice.Value,
 0375            SourceCurrencyCode = sourceCurrencyCode.ToString(),
 0376            PreferredSupplierId = preferredSupplierId,
 0377            PreferredSupplierCurrencyCode = preferredSupplierCurrencyCode?.ToString(),
 0378            ProviderData = new Dictionary<string, string?>
 0379            {
 0380                ["providerKey"] = providerDisplayName == "Армтек BY" ? "ArmtekBy" : "Armtek",
 0381                ["artId"] = offer.ArtId,
 0382                ["keyzak"] = offer.Keyzak,
 0383                ["warehouseName"] = keyzakName,
 0384                ["partnerWarehouseCode"] = offer.PartnerWarehouseCode,
 0385                ["deliveryAtUtc"] = offer.DeliveryDate
 0386            }
 0387        };
 388    }
 389
 0390    private async Task<decimal> GetRateToRubAsync(CurrencyCode currencyCode) => currencyCode switch
 0391    {
 0392        CurrencyCode.RUB => 1m,
 0393        CurrencyCode.USD => await CurrencyRateFetcher.GetUSDRateAsync(),
 0394        CurrencyCode.EUR => await CurrencyRateFetcher.GetEURRateAsync(),
 0395        CurrencyCode.BYN => await CurrencyRateFetcher.GetBYNRateAsync(),
 0396        _ => 1m
 0397    };
 398
 399    private static CurrencyCode ResolveCurrency(string? value, CurrencyCode fallback)
 400    {
 0401        return Enum.TryParse<CurrencyCode>(value, ignoreCase: true, out var currency)
 0402            ? currency
 0403            : fallback;
 404    }
 405
 406    private static int? TryGetDeliveryDays(string? deliveryDateRaw)
 407    {
 0408        if (string.IsNullOrWhiteSpace(deliveryDateRaw))
 0409            return null;
 410
 0411        if (!DateTime.TryParseExact(
 0412                deliveryDateRaw.Trim(),
 0413                "yyyyMMddHHmmss",
 0414                System.Globalization.CultureInfo.InvariantCulture,
 0415                System.Globalization.DateTimeStyles.None,
 0416                out var deliveryDate))
 0417            return null;
 418
 0419        var now = DateTime.Now;
 0420        var days = (int)Math.Ceiling((deliveryDate - now).TotalDays);
 0421        return Math.Max(0, days);
 422    }
 423
 424    private static decimal? ParseDecimalInvariant(string? value)
 425    {
 0426        if (string.IsNullOrWhiteSpace(value))
 0427            return null;
 428
 0429        var normalized = value.Replace(" ", string.Empty).Replace(",", ".");
 0430        return decimal.TryParse(
 0431            normalized,
 0432            System.Globalization.NumberStyles.Any,
 0433            System.Globalization.CultureInfo.InvariantCulture,
 0434            out var parsed)
 0435            ? parsed
 0436            : null;
 437    }
 438
 439    private static string BuildDescription(ArmtekSearchItem offer, int? deliveryDays, decimal rateToRub)
 440    {
 0441        var parts = new List<string>();
 0442        if (!string.IsNullOrWhiteSpace(offer.Pin)) parts.Add($"Артикул: {offer.Pin}");
 0443        if (!string.IsNullOrWhiteSpace(offer.Brand)) parts.Add($"Бренд: {offer.Brand}");
 0444        if (!string.IsNullOrWhiteSpace(offer.Keyzak)) parts.Add($"Склад: {offer.Keyzak}");
 0445        if (!string.IsNullOrWhiteSpace(offer.Rvalue)) parts.Add($"Наличие: {offer.Rvalue}");
 0446        if (!string.IsNullOrWhiteSpace(offer.Price)) parts.Add($"Цена API: {offer.Price} {offer.Currency}; курс: {rateTo
 0447        if (deliveryDays.HasValue) parts.Add($"Срок: {deliveryDays.Value} дн.");
 0448        if (!string.IsNullOrWhiteSpace(offer.Analog)) parts.Add($"Аналог: {offer.Analog}");
 0449        return string.Join("\n", parts);
 450    }
 451
 452    private static string BuildPriceDescription(decimal sourcePrice, CurrencyCode currencyCode,
 453        decimal rateToRub, decimal rubPrice) =>
 0454        currencyCode == CurrencyCode.RUB
 0455            ? $"Цена API: {sourcePrice:0.##} RUB"
 0456            : $"Цена до перевода: {sourcePrice:0.##} {currencyCode}; курс ЦБ РФ: {rateToRub:0.####}; в таблице: {rubPric
 457
 458    private static string BuildDirectionLabel(string keyzak, string? keyzakName, string partnerWarehouseCode, string pro
 459    {
 0460        if (!string.IsNullOrWhiteSpace(keyzakName))
 0461            return $"{keyzakName} ({keyzak})";
 0462        if (!string.IsNullOrWhiteSpace(keyzak))
 0463            return keyzak;
 0464        if (!string.IsNullOrWhiteSpace(partnerWarehouseCode))
 0465            return $"Партнёрский склад ({partnerWarehouseCode})";
 0466        return providerDisplayName;
 467    }
 468
 469    private static string BuildSupplierLabel(string keyzak, string? keyzakName, string partnerWarehouseCode, string prov
 470    {
 0471        if (!string.IsNullOrWhiteSpace(keyzakName))
 0472            return $"{providerDisplayName}: {keyzakName}";
 0473        if (!string.IsNullOrWhiteSpace(keyzak))
 0474            return $"{providerDisplayName}: {keyzak}";
 0475        if (!string.IsNullOrWhiteSpace(partnerWarehouseCode))
 0476            return $"{providerDisplayName} (партнёр): {partnerWarehouseCode}";
 0477        return providerDisplayName;
 478    }
 479}

Methods/Properties

.ctor(Services.ApiServices.ArmtekApi.ArmtekDataManager,Services.ApiSupplierSettingsServices.ApiSupplierSettingsService,Services.PriceСlculationServices.ICurrencyRateFetcher,Microsoft.Extensions.Caching.Memory.IMemoryCache,Services.ApiSupplierSettingsServices.ArmtekDeliveryAddressMappingService,Services.ApiServices.ArmtekApi.ArmtekStoreDirectory)
get_ProviderDisplayName()
get_ProviderCacheKey()
GetSettingsAsync(System.Threading.CancellationToken)
Warmup()
Search()
ResolveKunnrRg(Services.ApiServices.ArmtekApi.Models.Response.ArmtekUserInfoResponse)
AddDeliveryMappingMetadata(Services.ApiServices.TradesoftApi.Models.Response.PreOrderItem,Services.ApiSupplierSettingsServices.ArmtekDeliveryAddressSelection,Services.ApiServices.ArmtekApi.Models.Response.ArmtekUserInfoResponse,System.String)
BuildArmtekErrorMessage(System.Collections.Generic.IEnumerable`1<Services.ApiServices.ArmtekApi.Models.Envelope.ArmtekMessage>)
ResolveStoreNamesByKeyzak(System.String)
GetUserInfoAsync()
ResolveAssortmentSearchInputAsync()
GetSearchResponseAsync()
ResolveVkorgCachePart(System.String)
NormalizeForMatch(System.String)
get_Pin()
MapToPreOrderItem(Services.ApiServices.ArmtekApi.Models.Response.ArmtekSearchItem,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>,System.Decimal,OzonDomains.CurrencyCode,System.Nullable`1<System.Int32>,System.Nullable`1<OzonDomains.CurrencyCode>,System.String)
GetRateToRubAsync()
ResolveCurrency(System.String,OzonDomains.CurrencyCode)
TryGetDeliveryDays(System.String)
ParseDecimalInvariant(System.String)
BuildDescription(Services.ApiServices.ArmtekApi.Models.Response.ArmtekSearchItem,System.Nullable`1<System.Int32>,System.Decimal)
BuildPriceDescription(System.Decimal,OzonDomains.CurrencyCode,System.Decimal,System.Decimal)
BuildDirectionLabel(System.String,System.String,System.String,System.String)
BuildSupplierLabel(System.String,System.String,System.String,System.String)