< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.Grid.SupplierCarts.SupplierCartsProcessor
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/SupplierCarts/SupplierCartsProcessor.cs
Line coverage
39%
Covered lines: 327
Uncovered lines: 496
Coverable lines: 823
Total lines: 1324
Line coverage: 39.7%
Branch coverage
25%
Covered branches: 127
Total branches: 496
Branch coverage: 25.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_OzonShipResult()100%210%
get_ReportRows()100%11100%
get_HasQueues()50%44100%
Bind(...)75%44100%
Get(...)75%44100%
GetAll(...)50%44100%
GuardAsync()100%11100%
ArmtekConfirmedOrderIds()50%22100%
PreviewArmtekAsync()0%2040%
CheckoutDirectAsync()50%22100%
CheckoutDirectCoreAsync()21.96%667913227.84%
PlaceBasketsAsync()51.78%825679.76%
FilterBlockedBasketQueuesAsync()75%1212100%
AppendDirectCheckoutBlock(...)2.5%1490403.22%
GetMlAutoBasketAppendixAsync()50%7671.42%
ParseMlAutoBasketQueue(...)50%3233.33%
GetMlAutoRuBasketAppendixAsync()16.66%11650%
GetJapartsBasketAppendixAsync()16.66%921217.64%
GetBergBasketAppendixAsync()16.66%921217.64%
ParseBergBasketQueue(...)0%110100%
ReadBergQueueItem(...)0%2756520%
ParseJapartsBasketQueue(...)0%110100%
ReadJapartsQueueItem(...)0%702260%
ReadJapartsQueueString(...)0%7280%
ReadJapartsQueueDecimal(...)0%110100%
GetAvdBasketAppendixAsync()10%631019.04%
GetArmtekBasketAppendixAsync()16.66%17633.33%
ParseArmtekBasketQueue(...)50%2293.33%
GetForumAutoBasketAppendixAsync()16.66%17633.33%
ParseForumAutoBasketQueue(...)0%620%
GetFavoritPartsBasketAppendixAsync()25%8437.5%
GetFavoritPartsIstraBasketAppendixAsync()25%7442.85%
GetFavoritPartsRostovBasketAppendixAsync()25%7442.85%
GetProfitLigaBasketAppendixAsync()10%521025%
GetNikeiBasketAppendixAsync()16.66%15637.5%
GetMoskvorechieBasketAppendixAsync()25%7442.85%
GetMikadoBasketAppendixAsync()16.66%17633.33%
GetMotexBasketAppendixAsync()25%7442.85%
GetShateMBasketAppendixAsync()33.33%15637.5%
GetShateMDirectCheckoutAppendixAsync()75%10869.23%
ParseShateMOptions(...)50%6685%
TryParseShateMDeliveryDate(...)50%22100%
ParseShateMBasketQueue(...)50%2295%
GetZZapBasketAppendixAsync()25%8437.5%
ParseZZapBasketQueue(...)0%620%
ParseMotexBasketQueue(...)0%620%
ParseProfitLigaBasketQueue(...)0%620%
ParseNikeiBasketQueue(...)0%620%
ParseMoskvorechieBasketQueue(...)0%620%
ParseFavoritPartsBasketQueue(...)0%620%
ReadString(...)50%22100%
ReadNullableString(...)80%1010100%
ReadInt(...)50%22100%
ReadNullableInt(...)50%22100%
ReadDecimal(...)100%11100%
ReadNullableDecimal(...)50%44100%
ReadBool(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/SupplierCarts/SupplierCartsProcessor.cs

#LineLine coverage
 1// Порт логики корзин API-поставщиков из OrderToSupplierTransactionController (ExecutePreTransactionAsync,
 2// GetPostTransactionSuccessAppendixAsync и парсеры очередей) без зависимости от HttpContext: очереди приходят
 3// словарём «имя поля формы → JSON», результаты хранятся в экземпляре. Форматы очередей клиента прежние.
 4// Сами *CartService не меняются.
 5using System.Text.Json;
 6using System.Globalization;
 7using OzonDomains;
 8using OzonDomains.Models;
 9using OzonOrdersWeb.TransactionServices;
 10using MikadoCartService = OzonOrdersWeb.TransactionServices.MikadoCartService;
 11using MikadoQueueItem = OzonOrdersWeb.TransactionServices.MikadoQueueItem;
 12using ZZapCartService = OzonOrdersWeb.TransactionServices.ZZapCartService;
 13using ZZapCartQueueItem = OzonOrdersWeb.TransactionServices.ZZapCartQueueItem;
 14using ZZapCartResult = OzonOrdersWeb.TransactionServices.ZZapCartResult;
 15using Services.ApiServices.JapartsApi;
 16using Services.ApiServices.BergApi;
 17
 18namespace OzonOrdersWeb.WebServices.Transactions.Grid.SupplierCarts;
 19
 20/// <summary>
 21/// Корзины API-поставщиков для грида транзакций. Один экземпляр на проведение: <see cref="Bind"/> задаёт очереди,
 22/// пользователя и отмену; <see cref="CheckoutDirectAsync"/> — до смены статусов, <see cref="PlaceBasketsAsync"/> — посл
 23/// </summary>
 24public sealed class SupplierCartsProcessor
 25{
 26    private const string ArmtekDirectCheckoutResultKey = "ArmtekDirectCheckoutResult";
 27    private const string MlAutoDirectCheckoutResultKey = "MlAutoDirectCheckoutResult";
 28    private const string MlAutoRuDirectCheckoutResultKey = "MlAutoRuDirectCheckoutResult";
 29    private const string AvdDirectCheckoutResultKey = "AvdDirectCheckoutResult";
 30    private const string ForumAutoDirectCheckoutResultKey = "ForumAutoDirectCheckoutResult";
 31    private const string FavoritPartsDirectCheckoutResultKey = "FavoritPartsDirectCheckoutResult";
 32    private const string FavoritPartsIstraDirectCheckoutResultKey = "FavoritPartsIstraDirectCheckoutResult";
 33    private const string FavoritPartsRostovDirectCheckoutResultKey = "FavoritPartsRostovDirectCheckoutResult";
 34    private const string ProfitLigaDirectCheckoutResultKey = "ProfitLigaDirectCheckoutResult";
 35    private const string MoskvorechieIstraDirectCheckoutResultKey = "MoskvorechieIstraDirectCheckoutResult";
 36    private const string MotexDirectCheckoutResultKey = "MotexDirectCheckoutResult";
 37    private const string ZZapMoscowDirectCheckoutResultKey = "ZZapMoscowDirectCheckoutResult";
 38    private const string JapartsDirectCheckoutResultKey = "JapartsDirectCheckoutResult";
 39    private const string BergDirectCheckoutResultKey = "BergDirectCheckoutResult";
 40    private const string NikeiDirectCheckoutResultKey = "NikeiDirectCheckoutResult";
 41
 2242    private readonly Dictionary<string, object> _results = new(StringComparer.Ordinal);
 2243    private IReadOnlyDictionary<string, string> _queues = new Dictionary<string, string>();
 2244    private string _userName = "unknown";
 45    private CancellationToken _ct;
 46    private SupplierOrderConfirmation? _confirmation;
 47    private SupplierOrderGuardResult? _guardResult;
 48    private SupplierDispatch? _dispatch;
 2249    private string _requestId = Guid.NewGuid().ToString("N");
 2250    private Func<string, string, string?, Task> _stage = (_, _, _) => Task.CompletedTask;
 51
 52    /// <summary>Итог сборки отправления Ozon (для AI-комментария).</summary>
 053    public OrderToSupplierOzonShipResult? OzonShipResult { get; private set; }
 54
 55    /// <summary>Структурированные строки результата (Ozon, заказы поставщикам, корзины) для таблицы в модалке проведени
 2356    public List<TransactionReportRow> ReportRows { get; } = new();
 57
 58    /// <summary>Есть ли хоть одна непустая очередь.</summary>
 1459    public bool HasQueues => _queues.Values.Any(v => !string.IsNullOrWhiteSpace(v) && v.Trim() != "[]" && v.Trim() != "{
 60
 61    /// <param name="confirmation">Подтверждение пользователя для конфликтов «уже заказано у поставщика»; <c>null</c> — 
 62    public void Bind(IReadOnlyDictionary<string, string> queues, string userName, Func<string, string, string?, Task> st
 63        SupplierOrderConfirmation? confirmation = null, string? requestId = null)
 64    {
 2165        _confirmation = confirmation;
 3466        if (!string.IsNullOrWhiteSpace(requestId)) _requestId = requestId;
 2167        _queues = queues;
 2168        _userName = string.IsNullOrWhiteSpace(userName) ? "unknown" : userName;
 2169        _stage = stage;
 2170        _ct = ct;
 2171    }
 72
 10373    private string Get(string key) => _queues.TryGetValue(key, out var v) ? v ?? string.Empty : string.Empty;
 74
 1475    private List<string> GetAll(string key) => _queues.TryGetValue(key, out var v) && !string.IsNullOrWhiteSpace(v) ? ne
 76
 77    private readonly OrderToSupplierOzonShipService _orderToSupplierOzonShipService;
 78    private readonly AvdBasketPlacementService _avdBasketPlacementService;
 79    private readonly JapartsCartService _japartsCartService;
 80    private readonly BergCartService _bergCartService;
 81    private readonly ArmtekCartService _armtekCartService;
 82    private readonly MlAutoCartService _mlAutoCartService;
 83    private readonly MlAutoRuCartService _mlAutoRuCartService;
 84    private readonly ForumAutoCartService _forumAutoCartService;
 85    private readonly FavoritPartsCartService _favoritPartsCartService;
 86    private readonly FavoritPartsIstraCartService _favoritPartsIstraCartService;
 87    private readonly FavoritPartsRostovCartService _favoritPartsRostovCartService;
 88    private readonly MoskvorechieIstraCartService _moskvorechieIstraCartService;
 89    private readonly ProfitLigaCartService _profitLigaCartService;
 90    private readonly NikeiCartService _nikeiCartService;
 91    private readonly MotexCartService _motexCartService;
 92    private readonly ShateMCartService _shateMCartService;
 93    private readonly MikadoCartService _mikadoCartService;
 94    private readonly ZZapCartService _zzapCartService;
 95    private readonly ISupplierOrderGuard _guard;
 96
 2297    public SupplierCartsProcessor(
 2298        OrderToSupplierOzonShipService orderToSupplierOzonShipService,
 2299        AvdBasketPlacementService avdBasketPlacementService,
 22100        JapartsCartService japartsCartService,
 22101        BergCartService bergCartService,
 22102        ArmtekCartService armtekCartService, MlAutoCartService mlAutoCartService, MlAutoRuCartService mlAutoRuCartServic
 22103        ForumAutoCartService forumAutoCartService, FavoritPartsCartService favoritPartsCartService, FavoritPartsIstraCar
 22104        ISupplierOrderGuard guard)
 105    {
 22106        _guard = guard;
 22107        _orderToSupplierOzonShipService = orderToSupplierOzonShipService;
 22108        _avdBasketPlacementService = avdBasketPlacementService;
 22109        _japartsCartService = japartsCartService;
 22110        _bergCartService = bergCartService;
 22111        _armtekCartService = armtekCartService;
 22112        _mlAutoCartService = mlAutoCartService;
 22113        _mlAutoRuCartService = mlAutoRuCartService;
 22114        _forumAutoCartService = forumAutoCartService;
 22115        _favoritPartsCartService = favoritPartsCartService;
 22116        _favoritPartsIstraCartService = favoritPartsIstraCartService;
 22117        _favoritPartsRostovCartService = favoritPartsRostovCartService;
 22118        _moskvorechieIstraCartService = moskvorechieIstraCartService;
 22119        _profitLigaCartService = profitLigaCartService;
 22120        _nikeiCartService = nikeiCartService;
 22121        _motexCartService = motexCartService;
 22122        _shateMCartService = shateMCartService;
 22123        _mikadoCartService = mikadoCartService;
 22124        _zzapCartService = zzapCartService;
 22125    }
 126
 127    /// <summary>
 128    /// Защита от повторных заказов: до любых действий у поставщиков проверяет, что заказы Studio2 из очередей
 129    /// ещё не заказаны ни у этого, ни у другого поставщика. Без подтверждения пользователя конфликт блокирует проведени
 130    /// </summary>
 131    public async Task<SupplierOrderGuardResult> GuardAsync(IReadOnlyCollection<Order> orders)
 132    {
 133        var permitted = orders.Select(x => x.Id).ToHashSet();
 36134        var targets = SupplierQueueTargets.Extract(_queues).Where(x => permitted.Contains(x.OrderId)).ToList();
 18135        _guardResult = await _guard.EvaluateAsync(targets, _confirmation, _userName, _ct);
 18136        return _guardResult;
 18137    }
 138
 139    /// <summary>Заказы Armtek, по которым пользователь подтвердил повтор: для них защита самого сервиса не срабатывает.
 8140    private IReadOnlySet<int>? ArmtekConfirmedOrderIds() => _guardResult is null
 8141        ? null
 8142        : _guardResult.ConfirmedSameProviderOrderIds(OzonDomains.Models.ApiSupplierSettings.ArmtekProviderKey)
 8143            .Union(_guardResult.ConfirmedSameProviderOrderIds(OzonDomains.Models.ApiSupplierSettings.ArmtekByProviderKey
 8144            .ToHashSet();
 145
 146    /// <summary>Предпросмотр прямого заказа Armtek (кнопка «Запрос Armtek» на странице).</summary>
 147    public async Task<(bool Ok, string? Error, object? Previews)> PreviewArmtekAsync(string? armtekCheckoutQueueJson, Ca
 148    {
 0149        if (string.IsNullOrWhiteSpace(armtekCheckoutQueueJson))
 0150            return (false, "Не выбраны позиции Armtek для оформления.", null);
 151        try
 152        {
 0153            var queue = ParseArmtekBasketQueue(armtekCheckoutQueueJson);
 0154            var result = await _armtekCartService.BuildDirectOrderPreviewAsync(queue, ct);
 0155            return result.Errors.Count > 0 ? (false, string.Join(" ", result.Errors), null) : (true, null, result.Previe
 156        }
 0157        catch (JsonException)
 158        {
 0159            return (false, "Не удалось прочитать выбранные позиции Armtek.", null);
 160        }
 0161    }
 162
 163    /// <summary>
 164    /// Прямое оформление (mode=direct) до смены статусов. Возвращает текст ошибки или null.
 165    /// Перед обращением к поставщикам намерения записываются (Placing) отдельной транзакцией; после ответа —
 166    /// Placed/Failed. Если до этого не дошло (исключение, обрыв), намерения остаются Placing и блокируют повтор.
 167    /// </summary>
 168    public async Task<string?> CheckoutDirectAsync(IReadOnlyCollection<Order> orders)
 169    {
 170        var permitted = orders.Select(x => x.Id).ToHashSet();
 171        // Шате-М оформляется после записи транзакции (GetShateMDirectCheckoutAppendixAsync) и ведёт свои намерения там:
 172        // завершить его здесь значило бы пометить заказ Failed до того, как он вообще отправлен.
 10173        var targets = SupplierQueueTargets.Extract(_queues)
 10174            .Where(x => x.Intent == SupplierOrderAction.Order && permitted.Contains(x.OrderId)
 10175                        && !string.Equals(x.ProviderKey, OzonDomains.Models.ApiSupplierSettings.ShateMProviderKey, Strin
 10176            .ToList();
 10177        var start = await _guard.BeginDispatchAsync(_requestId, "Grid", targets, _guardResult, _userName, _ct);
 10178        if (!start.CanProceed) return start.Message;
 179
 10180        _dispatch = start.Dispatch;
 10181        var error = await CheckoutDirectCoreAsync(orders);
 10182        await _guard.CompleteDispatchAsync(start.Dispatch!);
 10183        return error;
 10184    }
 185
 186    private async Task<string?> CheckoutDirectCoreAsync(IReadOnlyCollection<Order> orders)
 187    {
 10188        var raw = Get("ArmtekCheckoutQueueJson");
 12189        if (string.IsNullOrWhiteSpace(raw)) raw = "[]";
 190        List<ArmtekCartQueueItem> queue;
 20191        try { queue = ParseArmtekBasketQueue(raw); }
 0192        catch (JsonException) { return "Не удалось прочитать выбранные позиции Armtek."; }
 193
 194        var permittedOrderIds = orders.Select(x => x.Id).ToHashSet();
 195        // Пустая очередь Armtek означает только, что прямой заказ выбран для другого поставщика.
 10196        if (queue.Count > 0)
 197        {
 16198            if (queue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0199                return "Состав предложений Armtek не соответствует проводимым заказам. Обновите страницу и повторите поп
 200            try
 201            {
 8202                var result = await _armtekCartService.CheckoutDirectAsync(queue, _userName, _ct, ArmtekConfirmedOrderIds
 5203                if (!result.IsSuccess) return string.Join(" ", result.Messages);
 3204                _results[ArmtekDirectCheckoutResultKey] = result;
 3205            }
 4206            catch (Exception ex) when (ex is Services.ApiServices.ArmtekApi.ArmtekApiException or HttpRequestException)
 207            {
 208                // Обрыв соединения мог случиться после того, как Армтек принял заказ — исход неизвестен, нужна сверка.
 4209                if (ex is HttpRequestException)
 210                {
 4211                    _dispatch?.MarkUncertain(OzonDomains.Models.ApiSupplierSettings.ArmtekProviderKey);
 4212                    _dispatch?.MarkUncertain(OzonDomains.Models.ApiSupplierSettings.ArmtekByProviderKey);
 213                }
 214
 4215                return $"Армтек: {ex.Message}";
 216            }
 217        }
 218
 5219        var user = _userName;
 5220        var mlAutoRaw = Get("MlAutoCheckoutQueueJson");
 5221        if (!string.IsNullOrWhiteSpace(mlAutoRaw))
 222        {
 223            try
 224            {
 0225                var directQueue = ParseMlAutoBasketQueue(mlAutoRaw);
 0226                if (directQueue.Count > 0)
 227                {
 0228                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений ML-Auto
 0229                    var result = await _mlAutoCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0230                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0231                    _results[MlAutoDirectCheckoutResultKey] = result;
 232                }
 0233            }
 0234            catch (JsonException) { return "Не удалось прочитать выбранные позиции ML-Auto BY."; }
 0235            catch (Services.ApiServices.MlAutoApi.MlAutoApiException ex) { return $"ML-Auto BY: {ex.Message}"; }
 236        }
 237
 5238        var mlAutoRuRaw = Get("MlAutoRuCheckoutQueueJson");
 5239        if (!string.IsNullOrWhiteSpace(mlAutoRuRaw))
 240        {
 241            try
 242            {
 243                var directQueue = ParseMlAutoBasketQueue(mlAutoRuRaw).Select(x => new MlAutoRuCartQueueItem(x.OrderId, x
 0244                if (directQueue.Count > 0)
 245                {
 0246                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений ML-Auto
 0247                    var result = await _mlAutoRuCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0248                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0249                    _results[MlAutoRuDirectCheckoutResultKey] = result;
 250                }
 0251            }
 0252            catch (JsonException) { return "Не удалось прочитать выбранные позиции ML-Auto RU."; }
 0253            catch (Services.ApiServices.MlAutoApi.MlAutoApiException ex) { return $"ML-Auto RU: {ex.Message}"; }
 254        }
 255
 5256        var avdRaw = Get("AvdCheckoutQueueJson");
 5257        if (!string.IsNullOrWhiteSpace(avdRaw))
 258        {
 259            try
 260            {
 0261                var directQueue = JsonSerializer.Deserialize<List<AvdBasketQueueItem>>(avdRaw, new JsonSerializerOptions
 0262                if (directQueue.Count > 0)
 263                {
 0264                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений АВД не 
 0265                    var result = await _avdBasketPlacementService.CheckoutDirectAsync(directQueue, user, _ct);
 0266                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0267                    _results[AvdDirectCheckoutResultKey] = result;
 268                }
 0269            }
 0270            catch (JsonException) { return "Не удалось прочитать выбранные позиции АВД."; }
 0271            catch (Services.ApiServices.AvdApi.AvdApiException ex) { return $"АВД: {ex.Message}"; }
 272        }
 273
 5274        var japartsRaw = GetAll("JapartsCheckoutQueueJson");
 5275        if (japartsRaw.Count > 0 && !japartsRaw.All(string.IsNullOrWhiteSpace))
 276        {
 277            List<JapartsCartQueueItem> directQueue;
 0278            try { directQueue = ParseJapartsBasketQueue(japartsRaw).Items; }
 0279            catch (JsonException) { return "Не удалось прочитать выбранные позиции Japarts."; }
 0280            if (directQueue.Count > 0)
 281            {
 0282                if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0283                    return "Состав предложений Japarts не соответствует проводимым заказам. Обновите страницу и повторит
 0284                var result = await _japartsCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0285                if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0286                _results[JapartsDirectCheckoutResultKey] = result;
 287            }
 288        }
 289
 5290        var bergRaw = GetAll("BergCheckoutQueueJson");
 5291        if (bergRaw.Count > 0 && !bergRaw.All(string.IsNullOrWhiteSpace))
 292        {
 293            List<BergCartQueueItem> directQueue;
 0294            try { directQueue = ParseBergBasketQueue(bergRaw).Items; }
 0295            catch (JsonException) { return "Не удалось прочитать выбранные позиции Berg."; }
 0296            if (directQueue.Count > 0)
 297            {
 0298                if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0299                    return "Состав предложений Berg не соответствует проводимым заказам. Обновите страницу и повторите п
 0300                var result = await _bergCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0301                if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0302                _results[BergDirectCheckoutResultKey] = result;
 303            }
 304        }
 305
 5306        var nikeiRaw = Get("NikeiCheckoutQueueJson");
 5307        if (!string.IsNullOrWhiteSpace(nikeiRaw))
 308        {
 309            try
 310            {
 0311                var directQueue = ParseNikeiBasketQueue(nikeiRaw);
 0312                if (directQueue.Count > 0)
 313                {
 0314                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0315                        return "Состав предложений Nikei не соответствует проводимым заказам. Обновите страницу и повтор
 0316                    var result = await _nikeiCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0317                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0318                    _results[NikeiDirectCheckoutResultKey] = result;
 319                }
 0320            }
 0321            catch (JsonException) { return "Не удалось прочитать выбранные позиции Nikei."; }
 322        }
 323
 5324        var forumAutoRaw = Get("ForumAutoCheckoutQueueJson");
 5325        if (!string.IsNullOrWhiteSpace(forumAutoRaw))
 326        {
 327            try
 328            {
 0329                var directQueue = ParseForumAutoBasketQueue(forumAutoRaw);
 0330                if (directQueue.Count > 0)
 331                {
 0332                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0333                        return "Состав предложений Форум-Авто не соответствует проводимым заказам. Обновите страницу и п
 0334                    var result = await _forumAutoCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0335                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0336                    _results[ForumAutoDirectCheckoutResultKey] = result;
 337                }
 0338            }
 0339            catch (JsonException) { return "Не удалось прочитать выбранные позиции Форум-Авто."; }
 0340            catch (Services.ApiServices.ForumAutoApi.ForumAutoApiException ex) { return $"Форум-Авто: {ex.Message}"; }
 341        }
 342
 5343        var favoritPartsRaw = Get("FavoritPartsCheckoutQueueJson");
 5344        if (!string.IsNullOrWhiteSpace(favoritPartsRaw))
 345        {
 346            try
 347            {
 0348                var directQueue = ParseFavoritPartsBasketQueue(favoritPartsRaw);
 0349                if (directQueue.Count > 0)
 350                {
 0351                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0352                        return "Состав предложений FavoritParts не соответствует проводимым заказам. Обновите страницу и
 0353                    var result = await _favoritPartsCartService.CheckoutDirectAsync(directQueue, _userName, _ct);
 0354                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0355                    _results[FavoritPartsDirectCheckoutResultKey] = result;
 356                }
 0357            }
 0358            catch (JsonException) { return "Не удалось прочитать выбранные позиции FavoritParts."; }
 0359            catch (Services.ApiServices.FavoritPartsApi.FavoritPartsApiException ex) { return $"FavoritParts: {ex.Messag
 360        }
 361
 5362        var favoritPartsIstraRaw = Get("FavoritPartsIstraCheckoutQueueJson");
 5363        if (!string.IsNullOrWhiteSpace(favoritPartsIstraRaw))
 364        {
 365            try
 366            {
 0367                var directQueue = ParseFavoritPartsBasketQueue(favoritPartsIstraRaw);
 0368                if (directQueue.Count > 0)
 369                {
 0370                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0371                        return "Состав предложений Фаворит Истра не соответствует проводимым заказам. Обновите страницу 
 0372                    var result = await _favoritPartsIstraCartService.CheckoutDirectAsync(directQueue, _userName, _ct);
 0373                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0374                    _results[FavoritPartsIstraDirectCheckoutResultKey] = result;
 375                }
 0376            }
 0377            catch (JsonException) { return "Не удалось прочитать выбранные позиции Фаворит Истра."; }
 0378            catch (Services.ApiServices.FavoritPartsApi.FavoritPartsApiException ex) { return $"Фаворит Истра: {ex.Messa
 379        }
 380
 5381        var favoritPartsRostovRaw = Get("FavoritPartsRostovCheckoutQueueJson");
 5382        if (!string.IsNullOrWhiteSpace(favoritPartsRostovRaw))
 383        {
 384            try
 385            {
 0386                var directQueue = ParseFavoritPartsBasketQueue(favoritPartsRostovRaw);
 0387                if (directQueue.Count > 0)
 388                {
 0389                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0390                        return "Состав предложений Фаворит Ростов не соответствует проводимым заказам. Обновите страницу
 0391                    var result = await _favoritPartsRostovCartService.CheckoutDirectAsync(directQueue, _userName, _ct);
 0392                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0393                    _results[FavoritPartsRostovDirectCheckoutResultKey] = result;
 394                }
 0395            }
 0396            catch (JsonException) { return "Не удалось прочитать выбранные позиции Фаворит Ростов."; }
 0397            catch (Services.ApiServices.FavoritPartsApi.FavoritPartsApiException ex) { return $"Фаворит Ростов: {ex.Mess
 398        }
 399
 5400        var moskvorechieRaw = Get("MoskvorechieCheckoutQueueJson");
 5401        if (!string.IsNullOrWhiteSpace(moskvorechieRaw))
 402        {
 403            try
 404            {
 0405                var directQueue = ParseMoskvorechieBasketQueue(moskvorechieRaw);
 0406                if (directQueue.Count > 0)
 407                {
 0408                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений Москвор
 0409                    var result = await _moskvorechieIstraCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0410                    if (result.Failed > 0) return string.Join(" ", result.Messages);
 0411                    _results[MoskvorechieIstraDirectCheckoutResultKey] = result;
 412                }
 0413            }
 0414            catch (JsonException) { return "Не удалось прочитать выбранные позиции Москворечья."; }
 0415            catch (Services.ApiServices.MoskvorechieApi.MoskvorechieApiException ex) { return $"Москворечье: {ex.Message
 416        }
 417
 5418        var profitLigaRaw = Get("ProfitLigaCheckoutQueueJson");
 5419        if (!string.IsNullOrWhiteSpace(profitLigaRaw))
 420        {
 421            try
 422            {
 0423                var directQueue = ParseProfitLigaBasketQueue(profitLigaRaw);
 0424                if (directQueue.Count > 0)
 425                {
 0426                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0427                        return "Состав предложений Профит-Лиги не соответствует проводимым заказам. Обновите страницу и 
 0428                    var result = await _profitLigaCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0429                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0430                    _results[ProfitLigaDirectCheckoutResultKey] = result;
 431                }
 0432            }
 0433            catch (JsonException) { return "Не удалось прочитать выбранные позиции Профит-Лиги."; }
 0434            catch (Services.ApiServices.ProfitLigaApi.ProfitLigaApiException ex) { return $"Профит-Лига: {ex.Message}"; 
 435        }
 436
 5437        var motexRaw = Get("MotexCheckoutQueueJson");
 5438        if (!string.IsNullOrWhiteSpace(motexRaw))
 439        {
 440            try
 441            {
 0442                var directQueue = ParseMotexBasketQueue(motexRaw);
 0443                if (directQueue.Count > 0)
 444                {
 0445                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений МоТехС 
 0446                    var result = await _motexCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0447                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0448                    _results[MotexDirectCheckoutResultKey] = result;
 449                }
 0450            }
 0451            catch (JsonException) { return "Не удалось прочитать выбранные позиции МоТехС."; }
 452        }
 453
 5454        var zzapRaw = Get("ZZapMoscowCheckoutQueueJson");
 5455        if (!string.IsNullOrWhiteSpace(zzapRaw))
 456        {
 457            try
 458            {
 0459                var directQueue = ParseZZapBasketQueue(zzapRaw);
 0460                if (directQueue.Count > 0)
 461                {
 0462                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений ZZap не
 0463                    var result = await _zzapCartService.CheckoutDirectAsync(directQueue, user, _ct);
 0464                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0465                    _results[ZZapMoscowDirectCheckoutResultKey] = result;
 466                }
 0467            }
 0468            catch (JsonException) { return "Не удалось прочитать выбранные позиции ZZap."; }
 469        }
 470
 5471        return null;
 10472    }
 473
 474    /// <summary>Отложенные корзины (mode=basket) и сборка отправления Ozon после записи транзакции. HTML-блоки или null
 475    public async Task<string?> PlaceBasketsAsync(List<Order> ordersToTransaction, bool shipInOzon)
 476    {
 477
 2478        var orderIds = ordersToTransaction
 479            .Select(o => o.Id)
 480            .Where(id => id > 0)
 2481            .Distinct()
 2482            .ToList();
 483
 2484        var blocks = new List<string>();
 2485        var structuredBlocks = new HashSet<string>(StringComparer.Ordinal);
 2486        var profitLigaOrderIds = orderIds;
 487
 488        // Транзакция уже записана и откатить её нельзя — поэтому позиции, чей заказ уже заказан у поставщика
 489        // и не подтверждён пользователем, просто не попадают в корзины.
 2490        var basketGuardBlock = await FilterBlockedBasketQueuesAsync(orderIds);
 3491        if (!string.IsNullOrWhiteSpace(basketGuardBlock)) blocks.Add(basketGuardBlock);
 492
 2493        var shouldShipInOzon = shipInOzon;
 2494        if (shouldShipInOzon && orderIds.Count > 0)
 495        {
 0496            await _stage("ozon-ship", "Отгружаем в Ozon",
 0497                orderIds.Count == 1 ? "1 отправление" : $"отправлений: {orderIds.Count}");
 0498            var shipResult = await _orderToSupplierOzonShipService.ShipAsync(orderIds, _ct);
 0499            OzonShipResult = shipResult;
 0500            profitLigaOrderIds = shipResult.EligibleOrderIds.ToList();
 0501            var shipBlock = shipResult.ToHtmlBlock();
 0502            if (!string.IsNullOrWhiteSpace(shipBlock))
 503            {
 0504                blocks.Add(shipBlock);
 0505                structuredBlocks.Add(shipBlock);
 506            }
 0507            ReportRows.AddRange(TransactionReportMapper.FromOzonShip(shipResult));
 508        }
 509
 2510        await _stage("carts", "Размещаем позиции в корзинах поставщиков", null);
 2511        var avdBlock = await GetAvdBasketAppendixAsync(orderIds);
 2512        if (!string.IsNullOrWhiteSpace(avdBlock))
 0513            blocks.Add(avdBlock);
 2514        var japartsBlock = await GetJapartsBasketAppendixAsync(orderIds);
 2515        var bergBlock = await GetBergBasketAppendixAsync(orderIds);
 2516        if (!string.IsNullOrWhiteSpace(japartsBlock)) blocks.Add(japartsBlock);
 2517        if (!string.IsNullOrWhiteSpace(bergBlock)) blocks.Add(bergBlock);
 2518        var armtekBlock = await GetArmtekBasketAppendixAsync(orderIds);
 2519        if (!string.IsNullOrWhiteSpace(armtekBlock)) blocks.Add(armtekBlock);
 2520        if (_results.TryGetValue(ArmtekDirectCheckoutResultKey, out var directResultObject) &&
 2521            directResultObject is ArmtekDirectCheckoutResult directResult)
 522        {
 0523            var lines = string.Join(string.Empty, directResult.Messages.Select(message =>
 524                $"<li>{System.Net.WebUtility.HtmlEncode(message)}</li>"));
 0525            var armtekBlockHtml = $"<div class='alert alert-success mb-0'><strong>Заказ Armtek оформлен.</strong><ul cla
 0526            blocks.Add(armtekBlockHtml);
 0527            structuredBlocks.Add(armtekBlockHtml);
 0528            ReportRows.AddRange(TransactionReportMapper.FromSupplierMessages("Armtek", "Армтек", TransactionReportMapper
 0529                "Заказ оформлен у поставщика", directResult.Messages));
 530        }
 2531        AppendDirectCheckoutBlock(blocks, structuredBlocks, MlAutoDirectCheckoutResultKey, "Заказ ML-Auto BY оформлен.")
 2532        AppendDirectCheckoutBlock(blocks, structuredBlocks, MlAutoRuDirectCheckoutResultKey, "Заказ ML-Auto RU оформлен.
 2533        AppendDirectCheckoutBlock(blocks, structuredBlocks, AvdDirectCheckoutResultKey, "Заказ АВД оформлен.");
 2534        AppendDirectCheckoutBlock(blocks, structuredBlocks, ForumAutoDirectCheckoutResultKey, "Заказ Форум-Авто оформлен
 2535        AppendDirectCheckoutBlock(blocks, structuredBlocks, FavoritPartsDirectCheckoutResultKey, "Заказ FavoritParts офо
 2536        AppendDirectCheckoutBlock(blocks, structuredBlocks, FavoritPartsIstraDirectCheckoutResultKey, "Заказ Фаворит Ист
 2537        AppendDirectCheckoutBlock(blocks, structuredBlocks, FavoritPartsRostovDirectCheckoutResultKey, "Заказ Фаворит Ро
 2538        AppendDirectCheckoutBlock(blocks, structuredBlocks, MoskvorechieIstraDirectCheckoutResultKey, "Заказ Москворечья
 2539        AppendDirectCheckoutBlock(blocks, structuredBlocks, MotexDirectCheckoutResultKey, "Заказ МоТехС оформлен.");
 2540        AppendDirectCheckoutBlock(blocks, structuredBlocks, ZZapMoscowDirectCheckoutResultKey, "Заказ ZZap оформлен.");
 2541        AppendDirectCheckoutBlock(blocks, structuredBlocks, JapartsDirectCheckoutResultKey, "Заказ Japarts оформлен.");
 2542        AppendDirectCheckoutBlock(blocks, structuredBlocks, BergDirectCheckoutResultKey, "Заказ Berg оформлен.");
 2543        AppendDirectCheckoutBlock(blocks, structuredBlocks, NikeiDirectCheckoutResultKey, "Заказ Nikei оформлен.");
 2544        var mlAutoBlock = await GetMlAutoBasketAppendixAsync(orderIds);
 2545        if (!string.IsNullOrWhiteSpace(mlAutoBlock)) blocks.Add(mlAutoBlock);
 2546        var mlAutoRuBlock = await GetMlAutoRuBasketAppendixAsync(orderIds);
 2547        if (!string.IsNullOrWhiteSpace(mlAutoRuBlock)) blocks.Add(mlAutoRuBlock);
 2548        var forumAutoBlock = await GetForumAutoBasketAppendixAsync(orderIds);
 2549        if (!string.IsNullOrWhiteSpace(forumAutoBlock)) blocks.Add(forumAutoBlock);
 2550        var favoritPartsBlock = await GetFavoritPartsBasketAppendixAsync(orderIds);
 2551        if (!string.IsNullOrWhiteSpace(favoritPartsBlock)) blocks.Add(favoritPartsBlock);
 2552        var favoritPartsIstraBlock = await GetFavoritPartsIstraBasketAppendixAsync(orderIds);
 2553        var favoritPartsRostovBlock = await GetFavoritPartsRostovBasketAppendixAsync(orderIds);
 2554        if (!string.IsNullOrWhiteSpace(favoritPartsIstraBlock)) blocks.Add(favoritPartsIstraBlock);
 2555        if (!string.IsNullOrWhiteSpace(favoritPartsRostovBlock)) blocks.Add(favoritPartsRostovBlock);
 2556        AppendDirectCheckoutBlock(blocks, structuredBlocks, ProfitLigaDirectCheckoutResultKey, "Заказ Профит-Лиги оформл
 2557        var profitLigaBlock = await GetProfitLigaBasketAppendixAsync(profitLigaOrderIds,
 2558            orderIds.Except(profitLigaOrderIds).ToHashSet());
 2559        if (!string.IsNullOrWhiteSpace(profitLigaBlock)) blocks.Add(profitLigaBlock);
 2560        var nikeiBlock = await GetNikeiBasketAppendixAsync(orderIds);
 2561        if (!string.IsNullOrWhiteSpace(nikeiBlock)) blocks.Add(nikeiBlock);
 2562        var mikadoBlock = await GetMikadoBasketAppendixAsync(orderIds);
 2563        var zzapBlock = await GetZZapBasketAppendixAsync(orderIds);
 2564        if (!string.IsNullOrWhiteSpace(zzapBlock)) blocks.Add(zzapBlock);
 2565        if (!string.IsNullOrWhiteSpace(mikadoBlock)) blocks.Add(mikadoBlock);
 2566        var moskvorechieBlock = await GetMoskvorechieBasketAppendixAsync(orderIds);
 2567        if (!string.IsNullOrWhiteSpace(moskvorechieBlock)) blocks.Add(moskvorechieBlock);
 2568        var motexBlock = await GetMotexBasketAppendixAsync(orderIds);
 2569        if (!string.IsNullOrWhiteSpace(motexBlock)) blocks.Add(motexBlock);
 2570        var shateMBlock = await GetShateMBasketAppendixAsync(orderIds);
 2571        if (!string.IsNullOrWhiteSpace(shateMBlock)) blocks.Add(shateMBlock);
 2572        var shateMDirectBlock = await GetShateMDirectCheckoutAppendixAsync(orderIds);
 1573        if (!string.IsNullOrWhiteSpace(shateMDirectBlock)) blocks.Add(shateMDirectBlock);
 574
 575        // Блоки без структурированного результата (корзины, Шате-М) — в таблицу построчно из их HTML.
 5576        foreach (var block in blocks.Where(b => !structuredBlocks.Contains(b)))
 1577            ReportRows.AddRange(TransactionReportMapper.FromHtml(block, "Корзины поставщиков"));
 578
 1579        return blocks.Count == 0 ? null : string.Join("<br/>", blocks);
 1580    }
 581
 582    /// <summary>Убирает из очередей корзин позиции с неподтверждённым конфликтом; возвращает HTML-блок с причинами.</su
 583    private async Task<string?> FilterBlockedBasketQueuesAsync(IReadOnlyCollection<int> orderIds)
 584    {
 2585        var basketQueues = _queues
 586            .Where(x => x.Key.EndsWith("BasketQueueJson", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSp
 2587            .ToList();
 3588        if (basketQueues.Count == 0) return null;
 589
 2590        var targets = SupplierQueueTargets.Extract(basketQueues).Where(x => orderIds.Contains(x.OrderId)).ToList();
 1591        var guard = await _guard.EvaluateAsync(targets, _confirmation, _userName, _ct);
 1592        if (guard.Blocking.Count == 0 && guard.Warnings.Count == 0) return null;
 593
 1594        if (guard.Blocking.Count > 0)
 595        {
 1596            var blocked = guard.Blocking
 597                .Select(x => (x.OrderId, x.RequestedProviderKey))
 1598                .ToHashSet();
 1599            var filtered = new Dictionary<string, string>(_queues, StringComparer.OrdinalIgnoreCase);
 4600            foreach (var (key, json) in basketQueues)
 1601                filtered[key] = SupplierQueueTargets.RemoveBlocked(key, json, blocked);
 1602            _queues = filtered;
 603        }
 604
 605        var lines = guard.Blocking.Select(x => x.Message).Concat(guard.Warnings)
 606            .Select(x => $"<li>{System.Net.WebUtility.HtmlEncode(x)}</li>");
 1607        var title = guard.Blocking.Count > 0
 1608            ? "Часть позиций не добавлена в корзины поставщиков: заказ уже заказан у поставщика."
 1609            : "Предупреждение по корзинам поставщиков.";
 1610        return $"<div class='alert alert-warning mb-0'><strong>{title}</strong><ul class='mb-0'>{string.Join(string.Empt
 2611    }
 612
 613    private void AppendDirectCheckoutBlock(List<string> blocks, HashSet<string> structuredBlocks, string key, string tit
 614    {
 56615        if (!_results.TryGetValue(key, out var result)) return;
 0616        var messages = result switch
 0617        {
 0618            MlAutoDirectCheckoutResult value => value.Messages,
 0619            MlAutoRuDirectCheckoutResult value => value.Messages,
 0620            AvdDirectCheckoutResult value => value.Messages,
 0621            ForumAutoDirectCheckoutResult value => value.Messages,
 0622            FavoritPartsDirectCheckoutResult value => value.Messages,
 0623            ProfitLigaDirectCheckoutResult value => value.Messages,
 0624            MoskvorechieIstraCartResult value => value.Messages,
 0625            MotexCartResult value => value.Messages,
 0626            ZZapCartResult value => value.Messages,
 0627            JapartsCartResult value => value.Messages,
 0628            BergCartResult value => value.Messages,
 0629            NikeiCartOperationResult value => value.Messages,
 0630            _ => null
 0631        };
 0632        if (messages is null) return;
 0633        var hasPostError = messages.Any(m => m.Contains("не проведён", StringComparison.OrdinalIgnoreCase)
 0634                                             || m.Contains("не проведен", StringComparison.OrdinalIgnoreCase));
 0635        var alertClass = hasPostError ? "alert alert-warning" : "alert alert-success";
 0636        var headerTitle = hasPostError ? $"{title} (внимание: есть непроведённые заказы)" : title;
 0637        var lines = string.Join(string.Empty, messages.Select(message => $"<li>{System.Net.WebUtility.HtmlEncode(message
 0638        var block = $"<div class='{alertClass} mb-0'><strong>{headerTitle}</strong><ul class='mb-0'>{lines}</ul></div>";
 0639        blocks.Add(block);
 0640        structuredBlocks.Add(block);
 641
 642        // Ключ результата — «<Провайдер>DirectCheckoutResult»: из него берём провайдера для ссылки на корзину.
 0643        var providerKey = key.EndsWith("DirectCheckoutResult", StringComparison.Ordinal) ? key[..^"DirectCheckoutResult"
 0644        var supplierName = System.Text.RegularExpressions.Regex.Match(title, @"^Заказ (.+) оформлен\.$") is { Success: t
 0645        ReportRows.AddRange(TransactionReportMapper.FromSupplierMessages(providerKey, supplierName,
 0646            TransactionReportMapper.SectionSupplierOrder, "Заказ оформлен у поставщика", messages));
 0647    }
 648
 649    private async Task<string?> GetMlAutoBasketAppendixAsync(List<int> orderIds)
 650    {
 2651        var raw = Get("MlAutoBasketQueueJson");
 3652        if (string.IsNullOrWhiteSpace(raw)) return null;
 653        List<MlAutoCartQueueItem> queue;
 2654        try { queue = ParseMlAutoBasketQueue(raw); }
 0655        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина ML-Auto не обработана.</st
 1656        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 2657        if (selected.Count == 0) return null;
 658        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1)) return "<div class='alert alert-warning mb-0'><str
 0659        return (await _mlAutoCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock();
 2660    }
 661
 662    /// <summary>Браузер может сериализовать цену и количество ML-Auto как число, строку либо null.</summary>
 663    private static List<MlAutoCartQueueItem> ParseMlAutoBasketQueue(string raw)
 664    {
 1665        using var document = JsonDocument.Parse(raw);
 1666        if (document.RootElement.ValueKind != JsonValueKind.Array)
 0667            throw new JsonException("ML-Auto basket queue must be an array.");
 668
 1669        return document.RootElement.EnumerateArray()
 0670            .Where(item => item.ValueKind == JsonValueKind.Object)
 0671            .Select(item => new MlAutoCartQueueItem(
 0672                ReadInt(item, "orderId"), ReadString(item, "pin"), ReadString(item, "brand"),
 0673                ReadNullableString(item, "storageCode"), ReadNullableString(item, "warehouseName"),
 0674                ReadNullableString(item, "productName"), ReadNullableString(item, "shipmentNumber"),
 0675                ReadNullableString(item, "clientName"), ReadDecimal(item, "quantity"),
 0676                ReadDecimal(item, "minimumQuantity"), ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item,
 0677                ReadNullableString(item, "deliveryAtUtc"), ReadNullableString(item, "zasylWarehouseName"),
 0678                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 1679            .ToList();
 1680    }
 681
 682    private async Task<string?> GetMlAutoRuBasketAppendixAsync(List<int> orderIds)
 683    {
 2684        var raw = Get("MlAutoRuBasketQueueJson");
 4685        if (string.IsNullOrWhiteSpace(raw)) return null;
 686        List<MlAutoRuCartQueueItem> queue;
 687        try { queue = ParseMlAutoBasketQueue(raw).Select(x => new MlAutoRuCartQueueItem(x.OrderId, x.Pin, x.Brand, x.Sto
 0688        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина ML-Auto RU не обработана.<
 0689        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0690        if (selected.Count == 0) return null;
 691        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1)) return "<div class='alert alert-warning mb-0'><str
 0692        return (await _mlAutoRuCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock();
 2693    }
 694
 695    /// <summary>Размещение отложенной корзины АВД — последний шаг после проведения, позиции берём только из уже успешно
 696    private async Task<string?> GetJapartsBasketAppendixAsync(List<int> orderIds)
 697    {
 2698        var rawValues = GetAll("JapartsBasketQueueJson");
 4699        if (rawValues.Count == 0 || rawValues.All(string.IsNullOrWhiteSpace)) return null;
 700        List<JapartsCartQueueItem> queue;
 701        int skipped;
 0702        try { (queue, skipped) = ParseJapartsBasketQueue(rawValues); }
 0703        catch (JsonException exception)
 704        {
 0705            return "<div class='alert alert-warning mb-0'><strong>Корзина Japarts не обработана.</strong> " +
 0706                   $"Не удалось прочитать очередь выбранных позиций: {System.Net.WebUtility.HtmlEncode(exception.Message
 707        }
 0708        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0709        if (selected.Count == 0)
 0710            return skipped == 0
 0711                ? null
 0712                : $"<div class='alert alert-warning mb-0'><strong>Корзина Japarts не обработана.</strong> Позиций в очер
 713        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 0714            return "<div class='alert alert-warning mb-0'><strong>Корзина Japarts не обработана.</strong> Для одного зак
 715
 0716        var result = await _japartsCartService.PlaceQueuedItemsAsync(selected, _userName,
 0717            _ct);
 718        var lines = string.Join(string.Empty, result.Messages.Select(x => $"<li>{System.Net.WebUtility.HtmlEncode(x)}</l
 0719        return $"<div class='alert alert-{(result.IsSuccess ? "info" : "warning")} mb-0'><strong>Japarts.</strong><ul cl
 0720               "<div class='mt-2'>Заказ у поставщика не создан: оформите его вручную на странице " +
 0721               $"<a href='{"/Studio2/SupplierCart?provider=japarts&supplierTab=basket"}'>«Корзина Japarts»</a>.</div></d
 2722    }
 723
 724    /// <summary>Отложенная корзина Berg: у поставщика корзины нет, поэтому позиции ложатся
 725    /// в локальную таблицу, а заказ оформляется отдельным действием на странице корзины.</summary>
 726    private async Task<string?> GetBergBasketAppendixAsync(List<int> orderIds)
 727    {
 2728        var rawValues = GetAll("BergBasketQueueJson");
 4729        if (rawValues.Count == 0 || rawValues.All(string.IsNullOrWhiteSpace)) return null;
 730        List<BergCartQueueItem> queue;
 731        int skipped;
 0732        try { (queue, skipped) = ParseBergBasketQueue(rawValues); }
 0733        catch (JsonException exception)
 734        {
 0735            return "<div class='alert alert-warning mb-0'><strong>Корзина Berg не обработана.</strong> " +
 0736                   $"Не удалось прочитать очередь выбранных позиций: {System.Net.WebUtility.HtmlEncode(exception.Message
 737        }
 0738        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0739        if (selected.Count == 0)
 0740            return skipped == 0
 0741                ? null
 0742                : $"<div class='alert alert-warning mb-0'><strong>Корзина Berg не обработана.</strong> Позиций в очереди
 743        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 0744            return "<div class='alert alert-warning mb-0'><strong>Корзина Berg не обработана.</strong> Для одного заказа
 745
 0746        var result = await _bergCartService.PlaceQueuedItemsAsync(selected, _userName,
 0747            _ct);
 748        var lines = string.Join(string.Empty, result.Messages.Select(x => $"<li>{System.Net.WebUtility.HtmlEncode(x)}</l
 0749        return $"<div class='alert alert-{(result.IsSuccess ? "info" : "warning")} mb-0'><strong>Берг.</strong><ul class
 0750               "<div class='mt-2'>Заказ у поставщика не создан: оформите его на странице " +
 0751               $"<a href='{"/Studio2/SupplierCart?provider=berg&supplierTab=basket"}'>«Корзина Berg»</a>.</div></div>";
 2752    }
 753
 754    private static (List<BergCartQueueItem> Items, int Skipped) ParseBergBasketQueue(IEnumerable<string> rawValues)
 755    {
 0756        var queue = new List<BergCartQueueItem>();
 0757        var skipped = 0;
 0758        foreach (var raw in rawValues.Where(value => !string.IsNullOrWhiteSpace(value)))
 759        {
 0760            using var document = JsonDocument.Parse(raw);
 0761            var elements = document.RootElement.ValueKind switch
 0762            {
 0763                JsonValueKind.Array => document.RootElement.EnumerateArray().ToArray(),
 0764                JsonValueKind.Object => [document.RootElement],
 0765                _ => throw new JsonException("очередь должна быть массивом или объектом.")
 0766            };
 0767            foreach (var element in elements)
 768            {
 0769                var item = ReadBergQueueItem(element);
 0770                if (item is null) skipped++;
 0771                else queue.Add(item);
 772            }
 773        }
 774
 0775        return (queue.Distinct().ToList(), skipped);
 776    }
 777
 778    /// <summary>
 779    /// Предложение Berg адресуется парой resource_id + warehouse_id. Фронтенд шлёт их отдельными
 780    /// полями, но itemId вида «resource|warehouse|price|days» остаётся запасным источником:
 781    /// он приходит из проценки и переживает обновление страницы.
 782    /// </summary>
 783    private static BergCartQueueItem? ReadBergQueueItem(JsonElement element)
 784    {
 0785        if (element.ValueKind != JsonValueKind.Object) return null;
 0786        var orderId = ReadJapartsQueueDecimal(element, "orderId");
 0787        if (orderId is null or <= 0 || orderId != decimal.Truncate(orderId.Value)) return null;
 788
 0789        var resourceId = (long?)ReadJapartsQueueDecimal(element, "resourceId");
 0790        var warehouseId = (long?)ReadJapartsQueueDecimal(element, "warehouseId");
 0791        if (resourceId is null or <= 0 || warehouseId is null or <= 0)
 792        {
 0793            var parts = (ReadJapartsQueueString(element, "itemId") ?? string.Empty).Split('|');
 0794            if (parts.Length < 2 || !long.TryParse(parts[0], out var parsedResource) ||
 0795                !long.TryParse(parts[1], out var parsedWarehouse))
 0796                return null;
 0797            resourceId = parsedResource;
 0798            warehouseId = parsedWarehouse;
 799        }
 800
 0801        var quantity = ReadJapartsQueueDecimal(element, "quantity");
 0802        var multi = (int?)ReadJapartsQueueDecimal(element, "minQuantity");
 0803        return new BergCartQueueItem((int)orderId.Value, resourceId.Value, warehouseId.Value,
 0804            quantity is null or <= 0 ? 1m : quantity.Value,
 0805            ReadJapartsQueueDecimal(element, "price"), ReadJapartsQueueString(element, "article"),
 0806            ReadJapartsQueueString(element, "brand"), ReadJapartsQueueString(element, "productName"),
 0807            ReadJapartsQueueString(element, "shipmentNumber"), ReadJapartsQueueString(element, "clientName"),
 0808            multi is null or <= 0 ? 1 : multi.Value,
 0809            (int?)ReadJapartsQueueDecimal(element, "deliveryDaysMin"), (int?)ReadJapartsQueueDecimal(element, "deliveryD
 0810            ReadJapartsQueueString(element, "direction"));
 811    }
 812
 813    /// <summary>Форма может прислать несколько значений скрытого поля; каждое значение разбираем отдельно, не склеивая 
 814    /// через запятую. Числа читаем терпимо — браузер отдаёт их то числом, то строкой, а NaN сериализует в null, — иначе
 815    /// одна кривая позиция роняла разбор всей очереди. Второе значение кортежа — сколько позиций пропущено как неполные
 816    private static (List<JapartsCartQueueItem> Items, int Skipped) ParseJapartsBasketQueue(IEnumerable<string> rawValues
 817    {
 0818        var queue = new List<JapartsCartQueueItem>();
 0819        var skipped = 0;
 0820        foreach (var raw in rawValues.Where(value => !string.IsNullOrWhiteSpace(value)))
 821        {
 0822            using var document = JsonDocument.Parse(raw);
 0823            var elements = document.RootElement.ValueKind switch
 0824            {
 0825                JsonValueKind.Array => document.RootElement.EnumerateArray().ToArray(),
 0826                JsonValueKind.Object => [document.RootElement],
 0827                _ => throw new JsonException("очередь должна быть массивом или объектом.")
 0828            };
 0829            foreach (var element in elements)
 830            {
 0831                var item = ReadJapartsQueueItem(element);
 0832                if (item is null) skipped++;
 0833                else queue.Add(item);
 834            }
 835        }
 836
 0837        return (queue.Distinct().ToList(), skipped);
 838    }
 839
 840    private static JapartsCartQueueItem? ReadJapartsQueueItem(JsonElement element)
 841    {
 0842        if (element.ValueKind != JsonValueKind.Object) return null;
 0843        var orderId = ReadJapartsQueueDecimal(element, "orderId");
 0844        var priceId = ReadJapartsQueueString(element, "priceId")
 0845                      ?? ReadJapartsQueueString(element, "itemHash")
 0846                      ?? ReadJapartsQueueString(element, "itemId");
 0847        if (orderId is null or <= 0 || orderId != decimal.Truncate(orderId.Value) || string.IsNullOrWhiteSpace(priceId))
 0848            return null;
 0849        var quantity = ReadJapartsQueueDecimal(element, "quantity");
 0850        return new JapartsCartQueueItem((int)orderId.Value, priceId!, quantity is null or <= 0 ? 1m : quantity.Value,
 0851            ReadJapartsQueueDecimal(element, "price"), ReadJapartsQueueString(element, "article"),
 0852            ReadJapartsQueueString(element, "productName"), ReadJapartsQueueString(element, "shipmentNumber"),
 0853            ReadJapartsQueueString(element, "clientName"),
 0854            (int?)ReadJapartsQueueDecimal(element, "deliveryDaysMin"), (int?)ReadJapartsQueueDecimal(element, "deliveryD
 0855            ReadJapartsQueueString(element, "direction"));
 856    }
 857
 858    private static string? ReadJapartsQueueString(JsonElement element, string name)
 859    {
 0860        if (!element.TryGetProperty(name, out var value)) return null;
 0861        var text = value.ValueKind switch
 0862        {
 0863            JsonValueKind.String => value.GetString(),
 0864            JsonValueKind.Number => value.GetRawText(),
 0865            _ => null
 0866        };
 0867        return string.IsNullOrWhiteSpace(text) ? null : text.Trim();
 868    }
 869
 870    private static decimal? ReadJapartsQueueDecimal(JsonElement element, string name)
 871    {
 0872        if (!element.TryGetProperty(name, out var value)) return null;
 0873        return value.ValueKind switch
 0874        {
 0875            JsonValueKind.Number when value.TryGetDecimal(out var number) => number,
 0876            JsonValueKind.String when decimal.TryParse(value.GetString(), NumberStyles.Any,
 0877                CultureInfo.InvariantCulture, out var parsed) => parsed,
 0878            _ => null
 0879        };
 880    }
 881
 882    private async Task<string?> GetAvdBasketAppendixAsync(List<int> orderIds)
 883    {
 2884        var queueJson = Get("AvdBasketQueueJson");
 2885        if (string.IsNullOrWhiteSpace(queueJson))
 2886            return null;
 887
 888        List<AvdBasketQueueItem>? queue;
 889        try
 890        {
 0891            queue = JsonSerializer.Deserialize<List<AvdBasketQueueItem>>(queueJson,
 0892                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
 0893        }
 0894        catch (JsonException)
 895        {
 0896            return "<div class='alert alert-warning mb-0'><strong>Корзина АВД не обработана.</strong> " +
 0897                   "Не удалось прочитать очередь выбранных позиций.</div>";
 898        }
 899
 0900        var selectedItems = (queue ?? new List<AvdBasketQueueItem>())
 0901            .Where(x => orderIds.Contains(x.OrderId))
 0902            .ToList();
 0903        if (selectedItems.Count == 0)
 0904            return null;
 905
 906        if (selectedItems.Any(x => string.IsNullOrWhiteSpace(x.ItemHash) || x.Quantity <= 0))
 907        {
 0908            return "<div class='alert alert-warning mb-0'><strong>Корзина АВД не обработана.</strong> " +
 0909                   "У выбранной позиции отсутствует hash предложения или указано некорректное количество.</div>";
 910        }
 911
 912        if (selectedItems.GroupBy(x => x.OrderId).Any(group => group.Count() > 1))
 913        {
 0914            return "<div class='alert alert-warning mb-0'><strong>Корзина АВД не обработана.</strong> " +
 0915                   "Для одного заказа выбрано несколько предложений.</div>";
 916        }
 917
 0918        var changedBy = _userName;
 0919        var result = await _avdBasketPlacementService.PlaceQueuedItemsAsync(selectedItems, changedBy, _ct);
 0920        return result.ToHtmlBlock();
 2921    }
 922
 923    private async Task<string?> GetArmtekBasketAppendixAsync(List<int> orderIds)
 924    {
 2925        var raw = Get("ArmtekBasketQueueJson");
 4926        if (string.IsNullOrWhiteSpace(raw)) return null;
 927        List<ArmtekCartQueueItem> queue;
 0928        try { queue = ParseArmtekBasketQueue(raw); }
 0929        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Армтек не обработана.</str
 0930        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0931        if (selected.Count == 0) return null;
 932        if (selected.GroupBy(x => new { x.ProviderKey, x.OrderId }).Any(x => x.Count() > 1))
 0933            return "<div class='alert alert-warning mb-0'><strong>Корзина Армтек не обработана.</strong> Для заказа выбр
 0934        var result = await _armtekCartService.QueueAsync(selected, _userName, _ct);
 0935        return result.ToHtmlBlock();
 2936    }
 937
 938    /// <summary>Браузер передаёт снимок предложения: API может отдать цену/количество числом либо строкой.</summary>
 939    private static List<ArmtekCartQueueItem> ParseArmtekBasketQueue(string raw)
 940    {
 10941        using var document = JsonDocument.Parse(raw);
 10942        if (document.RootElement.ValueKind != JsonValueKind.Array)
 0943            throw new JsonException("Armtek basket queue must be an array.");
 944
 10945        return document.RootElement.EnumerateArray()
 8946            .Where(item => item.ValueKind == JsonValueKind.Object)
 8947            .Select(item => new ArmtekCartQueueItem(
 8948                ReadInt(item, "orderId"), ReadString(item, "providerKey"), ReadString(item, "artId"),
 8949                ReadString(item, "pin"), ReadString(item, "brand"), ReadNullableString(item, "keyzak"),
 8950                ReadNullableString(item, "warehouseName"), ReadNullableString(item, "partnerWarehouseCode"),
 8951                ReadNullableString(item, "productName"), ReadNullableString(item, "shipmentNumber"), ReadNullableString(
 8952                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 8953                ReadNullableString(item, "deliveryAtUtc"), ReadNullableString(item, "zasylWarehouseName"),
 8954                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 10955            .ToList();
 10956    }
 957
 958    private async Task<string?> GetForumAutoBasketAppendixAsync(List<int> orderIds)
 959    {
 2960        var raw = Get("ForumAutoBasketQueueJson");
 4961        if (string.IsNullOrWhiteSpace(raw)) return null;
 962        List<ForumAutoCartQueueItem> queue;
 0963        try { queue = ParseForumAutoBasketQueue(raw); }
 0964        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Форум-Авто не обработана.<
 0965        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0966        if (selected.Count == 0) return null;
 967        if (selected.GroupBy(x => new { x.ProviderKey, x.OrderId }).Any(x => x.Count() > 1))
 0968            return "<div class='alert alert-warning mb-0'><strong>Корзина Форум-Авто не обработана.</strong> Для заказа 
 0969        var result = await _forumAutoCartService.QueueAsync(selected, _userName, _ct);
 0970        return result.ToHtmlBlock();
 2971    }
 972
 973    /// <summary>Браузер передаёт снимок проценки: цена и количество могут прийти числом либо строкой.</summary>
 974    private static List<ForumAutoCartQueueItem> ParseForumAutoBasketQueue(string raw)
 975    {
 0976        using var document = JsonDocument.Parse(raw);
 0977        if (document.RootElement.ValueKind != JsonValueKind.Array)
 0978            throw new JsonException("Forum-Auto basket queue must be an array.");
 979
 0980        return document.RootElement.EnumerateArray()
 0981            .Where(item => item.ValueKind == JsonValueKind.Object)
 0982            .Select(item => new ForumAutoCartQueueItem(
 0983                ReadInt(item, "orderId"), ReadString(item, "providerKey"), ReadString(item, "tid"),
 0984                ReadString(item, "article"), ReadString(item, "brand"), ReadNullableString(item, "productName"),
 0985                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"), ReadDecimal(item, "q
 0986                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 0987                ReadNullableString(item, "deliveryAtUtc"),
 0988                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 0989            .ToList();
 0990    }
 991
 992    private async Task<string?> GetFavoritPartsBasketAppendixAsync(List<int> orderIds)
 993    {
 2994        var raw = Get("FavoritPartsBasketQueueJson");
 4995        if (string.IsNullOrWhiteSpace(raw)) return null;
 996        List<FavoritPartsQueueItem> queue;
 0997        try { queue = ParseFavoritPartsBasketQueue(raw); }
 0998        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина FavoritParts не обработана
 0999        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01000        if (selected.Count == 0) return null;
 01001        var result = await _favoritPartsCartService.QueueAsync(selected, _userName, _ct);
 01002        return result.ToHtmlBlock();
 21003    }
 1004
 1005    private async Task<string?> GetFavoritPartsIstraBasketAppendixAsync(List<int> orderIds)
 1006    {
 21007        var raw = Get("FavoritPartsIstraBasketQueueJson");
 41008        if (string.IsNullOrWhiteSpace(raw)) return null;
 1009        List<FavoritPartsQueueItem> queue;
 01010        try { queue = ParseFavoritPartsBasketQueue(raw); }
 01011        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Фаворит Истра не обработан
 01012        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01013        if (selected.Count == 0) return null;
 01014        return (await _favoritPartsIstraCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock("favoritpartsistra
 21015    }
 1016
 1017    private async Task<string?> GetFavoritPartsRostovBasketAppendixAsync(List<int> orderIds)
 1018    {
 21019        var raw = Get("FavoritPartsRostovBasketQueueJson");
 41020        if (string.IsNullOrWhiteSpace(raw)) return null;
 1021        List<FavoritPartsQueueItem> queue;
 01022        try { queue = ParseFavoritPartsBasketQueue(raw); }
 01023        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Фаворит Ростов не обработа
 01024        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01025        if (selected.Count == 0) return null;
 01026        return (await _favoritPartsRostovCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock("favoritpartsrost
 21027    }
 1028
 1029    private async Task<string?> GetProfitLigaBasketAppendixAsync(IReadOnlyCollection<int> orderIds,
 1030        IReadOnlySet<int> ozonFailedOrderIds)
 1031    {
 21032        var raw = Get("ProfitLigaBasketQueueJson");
 41033        if (string.IsNullOrWhiteSpace(raw)) return null;
 1034        List<ProfitLigaQueueItem> queue;
 01035        try { queue = ParseProfitLigaBasketQueue(raw); }
 01036        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Профит-Лиги не обработана.
 01037        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01038        var blocked = queue.Where(x => ozonFailedOrderIds.Contains(x.OrderId)).ToList();
 01039        var blocks = new List<string>();
 01040        if (blocked.Count > 0)
 1041        {
 1042            var shipments = string.Join(", ", blocked.Select(x => x.ShipmentNumber ?? $"ID {x.OrderId}").Distinct());
 01043            blocks.Add("<div class='alert alert-warning mb-0'><strong>Корзина Профит-Лиги: позиции не добавлены.</strong
 01044                       $"Отгрузка Ozon завершилась ошибкой: {System.Net.WebUtility.HtmlEncode(shipments)}.</div>");
 1045        }
 01046        if (selected.Count == 0) return blocks.Count == 0 ? null : string.Join("<br/>", blocks);
 1047        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 01048            return "<div class='alert alert-warning mb-0'><strong>Корзина Профит-Лиги не обработана.</strong> Для заказа
 01049        blocks.Add((await _profitLigaCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock());
 1050        return string.Join("<br/>", blocks.Where(x => !string.IsNullOrWhiteSpace(x)));
 21051    }
 1052
 1053    private async Task<string?> GetNikeiBasketAppendixAsync(List<int> orderIds)
 1054    {
 21055        var raw = Get("NikeiBasketQueueJson");
 41056        if (string.IsNullOrWhiteSpace(raw)) return null;
 1057        List<NikeiCartQueueItem> queue;
 01058        try { queue = ParseNikeiBasketQueue(raw); }
 01059        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Nikei не обработана.</stro
 01060        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01061        if (selected.Count == 0) return null;
 1062        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 01063            return "<div class='alert alert-warning mb-0'><strong>Корзина Nikei не обработана.</strong> Для заказа выбра
 01064        return (await _nikeiCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock();
 21065    }
 1066
 1067    private async Task<string?> GetMoskvorechieBasketAppendixAsync(List<int> orderIds)
 1068    {
 21069        var raw = Get("MoskvorechieBasketQueueJson");
 41070        if (string.IsNullOrWhiteSpace(raw)) return null;
 1071        List<MoskvorechieIstraQueueItem> queue;
 01072        try { queue = ParseMoskvorechieBasketQueue(raw); }
 01073        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Москворечья не обработана.
 01074        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01075        if (selected.Count == 0) return null;
 01076        return (await _moskvorechieIstraCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock();
 21077    }
 1078
 1079    private async Task<string?> GetMikadoBasketAppendixAsync(List<int> orderIds)
 1080    {
 21081        var raw = Get("MikadoBasketQueueJson");
 41082        if (string.IsNullOrWhiteSpace(raw)) return null;
 1083        try
 1084        {
 01085            using var document = JsonDocument.Parse(raw);
 1086            var queue = document.RootElement.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.Object)
 1087                .Select(x => new MikadoQueueItem(ReadInt(x, "orderId"), ReadString(x, "orderCode"), ReadInt(x, "stockId"
 01088            if (queue.Count == 0) return null;
 01089            var result = await _mikadoCartService.QueueAsync(queue, _userName, _ct);
 01090            var messages = string.Join("<br/>", result.Messages.Select(System.Net.WebUtility.HtmlEncode));
 01091            return $"<div class='alert alert-{(result.Failed > 0 ? "warning" : "success")} mb-0'><strong>Корзина Микадо:
 1092        }
 01093        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Микадо не обработана.</str
 21094    }
 1095
 1096    private async Task<string?> GetMotexBasketAppendixAsync(List<int> orderIds)
 1097    {
 21098        var raw = Get("MotexBasketQueueJson");
 41099        if (string.IsNullOrWhiteSpace(raw)) return null;
 1100        List<MotexCartQueueItem> queue;
 01101        try { queue = ParseMotexBasketQueue(raw); }
 01102        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина МоТехС не обработана.</str
 01103        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01104        if (selected.Count == 0) return null;
 01105        return (await _motexCartService.QueueAsync(selected, _userName, _ct)).ToHtmlBlock();
 21106    }
 1107
 1108    private async Task<string?> GetShateMBasketAppendixAsync(List<int> orderIds)
 1109    {
 21110        var raw = Get("ShateMBasketQueueJson");
 41111        if (string.IsNullOrWhiteSpace(raw) || raw == "[]") return null;
 1112        try
 1113        {
 01114            var queue = ParseShateMBasketQueue(raw);
 01115            var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01116            if (selected.Count == 0) return null;
 01117            var options = ParseShateMOptions(Get("ShateMCheckoutOptionsJson"));
 01118            return (await _shateMCartService.QueueAsync(selected, options, _userName, _ct)).ToHtmlBlock();
 1119        }
 01120        catch (JsonException ex) { return $"<div class='alert alert-warning mb-0'><strong>Корзина Шате‑М не обработана.<
 21121    }
 1122
 1123    private async Task<string?> GetShateMDirectCheckoutAppendixAsync(List<int> orderIds)
 1124    {
 21125        var raw = Get("ShateMCheckoutQueueJson");
 31126        if (string.IsNullOrWhiteSpace(raw) || raw == "[]") return null;
 1127        try
 1128        {
 11129            var queue = ParseShateMBasketQueue(raw);
 21130            var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 11131            if (selected.Count == 0) return null;
 11132            var options = ParseShateMOptions(Get("ShateMCheckoutOptionsJson"));
 1133
 1134            // Прямой заказ Шате-М уходит поставщику здесь, уже после записи транзакции, поэтому намерение пишется здесь
 1135            // Конфликт с другим запросом откатить нельзя — заказ просто не отправляется, причина в блоке результата.
 1136            var targets = selected.Select(x => new SupplierOrderTarget(x.OrderId, OzonDomains.Models.ApiSupplierSettings
 11137            var start = await _guard.BeginDispatchAsync(_requestId, "Grid", targets, _guardResult, _userName, _ct);
 11138            if (!start.CanProceed)
 01139                return $"<div class='alert alert-warning mb-0'><strong>Заказ Шате‑М не оформлен.</strong> {System.Net.We
 1140
 11141            var result = await _shateMCartService.CheckoutDirectAsync(selected, options, _userName, _ct);
 01142            await _guard.CompleteDispatchAsync(start.Dispatch!);
 01143            return result.ToHtmlBlock();
 1144        }
 01145        catch (JsonException ex) { return $"<div class='alert alert-warning mb-0'><strong>Заказ Шате‑М не оформлен.</str
 11146    }
 1147
 1148    private static ShateMCheckoutOptions ParseShateMOptions(string raw)
 1149    {
 11150        if (string.IsNullOrWhiteSpace(raw))
 01151            throw new JsonException("Не переданы параметры оформления Шате‑М.");
 1152
 11153        using var document = JsonDocument.Parse(raw);
 11154        if (document.RootElement.ValueKind != JsonValueKind.Object)
 01155            throw new JsonException("Параметры оформления Шате‑М должны быть JSON-объектом.");
 1156
 11157        var root = document.RootElement;
 11158        var agreementCode = ReadString(root, "agreementCode");
 11159        var deliveryAddressCode = ReadString(root, "deliveryAddressCode");
 11160        var deliveryType = ReadString(root, "deliveryType");
 11161        var deliveryAtRaw = ReadString(root, "deliveryAtUtc");
 11162        if (!TryParseShateMDeliveryDate(deliveryAtRaw, out var deliveryAt))
 01163            throw new JsonException("Некорректная дата доставки Шате‑М.");
 1164
 11165        return new ShateMCheckoutOptions(
 11166            agreementCode,
 11167            deliveryAddressCode,
 11168            deliveryType,
 11169            deliveryAt,
 11170            ReadBool(root, "agreeWithTermsOfDelivery"),
 11171            ReadBool(root, "agreeWithPersonalDataProcessingPolicyAndUserAgreement"));
 11172    }
 1173
 1174    /// <summary>Смещение слота сохраняется: Шате‑М считает днём доставки календарную дату в присланном смещении.</summa
 1175    internal static bool TryParseShateMDeliveryDate(string value, out DateTimeOffset result)
 1176    {
 11177        var styles = DateTimeStyles.AllowWhiteSpaces;
 11178        return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, styles, out result)
 11179               || DateTimeOffset.TryParse(value, CultureInfo.GetCultureInfo("ru-RU"), styles, out result);
 1180    }
 1181
 1182    /// <summary>Снимок проценки Шате‑М содержит артикул и priceId как число либо строку.</summary>
 1183    private static List<ShateMCartQueueItem> ParseShateMBasketQueue(string raw)
 1184    {
 11185        using var document = JsonDocument.Parse(raw);
 11186        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01187            throw new JsonException("Очередь Шате‑М должна быть JSON-массивом.");
 1188
 11189        return document.RootElement.EnumerateArray()
 11190            .Where(item => item.ValueKind == JsonValueKind.Object)
 11191            .Select(item => new ShateMCartQueueItem(
 11192                ReadInt(item, "orderId"),
 11193                ReadString(item, "article"),
 11194                ReadString(item, "brand"),
 11195                ReadNullableString(item, "productName"),
 11196                ReadNullableString(item, "priceId"),
 11197                ReadNullableString(item, "locationCode"),
 11198                ReadNullableString(item, "direction"),
 11199                ReadDecimal(item, "quantity"),
 11200                ReadNullableDecimal(item, "sourcePrice"),
 11201                ReadNullableString(item, "currencyCode"),
 11202                ReadNullableInt(item, "deliveryDaysMin"),
 11203                ReadNullableInt(item, "deliveryDaysMax")))
 11204            .ToList();
 11205    }
 1206
 1207    private async Task<string?> GetZZapBasketAppendixAsync(List<int> orderIds)
 1208    {
 21209        var raw = Get("ZZapMoscowBasketQueueJson");
 41210        if (string.IsNullOrWhiteSpace(raw)) return null;
 1211        List<ZZapCartQueueItem> queue;
 01212        try { queue = ParseZZapBasketQueue(raw); }
 01213        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина ZZap не обработана.</stron
 01214        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01215        if (selected.Count == 0) return null;
 01216        var result = await _zzapCartService.QueueAsync(selected, _userName, _ct);
 01217        return result.ToHtmlBlock();
 21218    }
 1219
 1220    /// <summary>Снимок предложения ZZap из проценки: code_doc_b — строка, чтобы не потерять точность int64 в JS.</summa
 1221    private static List<ZZapCartQueueItem> ParseZZapBasketQueue(string raw)
 1222    {
 01223        using var document = JsonDocument.Parse(raw);
 01224        if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("ZZap basket queue must be an
 01225        return document.RootElement.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.Object).Select(x => new ZZa
 01226            ReadInt(x, "orderId"), ReadString(x, "codeDocB"), ReadString(x, "article"), ReadString(x, "brand"), ReadNull
 01227            ReadNullableString(x, "sellerName"), ReadNullableString(x, "sellerUserKey"), ReadNullableString(x, "sellerLo
 01228            ReadInt(x, "quantity"), ReadInt(x, "minimumQuantity"), ReadNullableDecimal(x, "sourcePrice"), ReadInt(x, "de
 01229            ReadNullableInt(x, "codeRegion"), ReadNullableString(x, "direction"), ReadNullableInt(x, "deliveryDaysMin"),
 01230            ReadNullableInt(x, "deliveryDaysMax"), ReadNullableString(x, "shipmentNumber"), ReadNullableString(x, "clien
 01231            ReadNullableDecimal(x, "minSumOrder"), ReadNullableInt(x, "qtyMax"))).ToList();
 01232    }
 1233
 1234    private static List<MotexCartQueueItem> ParseMotexBasketQueue(string raw)
 1235    {
 01236        using var document = JsonDocument.Parse(raw);
 01237        if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("Motex basket queue must be a
 01238        return document.RootElement.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.Object).Select(x => new Mot
 01239            ReadInt(x, "orderId"), ReadString(x, "article"), ReadString(x, "brand"), ReadNullableString(x, "productName"
 01240            ReadNullableString(x, "storeCode"), ReadNullableString(x, "storeInfo"), ReadNullableString(x, "shipmentNumbe
 01241            ReadDecimal(x, "quantity"), ReadNullableDecimal(x, "sourcePrice"), ReadNullableString(x, "currencyCode"),
 01242            ReadNullableString(x, "deliveryAtUtc"), ReadNullableInt(x, "deliveryDaysMin"), ReadNullableInt(x, "deliveryD
 01243    }
 1244
 1245    /// <summary>Браузер передаёт снимок проценки: цена и количество могут прийти числом либо строкой.</summary>
 1246    private static List<ProfitLigaQueueItem> ParseProfitLigaBasketQueue(string raw)
 1247    {
 01248        using var document = JsonDocument.Parse(raw);
 01249        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01250            throw new JsonException("ProfitLiga basket queue must be an array.");
 1251
 01252        return document.RootElement.EnumerateArray()
 01253            .Where(item => item.ValueKind == JsonValueKind.Object)
 01254            .Select(item => new ProfitLigaQueueItem(
 01255                ReadInt(item, "orderId"), ReadString(item, "articleId"), ReadString(item, "warehouseId"),
 01256                ReadNullableString(item, "productCode"), ReadString(item, "article"), ReadString(item, "brand"),
 01257                ReadNullableString(item, "productName"), ReadNullableString(item, "warehouseName"),
 01258                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"),
 01259                ReadDecimal(item, "quantity"), ReadNullableInt(item, "multi") ?? 0,
 01260                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 01261                ReadNullableString(item, "zasylWarehouseName"), ReadNullableInt(item, "deliveryDaysMin"),
 01262                ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(item, "direction")))
 01263            .ToList();
 01264    }
 1265
 1266    private static List<NikeiCartQueueItem> ParseNikeiBasketQueue(string raw)
 1267    {
 01268        using var document = JsonDocument.Parse(raw);
 01269        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01270            throw new JsonException("Nikei basket queue must be an array.");
 01271        return document.RootElement.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.Object).Select(x =>
 01272            new NikeiCartQueueItem(ReadInt(x, "orderId"), ReadString(x, "toCart"), ReadNullableString(x, "productKey"),
 01273                ReadString(x, "article"), ReadString(x, "brand"), ReadNullableString(x, "productName"),
 01274                ReadNullableString(x, "warehouseName"), ReadInt(x, "quantity"), ReadNullableInt(x, "multiplicity") ?? 1,
 01275                ReadNullableInt(x, "stock"), ReadNullableDecimal(x, "sourcePrice"), ReadNullableInt(x, "deliveryDaysMin"
 01276                ReadNullableInt(x, "deliveryDaysMax"), ReadNullableString(x, "shipmentNumber"), ReadNullableString(x, "c
 01277                ReadNullableInt(x, "shippingType"), ReadNullableInt(x, "shippingAddress"))).ToList();
 01278    }
 1279
 1280    private static List<MoskvorechieIstraQueueItem> ParseMoskvorechieBasketQueue(string raw)
 1281    {
 01282        using var document = JsonDocument.Parse(raw);
 01283        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01284            throw new JsonException("Moskvorechie basket queue must be an array.");
 01285        return document.RootElement.EnumerateArray().Where(item => item.ValueKind == JsonValueKind.Object)
 01286            .Select(item => new MoskvorechieIstraQueueItem(ReadInt(item, "orderId"), ReadString(item, "offerId"),
 01287                ReadString(item, "article"), ReadString(item, "brand"), ReadNullableString(item, "productName"),
 01288                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"), ReadDecimal(item, "q
 01289                ReadDecimal(item, "minimumQuantity"), ReadNullableDecimal(item, "sourcePrice"), ReadNullableInt(item, "d
 01290                ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(item, "direction"))).ToList();
 01291    }
 1292
 1293    /// <summary>Браузер передаёт снимок проценки: цена и количество могут прийти числом либо строкой.</summary>
 1294    private static List<FavoritPartsQueueItem> ParseFavoritPartsBasketQueue(string raw)
 1295    {
 01296        using var document = JsonDocument.Parse(raw);
 01297        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01298            throw new JsonException("FavoritParts basket queue must be an array.");
 1299
 01300        return document.RootElement.EnumerateArray()
 01301            .Where(item => item.ValueKind == JsonValueKind.Object)
 01302            .Select(item => new FavoritPartsQueueItem(
 01303                ReadInt(item, "orderId"), ReadString(item, "goodsId"), ReadString(item, "warehouseGroupId"),
 01304                ReadString(item, "article"), ReadString(item, "brand"), ReadNullableString(item, "productName"),
 01305                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"), ReadDecimal(item, "q
 01306                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 01307                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 01308            .ToList();
 01309    }
 1310
 381311    private static string ReadString(JsonElement item, string name) => ReadNullableString(item, name) ?? string.Empty;
 1312    private static string? ReadNullableString(JsonElement item, string name) =>
 1701313        !item.TryGetProperty(name, out var value) || value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined ? 
 1701314        value.ValueKind == JsonValueKind.String ? value.GetString() : value.GetRawText();
 1315    private static int ReadInt(JsonElement item, string name) =>
 91316        int.TryParse(ReadNullableString(item, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) 
 1317    private static int? ReadNullableInt(JsonElement item, string name) =>
 181318        int.TryParse(ReadNullableString(item, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) 
 91319    private static decimal ReadDecimal(JsonElement item, string name) => ReadNullableDecimal(item, name) ?? 0m;
 1320    private static decimal? ReadNullableDecimal(JsonElement item, string name) =>
 181321        decimal.TryParse(ReadNullableString(item, name)?.Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCultu
 1322    private static bool ReadBool(JsonElement item, string name) =>
 21323        bool.TryParse(ReadNullableString(item, name), out var value) && value;
 1324}

Methods/Properties

.ctor(OzonOrdersWeb.TransactionServices.OrderToSupplierOzonShipService,OzonOrdersWeb.TransactionServices.AvdBasketPlacementService,OzonOrdersWeb.TransactionServices.JapartsCartService,OzonOrdersWeb.TransactionServices.BergCartService,OzonOrdersWeb.TransactionServices.ArmtekCartService,OzonOrdersWeb.TransactionServices.MlAutoCartService,OzonOrdersWeb.TransactionServices.MlAutoRuCartService,OzonOrdersWeb.TransactionServices.ForumAutoCartService,OzonOrdersWeb.TransactionServices.FavoritPartsCartService,OzonOrdersWeb.TransactionServices.FavoritPartsIstraCartService,OzonOrdersWeb.TransactionServices.FavoritPartsRostovCartService,OzonOrdersWeb.TransactionServices.MoskvorechieIstraCartService,OzonOrdersWeb.TransactionServices.ProfitLigaCartService,OzonOrdersWeb.TransactionServices.NikeiCartService,OzonOrdersWeb.TransactionServices.MotexCartService,OzonOrdersWeb.TransactionServices.ShateMCartService,OzonOrdersWeb.TransactionServices.MikadoCartService,OzonOrdersWeb.TransactionServices.ZZapCartService,OzonOrdersWeb.TransactionServices.ISupplierOrderGuard)
get_OzonShipResult()
get_ReportRows()
get_HasQueues()
Bind(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>,System.String,System.Func`4<System.String,System.String,System.String,System.Threading.Tasks.Task>,System.Threading.CancellationToken,OzonOrdersWeb.TransactionServices.SupplierOrderConfirmation,System.String)
Get(System.String)
GetAll(System.String)
GuardAsync()
ArmtekConfirmedOrderIds()
PreviewArmtekAsync()
CheckoutDirectAsync()
CheckoutDirectCoreAsync()
PlaceBasketsAsync()
FilterBlockedBasketQueuesAsync()
AppendDirectCheckoutBlock(System.Collections.Generic.List`1<System.String>,System.Collections.Generic.HashSet`1<System.String>,System.String,System.String)
GetMlAutoBasketAppendixAsync()
ParseMlAutoBasketQueue(System.String)
GetMlAutoRuBasketAppendixAsync()
GetJapartsBasketAppendixAsync()
GetBergBasketAppendixAsync()
ParseBergBasketQueue(System.Collections.Generic.IEnumerable`1<System.String>)
ReadBergQueueItem(System.Text.Json.JsonElement)
ParseJapartsBasketQueue(System.Collections.Generic.IEnumerable`1<System.String>)
ReadJapartsQueueItem(System.Text.Json.JsonElement)
ReadJapartsQueueString(System.Text.Json.JsonElement,System.String)
ReadJapartsQueueDecimal(System.Text.Json.JsonElement,System.String)
GetAvdBasketAppendixAsync()
GetArmtekBasketAppendixAsync()
ParseArmtekBasketQueue(System.String)
GetForumAutoBasketAppendixAsync()
ParseForumAutoBasketQueue(System.String)
GetFavoritPartsBasketAppendixAsync()
GetFavoritPartsIstraBasketAppendixAsync()
GetFavoritPartsRostovBasketAppendixAsync()
GetProfitLigaBasketAppendixAsync()
GetNikeiBasketAppendixAsync()
GetMoskvorechieBasketAppendixAsync()
GetMikadoBasketAppendixAsync()
GetMotexBasketAppendixAsync()
GetShateMBasketAppendixAsync()
GetShateMDirectCheckoutAppendixAsync()
ParseShateMOptions(System.String)
TryParseShateMDeliveryDate(System.String,System.DateTimeOffset&)
ParseShateMBasketQueue(System.String)
GetZZapBasketAppendixAsync()
ParseZZapBasketQueue(System.String)
ParseMotexBasketQueue(System.String)
ParseProfitLigaBasketQueue(System.String)
ParseNikeiBasketQueue(System.String)
ParseMoskvorechieBasketQueue(System.String)
ParseFavoritPartsBasketQueue(System.String)
ReadString(System.Text.Json.JsonElement,System.String)
ReadNullableString(System.Text.Json.JsonElement,System.String)
ReadInt(System.Text.Json.JsonElement,System.String)
ReadNullableInt(System.Text.Json.JsonElement,System.String)
ReadDecimal(System.Text.Json.JsonElement,System.String)
ReadNullableDecimal(System.Text.Json.JsonElement,System.String)
ReadBool(System.Text.Json.JsonElement,System.String)