< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.Transactions.OrderToSupplierTransactionController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Transactions/OrderToSupplierTransactionController.cs
Line coverage
0%
Covered lines: 3
Uncovered lines: 825
Coverable lines: 828
Total lines: 1361
Line coverage: 0.3%
Branch coverage
0%
Covered branches: 2
Total branches: 558
Branch coverage: 0.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetTransactionConfig()100%210%
ApplyQuantitySplitCancellationLogic()0%210140%
CreateOrderToSupplierTransactionV2(...)100%210%
CreateOrderToSupplierTransactionV2(...)100%210%
PreviewArmtekOrder()0%2040%
ExecutePreTransactionAsync()0%7280%
ExecutePreTransactionCoreAsync()0%191821380%
GetPostTransactionSuccessAppendixAsync()0%3192560%
GetSupplierVerificationBlockAsync()0%620%
AppendDirectCheckoutBlock(...)0%1056320%
GetMlAutoBasketAppendixAsync()0%110100%
ParseMlAutoBasketQueue(...)0%620%
GetMlAutoRuBasketAppendixAsync()0%110100%
GetJapartsBasketAppendixAsync()0%272160%
GetBergBasketAppendixAsync()0%272160%
ParseBergBasketQueue(...)0%110100%
ReadBergQueueItem(...)0%2756520%
ParseJapartsBasketQueue(...)0%110100%
ReadJapartsQueueItem(...)0%702260%
ReadJapartsQueueString(...)0%7280%
ReadJapartsQueueDecimal(...)0%110100%
GetAvdBasketAppendixAsync()0%272160%
GetArmtekBasketAppendixAsync()0%110100%
ParseArmtekBasketQueue(...)0%620%
GetForumAutoBasketAppendixAsync()0%110100%
ParseForumAutoBasketQueue(...)0%620%
GetFavoritPartsBasketAppendixAsync()0%7280%
GetFavoritPartsIstraBasketAppendixAsync()0%7280%
GetFavoritPartsRostovBasketAppendixAsync()0%7280%
GetProfitLigaBasketAppendixAsync()0%210140%
GetNikeiBasketAppendixAsync()0%110100%
GetMoskvorechieBasketAppendixAsync()0%7280%
GetMikadoBasketAppendixAsync()0%110100%
GetMotexBasketAppendixAsync()0%7280%
GetShateMBasketAppendixAsync()0%110100%
GetShateMDirectCheckoutAppendixAsync()0%110100%
ParseShateMOptions(...)0%4260%
TryParseShateMDeliveryDate(...)100%22100%
ParseShateMBasketQueue(...)0%620%
GetZZapBasketAppendixAsync()0%7280%
ParseZZapBasketQueue(...)0%620%
ParseMotexBasketQueue(...)0%620%
ParseProfitLigaBasketQueue(...)0%620%
ParseNikeiBasketQueue(...)0%620%
ParseMoskvorechieBasketQueue(...)0%620%
ParseFavoritPartsBasketQueue(...)0%620%
ReadString(...)0%620%
ReadNullableString(...)0%110100%
ReadInt(...)0%620%
ReadNullableInt(...)0%620%
ReadDecimal(...)100%210%
ReadNullableDecimal(...)0%2040%
ReadBool(...)100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Transactions/OrderToSupplierTransactionController.cs

