< Summary

Information
Class: OzonOrdersWeb.TransactionServices.MlAutoCartQueueItem
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/MlAutoCartService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1
Coverable lines: 1
Total lines: 368
Line coverage: 0%
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_OrderId()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/MlAutoCartService.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Net;
 3using System.Text.Json;
 4using Microsoft.EntityFrameworkCore;
 5using Microsoft.Extensions.Logging;
 6using Microsoft.Extensions.Logging.Abstractions;
 7using Newtonsoft.Json;
 8using Newtonsoft.Json.Linq;
 9using OzonDomains.Models;
 10using OzonOrdersWeb.TransactionServices;
 11using OzonRepositories.Context;
 12using Services.ApiServices.MlAutoApi;
 13using Services.DataServices;
 14using Services.ApiSupplierSettingsServices;
 15using Services.SupplierStatusServices;
 16
 17namespace OzonOrdersWeb.TransactionServices;
 18
 019public sealed record MlAutoCartQueueItem(int OrderId, string Pin, string Brand, string? StorageCode, string? WarehouseNa
 20public sealed record MlAutoCartResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages)
 21{
 22    public string ToHtmlBlock()
 23    {
 24        var rows = Messages.Take(20).Select(message => $"<li>{WebUtility.HtmlEncode(message)}</li>");
 25        var tail = Messages.Count > 20 ? $"<li>… и ещё {Messages.Count - 20}</li>" : string.Empty;
 26        var verificationNotice = Added > 0
 27            ? "<div class='alert alert-warning py-2 px-3 mt-2 mb-0 small'><i class='fas fa-triangle-exclamation me-1'></
 28            : string.Empty;
 29
 30        return "<div class='alert alert-info border-primary-subtle mb-0'>" +
 31               "<div class='fw-semibold mb-1'><i class='fas fa-cart-shopping me-1 text-primary'></i>Корзина ML-Auto BY</
 32               $"<div class='small mb-1'>Добавлено: <b>{Added}</b>, уже было в корзине: <b>{Skipped}</b>, с ошибками: <b
 33               $"<ul class='mb-0 small'>{string.Join(string.Empty, rows)}{tail}</ul>" +
 34               "<div class='d-flex flex-wrap gap-2 mt-2'><a class='btn btn-outline-primary btn-sm' href='/SupplierCart?p
 35               verificationNotice + "</div>";
 36    }
 37}
 38
 39public sealed record MlAutoCartRequoteOffer(string StorageCode, string WarehouseName, decimal Price, decimal? AvailableQ
 40public sealed record MlAutoCartRequoteResult(string OriginalWarehouse, string DeliveryAddress, IReadOnlyList<MlAutoCartR
 41public sealed record MlAutoDirectCheckoutResult(bool IsSuccess, IReadOnlyList<string> Messages);
 42
 43public sealed class MlAutoCartService
 44{
 45    private const string Logo = "https://www.ml-auto.by/media/files/settings/inside-placeholder-logo-mobile.png";
 46    private readonly ApplicationDbContext _db; private readonly MlAutoDataManager _api; private readonly MlAutoDeliveryA
 47    private readonly IOrderSupplierStateService _supplierState;
 48    private readonly ILogger<MlAutoCartService> _logger;
 49    public MlAutoCartService(ApplicationDbContext db, MlAutoDataManager api, MlAutoDeliveryAddressMappingService mapping
 50        IOrderHistoryDataService history, IOrderSupplierStateService supplierState, ILogger<MlAutoCartService>? logger =
 51    {
 52        _db = db; _api = api; _mappings = mappings; _history = history; _supplierState = supplierState;
 53        _logger = logger ?? NullLogger<MlAutoCartService>.Instance;
 54    }
 55
 56    /// <summary>
 57    /// Отражает строку корзины в отслеживании статусов поставщика. Переходы внутри корзины
 58    /// (Ready/Processing/Failed) отдельно не пишем — их читает LocalCartStatusReader из самой строки.
 59    /// Сбой журнала не должен ломать работу с корзиной.
 60    /// </summary>
 61    private async Task TrackAsync(Func<Task> track)
 62    {
 63        try { await track(); }
 64        catch (Exception exception) { _logger.LogWarning(exception, "Не удалось обновить состояние заказа у поставщика M
 65    }
 66
 67    public Task<List<MlAutoCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.MlAutoCartItems.Include(x =>
 68    public Task<List<MlAutoOrder>> GetOrdersAsync(CancellationToken ct = default) => _db.MlAutoOrders.AsNoTracking().Inc
 69    public Task<int> CountActiveAsync(CancellationToken ct = default) => _db.MlAutoCartItems.CountAsync(x => MlAutoCartI
 70
 71    public async Task<MlAutoCartResult> QueueAsync(IReadOnlyCollection<MlAutoCartQueueItem> queue, string user, Cancella
 72    {
 73        var added = 0; var skipped = 0; var failed = 0; var messages = new List<string>();
 74        foreach (var item in queue)
 75        {
 76            if (item.OrderId <= 0 || string.IsNullOrWhiteSpace(item.Pin) || string.IsNullOrWhiteSpace(item.Brand) || ite
 77            if (item.MinimumQuantity > 0 && item.Quantity < item.MinimumQuantity) { failed++; messages.Add($"{item.Pin}:
 78            if (await _db.MlAutoCartItems.AnyAsync(x => x.StudioOrderId == item.OrderId && MlAutoCartItemStatus.Active.C
 79            var destination = await _mappings.ResolveAsync(item.OrderId, item.ZasylWarehouseName, ct);
 80            var cart = new MlAutoCartItem { StudioOrderId = item.OrderId, ActiveStudioOrderId = item.OrderId, Pin = item
 81            _db.MlAutoCartItems.Add(cart); await _db.SaveChangesAsync(ct); added++; messages.Add(item.Pin + ": добавлен 
 82            await TrackAsync(() => _supplierState.MarkInCartAsync(cart.StudioOrderId, ApiSupplierSettings.MlAutoProvider
 83                deliveryDaysMin: item.DeliveryDaysMin, deliveryDaysMax: item.DeliveryDaysMax, direction: item.Direction,
 84        }
 85        return new MlAutoCartResult(added, skipped, failed, messages);
 86    }
 87
 88    /// <summary>Проверяет и оформляет предложения без промежуточного сохранения в локальную корзину.</summary>
 89    public async Task<MlAutoDirectCheckoutResult> CheckoutDirectAsync(IReadOnlyCollection<MlAutoCartQueueItem> queue, st
 90    {
 91        var messages = new List<string>();
 92        if (queue.Count == 0) return new(false, ["Не выбраны позиции ML-Auto BY для оформления."]);
 93        if (queue.GroupBy(x => x.OrderId).Any(x => x.Key <= 0 || x.Count() > 1))
 94            return new(false, ["Для прямого оформления ML-Auto BY требуется ровно одно предложение на заказ."]);
 95
 96        var prepared = new List<MlAutoCartItem>();
 97        foreach (var source in queue)
 98        {
 99            if (string.IsNullOrWhiteSpace(source.Pin) || string.IsNullOrWhiteSpace(source.Brand) || source.Quantity <= 0
 100                (source.MinimumQuantity > 0 && source.Quantity < source.MinimumQuantity))
 101            {
 102                messages.Add($"{source.Pin}: некорректное количество или предложение ML-Auto BY.");
 103                continue;
 104            }
 105
 106            var destination = await _mappings.ResolveAsync(source.OrderId, source.ZasylWarehouseName, ct);
 107            if (destination is null)
 108            {
 109                messages.Add($"{source.Pin}: для склада заказа не настроен адрес доставки ML-Auto BY.");
 110                continue;
 111            }
 112
 113            var item = new MlAutoCartItem
 114            {
 115                StudioOrderId = source.OrderId, Pin = source.Pin.Trim(), Brand = source.Brand.Trim(),
 116                StorageCode = source.StorageCode, WarehouseName = source.WarehouseName, ProductName = source.ProductName
 117                DeliveryAddressId = destination.DeliveryAddressId, DeliveryAddressName = destination.DeliveryAddressId,
 118                Quantity = source.Quantity, SourcePrice = source.SourcePrice, CurrencyCode = source.CurrencyCode ?? "BYN
 119                DeliveryAtUtc = ParseDate(source.DeliveryAtUtc), CreatedBy = user
 120            };
 121            try
 122            {
 123                await RepriceAsync(item, ct);
 124                var test = await _api.CreateOrderTestDetailedAsync(BuildRequest(item.DeliveryAddressId, [item], true), c
 125                item.TestResultJson = test.Response.ToString(Formatting.None);
 126                var confirmed = EnsureTestAccepted(test.Response, test.Message);
 127                item.SourcePrice = MlAutoValueParser.ParseDecimal(confirmed.Value<string>("PRICE")) ?? item.SourcePrice;
 128                prepared.Add(item);
 129            }
 130            catch (MlAutoApiException ex) { messages.Add($"{source.Pin}: {ex.Message}"); }
 131        }
 132
 133        if (messages.Count > 0) return new(false, messages);
 134        // Прямое оформление минует локальную корзину: срок/направление берём обратно из queue
 135        // по OrderId (проверка выше гарантирует не более одного предложения на заказ).
 136        var sourceByOrderId = queue.ToDictionary(x => x.OrderId);
 137        foreach (var group in prepared.GroupBy(x => x.DeliveryAddressId!))
 138        {
 139            try
 140            {
 141                var request = BuildRequest(group.Key, group.ToList(), true);
 142                var response = await _api.CreateOrderAsync(request, ct);
 143                var number = Find(response, "ORDER-NUMBER");
 144                if (string.IsNullOrWhiteSpace(number)) throw new MlAutoApiException("ML-Auto BY не создал заказ: в ответ
 145                var details = await _api.GetOrderByNumberAsync(number, ct);
 146                var order = new MlAutoOrder
 147                {
 148                    OrderNumber = number, DeliveryAddressId = group.Key, DeliveryAddressName = group.First().DeliveryAdd
 149                    IsCashless = true, CreatedBy = user, Status = Find(details, "STATUS") ?? "Created",
 150                    RequestJson = System.Text.Json.JsonSerializer.Serialize(request), ResponseJson = details.ToString(Fo
 151                };
 152                foreach (var item in group)
 153                {
 154                    order.Items.Add(new MlAutoOrderItem { StudioOrderId = item.StudioOrderId, Pin = item.Pin, Brand = it
 155                    await HistoryAsync(item, user, true, null, ApiSupplierBasketCartStatus.Ordered, number, ct);
 156                    // При прямом оформлении нет строки локальной корзины, но состояние заказа
 157                    // у поставщика должно появиться немедленно, до первого Hangfire-опроса.
 158                    sourceByOrderId.TryGetValue(item.StudioOrderId, out var source);
 159                    await TrackAsync(() => _supplierState.MarkOrderedAsync(
 160                        item.StudioOrderId, ApiSupplierSettings.MlAutoProviderKey, number, user: user,
 161                        deliveryDaysMin: source?.DeliveryDaysMin, deliveryDaysMax: source?.DeliveryDaysMax, direction: s
 162                }
 163                _db.MlAutoOrders.Add(order);
 164                await _db.SaveChangesAsync(ct);
 165                messages.Add($"Создан заказ ML-Auto BY №{number}.");
 166            }
 167            catch (MlAutoApiException ex) { messages.Add(ex.Message); return new(false, messages); }
 168        }
 169        return new(true, messages);
 170    }
 171
 172    public async Task<MlAutoCartResult> TestAsync(IReadOnlyCollection<long>? ids, string user, CancellationToken ct = de
 173    {
 174        var items = await SelectedAsync(ids, ct); if (items.Count == 0) return new(0, 0, 0, ["Корзина ML-Auto BY пуста."
 175        var result = new List<string>(); var ok = 0; var failed = 0;
 176        foreach (var item in items)
 177        {
 178            if (string.IsNullOrWhiteSpace(item.DeliveryAddressId)) { item.Status = MlAutoCartItemStatus.Failed; item.Las
 179            try
 180            {
 181                var reprice = await RepriceAsync(item, ct);
 182                var test = await _api.CreateOrderTestDetailedAsync(BuildRequest(item.DeliveryAddressId, [item], true), c
 183                item.TestResultJson = test.Response.ToString(Formatting.None);
 184                var confirmed = EnsureTestAccepted(test.Response, test.Message);
 185                var confirmedPrice = MlAutoValueParser.ParseDecimal(confirmed.Value<string>("PRICE"));
 186                if (confirmedPrice.HasValue) item.SourcePrice = confirmedPrice;
 187                var providerResult = confirmed.Value<string>("MSG");
 188                item.TestedAtUtc = DateTime.UtcNow; item.Status = MlAutoCartItemStatus.Ready; item.LastError = null; ok+
 189                result.Add($"{item.Pin}: принято ML-Auto BY; склад {item.StorageCode ?? "—"}, цена поставщика {item.Sour
 190                await HistoryAsync(item, user, true, null, ApiSupplierBasketCartStatus.Ready, null, ct);
 191            }
 192            catch (MlAutoApiException ex) { item.Status = MlAutoCartItemStatus.Failed; item.LastError = ex.Message; fail
 193            item.UpdatedAtUtc = DateTime.UtcNow;
 194        }
 195        await _db.SaveChangesAsync(ct); return new(ok, 0, failed, result);
 196    }
 197
 198    public async Task<MlAutoCartResult> CheckoutAsync(IReadOnlyCollection<long>? ids, bool cashless, string user, Cancel
 199    {
 200        var items = await SelectedAsync(ids, ct); if (items.Count == 0) return new(0, 0, 0, ["Корзина ML-Auto BY пуста."
 201        if (items.Any(x => x.Status != MlAutoCartItemStatus.Ready || !x.TestedAtUtc.HasValue || x.TestedAtUtc.Value < Da
 202        var messages = new List<string>(); var ok = 0; var failed = 0;
 203        foreach (var group in items.GroupBy(x => x.DeliveryAddressId))
 204        {
 205            if (string.IsNullOrWhiteSpace(group.Key)) { failed += group.Count(); messages.Add("Не выбран адрес доставки 
 206            foreach (var item in group) item.Status = MlAutoCartItemStatus.Processing;
 207            await _db.SaveChangesAsync(ct);
 208            try
 209            {
 210                var request = BuildRequest(group.Key, group.ToList(), cashless); var response = await _api.CreateOrderAs
 211                if (string.IsNullOrWhiteSpace(number)) throw new MlAutoApiException("ML-Auto не создал заказ: в ответе о
 212                var details = await _api.GetOrderByNumberAsync(number, ct);
 213                var order = new MlAutoOrder { OrderNumber = number, DeliveryAddressId = group.Key!, DeliveryAddressName 
 214                foreach (var item in group) { item.Status = MlAutoCartItemStatus.Ordered; item.ActiveStudioOrderId = nul
 215                _db.MlAutoOrders.Add(order); await _db.SaveChangesAsync(ct); ok += group.Count(); messages.Add("Создан з
 216            }
 217            catch (MlAutoApiException ex) { foreach (var item in group) { item.Status = MlAutoCartItemStatus.Failed; ite
 218        }
 219        return new(ok, 0, failed, messages);
 220    }
 221
 222    public async Task<bool> UpdateAddressAsync(long id, string addressId, string? addressName, CancellationToken ct = de
 223
 224    /// <summary>Возвращает актуальные варианты ML-Auto для сохранённого адреса без привязки к устаревшему складу.</summ
 225    public async Task<MlAutoCartRequoteResult> RequoteAsync(long cartItemId, CancellationToken ct = default)
 226    {
 227        var item = await _db.MlAutoCartItems.AsNoTracking().FirstOrDefaultAsync(x => x.Id == cartItemId && MlAutoCartIte
 228            ?? throw new InvalidOperationException("Позиция корзины не найдена или уже оформляется.");
 229        if (string.IsNullOrWhiteSpace(item.DeliveryAddressId))
 230            throw new InvalidOperationException("Сначала выберите адрес доставки ML-Auto для этой позиции.");
 231
 232        var response = await _api.PostAsync("/Search/", new Dictionary<string, string?> { ["ARTICLE"] = item.Pin, ["BRAN
 233        var offers = (response as JArray ?? new JArray()).OfType<JObject>()
 234            .Where(x => string.Equals(x.Value<string>("PIN"), item.Pin, StringComparison.OrdinalIgnoreCase) && string.Eq
 235            .Select(x => new MlAutoCartRequoteOffer(
 236                x.Value<string>("STORAGE_CODE") ?? string.Empty,
 237                x.Value<string>("STORAGE_NAME") ?? x.Value<string>("STORAGE") ?? "Склад ML-Auto",
 238                MlAutoValueParser.ParseDecimal(x.Value<string>("PRICE")) ?? -1m,
 239                MlAutoValueParser.ParseQuantity(x.Value<string>("QUANTITY")),
 240                MlAutoValueParser.ParseDate(x.Value<string>("DATE"))))
 241            .Where(x => x.Price >= 0 && x.AvailableQuantity.GetValueOrDefault() >= item.Quantity && !string.IsNullOrWhit
 242            .OrderBy(x => x.Price).ThenBy(x => x.DeliveryAtUtc ?? DateTime.MaxValue).Take(8).ToArray();
 243        return new MlAutoCartRequoteResult(
 244            string.IsNullOrWhiteSpace(item.WarehouseName) ? item.StorageCode ?? "—" : $"{item.WarehouseName} ({item.Stor
 245            item.DeliveryAddressName ?? item.DeliveryAddressId,
 246            offers);
 247    }
 248
 249    /// <summary>Заменяет строку корзины выбранным свежим предложением и требует повторную проверку.</summary>
 250    public async Task<bool> ReplaceWithRequotedOfferAsync(long cartItemId, string storageCode, CancellationToken ct = de
 251    {
 252        var item = await _db.MlAutoCartItems.FirstOrDefaultAsync(x => x.Id == cartItemId && MlAutoCartItemStatus.Active.
 253        if (item is null || item.Status == MlAutoCartItemStatus.Processing) return false;
 254        var offer = (await RequoteAsync(cartItemId, ct)).Offers.FirstOrDefault(x => string.Equals(x.StorageCode, storage
 255        if (offer is null) throw new InvalidOperationException("Выбранное предложение уже не актуально. Выполните процен
 256        item.StorageCode = offer.StorageCode;
 257        item.WarehouseName = offer.WarehouseName;
 258        item.SourcePrice = offer.Price;
 259        item.DeliveryAtUtc = offer.DeliveryAtUtc;
 260        item.Status = MlAutoCartItemStatus.Pending;
 261        item.TestedAtUtc = null;
 262        item.TestResultJson = null;
 263        item.LastError = "Выбрано актуальное предложение ML-Auto. Выполните проверку перед оформлением.";
 264        item.UpdatedAtUtc = DateTime.UtcNow;
 265        await _db.SaveChangesAsync(ct);
 266        return true;
 267    }
 268
 269    public async Task<MlAutoCartResult> ImportOrderAsync(string orderNumber, string user, CancellationToken ct = default
 270    {
 271        if (await _db.MlAutoOrders.AnyAsync(x => x.OrderNumber == orderNumber, ct))
 272            return new(0, 1, 0, [$"Заказ ML-Auto №{orderNumber} уже импортирован."]);
 273        var response = await _api.GetOrderByNumberAsync(orderNumber, ct);
 274        var sourceItems = FindToken(response, "ITEMS") as JArray ?? new JArray();
 275        if (sourceItems.Count == 0) return new(0, 0, 1, [$"ML-Auto не вернул позиции заказа №{orderNumber}."]);
 276        var order = new MlAutoOrder { OrderNumber = orderNumber, DeliveryAddressId = string.Empty, Status = Find(respons
 277        var imported = 0;
 278        foreach (var source in sourceItems.OfType<JObject>())
 279        {
 280            var pin = source.Value<string>("PIN") ?? string.Empty;
 281            var brand = source.Value<string>("BRAND") ?? string.Empty;
 282            var cart = await _db.MlAutoCartItems.FirstOrDefaultAsync(x => MlAutoCartItemStatus.Active.Contains(x.Status)
 283            if (cart is null) continue;
 284            cart.Status = MlAutoCartItemStatus.Ordered; cart.ActiveStudioOrderId = null; cart.LastError = null;
 285            order.DeliveryAddressId = string.IsNullOrWhiteSpace(order.DeliveryAddressId) ? cart.DeliveryAddressId ?? str
 286            order.DeliveryAddressName ??= cart.DeliveryAddressName;
 287            order.Items.Add(new MlAutoOrderItem { MlAutoCartItemId = cart.Id, StudioOrderId = cart.StudioOrderId, Pin = 
 288            await HistoryAsync(cart, user, true, null, ApiSupplierBasketCartStatus.Ordered, orderNumber, ct);
 289            await TrackAsync(() => _supplierState.MarkOrderedAsync(cart.StudioOrderId, ApiSupplierSettings.MlAutoProvide
 290            imported++;
 291        }
 292        if (imported == 0) return new(0, 0, 1, [$"В корзине нет активных позиций, совпадающих с заказом ML-Auto №{orderN
 293        _db.MlAutoOrders.Add(order); await _db.SaveChangesAsync(ct);
 294        return new(imported, 0, 0, [$"Заказ ML-Auto №{orderNumber} импортирован: позиций {imported}."]);
 295    }
 296    public async Task<int> RemoveAsync(IReadOnlyCollection<long>? ids, CancellationToken ct = default) { var items = awa
 297    private Task<List<MlAutoCartItem>> SelectedAsync(IReadOnlyCollection<long>? ids, CancellationToken ct) { var q = _db
 298    /// <summary>ML-Auto принимает массив позиций как PHP form-поля <c>ITEMS[1][PIN]</c>, а не JSON в поле ITEMS.</summa
 299    private static Dictionary<string, string?> BuildRequest(string address, IReadOnlyCollection<MlAutoCartItem> items, b
 300    {
 301        var request = new Dictionary<string, string?>
 302        {
 303            ["ADDRESS_ID"] = address,
 304            ["ONE-TIME-DELIVERY"] = "1",
 305            ["BEZNAL"] = cashless ? "1" : "0"
 306        };
 307        foreach (var (item, index) in items.Select((item, index) => (item, index + 1)))
 308        {
 309            var prefix = $"ITEMS[{index}]";
 310            request[$"{prefix}[PIN]"] = item.Pin;
 311            request[$"{prefix}[BRAND]"] = item.Brand;
 312            request[$"{prefix}[QUANTITY]"] = item.Quantity.ToString("0.###", CultureInfo.InvariantCulture);
 313            request[$"{prefix}[STORAGE-CODE]"] = item.StorageCode;
 314            request[$"{prefix}[MAX-PRICE]"] = item.SourcePrice?.ToString("0.####", CultureInfo.InvariantCulture);
 315            request[$"{prefix}[MAX-DATE]"] = item.DeliveryAtUtc?.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture
 316        }
 317        return request;
 318    }
 319    private async Task<string> RepriceAsync(MlAutoCartItem item, CancellationToken ct)
 320    {
 321        var response = await _api.PostAsync("/Search/", new Dictionary<string, string?> { ["ARTICLE"] = item.Pin, ["BRAN
 322        var offers = (response as JArray ?? new JArray()).OfType<JObject>()
 323            .Where(x => string.Equals(x.Value<string>("PIN"), item.Pin, StringComparison.OrdinalIgnoreCase) && string.Eq
 324            .Select(x => new { Token = x, Price = MlAutoValueParser.ParseDecimal(x.Value<string>("PRICE")), Quantity = M
 325            .Where(x => x.Price.HasValue && x.Quantity.GetValueOrDefault() >= item.Quantity && x.Minimum <= item.Quantit
 326            .OrderByDescending(x => string.Equals(x.Token.Value<string>("STORAGE_CODE"), item.StorageCode, StringCompari
 327            .ThenBy(x => x.Price).ThenBy(x => x.Date ?? DateTime.MaxValue).ToArray();
 328        var offer = offers.FirstOrDefault() ?? throw new MlAutoApiException("ML-Auto: актуальное предложение для проверк
 329        var changed = item.StorageCode != offer.Token.Value<string>("STORAGE_CODE") || item.SourcePrice != offer.Price |
 330        item.StorageCode = offer.Token.Value<string>("STORAGE_CODE");
 331        item.SourcePrice = offer.Price;
 332        item.DeliveryAtUtc = offer.Date;
 333        return changed ? " Данные корзины обновлены по актуальной проценке." : string.Empty;
 334    }
 335
 336    private static JObject EnsureTestAccepted(JToken response, string? providerMessage)
 337    {
 338        var items = FindToken(response, "ITEMS");
 339        if (items is JArray { Count: > 0 } array && array.OfType<JObject>().FirstOrDefault() is { } result) return resul
 340        if (items is JObject { HasValues: true } item) return item;
 341        throw new MlAutoApiException(string.IsNullOrWhiteSpace(providerMessage)
 342            ? "ML-Auto не подтвердил ни одной позиции. Проверьте минимальное количество, склад, цену и срок поставки."
 343            : $"ML-Auto: {providerMessage}. Поставщик не подтвердил позицию.");
 344    }
 345    private static string? Find(JToken token, string name) => (token is JContainer container ? container.DescendantsAndS
 346    private static JToken? FindToken(JToken token, string name) => (token is JContainer container ? container.Descendant
 347    private static DateTime? ParseDate(string? value) => DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTime
 348    private async Task HistoryAsync(MlAutoCartItem item, string user, bool success, string? error, string status, string
 349    {
 350        try
 351        {
 352            var entry = new ApiSupplierBasketHistoryEntry("MlAuto", "ML-Auto BY", Logo, success, error, null, item.Pin,
 353                item.ProductName ?? item.Brand, item.SourcePrice, item.CurrencyCode, item.Quantity, item.StudioOrder?.Sh
 354                CartStatus: status, SupplierOrderNumber: number, DeliveryAddressName: item.DeliveryAddressName,
 355                SupplierCartUrl: "/SupplierCart?provider=mlauto&supplierTab=basket");
 356            await _history.AddOrderHistory(new OrderHistory
 357            {
 358                OrderId = item.StudioOrderId, ColumnName = ApiSupplierBasketHistoryEntry.HistoryColumnName,
 359                ColumnDisplayName = "Корзина ML-Auto BY", NewValue = System.Text.Json.JsonSerializer.Serialize(entry, Ap
 360                ChangedAt = DateTime.Now, ChangedBy = user
 361            });
 362        }
 363        catch (Exception exception)
 364        {
 365            _logger.LogWarning(exception, "Не удалось записать историю корзины ML-Auto BY для заказа {OrderId}", item.St
 366        }
 367    }
 368}

Methods/Properties

get_OrderId()