< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.MlAutoPriceController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/MlAutoPriceController.cs
Line coverage
82%
Covered lines: 102
Uncovered lines: 21
Coverable lines: 123
Total lines: 185
Line coverage: 82.9%
Branch coverage
56%
Covered branches: 55
Total branches: 98
Branch coverage: 56.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Search()66.66%373690.62%
GetWarehousesAsync()50%22100%
<GetWarehousesAsync()50%2280%
GetSettingsUrl(...)0%620%
ResolveDeliveryDestinationAsync()0%620%
<ResolveDeliveryDestinationAsync()0%620%
GetValue(...)0%2040%
Map(...)54.16%2424100%
BuildDescription(...)66.66%66100%
CleanText(...)50%22100%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using System.Text.RegularExpressions;
 3using Microsoft.AspNetCore.Authorization;
 4using Microsoft.AspNetCore.Mvc;
 5using Microsoft.EntityFrameworkCore;
 6using Microsoft.Extensions.Caching.Memory;
 7using Newtonsoft.Json.Linq;
 8using OzonDomains;
 9using Services.ApiServices.MlAutoApi;
 10using Services.ApiServices.TradesoftApi.Models.Response;
 11using Services.ApiSupplierSettingsServices;
 12using Services.PriceСlculationServices;
 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 sealed class MlAutoPriceController : Controller
 23{
 24    private readonly MlAutoDataManager _mlAuto;
 25    private readonly ApiSupplierSettingsService _settings;
 26    private readonly ICurrencyRateFetcher _rates;
 27    private readonly IMemoryCache _cache;
 28    private readonly MlAutoDeliveryAddressMappingService? _deliveryMappings;
 29
 730    public MlAutoPriceController(MlAutoDataManager mlAuto, ApiSupplierSettingsService settings, ICurrencyRateFetcher rat
 731        MlAutoDeliveryAddressMappingService? deliveryMappings = null)
 32    {
 733        _mlAuto = mlAuto;
 734        _settings = settings;
 735        _rates = rates;
 736        _cache = cache;
 737        _deliveryMappings = deliveryMappings;
 738    }
 39
 40    [HttpGet]
 41    public async Task<IActionResult> Search(string code, string? brand, int? orderId = null, string? zasylWarehouseName 
 42    {
 743        if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(brand))
 644            return Ok(Array.Empty<PreOrderItem>());
 45        try
 46        {
 147            var settings = await _settings.GetMlAutoAsync(HttpContext.RequestAborted);
 148            if (!settings.IsQuotationEnabled) return Ok(Array.Empty<PreOrderItem>());
 149            var warehouse = orderId.HasValue && _deliveryMappings is not null
 150                ? await _deliveryMappings.ResolveWarehouseAsync(orderId.Value, zasylWarehouseName, HttpContext.RequestAb
 151            var deliveryMapping = orderId.HasValue && _deliveryMappings is not null
 152                ? await _deliveryMappings.ResolveAsync(orderId.Value, zasylWarehouseName, HttpContext.RequestAborted) : 
 153            var apiDeliveryDestination = deliveryMapping is null
 154                ? null
 155                : await ResolveDeliveryDestinationAsync(deliveryMapping.DeliveryAddressId, HttpContext.RequestAborted);
 156            var deliveryDestination = settings.DeliveryAddress ?? apiDeliveryDestination;
 157            var warehouses = await GetWarehousesAsync(HttpContext.RequestAborted);
 158            var response = await _mlAuto.PostAsync("/Search/", new Dictionary<string, string?>
 159            {
 160                ["ARTICLE"] = code.Trim(), ["BRAND"] = brand.Trim(), ["SEARCH_TYPE"] = "0"
 161            }, HttpContext.RequestAborted);
 162            var rate = await _rates.GetBYNRateAsync();
 163            var items = (response as JArray ?? new JArray()).OfType<JObject>()
 264                .Where(x => string.Equals(x.Value<string>("ANALOG"), "NO", StringComparison.OrdinalIgnoreCase))
 165                .Select(x => Map(x, warehouses, settings.SupplierId, settings.Supplier?.CurrencyCode, rate, deliveryMapp
 166                    deliveryMapping is null ? null : Url.Action("MlAuto", "ApiSupplierSettings", new { area = "Studio2" 
 267                .Where(x => x != null).Cast<PreOrderItem>().ToList();
 168            var warning = orderId.HasValue && _deliveryMappings is not null && deliveryMapping is null
 169                ? warehouse.IsUnknownZasyl
 170                    ? $"Засыл «{warehouse.WarehouseName}» не найден среди складов Studio2. Проценка ML-Auto BY выполнена
 171                    : "Адрес доставки ML-Auto BY не связан с эффективным складом заказа. Проценка выполнена без адреса д
 172                : null;
 173            var settingsUrl = orderId.HasValue && _deliveryMappings is not null && deliveryMapping is null
 174                ? GetSettingsUrl(warehouse.WarehouseId)
 175                : null;
 176            return Ok(warning is null ? items : new { items, warning, settingsUrl });
 77        }
 078        catch (MlAutoApiException ex) { return Ok(new { items = Array.Empty<PreOrderItem>(), error = ex.Message }); }
 079        catch (Exception ex) when (ex is HttpRequestException or TimeoutException or TaskCanceledException)
 80        {
 081            return Ok(new { items = Array.Empty<PreOrderItem>(), error = "Сервис ML-Auto BY временно недоступен." });
 82        }
 783    }
 84
 85    private async Task<IReadOnlyDictionary<string, string>> GetWarehousesAsync(CancellationToken cancellationToken)
 86    {
 187        return await _cache.GetOrCreateAsync("api-supplier:ml-auto:warehouses", async entry =>
 188        {
 189            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
 190            try
 191            {
 192                var response = await _mlAuto.PostAsync("/ImportersSearch/", null, cancellationToken);
 193                return (response as JArray ?? new JArray()).OfType<JObject>()
 194                    .Select(x => new { Code = x.Value<string>("STORAGE_CODE") ?? x.Value<string>("CODE") ?? x.Value<stri
 195                    .Where(x => !string.IsNullOrWhiteSpace(x.Code) && !string.IsNullOrWhiteSpace(x.Name))
 496                    .GroupBy(x => x.Code!, StringComparer.OrdinalIgnoreCase).ToDictionary(x => x.Key, x => x.First().Nam
 197            }
 098            catch (MlAutoApiException) { return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); }
 299        }) ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 1100    }
 101
 102    private string? GetSettingsUrl(int? warehouseId) =>
 0103        Url.Action("MlAuto", "ApiSupplierSettings", new { area = "Studio2" }) +
 0104        (warehouseId.HasValue ? $"#mlauto-mapping-{warehouseId.Value}" : string.Empty);
 105
 106    private async Task<string?> ResolveDeliveryDestinationAsync(string addressId, CancellationToken cancellationToken)
 107    {
 0108        var addresses = await _cache.GetOrCreateAsync("api-supplier:ml-auto:delivery-addresses", async entry =>
 0109        {
 0110            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
 0111            var response = await _mlAuto.GetDeliveryAddressesAsync(cancellationToken);
 0112            return (response is JContainer container ? container.DescendantsAndSelf() : [response]).OfType<JObject>()
 0113                .Select(x => new { Id = GetValue(x, "ADDRESS_ID", "ID"), Name = GetValue(x, "ADDRESS_NAME", "NAME", "ADD
 0114                .Where(x => !string.IsNullOrWhiteSpace(x.Id) && !string.IsNullOrWhiteSpace(x.Name))
 0115                .GroupBy(x => x.Id!, StringComparer.OrdinalIgnoreCase)
 0116                .ToDictionary(x => x.Key, x => x.First().Name!, StringComparer.OrdinalIgnoreCase);
 0117        });
 0118        return addresses?.GetValueOrDefault(addressId);
 0119    }
 120
 0121    private static string? GetValue(JObject source, params string[] names) => names
 0122        .Select(name => source.GetValue(name, StringComparison.OrdinalIgnoreCase)?.ToString()?.Trim())
 0123        .FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
 124
 125    private static PreOrderItem? Map(JObject offer, IReadOnlyDictionary<string, string> warehouses, int? supplierId, Cur
 126        MlAutoDeliveryAddressSelection? deliveryMapping, string? deliveryDestination, string? settingsUrl)
 127    {
 1128        if (!decimal.TryParse(offer.Value<string>("PRICE")?.Replace(',', '.'), NumberStyles.Number, CultureInfo.Invarian
 1129        var quantity = MlAutoValueParser.ParseQuantity(offer.Value<string>("QUANTITY")) ?? 0m;
 1130        var min = int.TryParse(offer.Value<string>("MIN"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var p
 1131        var storageCode = offer.Value<string>("STORAGE_CODE") ?? string.Empty;
 1132        var chance = CleanText(offer.Value<string>("CHANCE"));
 133        // В официальном ImportersSearch нет поля локации: ID — код склада, DESCR — условия поставки.
 134        // Для текущего белорусского аккаунта предложения на витрине ML-Auto отображаются как «Минск»;
 135        // код склада дописываем через пробел, иначе предложения разных складов неразличимы в колонке «Направление».
 1136        var direction = string.IsNullOrWhiteSpace(storageCode) ? "Минск" : $"Минск {storageCode}";
 1137        var deliveryConditions = CleanText(warehouses.GetValueOrDefault(storageCode));
 1138        var delivery = DateTime.TryParseExact(offer.Value<string>("DATE"), "yyyyMMddHHmmss", CultureInfo.InvariantCultur
 1139            ? Math.Max(0, (date.Date - DateTime.Today).Days) : 0;
 1140        var rub = Math.Round(sourcePrice * rate, 0, MidpointRounding.AwayFromZero);
 1141        var itemId = string.Join("|", offer.Value<string>("PIN"), storageCode, sourcePrice.ToString(CultureInfo.Invarian
 1142        return new PreOrderItem
 1143        {
 1144            Caption = offer.Value<string>("NAME") ?? offer.Value<string>("PIN"), Producer = offer.Value<string>("BRAND")
 1145            Rest = quantity.ToString("0.##", CultureInfo.InvariantCulture), MinQuantity = min, DeliveryDays = delivery.T
 1146            Price = rub.ToString("0", CultureInfo.InvariantCulture), CostPrice = rub.ToString("0", CultureInfo.Invariant
 1147            SourcePrice = sourcePrice, SourceCurrencyCode = "BYN", Direction = direction, ItemId = itemId, ItemHash = it
 1148            PreferredSupplierId = supplierId, PreferredSupplierCurrencyCode = supplierCurrency?.ToString(),
 1149            Description = BuildDescription(offer, quantity, storageCode, deliveryConditions),
 1150            ProviderData = new Dictionary<string, string?>
 1151            {
 1152                ["providerKey"] = "MlAuto",
 1153                ["storageCode"] = storageCode,
 1154                ["deliveryAtUtc"] = offer.Value<string>("DATE"),
 1155                ["deliveryConditions"] = deliveryConditions,
 1156                ["supplyChance"] = chance,
 1157                ["deliveryMappingLinked"] = deliveryMapping is null ? "false" : "true",
 1158                ["deliveryMappingAddressId"] = deliveryMapping?.DeliveryAddressId,
 1159                ["deliveryMappingAddress"] = deliveryDestination ?? (deliveryMapping is null ? null : $"Адрес ML-Auto BY
 1160                ["deliveryDestination"] = deliveryDestination,
 1161                ["deliveryMappingSettingsUrl"] = settingsUrl
 1162            },
 1163            PriceDescription = $"Цена до перевода: {sourcePrice:0.##} BYN; курс ЦБ РФ: {rate:0.####}; в таблице: {rub:0}
 1164        };
 165    }
 166
 167    private static string BuildDescription(JObject offer, decimal quantity, string storageCode, string? deliveryConditio
 168    {
 1169        return string.Join("\n", new[]
 1170        {
 1171            $"Артикул: {offer.Value<string>("PIN")}",
 1172            $"Бренд: {offer.Value<string>("BRAND")}",
 1173            "Локация: Минск",
 1174            $"Код склада API: {storageCode}",
 1175            $"Наличие: {quantity:0.##}",
 1176            $"Вероятность: {offer.Value<string>("CHANCE") ?? "—"}%",
 1177            $"Возврат: {offer.Value<string>("RETURN_PERIOD") ?? "—"} дн.",
 1178            string.IsNullOrWhiteSpace(deliveryConditions) ? null : $"Условия поставки: {deliveryConditions}"
 9179        }.Where(x => !string.IsNullOrWhiteSpace(x))!);
 180    }
 181
 2182    private static string? CleanText(string? value) => string.IsNullOrWhiteSpace(value)
 2183        ? null
 2184        : Regex.Replace(value, "<.*?>", string.Empty, RegexOptions.Singleline).Trim();
 185}