#LineLine coverage
 1using System.Text.Json;
 2using System.Globalization;
 3using Microsoft.AspNetCore.Mvc;
 4using Microsoft.EntityFrameworkCore;
 5using Microsoft.Extensions.DependencyInjection;
 6using OzonDomains;
 7using OzonDomains.Models;
 8using OzonDomains.Models.SupplierStatuses;
 9using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 10using OzonRepositories.Context;
 11using PartsInfo.HttpUtils;
 12using Services.ApiServices;
 13using Services.ApiServices.OzonApi;
 14using Services.CacheServices.Cache.OzonOrdersCache;
 15using Services.CacheServices.Cache.UserCacheService;
 16using Services.DataServices;
 17using OzonOrdersWeb.WebServices.Currency;
 18using Services.TransactionUtilsServices;
 19using Services.CacheServices.Cache;
 20using OzonOrdersWeb.TransactionServices;
 21using MikadoCartService = OzonOrdersWeb.TransactionServices.MikadoCartService;
 22using MikadoQueueItem = OzonOrdersWeb.TransactionServices.MikadoQueueItem;
 23using ZZapCartService = OzonOrdersWeb.TransactionServices.ZZapCartService;
 24using ZZapCartQueueItem = OzonOrdersWeb.TransactionServices.ZZapCartQueueItem;
 25using ZZapCartResult = OzonOrdersWeb.TransactionServices.ZZapCartResult;
 26using Services.ApiServices.JapartsApi;
 27using Services.ApiServices.BergApi;
 28using Services.SupplierStatusServices;
 29using OzonOrdersWeb.WebServices.Transactions;
 30
 31namespace OzonOrdersWeb.Areas.Studio2.Controllers.Transactions;
 32
 33/// <summary>
 34/// Контроллер транзакции "Заказан поставщику"
 35/// </summary>
 36public class OrderToSupplierTransactionController : BaseTransactionController
 37{
 38    private const string ArmtekDirectCheckoutResultKey = "ArmtekDirectCheckoutResult";
 39    private const string MlAutoDirectCheckoutResultKey = "MlAutoDirectCheckoutResult";
 40    private const string MlAutoRuDirectCheckoutResultKey = "MlAutoRuDirectCheckoutResult";
 41    private const string AvdDirectCheckoutResultKey = "AvdDirectCheckoutResult";
 42    private const string ForumAutoDirectCheckoutResultKey = "ForumAutoDirectCheckoutResult";
 43    private const string FavoritPartsDirectCheckoutResultKey = "FavoritPartsDirectCheckoutResult";
 44    private const string FavoritPartsIstraDirectCheckoutResultKey = "FavoritPartsIstraDirectCheckoutResult";
 45    private const string FavoritPartsRostovDirectCheckoutResultKey = "FavoritPartsRostovDirectCheckoutResult";
 46    private const string ProfitLigaDirectCheckoutResultKey = "ProfitLigaDirectCheckoutResult";
 47    private const string MoskvorechieIstraDirectCheckoutResultKey = "MoskvorechieIstraDirectCheckoutResult";
 48    private const string MotexDirectCheckoutResultKey = "MotexDirectCheckoutResult";
 49    private const string ZZapMoscowDirectCheckoutResultKey = "ZZapMoscowDirectCheckoutResult";
 50    private const string JapartsDirectCheckoutResultKey = "JapartsDirectCheckoutResult";
 51    private const string BergDirectCheckoutResultKey = "BergDirectCheckoutResult";
 52    private readonly OrderToSupplierOzonShipService _orderToSupplierOzonShipService;
 53    private readonly AvdBasketPlacementService _avdBasketPlacementService;
 54    private readonly JapartsCartService _japartsCartService;
 55    private readonly BergCartService _bergCartService;
 56    private readonly ArmtekCartService _armtekCartService;
 57    private readonly MlAutoCartService _mlAutoCartService;
 58    private readonly MlAutoRuCartService _mlAutoRuCartService;
 59    private readonly ForumAutoCartService _forumAutoCartService;
 60    private readonly FavoritPartsCartService _favoritPartsCartService;
 61    private readonly FavoritPartsIstraCartService _favoritPartsIstraCartService;
 62    private readonly FavoritPartsRostovCartService _favoritPartsRostovCartService;
 63    private readonly MoskvorechieIstraCartService _moskvorechieIstraCartService;
 64    private readonly ProfitLigaCartService _profitLigaCartService;
 65    private readonly NikeiCartService _nikeiCartService;
 66    private readonly MotexCartService _motexCartService;
 67    private readonly ShateMCartService _shateMCartService;
 68    private readonly MikadoCartService _mikadoCartService;
 69    private readonly ZZapCartService _zzapCartService;
 70
 71    public OrderToSupplierTransactionController(
 72        ApplicationDbContext context,
 73        AppStatusDataServices appStatusServices,
 74        OrdersDataServices orderServices,
 75        TransactionManager transactionManager,
 76        TransactionCache transactionCache,
 77        SupplierDataServices supplierDataServices,
 78        IUserCacheService userCacheService,
 79        ICurrencyRatesViewBagService currencyRatesViewBagService,
 80        CacheUpdater<Order> cacheUpdater,
 81        OrderCache orderCache,
 82        OneCDataManager oneCDataManager,
 83        WarehouseDataServices warehouseDataServices,
 84        ProxyHttpClientService proxyHttpClientService,
 85        OrderCartServices orderCartServices,
 86        ExpenseTypeDataServices expenseTypeDataServices,
 87        OrderExtraExpenseDataServices orderExtraExpenseDataServices,
 88        StatusTransitionDataServices statusTransitionDataServices,
 89        OrderToSupplierOzonShipService orderToSupplierOzonShipService,
 90        AvdBasketPlacementService avdBasketPlacementService,
 91        JapartsCartService japartsCartService,
 92        BergCartService bergCartService,
 93        ArmtekCartService armtekCartService, MlAutoCartService mlAutoCartService, MlAutoRuCartService mlAutoRuCartServic
 94        ForumAutoCartService forumAutoCartService, FavoritPartsCartService favoritPartsCartService, FavoritPartsIstraCar
 95        OzonOrderCancellationService? ozonCancellationService = null)
 096        : base(context, appStatusServices, orderServices, transactionManager, transactionCache,
 097            supplierDataServices, userCacheService, currencyRatesViewBagService, cacheUpdater, orderCache,
 098            oneCDataManager, warehouseDataServices, proxyHttpClientService: proxyHttpClientService,
 099            orderCartServices: orderCartServices, expenseTypeDataServices: expenseTypeDataServices,
 0100            orderExtraExpenseDataServices: orderExtraExpenseDataServices,
 0101            statusTransitionDataServices: statusTransitionDataServices,
 0102            ozonCancellationService: ozonCancellationService)
 103    {
 0104        _orderToSupplierOzonShipService = orderToSupplierOzonShipService;
 0105        _avdBasketPlacementService = avdBasketPlacementService;
 0106        _japartsCartService = japartsCartService;
 0107        _bergCartService = bergCartService;
 0108        _armtekCartService = armtekCartService;
 0109        _mlAutoCartService = mlAutoCartService;
 0110        _mlAutoRuCartService = mlAutoRuCartService;
 0111        _forumAutoCartService = forumAutoCartService;
 0112        _favoritPartsCartService = favoritPartsCartService;
 0113        _favoritPartsIstraCartService = favoritPartsIstraCartService;
 0114        _favoritPartsRostovCartService = favoritPartsRostovCartService;
 0115        _moskvorechieIstraCartService = moskvorechieIstraCartService;
 0116        _profitLigaCartService = profitLigaCartService;
 0117        _nikeiCartService = nikeiCartService;
 0118        _motexCartService = motexCartService;
 0119        _shateMCartService = shateMCartService;
 0120        _mikadoCartService = mikadoCartService;
 0121        _zzapCartService = zzapCartService;
 0122    }
 123
 0124    protected override TransactionConfig GetTransactionConfig() => new()
 0125    {
 0126        TransactionType = TransactionType.OrderedToSupplier,
 0127        PrimaryStatusName = "Заказан поставщику",
 0128        AdditionalStatusNames = new List<string> { "Заказан реализатору" },
 0129        UseStatusTransitions = true,
 0130        RequiredSourceStatuses = new List<string> { "Не указан", "Заказан поставщику", "Заказан реализатору", "К отмене"
 0131        ClearPurchasePriceOnLoad = true,
 0132        ViewName = "CreateOrderToSupplierTransactionV2",
 0133        IncludeCancelledOrders = true,
 0134        IncludeToCancellationStatus = true,
 0135        IncludeNotReadyStatus = true,
 0136        SupportsOzonCancelPanel = true,
 0137        OrderFilter = TransactionConfig.StandardOrderFilter,
 0138        CreateTransactionMethod = (tm, orders, userName, createAt, comment) =>
 0139            tm.CreateOrderToSupplierTransaction(orders, userName, createAt, comment),
 0140        SuccessMessageBuilder = (changeCount, cancelCount, dateTime, userName, comment, orders) =>
 0141        {
 0142            // Базовое стандартное сообщение
 0143            var baseMsg = TransactionConfig.BuildStandardSuccessMessage(
 0144                changeCount, cancelCount, dateTime, userName, comment, orders, "Заказан поставщику");
 0145
 0146            // Дополнительная информация про разбиение заказов и статус "К отмене"
 0147            return TransactionConfig.BuildStandardSuccessMessage(
 0148                changeCount,
 0149                cancelCount,
 0150                dateTime,
 0151                userName,
 0152                comment,
 0153                orders,
 0154                "Заказан поставщику",
 0155                "<div class='alert alert-warning mb-0'>" +
 0156                "<strong>Внимание по количеству</strong><br/>" +
 0157                "Часть заказов с уменьшенным количеством была автоматически разделена, " +
 0158                "а остатки переведены в статус <b>К отмене</b>." +
 0159                "</div>");
 0160        },
 0161        // Специальная логика только для транзакции "Заказан поставщику":
 0162        // при уменьшении количества создаём часть "к отмене".
 0163        AfterTransactOrdersAsync = async updatedOrders =>
 0164            await ApplyQuantitySplitCancellationLogic(updatedOrders)
 0165    };
 166
 167    /// <summary>
 168    /// Специальная логика только для OrderToSupplierTransaction:
 169    /// - если количество уменьшено (SplitOrder создал /1 и /2), то:
 170    ///   * часть /1 переводим в статус "К отмене"
 171    ///   * часть /2 остаётся для статуса "Заказан поставщику"
 172    /// - если количество уменьшено до 0, то заказ переводим в статус "К отмене".
 173    /// </summary>
 174    private async Task ApplyQuantitySplitCancellationLogic(List<Order> updatedOrders)
 175    {
 0176        if (updatedOrders == null || updatedOrders.Count == 0)
 177        {
 0178            return;
 179        }
 180
 181        // Статус "К отмене"
 0182        var toCancellationStatus = await GetOrCreateAppStatus("К отмене");
 183
 0184        var now = DateTime.Now;
 185
 186        // 1) Полное уменьшение количества до 0 — переводим заказ в "К отмене"
 187        foreach (var order in updatedOrders.Where(o => (o.Quantity ?? 0) == 0))
 188        {
 0189            order.AppStatusId = toCancellationStatus.Id;
 0190            order.AppStatus = toCancellationStatus;
 0191            order.LastStatusChangeDate = now;
 0192            order.IsVerified = true;
 193        }
 194
 195        // 2) Частичное уменьшение: SplitOrder создаёт пару /1 и /2.
 196        // updatedOrders содержит только вторую часть (/2) с новым количеством.
 0197        var secondParts = updatedOrders
 198            .Where(o => !string.IsNullOrEmpty(o.ShipmentNumber) &&
 199                        o.ShipmentNumber!.EndsWith("/2"))
 0200            .ToList();
 201
 0202        if (secondParts.Count > 0)
 203        {
 204            // Для каждого /2 находим соответствующий /1 и переводим его в "К отмене"
 0205            var firstShipmentNumbers = secondParts
 206                .Select(o => o.ShipmentNumber![..^2] + "/1")
 0207                .Distinct()
 0208                .ToList();
 209
 0210            var firstParts = await Context.Orders
 0211                .Where(o => o.ShipmentNumber != null &&
 0212                            firstShipmentNumbers.Contains(o.ShipmentNumber))
 0213                .ToListAsync();
 214
 215
 0216            foreach (var original in firstParts)
 217            {
 218                // Если после разбиения в первой части что‑то осталось, помечаем её "К отмене"
 0219                if (original.Quantity is > 0)
 220                {
 0221                    original.AppStatusId = toCancellationStatus.Id;
 0222                    original.AppStatus = toCancellationStatus;
 0223                    original.LastStatusChangeDate = now;
 0224                    original.IsVerified = true;
 225                }
 226            }
 227        }
 228
 0229        await Context.SaveChangesAsync();
 0230    }
 231
 232    public Task<IActionResult> CreateOrderToSupplierTransactionV2(string ids, int page)
 0233        => CreateTransactionBase(ids, page);
 234
 235    [HttpPost]
 236    [ValidateAntiForgeryToken]
 237    public Task<IActionResult> CreateOrderToSupplierTransactionV2(
 238        List<Order> orders, string userName, string? comment,
 239        int page, string? deletedOrders, Dictionary<int, string?>? orderSupplierMap)
 0240        => CreateTransactionPostBase(orders, userName, comment, page, deletedOrders, orderSupplierMap);
 241
 242    [HttpPost]
 243    [ValidateAntiForgeryToken]
 244    public async Task<IActionResult> PreviewArmtekOrder([FromForm] string? armtekCheckoutQueueJson, CancellationToken ct
 245    {
 0246        if (string.IsNullOrWhiteSpace(armtekCheckoutQueueJson))
 0247            return BadRequest(new { error = "Не выбраны позиции Armtek для оформления." });
 248        try
 249        {
 0250            var queue = ParseArmtekBasketQueue(armtekCheckoutQueueJson);
 0251            var result = await _armtekCartService.BuildDirectOrderPreviewAsync(queue, ct);
 0252            return result.Errors.Count > 0
 0253                ? BadRequest(new { error = string.Join(" ", result.Errors) })
 0254                : Json(new { orders = result.Previews });
 255        }
 0256        catch (JsonException)
 257        {
 0258            return BadRequest(new { error = "Не удалось прочитать выбранные позиции Armtek." });
 259        }
 0260    }
 261
 262    protected override async Task<string?> ExecutePreTransactionAsync(List<Order> orders)
 263    {
 264        // Классический поток не умеет запрашивать подтверждение: повторный заказ у поставщика здесь блокируется всегда.
 265        // Подтвердить повтор можно только в новом гриде проведения (TransactionsGrid).
 266        var permittedForGuard = orders.Select(x => x.Id).ToHashSet();
 0267        var formQueues = Request.Form
 268            .Where(x => x.Key.EndsWith("QueueJson", StringComparison.OrdinalIgnoreCase))
 269            .Select(x => new KeyValuePair<string, string>(x.Key, x.Value.ToString()));
 0270        var guardTargets = SupplierQueueTargets.Extract(formQueues).Where(x => permittedForGuard.Contains(x.OrderId)).To
 0271        var supplierGuard = HttpContext.RequestServices.GetRequiredService<ISupplierOrderGuard>();
 0272        var guardResult = await supplierGuard.EvaluateAsync(guardTargets, null, User.Identity?.Name, HttpContext.Request
 0273        if (!guardResult.IsAllowed)
 0274            return guardResult.BlockingMessage().Replace(" Чтобы оформить повторно, подтвердите это явно.",
 0275                " Повторное оформление возможно только в новом гриде проведения с явным подтверждением.", StringComparis
 276
 277        // Намерения (Placing) фиксируются до обращения к поставщикам; после ответа — Placed/Failed.
 278        // Шате-М в классическом потоке оформляется после записи транзакции и намерений не ведёт: завершить их здесь зна
 279        // пометить заказ Failed до отправки.
 0280        var dispatchTargets = guardTargets
 281            .Where(x => !string.Equals(x.ProviderKey, ApiSupplierSettings.ShateMProviderKey, StringComparison.OrdinalIgn
 0282            .ToList();
 0283        var dispatchStart = await supplierGuard.BeginDispatchAsync(Guid.NewGuid().ToString("N"), "Legacy", dispatchTarge
 0284            guardResult, User.Identity?.Name, HttpContext.RequestAborted);
 0285        if (!dispatchStart.CanProceed) return dispatchStart.Message;
 286
 0287        var error = await ExecutePreTransactionCoreAsync(orders);
 0288        await supplierGuard.CompleteDispatchAsync(dispatchStart.Dispatch!);
 0289        return error;
 0290    }
 291
 292    private async Task<string?> ExecutePreTransactionCoreAsync(List<Order> orders)
 293    {
 0294        var raw = Request.Form["ArmtekCheckoutQueueJson"].ToString();
 0295        if (string.IsNullOrWhiteSpace(raw)) raw = "[]";
 296        List<ArmtekCartQueueItem> queue;
 0297        try { queue = ParseArmtekBasketQueue(raw); }
 0298        catch (JsonException) { return "Не удалось прочитать выбранные позиции Armtek."; }
 299
 300        var permittedOrderIds = orders.Select(x => x.Id).ToHashSet();
 301        // Пустая очередь Armtek означает только, что прямой заказ выбран для другого поставщика.
 0302        if (queue.Count > 0)
 303        {
 0304            if (queue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0305                return "Состав предложений Armtek не соответствует проводимым заказам. Обновите страницу и повторите поп
 306            try
 307            {
 0308                var result = await _armtekCartService.CheckoutDirectAsync(queue, User.Identity?.Name ?? "unknown", HttpC
 0309                if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0310                HttpContext.Items[ArmtekDirectCheckoutResultKey] = result;
 0311            }
 0312            catch (Exception ex) when (ex is Services.ApiServices.ArmtekApi.ArmtekApiException or HttpRequestException)
 313            {
 0314                return $"Армтек: {ex.Message}";
 315            }
 316        }
 317
 0318        var user = User.Identity?.Name ?? "unknown";
 0319        var mlAutoRaw = Request.Form["MlAutoCheckoutQueueJson"].ToString();
 0320        if (!string.IsNullOrWhiteSpace(mlAutoRaw))
 321        {
 322            try
 323            {
 0324                var directQueue = ParseMlAutoBasketQueue(mlAutoRaw);
 0325                if (directQueue.Count > 0)
 326                {
 0327                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений ML-Auto
 0328                    var result = await _mlAutoCartService.CheckoutDirectAsync(directQueue, user, HttpContext.RequestAbor
 0329                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0330                    HttpContext.Items[MlAutoDirectCheckoutResultKey] = result;
 331                }
 0332            }
 0333            catch (JsonException) { return "Не удалось прочитать выбранные позиции ML-Auto BY."; }
 0334            catch (Services.ApiServices.MlAutoApi.MlAutoApiException ex) { return $"ML-Auto BY: {ex.Message}"; }
 335        }
 336
 0337        var mlAutoRuRaw = Request.Form["MlAutoRuCheckoutQueueJson"].ToString();
 0338        if (!string.IsNullOrWhiteSpace(mlAutoRuRaw))
 339        {
 340            try
 341            {
 342                var directQueue = ParseMlAutoBasketQueue(mlAutoRuRaw).Select(x => new MlAutoRuCartQueueItem(x.OrderId, x
 0343                if (directQueue.Count > 0)
 344                {
 0345                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений ML-Auto
 0346                    var result = await _mlAutoRuCartService.CheckoutDirectAsync(directQueue, user, HttpContext.RequestAb
 0347                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0348                    HttpContext.Items[MlAutoRuDirectCheckoutResultKey] = result;
 349                }
 0350            }
 0351            catch (JsonException) { return "Не удалось прочитать выбранные позиции ML-Auto RU."; }
 0352            catch (Services.ApiServices.MlAutoApi.MlAutoApiException ex) { return $"ML-Auto RU: {ex.Message}"; }
 353        }
 354
 0355        var avdRaw = Request.Form["AvdCheckoutQueueJson"].ToString();
 0356        if (!string.IsNullOrWhiteSpace(avdRaw))
 357        {
 358            try
 359            {
 0360                var directQueue = JsonSerializer.Deserialize<List<AvdBasketQueueItem>>(avdRaw, new JsonSerializerOptions
 0361                if (directQueue.Count > 0)
 362                {
 0363                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений АВД не 
 0364                    var result = await _avdBasketPlacementService.CheckoutDirectAsync(directQueue, user, HttpContext.Req
 0365                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0366                    HttpContext.Items[AvdDirectCheckoutResultKey] = result;
 367                }
 0368            }
 0369            catch (JsonException) { return "Не удалось прочитать выбранные позиции АВД."; }
 0370            catch (Services.ApiServices.AvdApi.AvdApiException ex) { return $"АВД: {ex.Message}"; }
 371        }
 372
 0373        var japartsRaw = Request.Form["JapartsCheckoutQueueJson"];
 0374        if (japartsRaw.Count > 0 && !japartsRaw.All(string.IsNullOrWhiteSpace))
 375        {
 376            List<JapartsCartQueueItem> directQueue;
 0377            try { directQueue = ParseJapartsBasketQueue(japartsRaw).Items; }
 0378            catch (JsonException) { return "Не удалось прочитать выбранные позиции Japarts."; }
 0379            if (directQueue.Count > 0)
 380            {
 0381                if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0382                    return "Состав предложений Japarts не соответствует проводимым заказам. Обновите страницу и повторит
 0383                var result = await _japartsCartService.CheckoutDirectAsync(directQueue, user, HttpContext.RequestAborted
 0384                if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0385                HttpContext.Items[JapartsDirectCheckoutResultKey] = result;
 386            }
 387        }
 388
 0389        var bergRaw = Request.Form["BergCheckoutQueueJson"];
 0390        if (bergRaw.Count > 0 && !bergRaw.All(string.IsNullOrWhiteSpace))
 391        {
 392            List<BergCartQueueItem> directQueue;
 0393            try { directQueue = ParseBergBasketQueue(bergRaw).Items; }
 0394            catch (JsonException) { return "Не удалось прочитать выбранные позиции Berg."; }
 0395            if (directQueue.Count > 0)
 396            {
 0397                if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0398                    return "Состав предложений Berg не соответствует проводимым заказам. Обновите страницу и повторите п
 0399                var result = await _bergCartService.CheckoutDirectAsync(directQueue, user, HttpContext.RequestAborted);
 0400                if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0401                HttpContext.Items[BergDirectCheckoutResultKey] = result;
 402            }
 403        }
 404
 0405        var forumAutoRaw = Request.Form["ForumAutoCheckoutQueueJson"].ToString();
 0406        if (!string.IsNullOrWhiteSpace(forumAutoRaw))
 407        {
 408            try
 409            {
 0410                var directQueue = ParseForumAutoBasketQueue(forumAutoRaw);
 0411                if (directQueue.Count > 0)
 412                {
 0413                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0414                        return "Состав предложений Форум-Авто не соответствует проводимым заказам. Обновите страницу и п
 0415                    var result = await _forumAutoCartService.CheckoutDirectAsync(directQueue, user, HttpContext.RequestA
 0416                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0417                    HttpContext.Items[ForumAutoDirectCheckoutResultKey] = result;
 418                }
 0419            }
 0420            catch (JsonException) { return "Не удалось прочитать выбранные позиции Форум-Авто."; }
 0421            catch (Services.ApiServices.ForumAutoApi.ForumAutoApiException ex) { return $"Форум-Авто: {ex.Message}"; }
 422        }
 423
 0424        var favoritPartsRaw = Request.Form["FavoritPartsCheckoutQueueJson"].ToString();
 0425        if (!string.IsNullOrWhiteSpace(favoritPartsRaw))
 426        {
 427            try
 428            {
 0429                var directQueue = ParseFavoritPartsBasketQueue(favoritPartsRaw);
 0430                if (directQueue.Count > 0)
 431                {
 0432                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0433                        return "Состав предложений FavoritParts не соответствует проводимым заказам. Обновите страницу и
 0434                    var result = await _favoritPartsCartService.CheckoutDirectAsync(directQueue, User.Identity?.Name ?? 
 0435                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0436                    HttpContext.Items[FavoritPartsDirectCheckoutResultKey] = result;
 437                }
 0438            }
 0439            catch (JsonException) { return "Не удалось прочитать выбранные позиции FavoritParts."; }
 0440            catch (Services.ApiServices.FavoritPartsApi.FavoritPartsApiException ex) { return $"FavoritParts: {ex.Messag
 441        }
 442
 0443        var favoritPartsIstraRaw = Request.Form["FavoritPartsIstraCheckoutQueueJson"].ToString();
 0444        if (!string.IsNullOrWhiteSpace(favoritPartsIstraRaw))
 445        {
 446            try
 447            {
 0448                var directQueue = ParseFavoritPartsBasketQueue(favoritPartsIstraRaw);
 0449                if (directQueue.Count > 0)
 450                {
 0451                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0452                        return "Состав предложений Фаворит Истра не соответствует проводимым заказам. Обновите страницу 
 0453                    var result = await _favoritPartsIstraCartService.CheckoutDirectAsync(directQueue, User.Identity?.Nam
 0454                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0455                    HttpContext.Items[FavoritPartsIstraDirectCheckoutResultKey] = result;
 456                }
 0457            }
 0458            catch (JsonException) { return "Не удалось прочитать выбранные позиции Фаворит Истра."; }
 0459            catch (Services.ApiServices.FavoritPartsApi.FavoritPartsApiException ex) { return $"Фаворит Истра: {ex.Messa
 460        }
 461
 0462        var favoritPartsRostovRaw = Request.Form["FavoritPartsRostovCheckoutQueueJson"].ToString();
 0463        if (!string.IsNullOrWhiteSpace(favoritPartsRostovRaw))
 464        {
 465            try
 466            {
 0467                var directQueue = ParseFavoritPartsBasketQueue(favoritPartsRostovRaw);
 0468                if (directQueue.Count > 0)
 469                {
 0470                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0471                        return "Состав предложений Фаворит Ростов не соответствует проводимым заказам. Обновите страницу
 0472                    var result = await _favoritPartsRostovCartService.CheckoutDirectAsync(directQueue, User.Identity?.Na
 0473                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0474                    HttpContext.Items[FavoritPartsRostovDirectCheckoutResultKey] = result;
 475                }
 0476            }
 0477            catch (JsonException) { return "Не удалось прочитать выбранные позиции Фаворит Ростов."; }
 0478            catch (Services.ApiServices.FavoritPartsApi.FavoritPartsApiException ex) { return $"Фаворит Ростов: {ex.Mess
 479        }
 480
 0481        var moskvorechieRaw = Request.Form["MoskvorechieCheckoutQueueJson"].ToString();
 0482        if (!string.IsNullOrWhiteSpace(moskvorechieRaw))
 483        {
 484            try
 485            {
 0486                var directQueue = ParseMoskvorechieBasketQueue(moskvorechieRaw);
 0487                if (directQueue.Count > 0)
 488                {
 0489                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений Москвор
 0490                    var result = await _moskvorechieIstraCartService.CheckoutDirectAsync(directQueue, user, HttpContext.
 0491                    if (result.Failed > 0) return string.Join(" ", result.Messages);
 0492                    HttpContext.Items[MoskvorechieIstraDirectCheckoutResultKey] = result;
 493                }
 0494            }
 0495            catch (JsonException) { return "Не удалось прочитать выбранные позиции Москворечья."; }
 0496            catch (Services.ApiServices.MoskvorechieApi.MoskvorechieApiException ex) { return $"Москворечье: {ex.Message
 497        }
 498
 0499        var profitLigaRaw = Request.Form["ProfitLigaCheckoutQueueJson"].ToString();
 0500        if (!string.IsNullOrWhiteSpace(profitLigaRaw))
 501        {
 502            try
 503            {
 0504                var directQueue = ParseProfitLigaBasketQueue(profitLigaRaw);
 0505                if (directQueue.Count > 0)
 506                {
 0507                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId)))
 0508                        return "Состав предложений Профит-Лиги не соответствует проводимым заказам. Обновите страницу и 
 0509                    var result = await _profitLigaCartService.CheckoutDirectAsync(directQueue, user, HttpContext.Request
 0510                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0511                    HttpContext.Items[ProfitLigaDirectCheckoutResultKey] = result;
 512                }
 0513            }
 0514            catch (JsonException) { return "Не удалось прочитать выбранные позиции Профит-Лиги."; }
 0515            catch (Services.ApiServices.ProfitLigaApi.ProfitLigaApiException ex) { return $"Профит-Лига: {ex.Message}"; 
 516        }
 517
 0518        var motexRaw = Request.Form["MotexCheckoutQueueJson"].ToString();
 0519        if (!string.IsNullOrWhiteSpace(motexRaw))
 520        {
 521            try
 522            {
 0523                var directQueue = ParseMotexBasketQueue(motexRaw);
 0524                if (directQueue.Count > 0)
 525                {
 0526                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений МоТехС 
 0527                    var result = await _motexCartService.CheckoutDirectAsync(directQueue, user, HttpContext.RequestAbort
 0528                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0529                    HttpContext.Items[MotexDirectCheckoutResultKey] = result;
 530                }
 0531            }
 0532            catch (JsonException) { return "Не удалось прочитать выбранные позиции МоТехС."; }
 533        }
 534
 0535        var zzapRaw = Request.Form["ZZapMoscowCheckoutQueueJson"].ToString();
 0536        if (!string.IsNullOrWhiteSpace(zzapRaw))
 537        {
 538            try
 539            {
 0540                var directQueue = ParseZZapBasketQueue(zzapRaw);
 0541                if (directQueue.Count > 0)
 542                {
 0543                    if (directQueue.Any(x => !permittedOrderIds.Contains(x.OrderId))) return "Состав предложений ZZap не
 0544                    var result = await _zzapCartService.CheckoutDirectAsync(directQueue, user, HttpContext.RequestAborte
 0545                    if (!result.IsSuccess) return string.Join(" ", result.Messages);
 0546                    HttpContext.Items[ZZapMoscowDirectCheckoutResultKey] = result;
 547                }
 0548            }
 0549            catch (JsonException) { return "Не удалось прочитать выбранные позиции ZZap."; }
 550        }
 551
 0552        return null;
 0553    }
 554
 555    protected override async Task<string?> GetPostTransactionSuccessAppendixAsync(
 556        List<Order> ordersToTransaction,
 557        TransactionConfig config)
 558    {
 0559        if (config.TransactionType != TransactionType.OrderedToSupplier)
 0560            return null;
 561
 0562        var orderIds = ordersToTransaction
 563            .Select(o => o.Id)
 564            .Where(id => id > 0)
 0565            .Distinct()
 0566            .ToList();
 567
 0568        var blocks = new List<string>();
 0569        var profitLigaOrderIds = orderIds;
 570
 0571        var shouldShipInOzon = Request.Form.TryGetValue("shipInOzon", out var rawValues) &&
 572                               rawValues.Any(v => string.Equals(v, "true", StringComparison.OrdinalIgnoreCase));
 0573        if (shouldShipInOzon && orderIds.Count > 0)
 574        {
 0575            await ReportStageAsync("ozon-ship", "Отгружаем в Ozon",
 0576                orderIds.Count == 1 ? "1 отправление" : $"отправлений: {orderIds.Count}");
 0577            var shipResult = await _orderToSupplierOzonShipService.ShipAsync(orderIds, HttpContext.RequestAborted);
 0578            HttpContext.Items[TransactionCommentAiHttpContextKeys.OzonShipResult] = shipResult;
 0579            profitLigaOrderIds = shipResult.EligibleOrderIds.ToList();
 0580            var shipBlock = shipResult.ToHtmlBlock();
 0581            if (!string.IsNullOrWhiteSpace(shipBlock))
 0582                blocks.Add(shipBlock);
 583        }
 584
 0585        await ReportStageAsync("carts", "Размещаем позиции в корзинах поставщиков");
 0586        var avdBlock = await GetAvdBasketAppendixAsync(orderIds);
 0587        if (!string.IsNullOrWhiteSpace(avdBlock))
 0588            blocks.Add(avdBlock);
 0589        var japartsBlock = await GetJapartsBasketAppendixAsync(orderIds);
 0590        var bergBlock = await GetBergBasketAppendixAsync(orderIds);
 0591        if (!string.IsNullOrWhiteSpace(japartsBlock)) blocks.Add(japartsBlock);
 0592        if (!string.IsNullOrWhiteSpace(bergBlock)) blocks.Add(bergBlock);
 0593        var armtekBlock = await GetArmtekBasketAppendixAsync(orderIds);
 0594        if (!string.IsNullOrWhiteSpace(armtekBlock)) blocks.Add(armtekBlock);
 0595        if (HttpContext.Items.TryGetValue(ArmtekDirectCheckoutResultKey, out var directResultObject) &&
 0596            directResultObject is ArmtekDirectCheckoutResult directResult)
 597        {
 0598            var lines = string.Join(string.Empty, directResult.Messages.Select(message =>
 599                $"<li>{System.Net.WebUtility.HtmlEncode(message)}</li>"));
 0600            blocks.Add($"<div class='alert alert-success mb-0'><strong>Заказ Armtek оформлен.</strong><ul class='mb-0'>{
 601        }
 0602        AppendDirectCheckoutBlock(blocks, MlAutoDirectCheckoutResultKey, "Заказ ML-Auto BY оформлен.");
 0603        AppendDirectCheckoutBlock(blocks, MlAutoRuDirectCheckoutResultKey, "Заказ ML-Auto RU оформлен.");
 0604        AppendDirectCheckoutBlock(blocks, AvdDirectCheckoutResultKey, "Заказ АВД оформлен.");
 0605        AppendDirectCheckoutBlock(blocks, ForumAutoDirectCheckoutResultKey, "Заказ Форум-Авто оформлен.");
 0606        AppendDirectCheckoutBlock(blocks, FavoritPartsDirectCheckoutResultKey, "Заказ FavoritParts оформлен.");
 0607        AppendDirectCheckoutBlock(blocks, FavoritPartsIstraDirectCheckoutResultKey, "Заказ Фаворит Истра оформлен.");
 0608        AppendDirectCheckoutBlock(blocks, FavoritPartsRostovDirectCheckoutResultKey, "Заказ Фаворит Ростов оформлен.");
 0609        AppendDirectCheckoutBlock(blocks, MoskvorechieIstraDirectCheckoutResultKey, "Заказ Москворечья оформлен.");
 0610        AppendDirectCheckoutBlock(blocks, MotexDirectCheckoutResultKey, "Заказ МоТехС оформлен.");
 0611        AppendDirectCheckoutBlock(blocks, ZZapMoscowDirectCheckoutResultKey, "Заказ ZZap оформлен.");
 0612        AppendDirectCheckoutBlock(blocks, JapartsDirectCheckoutResultKey, "Заказ Japarts оформлен.");
 0613        AppendDirectCheckoutBlock(blocks, BergDirectCheckoutResultKey, "Заказ Berg оформлен.");
 0614        var mlAutoBlock = await GetMlAutoBasketAppendixAsync(orderIds);
 0615        if (!string.IsNullOrWhiteSpace(mlAutoBlock)) blocks.Add(mlAutoBlock);
 0616        var mlAutoRuBlock = await GetMlAutoRuBasketAppendixAsync(orderIds);
 0617        if (!string.IsNullOrWhiteSpace(mlAutoRuBlock)) blocks.Add(mlAutoRuBlock);
 0618        var forumAutoBlock = await GetForumAutoBasketAppendixAsync(orderIds);
 0619        if (!string.IsNullOrWhiteSpace(forumAutoBlock)) blocks.Add(forumAutoBlock);
 0620        var favoritPartsBlock = await GetFavoritPartsBasketAppendixAsync(orderIds);
 0621        if (!string.IsNullOrWhiteSpace(favoritPartsBlock)) blocks.Add(favoritPartsBlock);
 0622        var favoritPartsIstraBlock = await GetFavoritPartsIstraBasketAppendixAsync(orderIds);
 0623        var favoritPartsRostovBlock = await GetFavoritPartsRostovBasketAppendixAsync(orderIds);
 0624        if (!string.IsNullOrWhiteSpace(favoritPartsIstraBlock)) blocks.Add(favoritPartsIstraBlock);
 0625        if (!string.IsNullOrWhiteSpace(favoritPartsRostovBlock)) blocks.Add(favoritPartsRostovBlock);
 0626        AppendDirectCheckoutBlock(blocks, ProfitLigaDirectCheckoutResultKey, "Заказ Профит-Лиги оформлен.");
 0627        var profitLigaBlock = await GetProfitLigaBasketAppendixAsync(profitLigaOrderIds,
 0628            orderIds.Except(profitLigaOrderIds).ToHashSet());
 0629        if (!string.IsNullOrWhiteSpace(profitLigaBlock)) blocks.Add(profitLigaBlock);
 0630        var nikeiBlock = await GetNikeiBasketAppendixAsync(orderIds);
 0631        if (!string.IsNullOrWhiteSpace(nikeiBlock)) blocks.Add(nikeiBlock);
 0632        var mikadoBlock = await GetMikadoBasketAppendixAsync(orderIds);
 0633        var zzapBlock = await GetZZapBasketAppendixAsync(orderIds);
 0634        if (!string.IsNullOrWhiteSpace(zzapBlock)) blocks.Add(zzapBlock);
 0635        if (!string.IsNullOrWhiteSpace(mikadoBlock)) blocks.Add(mikadoBlock);
 0636        var moskvorechieBlock = await GetMoskvorechieBasketAppendixAsync(orderIds);
 0637        if (!string.IsNullOrWhiteSpace(moskvorechieBlock)) blocks.Add(moskvorechieBlock);
 0638        var motexBlock = await GetMotexBasketAppendixAsync(orderIds);
 0639        if (!string.IsNullOrWhiteSpace(motexBlock)) blocks.Add(motexBlock);
 0640        var shateMBlock = await GetShateMBasketAppendixAsync(orderIds);
 0641        if (!string.IsNullOrWhiteSpace(shateMBlock)) blocks.Add(shateMBlock);
 0642        var shateMDirectBlock = await GetShateMDirectCheckoutAppendixAsync(orderIds);
 0643        if (!string.IsNullOrWhiteSpace(shateMDirectBlock)) blocks.Add(shateMDirectBlock);
 644
 0645        var verificationBlock = await GetSupplierVerificationBlockAsync(orderIds);
 0646        if (!string.IsNullOrWhiteSpace(verificationBlock)) blocks.Add(verificationBlock);
 647
 0648        return blocks.Count == 0 ? null : string.Join("<br/>", blocks);
 0649    }
 650
 651    /// <summary>
 652    /// Независимая проверка у API-поставщиков, что заказы реально созданы (только сигнал: транзакция уже проведена).
 653    /// Пишет системную запись в историю заказа и возвращает HTML-блок для карточки результата.
 654    /// </summary>
 655    private async Task<string?> GetSupplierVerificationBlockAsync(IReadOnlyCollection<int> orderIds)
 656    {
 0657        var queueJsons = Request.Form
 658            .Where(x => x.Key.EndsWith("QueueJson", StringComparison.OrdinalIgnoreCase))
 659            .SelectMany(x => x.Value.AsEnumerable());
 0660        var required = SupplierQueueOrderIds.Extract(queueJsons).Where(orderIds.Contains).ToList();
 0661        if (required.Count == 0) return null;
 662
 0663        await ReportStageAsync("supplier-verify", "Проверяем заказы у поставщиков", $"позиций: {required.Count}");
 0664        var verification = HttpContext.RequestServices.GetRequiredService<ISupplierOrderVerificationService>();
 0665        var summary = await verification.VerifyAsync(orderIds, required, ct: HttpContext.RequestAborted);
 0666        return SupplierVerificationDto.ToHtmlBlock(summary);
 0667    }
 668
 669    private void AppendDirectCheckoutBlock(List<string> blocks, string key, string title)
 670    {
 0671        if (!HttpContext.Items.TryGetValue(key, out var result)) return;
 0672        var messages = result switch
 0673        {
 0674            MlAutoDirectCheckoutResult value => value.Messages,
 0675            MlAutoRuDirectCheckoutResult value => value.Messages,
 0676            AvdDirectCheckoutResult value => value.Messages,
 0677            ForumAutoDirectCheckoutResult value => value.Messages,
 0678            FavoritPartsDirectCheckoutResult value => value.Messages,
 0679            ProfitLigaDirectCheckoutResult value => value.Messages,
 0680            MoskvorechieIstraCartResult value => value.Messages,
 0681            MotexCartResult value => value.Messages,
 0682            ZZapCartResult value => value.Messages,
 0683            JapartsCartResult value => value.Messages,
 0684            BergCartResult value => value.Messages,
 0685            _ => null
 0686        };
 0687        if (messages is null) return;
 0688        var hasPostError = messages.Any(m => m.Contains("не проведён", StringComparison.OrdinalIgnoreCase)
 0689                                             || m.Contains("не проведен", StringComparison.OrdinalIgnoreCase));
 0690        var alertClass = hasPostError ? "alert alert-warning" : "alert alert-success";
 0691        var headerTitle = hasPostError ? $"{title} (внимание: есть непроведённые заказы)" : title;
 0692        var lines = string.Join(string.Empty, messages.Select(message => $"<li>{System.Net.WebUtility.HtmlEncode(message
 0693        blocks.Add($"<div class='{alertClass} mb-0'><strong>{headerTitle}</strong><ul class='mb-0'>{lines}</ul></div>");
 0694    }
 695
 696    private async Task<string?> GetMlAutoBasketAppendixAsync(List<int> orderIds)
 697    {
 0698        var raw = Request.Form["MlAutoBasketQueueJson"].ToString();
 0699        if (string.IsNullOrWhiteSpace(raw)) return null;
 700        List<MlAutoCartQueueItem> queue;
 0701        try { queue = ParseMlAutoBasketQueue(raw); }
 0702        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина ML-Auto не обработана.</st
 0703        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0704        if (selected.Count == 0) return null;
 705        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1)) return "<div class='alert alert-warning mb-0'><str
 0706        return (await _mlAutoCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.RequestAbort
 0707    }
 708
 709    /// <summary>Браузер может сериализовать цену и количество ML-Auto как число, строку либо null.</summary>
 710    private static List<MlAutoCartQueueItem> ParseMlAutoBasketQueue(string raw)
 711    {
 0712        using var document = JsonDocument.Parse(raw);
 0713        if (document.RootElement.ValueKind != JsonValueKind.Array)
 0714            throw new JsonException("ML-Auto basket queue must be an array.");
 715
 0716        return document.RootElement.EnumerateArray()
 0717            .Where(item => item.ValueKind == JsonValueKind.Object)
 0718            .Select(item => new MlAutoCartQueueItem(
 0719                ReadInt(item, "orderId"), ReadString(item, "pin"), ReadString(item, "brand"),
 0720                ReadNullableString(item, "storageCode"), ReadNullableString(item, "warehouseName"),
 0721                ReadNullableString(item, "productName"), ReadNullableString(item, "shipmentNumber"),
 0722                ReadNullableString(item, "clientName"), ReadDecimal(item, "quantity"),
 0723                ReadDecimal(item, "minimumQuantity"), ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item,
 0724                ReadNullableString(item, "deliveryAtUtc"), ReadNullableString(item, "zasylWarehouseName"),
 0725                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 0726            .ToList();
 0727    }
 728
 729    private async Task<string?> GetMlAutoRuBasketAppendixAsync(List<int> orderIds)
 730    {
 0731        var raw = Request.Form["MlAutoRuBasketQueueJson"].ToString();
 0732        if (string.IsNullOrWhiteSpace(raw)) return null;
 733        List<MlAutoRuCartQueueItem> queue;
 734        try { queue = ParseMlAutoBasketQueue(raw).Select(x => new MlAutoRuCartQueueItem(x.OrderId, x.Pin, x.Brand, x.Sto
 0735        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина ML-Auto RU не обработана.<
 0736        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0737        if (selected.Count == 0) return null;
 738        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1)) return "<div class='alert alert-warning mb-0'><str
 0739        return (await _mlAutoRuCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.RequestAbo
 0740    }
 741
 742    /// <summary>Размещение отложенной корзины АВД — последний шаг после проведения, позиции берём только из уже успешно
 743    private async Task<string?> GetJapartsBasketAppendixAsync(List<int> orderIds)
 744    {
 0745        var rawValues = Request.Form["JapartsBasketQueueJson"];
 0746        if (rawValues.Count == 0 || rawValues.All(string.IsNullOrWhiteSpace)) return null;
 747        List<JapartsCartQueueItem> queue;
 748        int skipped;
 0749        try { (queue, skipped) = ParseJapartsBasketQueue(rawValues); }
 0750        catch (JsonException exception)
 751        {
 0752            return "<div class='alert alert-warning mb-0'><strong>Корзина Japarts не обработана.</strong> " +
 0753                   $"Не удалось прочитать очередь выбранных позиций: {System.Net.WebUtility.HtmlEncode(exception.Message
 754        }
 0755        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0756        if (selected.Count == 0)
 0757            return skipped == 0
 0758                ? null
 0759                : $"<div class='alert alert-warning mb-0'><strong>Корзина Japarts не обработана.</strong> Позиций в очер
 760        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 0761            return "<div class='alert alert-warning mb-0'><strong>Корзина Japarts не обработана.</strong> Для одного зак
 762
 0763        var result = await _japartsCartService.PlaceQueuedItemsAsync(selected, User.Identity?.Name ?? "unknown",
 0764            HttpContext.RequestAborted);
 765        var lines = string.Join(string.Empty, result.Messages.Select(x => $"<li>{System.Net.WebUtility.HtmlEncode(x)}</l
 0766        return $"<div class='alert alert-{(result.IsSuccess ? "info" : "warning")} mb-0'><strong>Japarts.</strong><ul cl
 0767               "<div class='mt-2'>Заказ у поставщика не создан: оформите его вручную на странице " +
 0768               $"<a href='{Url.Action("Index", "SupplierCart", new { area = "Studio2", provider = "japarts", supplierTab
 0769    }
 770
 771    /// <summary>Отложенная корзина Berg: у поставщика корзины нет, поэтому позиции ложатся
 772    /// в локальную таблицу, а заказ оформляется отдельным действием на странице корзины.</summary>
 773    private async Task<string?> GetBergBasketAppendixAsync(List<int> orderIds)
 774    {
 0775        var rawValues = Request.Form["BergBasketQueueJson"];
 0776        if (rawValues.Count == 0 || rawValues.All(string.IsNullOrWhiteSpace)) return null;
 777        List<BergCartQueueItem> queue;
 778        int skipped;
 0779        try { (queue, skipped) = ParseBergBasketQueue(rawValues); }
 0780        catch (JsonException exception)
 781        {
 0782            return "<div class='alert alert-warning mb-0'><strong>Корзина Berg не обработана.</strong> " +
 0783                   $"Не удалось прочитать очередь выбранных позиций: {System.Net.WebUtility.HtmlEncode(exception.Message
 784        }
 0785        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0786        if (selected.Count == 0)
 0787            return skipped == 0
 0788                ? null
 0789                : $"<div class='alert alert-warning mb-0'><strong>Корзина Berg не обработана.</strong> Позиций в очереди
 790        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 0791            return "<div class='alert alert-warning mb-0'><strong>Корзина Berg не обработана.</strong> Для одного заказа
 792
 0793        var result = await _bergCartService.PlaceQueuedItemsAsync(selected, User.Identity?.Name ?? "unknown",
 0794            HttpContext.RequestAborted);
 795        var lines = string.Join(string.Empty, result.Messages.Select(x => $"<li>{System.Net.WebUtility.HtmlEncode(x)}</l
 0796        return $"<div class='alert alert-{(result.IsSuccess ? "info" : "warning")} mb-0'><strong>Берг.</strong><ul class
 0797               "<div class='mt-2'>Заказ у поставщика не создан: оформите его на странице " +
 0798               $"<a href='{Url.Action("Index", "SupplierCart", new { area = "Studio2", provider = "berg", supplierTab = 
 0799    }
 800
 801    private static (List<BergCartQueueItem> Items, int Skipped) ParseBergBasketQueue(IEnumerable<string> rawValues)
 802    {
 0803        var queue = new List<BergCartQueueItem>();
 0804        var skipped = 0;
 0805        foreach (var raw in rawValues.Where(value => !string.IsNullOrWhiteSpace(value)))
 806        {
 0807            using var document = JsonDocument.Parse(raw);
 0808            var elements = document.RootElement.ValueKind switch
 0809            {
 0810                JsonValueKind.Array => document.RootElement.EnumerateArray().ToArray(),
 0811                JsonValueKind.Object => [document.RootElement],
 0812                _ => throw new JsonException("очередь должна быть массивом или объектом.")
 0813            };
 0814            foreach (var element in elements)
 815            {
 0816                var item = ReadBergQueueItem(element);
 0817                if (item is null) skipped++;
 0818                else queue.Add(item);
 819            }
 820        }
 821
 0822        return (queue.Distinct().ToList(), skipped);
 823    }
 824
 825    /// <summary>
 826    /// Предложение Berg адресуется парой resource_id + warehouse_id. Фронтенд шлёт их отдельными
 827    /// полями, но itemId вида «resource|warehouse|price|days» остаётся запасным источником:
 828    /// он приходит из проценки и переживает обновление страницы.
 829    /// </summary>
 830    private static BergCartQueueItem? ReadBergQueueItem(JsonElement element)
 831    {
 0832        if (element.ValueKind != JsonValueKind.Object) return null;
 0833        var orderId = ReadJapartsQueueDecimal(element, "orderId");
 0834        if (orderId is null or <= 0 || orderId != decimal.Truncate(orderId.Value)) return null;
 835
 0836        var resourceId = (long?)ReadJapartsQueueDecimal(element, "resourceId");
 0837        var warehouseId = (long?)ReadJapartsQueueDecimal(element, "warehouseId");
 0838        if (resourceId is null or <= 0 || warehouseId is null or <= 0)
 839        {
 0840            var parts = (ReadJapartsQueueString(element, "itemId") ?? string.Empty).Split('|');
 0841            if (parts.Length < 2 || !long.TryParse(parts[0], out var parsedResource) ||
 0842                !long.TryParse(parts[1], out var parsedWarehouse))
 0843                return null;
 0844            resourceId = parsedResource;
 0845            warehouseId = parsedWarehouse;
 846        }
 847
 0848        var quantity = ReadJapartsQueueDecimal(element, "quantity");
 0849        var multi = (int?)ReadJapartsQueueDecimal(element, "minQuantity");
 0850        return new BergCartQueueItem((int)orderId.Value, resourceId.Value, warehouseId.Value,
 0851            quantity is null or <= 0 ? 1m : quantity.Value,
 0852            ReadJapartsQueueDecimal(element, "price"), ReadJapartsQueueString(element, "article"),
 0853            ReadJapartsQueueString(element, "brand"), ReadJapartsQueueString(element, "productName"),
 0854            ReadJapartsQueueString(element, "shipmentNumber"), ReadJapartsQueueString(element, "clientName"),
 0855            multi is null or <= 0 ? 1 : multi.Value,
 0856            (int?)ReadJapartsQueueDecimal(element, "deliveryDaysMin"), (int?)ReadJapartsQueueDecimal(element, "deliveryD
 0857            ReadJapartsQueueString(element, "direction"));
 858    }
 859
 860    /// <summary>Форма может прислать несколько значений скрытого поля; каждое значение разбираем отдельно, не склеивая 
 861    /// через запятую. Числа читаем терпимо — браузер отдаёт их то числом, то строкой, а NaN сериализует в null, — иначе
 862    /// одна кривая позиция роняла разбор всей очереди. Второе значение кортежа — сколько позиций пропущено как неполные
 863    private static (List<JapartsCartQueueItem> Items, int Skipped) ParseJapartsBasketQueue(IEnumerable<string> rawValues
 864    {
 0865        var queue = new List<JapartsCartQueueItem>();
 0866        var skipped = 0;
 0867        foreach (var raw in rawValues.Where(value => !string.IsNullOrWhiteSpace(value)))
 868        {
 0869            using var document = JsonDocument.Parse(raw);
 0870            var elements = document.RootElement.ValueKind switch
 0871            {
 0872                JsonValueKind.Array => document.RootElement.EnumerateArray().ToArray(),
 0873                JsonValueKind.Object => [document.RootElement],
 0874                _ => throw new JsonException("очередь должна быть массивом или объектом.")
 0875            };
 0876            foreach (var element in elements)
 877            {
 0878                var item = ReadJapartsQueueItem(element);
 0879                if (item is null) skipped++;
 0880                else queue.Add(item);
 881            }
 882        }
 883
 0884        return (queue.Distinct().ToList(), skipped);
 885    }
 886
 887    private static JapartsCartQueueItem? ReadJapartsQueueItem(JsonElement element)
 888    {
 0889        if (element.ValueKind != JsonValueKind.Object) return null;
 0890        var orderId = ReadJapartsQueueDecimal(element, "orderId");
 0891        var priceId = ReadJapartsQueueString(element, "priceId")
 0892                      ?? ReadJapartsQueueString(element, "itemHash")
 0893                      ?? ReadJapartsQueueString(element, "itemId");
 0894        if (orderId is null or <= 0 || orderId != decimal.Truncate(orderId.Value) || string.IsNullOrWhiteSpace(priceId))
 0895            return null;
 0896        var quantity = ReadJapartsQueueDecimal(element, "quantity");
 0897        return new JapartsCartQueueItem((int)orderId.Value, priceId!, quantity is null or <= 0 ? 1m : quantity.Value,
 0898            ReadJapartsQueueDecimal(element, "price"), ReadJapartsQueueString(element, "article"),
 0899            ReadJapartsQueueString(element, "productName"), ReadJapartsQueueString(element, "shipmentNumber"),
 0900            ReadJapartsQueueString(element, "clientName"),
 0901            (int?)ReadJapartsQueueDecimal(element, "deliveryDaysMin"), (int?)ReadJapartsQueueDecimal(element, "deliveryD
 0902            ReadJapartsQueueString(element, "direction"));
 903    }
 904
 905    private static string? ReadJapartsQueueString(JsonElement element, string name)
 906    {
 0907        if (!element.TryGetProperty(name, out var value)) return null;
 0908        var text = value.ValueKind switch
 0909        {
 0910            JsonValueKind.String => value.GetString(),
 0911            JsonValueKind.Number => value.GetRawText(),
 0912            _ => null
 0913        };
 0914        return string.IsNullOrWhiteSpace(text) ? null : text.Trim();
 915    }
 916
 917    private static decimal? ReadJapartsQueueDecimal(JsonElement element, string name)
 918    {
 0919        if (!element.TryGetProperty(name, out var value)) return null;
 0920        return value.ValueKind switch
 0921        {
 0922            JsonValueKind.Number when value.TryGetDecimal(out var number) => number,
 0923            JsonValueKind.String when decimal.TryParse(value.GetString(), NumberStyles.Any,
 0924                CultureInfo.InvariantCulture, out var parsed) => parsed,
 0925            _ => null
 0926        };
 927    }
 928
 929    private async Task<string?> GetAvdBasketAppendixAsync(List<int> orderIds)
 930    {
 0931        var queueJson = Request.Form["AvdBasketQueueJson"].ToString();
 0932        if (string.IsNullOrWhiteSpace(queueJson))
 0933            return null;
 934
 935        List<AvdBasketQueueItem>? queue;
 936        try
 937        {
 0938            queue = JsonSerializer.Deserialize<List<AvdBasketQueueItem>>(queueJson,
 0939                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
 0940        }
 0941        catch (JsonException)
 942        {
 0943            return "<div class='alert alert-warning mb-0'><strong>Корзина АВД не обработана.</strong> " +
 0944                   "Не удалось прочитать очередь выбранных позиций.</div>";
 945        }
 946
 0947        var selectedItems = (queue ?? new List<AvdBasketQueueItem>())
 0948            .Where(x => orderIds.Contains(x.OrderId))
 0949            .ToList();
 0950        if (selectedItems.Count == 0)
 0951            return null;
 952
 953        if (selectedItems.Any(x => string.IsNullOrWhiteSpace(x.ItemHash) || x.Quantity <= 0))
 954        {
 0955            return "<div class='alert alert-warning mb-0'><strong>Корзина АВД не обработана.</strong> " +
 0956                   "У выбранной позиции отсутствует hash предложения или указано некорректное количество.</div>";
 957        }
 958
 959        if (selectedItems.GroupBy(x => x.OrderId).Any(group => group.Count() > 1))
 960        {
 0961            return "<div class='alert alert-warning mb-0'><strong>Корзина АВД не обработана.</strong> " +
 0962                   "Для одного заказа выбрано несколько предложений.</div>";
 963        }
 964
 0965        var changedBy = User?.Identity?.Name ?? "unknown";
 0966        var result = await _avdBasketPlacementService.PlaceQueuedItemsAsync(selectedItems, changedBy, HttpContext.Reques
 0967        return result.ToHtmlBlock();
 0968    }
 969
 970    private async Task<string?> GetArmtekBasketAppendixAsync(List<int> orderIds)
 971    {
 0972        var raw = Request.Form["ArmtekBasketQueueJson"].ToString();
 0973        if (string.IsNullOrWhiteSpace(raw)) return null;
 974        List<ArmtekCartQueueItem> queue;
 0975        try { queue = ParseArmtekBasketQueue(raw); }
 0976        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Армтек не обработана.</str
 0977        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 0978        if (selected.Count == 0) return null;
 979        if (selected.GroupBy(x => new { x.ProviderKey, x.OrderId }).Any(x => x.Count() > 1))
 0980            return "<div class='alert alert-warning mb-0'><strong>Корзина Армтек не обработана.</strong> Для заказа выбр
 0981        var result = await _armtekCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.Request
 0982        return result.ToHtmlBlock();
 0983    }
 984
 985    /// <summary>Браузер передаёт снимок предложения: API может отдать цену/количество числом либо строкой.</summary>
 986    private static List<ArmtekCartQueueItem> ParseArmtekBasketQueue(string raw)
 987    {
 0988        using var document = JsonDocument.Parse(raw);
 0989        if (document.RootElement.ValueKind != JsonValueKind.Array)
 0990            throw new JsonException("Armtek basket queue must be an array.");
 991
 0992        return document.RootElement.EnumerateArray()
 0993            .Where(item => item.ValueKind == JsonValueKind.Object)
 0994            .Select(item => new ArmtekCartQueueItem(
 0995                ReadInt(item, "orderId"), ReadString(item, "providerKey"), ReadString(item, "artId"),
 0996                ReadString(item, "pin"), ReadString(item, "brand"), ReadNullableString(item, "keyzak"),
 0997                ReadNullableString(item, "warehouseName"), ReadNullableString(item, "partnerWarehouseCode"),
 0998                ReadNullableString(item, "productName"), ReadNullableString(item, "shipmentNumber"), ReadNullableString(
 0999                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 01000                ReadNullableString(item, "deliveryAtUtc"), ReadNullableString(item, "zasylWarehouseName"),
 01001                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 01002            .ToList();
 01003    }
 1004
 1005    private async Task<string?> GetForumAutoBasketAppendixAsync(List<int> orderIds)
 1006    {
 01007        var raw = Request.Form["ForumAutoBasketQueueJson"].ToString();
 01008        if (string.IsNullOrWhiteSpace(raw)) return null;
 1009        List<ForumAutoCartQueueItem> queue;
 01010        try { queue = ParseForumAutoBasketQueue(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;
 1014        if (selected.GroupBy(x => new { x.ProviderKey, x.OrderId }).Any(x => x.Count() > 1))
 01015            return "<div class='alert alert-warning mb-0'><strong>Корзина Форум-Авто не обработана.</strong> Для заказа 
 01016        var result = await _forumAutoCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.Requ
 01017        return result.ToHtmlBlock();
 01018    }
 1019
 1020    /// <summary>Браузер передаёт снимок проценки: цена и количество могут прийти числом либо строкой.</summary>
 1021    private static List<ForumAutoCartQueueItem> ParseForumAutoBasketQueue(string raw)
 1022    {
 01023        using var document = JsonDocument.Parse(raw);
 01024        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01025            throw new JsonException("Forum-Auto basket queue must be an array.");
 1026
 01027        return document.RootElement.EnumerateArray()
 01028            .Where(item => item.ValueKind == JsonValueKind.Object)
 01029            .Select(item => new ForumAutoCartQueueItem(
 01030                ReadInt(item, "orderId"), ReadString(item, "providerKey"), ReadString(item, "tid"),
 01031                ReadString(item, "article"), ReadString(item, "brand"), ReadNullableString(item, "productName"),
 01032                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"), ReadDecimal(item, "q
 01033                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 01034                ReadNullableString(item, "deliveryAtUtc"),
 01035                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 01036            .ToList();
 01037    }
 1038
 1039    private async Task<string?> GetFavoritPartsBasketAppendixAsync(List<int> orderIds)
 1040    {
 01041        var raw = Request.Form["FavoritPartsBasketQueueJson"].ToString();
 01042        if (string.IsNullOrWhiteSpace(raw)) return null;
 1043        List<FavoritPartsQueueItem> queue;
 01044        try { queue = ParseFavoritPartsBasketQueue(raw); }
 01045        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина FavoritParts не обработана
 01046        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01047        if (selected.Count == 0) return null;
 01048        var result = await _favoritPartsCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.R
 01049        return result.ToHtmlBlock();
 01050    }
 1051
 1052    private async Task<string?> GetFavoritPartsIstraBasketAppendixAsync(List<int> orderIds)
 1053    {
 01054        var raw = Request.Form["FavoritPartsIstraBasketQueueJson"].ToString();
 01055        if (string.IsNullOrWhiteSpace(raw)) return null;
 1056        List<FavoritPartsQueueItem> queue;
 01057        try { queue = ParseFavoritPartsBasketQueue(raw); }
 01058        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Фаворит Истра не обработан
 01059        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01060        if (selected.Count == 0) return null;
 01061        return (await _favoritPartsIstraCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.R
 01062    }
 1063
 1064    private async Task<string?> GetFavoritPartsRostovBasketAppendixAsync(List<int> orderIds)
 1065    {
 01066        var raw = Request.Form["FavoritPartsRostovBasketQueueJson"].ToString();
 01067        if (string.IsNullOrWhiteSpace(raw)) return null;
 1068        List<FavoritPartsQueueItem> queue;
 01069        try { queue = ParseFavoritPartsBasketQueue(raw); }
 01070        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Фаворит Ростов не обработа
 01071        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01072        if (selected.Count == 0) return null;
 01073        return (await _favoritPartsRostovCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.
 01074    }
 1075
 1076    private async Task<string?> GetProfitLigaBasketAppendixAsync(IReadOnlyCollection<int> orderIds,
 1077        IReadOnlySet<int> ozonFailedOrderIds)
 1078    {
 01079        var raw = Request.Form["ProfitLigaBasketQueueJson"].ToString();
 01080        if (string.IsNullOrWhiteSpace(raw)) return null;
 1081        List<ProfitLigaQueueItem> queue;
 01082        try { queue = ParseProfitLigaBasketQueue(raw); }
 01083        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Профит-Лиги не обработана.
 01084        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01085        var blocked = queue.Where(x => ozonFailedOrderIds.Contains(x.OrderId)).ToList();
 01086        var blocks = new List<string>();
 01087        if (blocked.Count > 0)
 1088        {
 1089            var shipments = string.Join(", ", blocked.Select(x => x.ShipmentNumber ?? $"ID {x.OrderId}").Distinct());
 01090            blocks.Add("<div class='alert alert-warning mb-0'><strong>Корзина Профит-Лиги: позиции не добавлены.</strong
 01091                       $"Отгрузка Ozon завершилась ошибкой: {System.Net.WebUtility.HtmlEncode(shipments)}.</div>");
 1092        }
 01093        if (selected.Count == 0) return blocks.Count == 0 ? null : string.Join("<br/>", blocks);
 1094        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 01095            return "<div class='alert alert-warning mb-0'><strong>Корзина Профит-Лиги не обработана.</strong> Для заказа
 01096        blocks.Add((await _profitLigaCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.Requ
 1097        return string.Join("<br/>", blocks.Where(x => !string.IsNullOrWhiteSpace(x)));
 01098    }
 1099
 1100    private async Task<string?> GetNikeiBasketAppendixAsync(List<int> orderIds)
 1101    {
 01102        var raw = Request.Form["NikeiBasketQueueJson"].ToString();
 01103        if (string.IsNullOrWhiteSpace(raw)) return null;
 1104        List<NikeiCartQueueItem> queue;
 01105        try { queue = ParseNikeiBasketQueue(raw); }
 01106        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Nikei не обработана.</stro
 01107        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01108        if (selected.Count == 0) return null;
 1109        if (selected.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 01110            return "<div class='alert alert-warning mb-0'><strong>Корзина Nikei не обработана.</strong> Для заказа выбра
 01111        return (await _nikeiCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.RequestAborte
 01112    }
 1113
 1114    private async Task<string?> GetMoskvorechieBasketAppendixAsync(List<int> orderIds)
 1115    {
 01116        var raw = Request.Form["MoskvorechieBasketQueueJson"].ToString();
 01117        if (string.IsNullOrWhiteSpace(raw)) return null;
 1118        List<MoskvorechieIstraQueueItem> queue;
 01119        try { queue = ParseMoskvorechieBasketQueue(raw); }
 01120        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Москворечья не обработана.
 01121        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01122        if (selected.Count == 0) return null;
 01123        return (await _moskvorechieIstraCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.R
 01124    }
 1125
 1126    private async Task<string?> GetMikadoBasketAppendixAsync(List<int> orderIds)
 1127    {
 01128        var raw = Request.Form["MikadoBasketQueueJson"].ToString();
 01129        if (string.IsNullOrWhiteSpace(raw)) return null;
 1130        try
 1131        {
 01132            using var document = JsonDocument.Parse(raw);
 1133            var queue = document.RootElement.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.Object)
 1134                .Select(x => new MikadoQueueItem(ReadInt(x, "orderId"), ReadString(x, "orderCode"), ReadInt(x, "stockId"
 01135            if (queue.Count == 0) return null;
 01136            var result = await _mikadoCartService.QueueAsync(queue, User.Identity?.Name ?? "unknown", HttpContext.Reques
 01137            var messages = string.Join("<br/>", result.Messages.Select(System.Net.WebUtility.HtmlEncode));
 01138            return $"<div class='alert alert-{(result.Failed > 0 ? "warning" : "success")} mb-0'><strong>Корзина Микадо:
 1139        }
 01140        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина Микадо не обработана.</str
 01141    }
 1142
 1143    private async Task<string?> GetMotexBasketAppendixAsync(List<int> orderIds)
 1144    {
 01145        var raw = Request.Form["MotexBasketQueueJson"].ToString();
 01146        if (string.IsNullOrWhiteSpace(raw)) return null;
 1147        List<MotexCartQueueItem> queue;
 01148        try { queue = ParseMotexBasketQueue(raw); }
 01149        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина МоТехС не обработана.</str
 01150        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01151        if (selected.Count == 0) return null;
 01152        return (await _motexCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.RequestAborte
 01153    }
 1154
 1155    private async Task<string?> GetShateMBasketAppendixAsync(List<int> orderIds)
 1156    {
 01157        var raw = Request.Form["ShateMBasketQueueJson"].ToString();
 01158        if (string.IsNullOrWhiteSpace(raw) || raw == "[]") return null;
 1159        try
 1160        {
 01161            var queue = ParseShateMBasketQueue(raw);
 01162            var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01163            if (selected.Count == 0) return null;
 01164            var options = ParseShateMOptions(Request.Form["ShateMCheckoutOptionsJson"].ToString());
 01165            return (await _shateMCartService.QueueAsync(selected, options, User.Identity?.Name ?? "unknown", HttpContext
 1166        }
 01167        catch (JsonException ex) { return $"<div class='alert alert-warning mb-0'><strong>Корзина Шате‑М не обработана.<
 01168    }
 1169
 1170    private async Task<string?> GetShateMDirectCheckoutAppendixAsync(List<int> orderIds)
 1171    {
 01172        var raw = Request.Form["ShateMCheckoutQueueJson"].ToString();
 01173        if (string.IsNullOrWhiteSpace(raw) || raw == "[]") return null;
 1174        try
 1175        {
 01176            var queue = ParseShateMBasketQueue(raw);
 01177            var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01178            if (selected.Count == 0) return null;
 01179            var options = ParseShateMOptions(Request.Form["ShateMCheckoutOptionsJson"].ToString());
 01180            return (await _shateMCartService.CheckoutDirectAsync(selected, options, User.Identity?.Name ?? "unknown", Ht
 1181        }
 01182        catch (JsonException ex) { return $"<div class='alert alert-warning mb-0'><strong>Заказ Шате‑М не оформлен.</str
 01183    }
 1184
 1185    private static ShateMCheckoutOptions ParseShateMOptions(string raw)
 1186    {
 01187        if (string.IsNullOrWhiteSpace(raw))
 01188            throw new JsonException("Не переданы параметры оформления Шате‑М.");
 1189
 01190        using var document = JsonDocument.Parse(raw);
 01191        if (document.RootElement.ValueKind != JsonValueKind.Object)
 01192            throw new JsonException("Параметры оформления Шате‑М должны быть JSON-объектом.");
 1193
 01194        var root = document.RootElement;
 01195        var agreementCode = ReadString(root, "agreementCode");
 01196        var deliveryAddressCode = ReadString(root, "deliveryAddressCode");
 01197        var deliveryType = ReadString(root, "deliveryType");
 01198        var deliveryAtRaw = ReadString(root, "deliveryAtUtc");
 01199        if (!TryParseShateMDeliveryDate(deliveryAtRaw, out var deliveryAt))
 01200            throw new JsonException("Некорректная дата доставки Шате‑М.");
 1201
 01202        return new ShateMCheckoutOptions(
 01203            agreementCode,
 01204            deliveryAddressCode,
 01205            deliveryType,
 01206            deliveryAt,
 01207            ReadBool(root, "agreeWithTermsOfDelivery"),
 01208            ReadBool(root, "agreeWithPersonalDataProcessingPolicyAndUserAgreement"));
 01209    }
 1210
 1211    /// <summary>Смещение слота сохраняется: Шате‑М считает днём доставки календарную дату в присланном смещении.</summa
 1212    internal static bool TryParseShateMDeliveryDate(string value, out DateTimeOffset result)
 1213    {
 31214        var styles = DateTimeStyles.AllowWhiteSpaces;
 31215        return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, styles, out result)
 31216               || DateTimeOffset.TryParse(value, CultureInfo.GetCultureInfo("ru-RU"), styles, out result);
 1217    }
 1218
 1219    /// <summary>Снимок проценки Шате‑М содержит артикул и priceId как число либо строку.</summary>
 1220    private static List<ShateMCartQueueItem> ParseShateMBasketQueue(string raw)
 1221    {
 01222        using var document = JsonDocument.Parse(raw);
 01223        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01224            throw new JsonException("Очередь Шате‑М должна быть JSON-массивом.");
 1225
 01226        return document.RootElement.EnumerateArray()
 01227            .Where(item => item.ValueKind == JsonValueKind.Object)
 01228            .Select(item => new ShateMCartQueueItem(
 01229                ReadInt(item, "orderId"),
 01230                ReadString(item, "article"),
 01231                ReadString(item, "brand"),
 01232                ReadNullableString(item, "productName"),
 01233                ReadNullableString(item, "priceId"),
 01234                ReadNullableString(item, "locationCode"),
 01235                ReadNullableString(item, "direction"),
 01236                ReadDecimal(item, "quantity"),
 01237                ReadNullableDecimal(item, "sourcePrice"),
 01238                ReadNullableString(item, "currencyCode"),
 01239                ReadNullableInt(item, "deliveryDaysMin"),
 01240                ReadNullableInt(item, "deliveryDaysMax")))
 01241            .ToList();
 01242    }
 1243
 1244    private async Task<string?> GetZZapBasketAppendixAsync(List<int> orderIds)
 1245    {
 01246        var raw = Request.Form["ZZapMoscowBasketQueueJson"].ToString();
 01247        if (string.IsNullOrWhiteSpace(raw)) return null;
 1248        List<ZZapCartQueueItem> queue;
 01249        try { queue = ParseZZapBasketQueue(raw); }
 01250        catch (JsonException) { return "<div class='alert alert-warning mb-0'><strong>Корзина ZZap не обработана.</stron
 01251        var selected = queue.Where(x => orderIds.Contains(x.OrderId)).ToList();
 01252        if (selected.Count == 0) return null;
 01253        var result = await _zzapCartService.QueueAsync(selected, User.Identity?.Name ?? "unknown", HttpContext.RequestAb
 01254        return result.ToHtmlBlock();
 01255    }
 1256
 1257    /// <summary>Снимок предложения ZZap из проценки: code_doc_b — строка, чтобы не потерять точность int64 в JS.</summa
 1258    private static List<ZZapCartQueueItem> ParseZZapBasketQueue(string raw)
 1259    {
 01260        using var document = JsonDocument.Parse(raw);
 01261        if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("ZZap basket queue must be an
 01262        return document.RootElement.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.Object).Select(x => new ZZa
 01263            ReadInt(x, "orderId"), ReadString(x, "codeDocB"), ReadString(x, "article"), ReadString(x, "brand"), ReadNull
 01264            ReadNullableString(x, "sellerName"), ReadNullableString(x, "sellerUserKey"), ReadNullableString(x, "sellerLo
 01265            ReadInt(x, "quantity"), ReadInt(x, "minimumQuantity"), ReadNullableDecimal(x, "sourcePrice"), ReadInt(x, "de
 01266            ReadNullableInt(x, "codeRegion"), ReadNullableString(x, "direction"), ReadNullableInt(x, "deliveryDaysMin"),
 01267            ReadNullableInt(x, "deliveryDaysMax"), ReadNullableString(x, "shipmentNumber"), ReadNullableString(x, "clien
 01268            ReadNullableDecimal(x, "minSumOrder"), ReadNullableInt(x, "qtyMax"))).ToList();
 01269    }
 1270
 1271    private static List<MotexCartQueueItem> ParseMotexBasketQueue(string raw)
 1272    {
 01273        using var document = JsonDocument.Parse(raw);
 01274        if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("Motex basket queue must be a
 01275        return document.RootElement.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.Object).Select(x => new Mot
 01276            ReadInt(x, "orderId"), ReadString(x, "article"), ReadString(x, "brand"), ReadNullableString(x, "productName"
 01277            ReadNullableString(x, "storeCode"), ReadNullableString(x, "storeInfo"), ReadNullableString(x, "shipmentNumbe
 01278            ReadDecimal(x, "quantity"), ReadNullableDecimal(x, "sourcePrice"), ReadNullableString(x, "currencyCode"),
 01279            ReadNullableString(x, "deliveryAtUtc"), ReadNullableInt(x, "deliveryDaysMin"), ReadNullableInt(x, "deliveryD
 01280    }
 1281
 1282    /// <summary>Браузер передаёт снимок проценки: цена и количество могут прийти числом либо строкой.</summary>
 1283    private static List<ProfitLigaQueueItem> ParseProfitLigaBasketQueue(string raw)
 1284    {
 01285        using var document = JsonDocument.Parse(raw);
 01286        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01287            throw new JsonException("ProfitLiga basket queue must be an array.");
 1288
 01289        return document.RootElement.EnumerateArray()
 01290            .Where(item => item.ValueKind == JsonValueKind.Object)
 01291            .Select(item => new ProfitLigaQueueItem(
 01292                ReadInt(item, "orderId"), ReadString(item, "articleId"), ReadString(item, "warehouseId"),
 01293                ReadNullableString(item, "productCode"), ReadString(item, "article"), ReadString(item, "brand"),
 01294                ReadNullableString(item, "productName"), ReadNullableString(item, "warehouseName"),
 01295                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"),
 01296                ReadDecimal(item, "quantity"), ReadNullableInt(item, "multi") ?? 0,
 01297                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 01298                ReadNullableString(item, "zasylWarehouseName"), ReadNullableInt(item, "deliveryDaysMin"),
 01299                ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(item, "direction")))
 01300            .ToList();
 01301    }
 1302
 1303    private static List<NikeiCartQueueItem> ParseNikeiBasketQueue(string raw)
 1304    {
 01305        using var document = JsonDocument.Parse(raw);
 01306        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01307            throw new JsonException("Nikei basket queue must be an array.");
 01308        return document.RootElement.EnumerateArray().Where(item => item.ValueKind == JsonValueKind.Object)
 01309            .Select(item => new NikeiCartQueueItem(ReadInt(item, "orderId"), ReadString(item, "toCart"),
 01310                ReadNullableString(item, "productKey"), ReadString(item, "article"), ReadString(item, "brand"),
 01311                ReadNullableString(item, "productName"), ReadNullableString(item, "warehouseName"), ReadInt(item, "quant
 01312                ReadNullableInt(item, "multiplicity") ?? 1, ReadNullableInt(item, "stock"), ReadNullableDecimal(item, "s
 01313                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"),
 01314                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"))).ToList();
 01315    }
 1316
 1317    private static List<MoskvorechieIstraQueueItem> ParseMoskvorechieBasketQueue(string raw)
 1318    {
 01319        using var document = JsonDocument.Parse(raw);
 01320        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01321            throw new JsonException("Moskvorechie basket queue must be an array.");
 01322        return document.RootElement.EnumerateArray().Where(item => item.ValueKind == JsonValueKind.Object)
 01323            .Select(item => new MoskvorechieIstraQueueItem(ReadInt(item, "orderId"), ReadString(item, "offerId"),
 01324                ReadString(item, "article"), ReadString(item, "brand"), ReadNullableString(item, "productName"),
 01325                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"), ReadDecimal(item, "q
 01326                ReadDecimal(item, "minimumQuantity"), ReadNullableDecimal(item, "sourcePrice"), ReadNullableInt(item, "d
 01327                ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(item, "direction"))).ToList();
 01328    }
 1329
 1330    /// <summary>Браузер передаёт снимок проценки: цена и количество могут прийти числом либо строкой.</summary>
 1331    private static List<FavoritPartsQueueItem> ParseFavoritPartsBasketQueue(string raw)
 1332    {
 01333        using var document = JsonDocument.Parse(raw);
 01334        if (document.RootElement.ValueKind != JsonValueKind.Array)
 01335            throw new JsonException("FavoritParts basket queue must be an array.");
 1336
 01337        return document.RootElement.EnumerateArray()
 01338            .Where(item => item.ValueKind == JsonValueKind.Object)
 01339            .Select(item => new FavoritPartsQueueItem(
 01340                ReadInt(item, "orderId"), ReadString(item, "goodsId"), ReadString(item, "warehouseGroupId"),
 01341                ReadString(item, "article"), ReadString(item, "brand"), ReadNullableString(item, "productName"),
 01342                ReadNullableString(item, "shipmentNumber"), ReadNullableString(item, "clientName"), ReadDecimal(item, "q
 01343                ReadNullableDecimal(item, "sourcePrice"), ReadNullableString(item, "currencyCode"),
 01344                ReadNullableInt(item, "deliveryDaysMin"), ReadNullableInt(item, "deliveryDaysMax"), ReadNullableString(i
 01345            .ToList();
 01346    }
 1347
 01348    private static string ReadString(JsonElement item, string name) => ReadNullableString(item, name) ?? string.Empty;
 1349    private static string? ReadNullableString(JsonElement item, string name) =>
 01350        !item.TryGetProperty(name, out var value) || value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined ? 
 01351        value.ValueKind == JsonValueKind.String ? value.GetString() : value.GetRawText();
 1352    private static int ReadInt(JsonElement item, string name) =>
 01353        int.TryParse(ReadNullableString(item, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) 
 1354    private static int? ReadNullableInt(JsonElement item, string name) =>
 01355        int.TryParse(ReadNullableString(item, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) 
 01356    private static decimal ReadDecimal(JsonElement item, string name) => ReadNullableDecimal(item, name) ?? 0m;
 1357    private static decimal? ReadNullableDecimal(JsonElement item, string name) =>
 01358        decimal.TryParse(ReadNullableString(item, name)?.Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCultu
 1359    private static bool ReadBool(JsonElement item, string name) =>
 01360        bool.TryParse(ReadNullableString(item, name), out var value) && value;
 1361}

Methods/Properties

.ctor(OzonRepositories.Context.ApplicationDbContext,Services.DataServices.AppStatusDataServices,Services.DataServices.OrdersDataServices,Services.TransactionUtilsServices.TransactionManager,Services.CacheServices.Cache.OzonOrdersCache.TransactionCache,Services.DataServices.SupplierDataServices,Services.CacheServices.Cache.UserCacheService.IUserCacheService,OzonOrdersWeb.WebServices.Currency.ICurrencyRatesViewBagService,Services.CacheServices.Cache.CacheUpdater`1<OzonDomains.Models.Order>,Services.CacheServices.Cache.OzonOrdersCache.OrderCache,Services.ApiServices.OneCDataManager,Services.DataServices.WarehouseDataServices,PartsInfo.HttpUtils.ProxyHttpClientService,Services.DataServices.OrderCartServices,Services.DataServices.ExpenseTypeDataServices,Services.DataServices.OrderExtraExpenseDataServices,Services.DataServices.StatusTransitionDataServices,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,Services.ApiServices.OzonApi.OzonOrderCancellationService)
GetTransactionConfig()
ApplyQuantitySplitCancellationLogic()
CreateOrderToSupplierTransactionV2(System.String,System.Int32)
CreateOrderToSupplierTransactionV2(System.Collections.Generic.List`1<OzonDomains.Models.Order>,System.String,System.String,System.Int32,System.String,System.Collections.Generic.Dictionary`2<System.Int32,System.String>)
PreviewArmtekOrder()
ExecutePreTransactionAsync()
ExecutePreTransactionCoreAsync()
GetPostTransactionSuccessAppendixAsync()
GetSupplierVerificationBlockAsync()
AppendDirectCheckoutBlock(System.Collections.Generic.List`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)