< Summary

Information
Class: Services.ParserServices.OrderCaster
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ParserServices/OrderCaster.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1404
Coverable lines: 1404
Total lines: 3440
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 1258
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ParserServices/OrderCaster.cs

#LineLine coverage
 1#pragma warning disable CS8600, CS8602, CS8603, CS8604, CS8601, CS8619, CS8629
 2using System.Diagnostics.CodeAnalysis;
 3using System.Net.Http;
 4using Microsoft.Extensions.Configuration;
 5using Newtonsoft.Json.Linq;
 6using OzonDomains;
 7using OzonDomains.Models;
 8using OzonRepositories.Context;
 9using Services.DataServices;
 10using Services.ParserServices.HelpDictEnum;
 11using Services.ReleasServices.ReleaseManager;
 12using System.Globalization;
 13using System.Text.RegularExpressions;
 14using Microsoft.Extensions.Caching.Memory;
 15using Services.CacheServices.Cache;
 16using SlqStudio.Application.Services.EmailService;
 17using SlqStudio.Application.Services.EmailService.Implementation;
 18using Microsoft.EntityFrameworkCore;
 19using MySqlConnector;
 20
 21
 22namespace Services.ParserServices
 23
 24{
 25    public class OrderCaster
 26
 27    {
 28        // Кампании Яндекса, чьё имя (campaign.domain) не совпадает с названием склада
 29        // отгрузки в Studio2: имя кампании становится доставкой, склад берём отсюда.
 030        private static readonly Dictionary<string, string> YandexCampaignWarehouses = new(StringComparer.Ordinal)
 031        {
 032            ["Interparts 2.0 V11"] = "V11",
 033            ["Interparts 2.0 M1"] = "M1"
 034        };
 35
 36        private const string YandexDeliveryProviderName = "Яндекс Маркет";
 37
 38        private readonly ApplicationDbContext _context;
 39
 40        private readonly ReleaseManager _releaseManager;
 41
 42        private readonly OrdersFileMetadataDataService _ordersFileMetadataDataService;
 43
 44        private readonly EtProducerDataServices _etProducerDataServices;
 45
 46        private readonly AppCache _appCache;
 47
 48        private readonly IEmailService _emailService;
 49
 50        private readonly DeliveryDataServices _deliveryService;
 51
 52        private readonly OrdersDataServices _ordersDataServices;
 53
 54        private readonly string? _imageBaseUrl;
 55
 56        private readonly IHttpClientFactory _httpClientFactory;
 57
 58
 059        public OrderCaster(ApplicationDbContext context,
 060            ReleaseManager releaseManager,
 061            OrdersFileMetadataDataService ordersFileMetadataDataService,
 062            EtProducerDataServices etProducerDataServices,
 063            AppCache appCache,
 064            IEmailService emailService,
 065            DeliveryDataServices deliveryService,
 066            OrdersDataServices ordersDataServices,
 067            IConfiguration configuration,
 068            IHttpClientFactory httpClientFactory)
 69
 70        {
 071            _context = context;
 72
 073            _releaseManager = releaseManager;
 74
 075            _ordersFileMetadataDataService = ordersFileMetadataDataService;
 76
 077            _etProducerDataServices = etProducerDataServices;
 78
 079            _appCache = appCache;
 80
 081            _emailService = emailService;
 82
 083            _deliveryService = deliveryService;
 84
 085            _ordersDataServices = ordersDataServices;
 86
 087            _imageBaseUrl = configuration["ImageStorage:BaseUrl"];
 88
 089            _httpClientFactory = httpClientFactory;
 090        }
 91
 92
 93        public async Task<List<Order>> JsonToOrders(JArray orders)
 94
 95        {
 096            List<Order> resultOrders = new List<Order>();
 97
 98            const int maxAttempts = 3;
 99
 0100            foreach (var order in orders)
 101
 102            {
 0103                for (var attempt = 1; attempt <= maxAttempts; attempt++)
 104                {
 105                    try
 106
 107                    {
 0108                        resultOrders.Add(await CastToModel(order));
 0109                        break;
 110                    }
 111
 112                    catch (Exception e)
 113                    {
 114                        // Неудачный SaveChanges оставляет сущности в состоянии Added в трекере —
 115                        // без отсоединения повторная попытка вставила бы их ещё раз.
 0116                        DetachAddedEntries();
 117
 0118                        var shipNum = order?["Номер отправления"]?.ToString() ?? "?";
 0119                        var art = order?["Артикул"]?.ToString() ?? "?";
 120
 0121                        if (IsMariaDbReturningBatchError(e))
 122                        {
 0123                            if (attempt < maxAttempts)
 124                            {
 0125                                await Task.Delay(TimeSpan.FromMilliseconds(300 * attempt));
 0126                                continue;
 127                            }
 128
 129                            // Известный сбой MariaDB "syntax error near RETURNING" (батчинг INSERT):
 130                            // письмо не шлём, чтобы не спамить почту — только лог.
 0131                            Console.WriteLine(
 0132                                $"[OrderCaster] Заказ {shipNum} (артикул {art}) пропущен после {maxAttempts} попыток: Ma
 0133                            break;
 134                        }
 135
 0136                        var subject = $"[OrderCaster] Ошибка при обработке заказа {shipNum} (артикул {art}): {e.Message}
 0137                        await _emailService.SendEmailAsync(subject, e.ToString());
 0138                        break;
 139                    }
 140                }
 0141            }
 142
 143
 0144            return resultOrders;
 0145        }
 146
 147        /// <summary>
 148        /// Отсоединяет от трекера все сущности в состоянии Added — откат незасейвленных
 149        /// вставок после неудачного SaveChanges. Уже сохранённые сущности (Unchanged) не трогает.
 150        /// </summary>
 151        private void DetachAddedEntries()
 152        {
 0153            foreach (var entry in _context.ChangeTracker.Entries()
 0154                         .Where(en => en.State == EntityState.Added).ToList())
 155            {
 0156                entry.State = EntityState.Detached;
 157            }
 0158        }
 159
 160        /// <summary>
 161        /// Известная ошибка MariaDB: "You have an error in your SQL syntax ... near 'RETURNING ...'"
 162        /// при склейке нескольких INSERT c RETURNING в один пакет. Транзиентная — лечится повтором.
 163        /// </summary>
 164        private static bool IsMariaDbReturningBatchError(Exception exception)
 165        {
 0166            for (Exception? ex = exception; ex != null; ex = ex.InnerException)
 167            {
 0168                if (ex is MySqlException && ex.Message.Contains("RETURNING"))
 169                {
 0170                    return true;
 171                }
 172            }
 173
 0174            return false;
 175        }
 176
 177
 178        public async Task<List<Order>> ExcelToOrders(List<Dictionary<string, string>> table,
 179            OzonClient client,
 180            Manufacturer manufacturer,
 181            Warehouse warehouse,
 182            Supplier supplier,
 183            string clientStatus,
 184            CurrencyCode currencyCode,
 185            DateTime? selectedShippingDate,
 186            DateTime? selectedProcessingDate)
 187
 188        {
 0189            string cacheKey = _appCache.GenerateCacheKey(table);
 190
 191
 0192            if (_appCache.GetCache().TryGetValue(cacheKey, out List<Order> cachedOrders))
 193
 194            {
 0195                Console.WriteLine($"Данные Excel найдены в кэше: {cacheKey}");
 196
 0197                return cachedOrders;
 198            }
 199
 200
 0201            List<Order> resultOrders = new List<Order>();
 202
 203
 204            try
 205
 206            {
 0207                foreach (var row in table)
 208
 209                {
 0210                    resultOrders.Add(await CastToModelFromExcel(row, client, manufacturer, warehouse, supplier,
 0211                        clientStatus, currencyCode, selectedShippingDate, selectedProcessingDate));
 212                }
 0213            }
 214
 0215            catch (Exception ex)
 216
 217            {
 0218                Console.WriteLine($"Ошибка при обработке данных из Excel: {ex.Message}");
 0219            }
 220
 221
 0222            _appCache.GetCache().Set(cacheKey, resultOrders, TimeSpan.FromMinutes(10));
 223
 224
 0225            return resultOrders;
 0226        }
 227
 228
 229        public async Task<List<Order>> ExcelToOrdersForDropbox(List<Dictionary<string, string>> table,
 230            OzonClient client,
 231            Manufacturer manufacturer,
 232            Warehouse warehouse,
 233            Supplier supplier,
 234            string clientStatus,
 235            CurrencyCode currencyCode,
 236            DateTime? selectedShippingDate,
 237            DateTime? selectedProcessingDate,
 238            string delivery)
 239
 240        {
 0241            string cacheKey = _appCache.GenerateCacheKey(table);
 242
 243
 0244            if (_appCache.GetCache().TryGetValue(cacheKey, out List<Order> cachedOrders))
 245
 246            {
 0247                Console.WriteLine($"Данные Excel найдены в кэше: {cacheKey}");
 248
 0249                return cachedOrders;
 250            }
 251
 252
 0253            List<Order> resultOrders = new List<Order>();
 254
 255
 256            try
 257
 258            {
 0259                foreach (var row in table)
 260
 261                {
 0262                    resultOrders.Add(await CastToModelFromExcelForDropBox(row, client, manufacturer, warehouse,
 0263                        supplier,
 0264                        clientStatus, currencyCode, selectedShippingDate, selectedProcessingDate, delivery));
 265                }
 0266            }
 267
 0268            catch (Exception ex)
 269
 270            {
 0271                Console.WriteLine($"Ошибка при обработке данных из Excel: {ex.Message}");
 0272            }
 273
 274
 0275            _appCache.GetCache().Set(cacheKey, resultOrders, TimeSpan.FromMinutes(10));
 276
 277
 0278            return resultOrders;
 0279        }
 280
 281
 282        public async Task<List<Order>> YandexToOrders(JArray orders)
 283
 284        {
 0285            List<Order> resultOrders = new List<Order>();
 286
 287
 288            try
 289
 290            {
 0291                foreach (var order in orders)
 292
 293                {
 0294                    resultOrders.Add(await CastToModelFromYandex(order));
 295                }
 0296            }
 297
 298            catch (Exception e)
 299
 300            {
 0301                Console.WriteLine(e);
 302
 0303                throw;
 304            }
 305
 306
 0307            return resultOrders;
 0308        }
 309
 310
 311        public async Task<List<Order>> XlsToOrdersForDropbox(List<Dictionary<string, string>> table,
 312            OzonClient client,
 313            Warehouse warehouse,
 314            DateTime selectedShippingDate,
 315            DateTime selectedProcessingDate,
 316            string delivery)
 317
 318        {
 0319            string cacheKey = _appCache.GenerateCacheKey(table);
 320
 321
 0322            if (_appCache.GetCache().TryGetValue(cacheKey, out List<Order> cachedOrders))
 323
 324            {
 0325                Console.WriteLine($"Данные Excel найдены в кэше: {cacheKey}");
 326
 0327                return cachedOrders;
 328            }
 329
 330
 0331            List<Order> resultOrders = new List<Order>();
 332
 333
 334            try
 335
 336            {
 0337                foreach (var row in table)
 338
 339                {
 0340                    resultOrders.Add(await CastToModelFromExcelForEmEx(row,
 0341                        client,
 0342                        warehouse,
 0343                        selectedShippingDate,
 0344                        selectedProcessingDate,
 0345                        delivery));
 346                }
 0347            }
 348
 0349            catch (Exception ex)
 350
 351            {
 0352                Console.WriteLine($"Ошибка при обработке данных из Excel: {ex.Message}");
 0353            }
 354
 355
 0356            _appCache.GetCache().Set(cacheKey, resultOrders, TimeSpan.FromMinutes(10));
 357
 358
 0359            return resultOrders;
 0360        }
 361
 362
 363        public async Task<List<Order>> SetFileDataAsync(List<Order> orders, string filePath, string fileName)
 364
 365        {
 0366            var fileMetadata = await _ordersFileMetadataDataService.GetOrdersFileMetadataAsync(new OrdersFileMetadata
 0367
 0368            {
 0369                FileName = fileName,
 0370
 0371                FolderName = filePath
 0372            });
 373
 374
 0375            if (fileMetadata == null)
 376
 377            {
 0378                fileMetadata = new OrdersFileMetadata
 0379
 0380                {
 0381                    FileName = fileName,
 0382
 0383                    FolderName = filePath
 0384                };
 385
 386
 0387                await _ordersFileMetadataDataService.AddOrdersFileMetadata(fileMetadata);
 388
 0389                fileMetadata = await _ordersFileMetadataDataService.GetOrdersFileMetadataAsync(fileMetadata);
 390            }
 391
 392
 0393            foreach (var order in orders)
 394
 395            {
 0396                order.ExcelFileData = fileMetadata;
 397            }
 398
 399
 0400            return orders;
 0401        }
 402
 403
 404        /// <summary>
 405        /// Безопасно читает обязательное поле из JToken-источника.
 406        /// Если источник null или поле отсутствует/пусто — добавляет запись в <paramref name="missing"/> и возвращает n
 407        /// </summary>
 408        private static string? SafeField(JToken? source, string sourceName, string fieldName, List<string> missing)
 409        {
 0410            if (source == null)
 411            {
 0412                missing.Add($"{sourceName} отсутствует → поле «{fieldName}» недоступно");
 0413                return null;
 414            }
 0415            var value = source[fieldName]?.ToString();
 0416            if (string.IsNullOrWhiteSpace(value))
 0417                missing.Add($"«{fieldName}» отсутствует или пусто в {sourceName}");
 0418            return value;
 419        }
 420
 421        /// <summary>
 422        /// Безопасно читает необязательное поле из JToken-источника.
 423        /// Пустое значение и null-источник — норма, возвращает null без предупреждений.
 424        /// </summary>
 425        private static string? SafeOptionalField(JToken? source, string fieldName)
 426        {
 0427            return source?[fieldName]?.ToString();
 428        }
 429
 430        [SuppressMessage("ReSharper.DPA", "DPA0006: Large number of DB commands", MessageId = "count: 178")]
 431        private async Task<Order> CastToModel(JToken? jsonOrder)
 432        {
 0433            Order order = new();
 0434            CultureInfo culture = new("en-US");
 435
 0436            NumberStyles style = NumberStyles.Number;
 437
 438
 0439            order.Key = jsonOrder["Номер отправления"].ToString() + jsonOrder["Артикул"].ToString();
 440
 441
 0442            order.ShipmentNumber = jsonOrder["Номер отправления"].ToString();
 443
 444
 0445            order.ProcessingDate = DateTime.TryParse(jsonOrder["Принят в обработку"].ToString(), culture,
 0446                out var processingDate)
 0447                ? processingDate
 0448                : null;
 449
 450
 0451            order.ShippingDate = DateTime.TryParse(jsonOrder["Дата отгрузки"].ToString(), culture, out var shipingData)
 0452                ? shipingData
 0453                : null;
 454
 0455            if (order.ShipmentNumber == "66929352-0158-1")
 456            {
 457            }
 458
 0459            order.ClientStatus = await GetOrCreateClientStatusAsync(jsonOrder["Статус"]?.ToString());
 460
 461
 0462            order.ShipmentAmount = decimal.TryParse(DelSubStr(jsonOrder["Сумма отправления"].ToString()), style,
 0463                culture, out var shipmentAmount)
 0464                ? shipmentAmount
 0465                : null;
 466
 467
 0468            order.ProductName = jsonOrder["Название товара"].ToString();
 469
 470
 0471            order.ProductKey = jsonOrder["Артикул"].ToString();
 472
 0473            order.Quantity = int.TryParse(DelSubStr(jsonOrder["Количество"].ToString()), style, culture,
 0474                out var quantity)
 0475                ? quantity
 0476                : null;
 477
 478
 479            // Извлекаем значение из JSON до выполнения запроса к БД
 480
 0481            string warehouseName = jsonOrder["productWarehousesAndCitysWithNumber"]?["delivery_method"]?["warehouse"]
 0482                ?.ToString();
 483
 484
 0485            Warehouse shipmentWarehouse = null;
 486
 0487            if (!string.IsNullOrEmpty(warehouseName))
 488
 489            {
 0490                shipmentWarehouse = _context.Warehouses.FirstOrDefault(w => w.Name == warehouseName);
 491            }
 492
 493
 0494            if (shipmentWarehouse != null)
 495
 496            {
 0497                order.ShipmentWarehouse = shipmentWarehouse;
 498            }
 499
 0500            else if (!string.IsNullOrEmpty(warehouseName))
 501
 502            {
 0503                var newWarehouse = new Warehouse
 0504
 0505                {
 0506                    Name = warehouseName
 0507                };
 508
 509
 0510                _context.Warehouses.Add(newWarehouse);
 0511                await _context.SaveChangesAsync();
 512
 0513                order.ShipmentWarehouse = newWarehouse;
 0514            }
 515
 516
 0517            string extractedCode = Regex.Match(jsonOrder["Артикул"].ToString(), @"=(\w{3})").Groups[1].Value;
 518
 519
 0520            if (string.IsNullOrEmpty(extractedCode))
 521
 522            {
 0523                extractedCode = Regex.Match(jsonOrder["Артикул"].ToString(), @"=(\w{2})").Groups[1].Value;
 524            }
 525
 526
 0527            order = await SetEtProducerByCode(order, extractedCode);
 528
 529
 0530            string currencyCode = jsonOrder["Код валюты отправления"].ToString();
 531
 532
 0533            Currency currency = _context.Currencys
 0534                .FirstOrDefault(c => c.Name == currencyCode);
 535
 536
 0537            if (currency != null)
 538
 539            {
 0540                order.Сurrency = currency;
 541            }
 542
 543            else
 544
 545            {
 0546                var newCurrency = new Currency
 0547
 0548                {
 0549                    Name = currencyCode
 0550                };
 551
 552
 0553                _context.Currencys.Add(newCurrency);
 0554                await _context.SaveChangesAsync();
 555
 0556                order.Сurrency = newCurrency;
 0557            }
 558
 559
 0560            order.Supplier = null;
 561
 0562            order.PurchasePrice = null;
 563
 564
 0565            if (order.ProductInfo == null)
 566
 567            {
 568                // Извлекаем значение из JSON до выполнения запроса к БД
 569
 0570                string article = jsonOrder["Артикул"]?.ToString();
 0571                string? ozonProductIdFromJson = jsonOrder["productWithArticle"]?["Ozon Product ID"]?.ToString();
 572
 0573                Product product = null;
 574
 0575                if (!string.IsNullOrEmpty(article))
 576
 577                {
 0578                    product = _context.Products.FirstOrDefault(p => p.Article == article);
 579                }
 580
 581
 0582                if (product != null)
 583
 584                {
 585                    // Дозаполняем OzonProductId, если оно отсутствует в уже существующем продукте.
 0586                    if (string.IsNullOrWhiteSpace(product.OzonProductId) &&
 0587                        !string.IsNullOrWhiteSpace(ozonProductIdFromJson))
 588                    {
 0589                        product.OzonProductId = ozonProductIdFromJson;
 590                    }
 591
 0592                    if (string.IsNullOrEmpty(product.Barcode))
 593
 594                    {
 0595                        string barcode = jsonOrder["productWithArticle"]?["Штрихкод (Серийный номер / EAN)"]?.ToString()
 596
 0597                        if (!string.IsNullOrEmpty(barcode))
 598
 599                        {
 0600                            product.Barcode = barcode;
 601                        }
 602                    }
 603
 604                // Если картинка не заполнена — подгружаем primary_image по Ozon product_id.
 0605                if (string.IsNullOrWhiteSpace(product.ImageUrl) &&
 0606                    !string.IsNullOrWhiteSpace(product.OzonProductId))
 607                {
 0608                    var primaryImage = jsonOrder["productWithArticle"]?["primary_image"]?.ToString();
 0609                    if (!string.IsNullOrWhiteSpace(primaryImage))
 610                    {
 0611                        product.ImageUrl = primaryImage;
 612                    }
 613                }
 614
 615                    // Дозаполняем категорию, если она пуста в существующем продукте
 0616                    if (string.IsNullOrWhiteSpace(product.CommercialCategory))
 617                    {
 0618                        var category = jsonOrder["productWithArticle"]?["Категория"]?.ToString();
 0619                        if (!string.IsNullOrWhiteSpace(category))
 620                        {
 0621                            product.CommercialCategory = category;
 622                        }
 623                    }
 624
 625
 0626                    order.ProductInfo = product;
 627                }
 628
 629                else
 630
 631                {
 0632                    var missingFields = new List<string>();
 0633                    var pw = jsonOrder["productWithArticle"];
 634
 0635                    product = new Product();
 636
 0637                    product.Article = jsonOrder["Артикул"]?.ToString() ?? "";
 638
 0639                    product.OzonProductId = SafeField(pw, "productWithArticle", "Ozon Product ID", missingFields) ?? "";
 640
 0641                    var sku = SafeField(pw, "productWithArticle", "SKU", missingFields) ?? "";
 0642                    product.FboOzonSkuId = sku;
 0643                    product.FbsOzonSkuId = sku;
 644
 0645                    product.CommercialCategory = SafeField(pw, "productWithArticle", "Категория", missingFields) ?? "";
 646
 647                    // необязательные поля — пустое значение норма, предупреждение только если источник null
 0648                    var barcode = SafeOptionalField(pw, "Штрихкод (Серийный номер / EAN)");
 0649                    if (!string.IsNullOrEmpty(barcode))
 0650                        product.Barcode = barcode;
 651
 652                    // Для нового продукта сразу заполняем картинку, если поле пустое.
 0653                    var primaryImage = pw?["primary_image"]?.ToString();
 0654                    if (!string.IsNullOrWhiteSpace(primaryImage))
 0655                        product.ImageUrl = primaryImage;
 656
 0657                    var volumeStr = SafeOptionalField(pw, "Объем товара, л");
 0658                    product.Volume = double.TryParse(DelSubStr(volumeStr ?? ""), style, culture, out double volume)
 0659                        ? volume
 0660                        : null;
 661
 0662                    var volWeightStr = SafeOptionalField(pw, "Объемный вес, кг");
 0663                    product.VolumetricWeight = double.TryParse(DelSubStr(volWeightStr ?? ""), style, culture, out double
 0664                        ? volumetricWeight
 0665                        : null;
 666
 0667                    _context.Products.Add(product);
 668                    // В проде ловили SQL-синтаксис возле RETURNING при пакетной вставке нескольких новых сущностей.
 669                    // Фикс: сохраняем новый Product отдельной командой до следующих INSERT.
 0670                    await _context.SaveChangesAsync();
 0671                    order.ProductInfo = product;
 672
 0673                    if (missingFields.Count > 0)
 674                    {
 0675                        var shipNum = jsonOrder["Номер отправления"]?.ToString() ?? "?";
 0676                        var art = jsonOrder["Артикул"]?.ToString() ?? "?";
 0677                        var subject = $"[OrderCaster] Пропущены поля при создании товара — заказ {shipNum}, артикул {art
 0678                        var body = $"Заказ: {shipNum}\nАртикул: {art}\n\nПропущенные поля из отчёта Ozon:\n• {string.Joi
 0679                                   $"Возможные причины:\n— Ozon переименовал колонку в отчёте /v1/report/products/create
 0680                        await _emailService.SendEmailAsync(subject, body);
 681                    }
 0682                }
 0683            }
 684
 685
 0686            var wc = jsonOrder["productWarehousesAndCitysWithNumber"];
 0687            var city = wc?["analytics_data"]?["city"]?.ToString();
 0688            var region = wc?["analytics_data"]?["region"]?.ToString();
 0689            var clusterTo = wc?["financial_data"]?["cluster_to"]?.ToString();
 690
 691
 0692            order.DeliveryCity = !string.IsNullOrEmpty(city)
 0693                ? city
 0694                : !string.IsNullOrEmpty(region)
 0695                    ? region
 0696                    : clusterTo;
 697
 0698            var trackingNumber = jsonOrder["productWarehousesAndCitysWithNumber"]?["tracking_number"]?.ToString();
 0699            order.TrackingNumber = string.IsNullOrEmpty(trackingNumber) ? null : trackingNumber;
 700
 701            // ScanIt приходит в том же ответе /v3/posting/fbs/get; пустое значение не затирает
 702            // уже сохранённое: при обновлении заказа в БД переносятся только непустые поля.
 0703            var scanit = wc?["scanit"]?.ToString();
 0704            order.Scanit = string.IsNullOrWhiteSpace(scanit) ? null : scanit.Trim();
 705
 706
 0707            if (order.AppStatus == null)
 708
 709            {
 0710                string statusName = "Не указан";
 711
 712
 0713                AppStatus appStatus = _context.AppStatuses.FirstOrDefault(c => c.Name == statusName);
 714
 715
 0716                if (appStatus != null)
 717
 718                {
 0719                    order.AppStatus = appStatus;
 720                }
 721
 722                else
 723
 724                {
 0725                    var newStatus = new AppStatus
 0726
 0727                    {
 0728                        Name = statusName
 0729                    };
 730
 731
 0732                    _context.AppStatuses.Add(newStatus);
 0733                    await _context.SaveChangesAsync();
 734
 0735                    order.AppStatus = newStatus;
 0736                }
 737            }
 738
 739
 0740            if (order.Supplier == null)
 741
 742            {
 0743                string supplierName = "Не указан";
 744
 745
 0746                Supplier supplier = _context.Suppliers.FirstOrDefault(c => c.Name == supplierName);
 747
 748
 0749                if (supplier != null)
 750
 751                {
 0752                    order.Supplier = supplier;
 753                }
 754
 755                else
 756
 757                {
 0758                    var newSupplier = new Supplier
 0759
 0760                    {
 0761                        Name = supplierName
 0762                    };
 763
 764
 0765                    _context.Suppliers.Add(newSupplier);
 0766                    await _context.SaveChangesAsync();
 767
 0768                    order.Supplier = newSupplier;
 0769                }
 770            }
 771
 772
 0773            var deliveryName = jsonOrder["productWarehousesAndCitysWithNumber"]["delivery_method"]["name"]?.ToString();
 774
 0775            var deliveryProvider = jsonOrder["productWarehousesAndCitysWithNumber"]["delivery_method"]["tpl_provider"]
 0776                ?.ToString();
 777
 778
 0779            if (!string.IsNullOrWhiteSpace(deliveryName))
 780
 781            {
 0782                var delivery = await _deliveryService.GetOrCreateDeliveryAsync(deliveryName, deliveryProvider);
 783
 784
 0785                order.Delivery = delivery;
 786
 0787                order.DeliveryId = delivery.Id;
 788            }
 789
 790
 0791            order = SetCorrectProductKey(order);
 792
 0793            await CreateOrderPriceAndIndexes(order, jsonOrder);
 794
 795            // Снимок не использует OrderPrice: он фиксирует полный ответ Ozon и условия акций
 796            // отдельно, чтобы повторная загрузка заказа не изменила историю продажи.
 797            // Webhook и ручной импорт могут повторно прислать то же отправление: для такого
 798            // заказа снимок уже существует и уникальный индекс OrderId не допускает второй.
 0799            var snapshotAlreadyCaptured = await _context.OrderOzonPromotionSnapshots
 0800                .AsNoTracking()
 0801                .AnyAsync(snapshot => snapshot.Order.Key == order.Key);
 0802            if (!snapshotAlreadyCaptured)
 803            {
 0804                order.OzonPromotionSnapshot = CreateOzonPromotionSnapshot(jsonOrder);
 805            }
 806
 807
 0808            var (price, priceWithDiscount, maxComment, minComment, min, max) = CalculateСommissions(jsonOrder, order);
 809
 810
 0811            order.Price = price;
 812
 0813            order.ProductInfo.CurrentPriceWithDiscount = priceWithDiscount;
 814
 815
 0816            order.MaxOzonCommission = max;
 817
 0818            order.MinOzonCommission = min;
 819
 0820            order.MaxCommissionInfo = maxComment;
 821
 0822            order.MinCommissionInfo = minComment;
 823
 0824            order.UpdatedBy = "Ozon";
 825
 0826            await _context.SaveChangesAsync();
 827
 828
 0829            return order;
 0830        }
 831
 832
 833        /// <summary>
 834        /// Цена позиции из заказа Яндекс.Маркета: prices.payment (оплата покупателем) + prices.subsidy (субсидия Маркет
 835        /// </summary>
 836        private static decimal? GetYandexOrderItemPrice(JToken? item)
 837        {
 0838            var payment = item?["prices"]?["payment"]?["value"]?.Value<decimal>();
 839
 0840            if (payment is null or <= 0)
 841            {
 0842                return null;
 843            }
 844
 0845            var subsidy = item?["prices"]?["subsidy"]?["value"]?.Value<decimal>() ?? 0;
 846
 0847            return payment.Value + subsidy;
 848        }
 849
 850        public async Task<Order> CastToModelFromYandex(JToken? jsonOrder)
 851
 852        {
 0853            Order order = new();
 854
 855
 0856            CultureInfo culture = new("en-US");
 857
 0858            NumberStyles style = NumberStyles.Number;
 859
 860            // Добавлены форматы с timezone offset (например, +03:00)
 861
 0862            var dateFormats = new[]
 0863
 0864            {
 0865                "yyyy-MM-ddTHH:mm:ss.fffzzz", // С миллисекундами и timezone с : (2026-01-14T21:21:30.209+03:00)
 0866
 0867                "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", // Дополнительные варианты
 0868
 0869                "yyyy-MM-ddTHH:mm:ss.FFFK",
 0870
 0871                "yyyy-MM-ddTHH:mm:ssK",
 0872
 0873                "yyyy-MM-ddTHH:mm:ss.FFFFFFF",
 0874
 0875                "yyyy-MM-ddTHH:mm:ss.FFF",
 0876
 0877                "yyyy-MM-ddTHH:mm:ss",
 0878
 0879                "yyyy-MM-dd",
 0880
 0881                "dd.MM.yyyy", // 14.01.2026
 0882
 0883                "MM/dd/yyyy", // 01/14/2026
 0884
 0885                "yyyy/MM/dd"
 0886            };
 887
 888
 889            // 1. Исправление: проверка на null для обязательных полей
 890
 0891            if (jsonOrder == null)
 892
 0893                return order;
 894
 895
 0896            order.Key = jsonOrder["orderId"]?.ToString() + jsonOrder["items"]?[0]?["offerId"]?.ToString();
 897
 898
 0899            order.ShipmentNumber = jsonOrder["orderId"]?.ToString();
 900
 901
 0902            if (jsonOrder["creationDate"] != null)
 903
 904            {
 0905                var creationDateStr = jsonOrder["creationDate"].ToString();
 906
 907
 908                // Определяем разделитель (T или пробел)
 909
 0910                char separator = 'T';
 911
 0912                if (creationDateStr.Contains(' '))
 913
 914                {
 0915                    separator = ' ';
 916                }
 917
 0918                else if (creationDateStr.Contains('T'))
 919
 920                {
 0921                    separator = 'T';
 922                }
 923
 924
 925                // Извлекаем только дату (часть до разделителя)
 926
 927                string dateOnlyStr;
 928
 0929                if (creationDateStr.Contains(separator))
 930
 931                {
 0932                    dateOnlyStr = creationDateStr.Split(separator)[0]; // Берем часть до разделителя
 933                }
 934
 935                else
 936
 937                {
 0938                    dateOnlyStr = creationDateStr; // Если нет разделителя, берем всю строку
 939                }
 940
 941
 0942                if (DateTime.TryParseExact(
 0943                        dateOnlyStr,
 0944                        dateFormats,
 0945                        CultureInfo.InvariantCulture,
 0946                        DateTimeStyles.None,
 0947                        out var processingDate))
 948
 949                {
 0950                    order.ProcessingDate = processingDate;
 951                }
 952
 953                else
 954
 955                {
 956                    // Если не получилось через TryParseExact, пробуем обычный Parse
 957
 0958                    if (DateTime.TryParse(dateOnlyStr, CultureInfo.InvariantCulture, DateTimeStyles.None,
 0959                            out processingDate))
 960
 961                    {
 0962                        order.ProcessingDate = processingDate;
 963                    }
 964                }
 965            }
 966
 967
 968            // 3. Исправление: проверка вложенных полей доступа для даты отгрузки
 969
 970            // Сначала проверяем delivery.shipment.shipmentDate (новый формат)
 971
 0972            if (jsonOrder["delivery"]?["shipment"]?["shipmentDate"] != null)
 973
 974            {
 0975                var shipmentDateStr = jsonOrder["delivery"]["shipment"]["shipmentDate"]?.ToString();
 976
 0977                if (!string.IsNullOrEmpty(shipmentDateStr))
 978
 979                {
 0980                    if (DateTimeOffset.TryParse(
 0981                            shipmentDateStr,
 0982                            CultureInfo.InvariantCulture,
 0983                            DateTimeStyles.RoundtripKind,
 0984                            out var shippingOffset)
 0985                        || DateTimeOffset.TryParseExact(
 0986                            shipmentDateStr,
 0987                            dateFormats,
 0988                            CultureInfo.InvariantCulture,
 0989                            DateTimeStyles.RoundtripKind,
 0990                            out shippingOffset))
 991
 992                    {
 0993                        order.ShippingDate = shippingOffset.DateTime;
 994                    }
 995                }
 996            }
 997
 998            // Если не нашли, проверяем старый формат delivery.shipments
 999
 01000            else if (jsonOrder["delivery"]?["shipments"] != null)
 1001
 1002            {
 01003                var shipmentDateStr = jsonOrder["delivery"]["shipments"]?.FirstOrDefault()?["shipmentDate"]?.ToString();
 1004
 01005                if (!string.IsNullOrEmpty(shipmentDateStr))
 1006
 1007                {
 01008                    if (DateTimeOffset.TryParse(
 01009                            shipmentDateStr,
 01010                            CultureInfo.InvariantCulture,
 01011                            DateTimeStyles.RoundtripKind,
 01012                            out var shippingOffset)
 01013                        || DateTimeOffset.TryParseExact(
 01014                            shipmentDateStr,
 01015                            dateFormats,
 01016                            CultureInfo.InvariantCulture,
 01017                            DateTimeStyles.RoundtripKind,
 01018                            out shippingOffset))
 1019
 1020                    {
 01021                        order.ShippingDate = shippingOffset.DateTime;
 1022                    }
 1023                }
 1024            }
 1025
 1026
 1027            // 4. Исправление: безопасный доступ к статусу
 1028
 01029            string status = jsonOrder["status"]?.ToString() ?? "UNKNOWN";
 1030
 01031            order.ClientStatus = await GetOrCreateClientStatusAsync(YandexStatus.GetStatus(status));
 1032
 1033
 1034            // 5. Исправление: безопасный доступ к полям items
 1035
 01036            var firstItem = jsonOrder["items"]?[0];
 1037
 01038            if (firstItem != null)
 1039
 1040            {
 01041                order.ProductName = firstItem["offerName"]?.ToString() ?? string.Empty;
 1042
 01043                order.ProductKey = firstItem["offerId"]?.ToString() ?? string.Empty;
 1044
 01045                order.Article = firstItem["offerId"]?.ToString() ?? string.Empty;
 1046
 1047
 01048                if (int.TryParse(DelSubStr(firstItem["count"]?.ToString() ?? "0"), style, culture, out var quantity))
 1049
 1050                {
 01051                    order.Quantity = quantity;
 1052                }
 1053
 1054
 1055                // 6. Склад отгрузки из campaign. Для кампаний из YandexCampaignWarehouses
 1056                // имя кампании — доставка, а склад отгрузки в Studio2 должен совпадать
 1057                // с фактическим складом (V11, M1), а не с названием кампании.
 1058
 01059                var campaignName = jsonOrder["campaign"]?["domain"]?.ToString() ?? string.Empty;
 01060                var warehouseName = campaignName;
 1061
 01062                if (YandexCampaignWarehouses.TryGetValue(campaignName, out var mappedWarehouseName))
 1063                {
 01064                    warehouseName = mappedWarehouseName;
 1065
 01066                    var delivery = await _deliveryService.GetOrCreateDeliveryAsync(campaignName,
 01067                        YandexDeliveryProviderName);
 01068                    order.Delivery = delivery;
 01069                    order.DeliveryId = delivery.Id;
 1070                }
 1071
 1072
 01073                if (!string.IsNullOrEmpty(warehouseName))
 1074
 1075                {
 01076                    var shipmentWarehouse = _context.Warehouses.FirstOrDefault(w => w.Name == warehouseName);
 1077
 1078
 01079                    if (shipmentWarehouse != null)
 1080
 1081                    {
 01082                        order.ShipmentWarehouse = shipmentWarehouse;
 1083                    }
 1084
 1085                    else
 1086
 1087                    {
 01088                        var newWarehouse = new Warehouse { Name = warehouseName };
 1089
 01090                        _context.Warehouses.Add(newWarehouse);
 1091
 01092                        await _context.SaveChangesAsync();
 1093
 01094                        order.ShipmentWarehouse = newWarehouse;
 01095                    }
 1096                }
 1097
 1098
 1099                // 7. Исправление: извлечение кода производителя
 1100
 01101                string extractedCode = string.Empty;
 1102
 01103                string offerId = firstItem["offerId"]?.ToString() ?? string.Empty;
 1104
 1105
 01106                if (!string.IsNullOrEmpty(offerId))
 1107
 1108                {
 01109                    var match3 = Regex.Match(offerId, @"=(\w{3})");
 1110
 01111                    if (match3.Success)
 1112
 1113                    {
 01114                        extractedCode = match3.Groups[1].Value;
 1115                    }
 1116
 1117                    else
 1118
 1119                    {
 01120                        var match2 = Regex.Match(offerId, @"=(\w{2})");
 1121
 01122                        if (match2.Success)
 1123
 1124                        {
 01125                            extractedCode = match2.Groups[1].Value;
 1126                        }
 1127                    }
 1128                }
 1129
 1130
 01131                if (!string.IsNullOrEmpty(extractedCode))
 1132
 1133                {
 01134                    var manufacturer = _context.Manufacturers.FirstOrDefault(m => m.Code == extractedCode);
 1135
 1136
 01137                    if (manufacturer != null)
 1138
 1139                    {
 01140                        order.Manufacturer = manufacturer;
 1141                    }
 1142
 1143                    else
 1144
 1145                    {
 01146                        var newManufacturer = new Manufacturer { Code = extractedCode };
 1147
 01148                        _context.Manufacturers.Add(newManufacturer);
 1149
 01150                        await _context.SaveChangesAsync();
 1151
 01152                        order.Manufacturer = newManufacturer;
 01153                    }
 1154
 1155
 01156                    order = await SetEtProducerByCode(order, extractedCode);
 1157                }
 1158
 1159
 1160                // 8. Исправление: валюта
 1161
 01162                string currencyCode = firstItem["prices"]?["payment"]?["currencyId"]?.ToString() ?? "RUR";
 1163
 1164
 01165                var currency = _context.Currencys.FirstOrDefault(c => c.Name == currencyCode);
 1166
 1167
 01168                if (currency != null)
 1169
 1170                {
 01171                    order.Сurrency = currency;
 1172                }
 1173
 1174                else
 1175
 1176                {
 01177                    var newCurrency = new Currency { Name = currencyCode };
 1178
 01179                    _context.Currencys.Add(newCurrency);
 1180
 01181                    order.Сurrency = newCurrency;
 1182                }
 1183
 1184
 01185                order.Supplier = null;
 1186
 01187                order.PurchasePrice = null;
 1188
 1189
 1190                // 9. Исправление: ProductInfo
 1191
 01192                if (order.ProductInfo == null)
 1193
 1194                {
 01195                    string article = firstItem["offerId"]?.ToString() ?? string.Empty;
 1196
 01197                    var primaryImageUrl = GetYandexPrimaryImageUrl(firstItem);
 1198
 01199                    var product = _context.Products.FirstOrDefault(p => p.Article == article);
 1200
 1201
 01202                    if (product != null)
 1203
 1204                    {
 1205                        // Аналогично Ozon: не перезаписываем уже заданное фото,
 1206                        // но дополняем карточку изображением из offer-mappings Яндекс.Маркета.
 01207                        if (string.IsNullOrWhiteSpace(product.ImageUrl) && !string.IsNullOrWhiteSpace(primaryImageUrl))
 1208                        {
 01209                            product.ImageUrl = primaryImageUrl;
 1210                        }
 1211
 01212                        order.ProductInfo = product;
 1213                    }
 1214
 1215                    else
 1216
 1217                    {
 01218                        var offerMapping = firstItem["offerMapping"];
 1219
 01220                        var mapping = offerMapping?["mapping"];
 1221
 01222                        var offer = offerMapping?["offer"];
 1223
 1224
 01225                        product = new Product
 01226
 01227                        {
 01228                            Article = article,
 01229
 01230                            CommercialCategory = mapping?["marketCategoryName"]?.ToString() ?? string.Empty,
 01231
 01232                            ImageUrl = primaryImageUrl
 01233                        };
 1234
 1235
 1236                        // Баркод из delivery.boxesLayout[0].barcode
 1237
 01238                        var barcode = jsonOrder["delivery"]?["boxesLayout"]?[0]?["barcode"]?.ToString();
 1239
 01240                        if (!string.IsNullOrEmpty(barcode))
 1241
 1242                        {
 01243                            product.Barcode = barcode;
 1244                        }
 1245
 1246
 1247                        // Вес из weightDimensions
 1248
 01249                        var weightDimensions = offer?["weightDimensions"];
 1250
 01251                        if (weightDimensions != null)
 1252
 1253                        {
 01254                            var weightStr = weightDimensions["weight"]?.ToString();
 1255
 01256                            if (!string.IsNullOrEmpty(weightStr))
 1257
 1258                            {
 01259                                if (double.TryParse(DelSubStr(weightStr), style, culture, out var weight))
 1260
 1261                                {
 01262                                    product.Weight = (decimal)weight;
 1263
 01264                                    product.VolumetricWeight = weight;
 1265
 1266
 1267                                    // Объем из габаритов (length * width * height / 1000000 для перевода в литры)
 1268
 01269                                    var lengthStr = weightDimensions["length"]?.ToString();
 1270
 01271                                    var widthStr = weightDimensions["width"]?.ToString();
 1272
 01273                                    var heightStr = weightDimensions["height"]?.ToString();
 1274
 1275
 01276                                    if (!string.IsNullOrEmpty(lengthStr) && !string.IsNullOrEmpty(widthStr) &&
 01277                                        !string.IsNullOrEmpty(heightStr))
 1278
 1279                                    {
 01280                                        if (double.TryParse(DelSubStr(lengthStr), style, culture, out var length) &&
 01281                                            double.TryParse(DelSubStr(widthStr), style, culture, out var width) &&
 01282                                            double.TryParse(DelSubStr(heightStr), style, culture, out var height))
 1283
 1284                                        {
 01285                                            product.Volume = (length * width * height) / 1000000.0; // см³ в литры
 1286                                        }
 1287                                    }
 1288                                }
 1289                            }
 1290                        }
 1291
 1292
 01293                        _context.Products.Add(product);
 1294
 01295                        await _context.SaveChangesAsync();
 1296
 01297                        order.ProductInfo = product;
 1298                    }
 01299                }
 1300
 1301
 1302                // Получаем базовую цену из offerMapping для OrderPrice
 1303
 01304                var basicPriceValue = firstItem["offerMapping"]?["offer"]?["basicPrice"]?["value"]?.ToString();
 1305
 01306                var discountBaseValue = firstItem["offerMapping"]?["offer"]?["basicPrice"]?["discountBase"]?.ToString();
 1307
 1308
 01309                decimal? basicPrice = null;
 1310
 01311                if (!string.IsNullOrEmpty(basicPriceValue))
 1312
 1313                {
 01314                    if (decimal.TryParse(DelSubStr(basicPriceValue), style, culture, out var parsed))
 1315
 1316                    {
 01317                        basicPrice = parsed;
 1318                    }
 1319                }
 1320
 1321
 01322                decimal? discountBase = null;
 1323
 01324                if (!string.IsNullOrEmpty(discountBaseValue))
 1325
 1326                {
 01327                    if (decimal.TryParse(DelSubStr(discountBaseValue), style, culture, out var parsedBase))
 1328
 1329                    {
 01330                        discountBase = parsedBase;
 1331                    }
 1332                }
 1333
 1334
 1335                // Расчет итоговых цен
 1336
 01337                order.ProductInfo.CurrentPriceWithDiscount = basicPrice;
 1338
 1339                // Цена заказа — реальная цена сделки (оплата покупателя + субсидия Маркета).
 1340                // basicPrice из каталога может быть заглушкой (например, 50000 у скрытого оффера),
 1341                // поэтому он только запасной вариант, если в заказе цены нет.
 01342                order.Price = GetYandexOrderItemPrice(firstItem) ?? basicPrice;
 1343
 01344                order.ShipmentAmount = order.Price * order.Quantity;
 1345
 1346
 1347                // Создаем OrderPrice с безопасными значениями
 1348
 01349                order.OrderPrice = new OrderPrice
 01350
 01351                {
 01352                    CurrencyCode = firstItem["offerMapping"]?["offer"]?["basicPrice"]?["currencyId"]?.ToString()
 01353                                   ?? firstItem["prices"]?["payment"]?["currencyId"]?.ToString()
 01354                                   ?? "RUR"
 01355                };
 1356
 1357
 1358                // Устанавливаем цены только если они есть
 1359
 01360                if (basicPrice.HasValue)
 1361
 1362                {
 01363                    order.OrderPrice.MarketingPrice = basicPrice.Value;
 1364
 01365                    order.OrderPrice.Price = basicPrice.Value;
 1366
 01367                    order.OrderPrice.RetailPrice = basicPrice.Value;
 1368                }
 1369
 1370
 01371                if (discountBase.HasValue)
 1372
 1373                {
 01374                    order.OrderPrice.OldPrice = discountBase.Value;
 1375                }
 1376
 1377
 1378                // 11. Исправление: расчет комиссий из tariffs
 1379
 01380                var tariffsArray = firstItem["tariffs"];
 1381
 01382                decimal? totalCommission = 0;
 1383
 1384
 1385                // Список допустимых типов комиссий
 1386
 01387                var allowedCommissionTypes = new HashSet<string>
 01388
 01389                {
 01390                    "AGENCY_COMMISSION", // Прием платежа покупателя
 01391
 01392                    "FEE", // Размещение товара на Маркете
 01393
 01394                    "DELIVERY_TO_CUSTOMER", // Доставка покупателю
 01395
 01396                    "CROSSREGIONAL_DELIVERY", // Доставка в федеральный округ, город или населенный пункт
 01397
 01398                    "EXPRESS_DELIVERY", // Экспресс-доставка покупателю
 01399
 01400                    "MIDDLE_MILE" // Средняя миля
 01401                };
 1402
 1403
 01404                if (tariffsArray != null && tariffsArray.Any())
 1405
 1406                {
 01407                    decimal sum = 0;
 1408
 01409                    var commissionDetails = new List<string>();
 1410
 1411
 1412                    // Используем InvariantCulture для парсинга чисел, чтобы гарантировать работу на любой локали
 1413
 01414                    var invariantCulture = CultureInfo.InvariantCulture;
 1415
 01416                    var numberStyle = NumberStyles.Number | NumberStyles.AllowDecimalPoint;
 1417
 1418
 01419                    foreach (var tariff in tariffsArray)
 1420
 1421                    {
 01422                        var tariffType = tariff?["type"]?.ToString();
 1423
 01424                        var amountStr = tariff?["amount"]?.ToString();
 1425
 1426
 1427                        // Проверяем, что тип комиссии в списке допустимых
 1428
 01429                        if (!string.IsNullOrEmpty(tariffType) &&
 01430                            allowedCommissionTypes.Contains(tariffType) &&
 01431                            !string.IsNullOrEmpty(amountStr))
 1432
 1433                        {
 1434                            // Удаляем только апостроф в начале, если есть
 1435
 01436                            var cleanedAmountStr = DelSubStr(amountStr);
 1437
 1438
 1439                            // Нормализуем десятичный разделитель: заменяем запятую на точку
 1440
 1441                            // Это гарантирует работу на любой локали (ru-RU использует запятую, en-US - точку)
 1442
 01443                            cleanedAmountStr = cleanedAmountStr.Replace(',', '.');
 1444
 1445
 1446                            // Парсим с InvariantCulture для гарантии работы на любой локали
 1447
 01448                            if (decimal.TryParse(cleanedAmountStr, numberStyle, invariantCulture, out var amount))
 1449
 1450                            {
 01451                                sum += amount;
 1452
 01453                                commissionDetails.Add($"{tariffType}: {amountStr}");
 1454                            }
 1455                        }
 1456                    }
 1457
 1458
 01459                    totalCommission = sum > 0 ? sum + 25 : (decimal?)0;
 1460
 1461
 1462                    // Сохраняем информацию о комиссиях
 1463
 01464                    if (totalCommission.HasValue && totalCommission.Value > 0)
 1465
 1466                    {
 01467                        var commissionInfo = string.Join(", ", commissionDetails);
 1468
 01469                        order.MaxCommissionInfo = commissionInfo;
 1470
 01471                        order.MinCommissionInfo = commissionInfo;
 1472                    }
 1473                }
 1474
 1475
 01476                order.MaxOzonCommission = totalCommission;
 1477
 01478                order.MinOzonCommission = totalCommission;
 01479            }
 1480
 1481
 1482            // 12. Исправление: город доставки и категория
 1483
 1484            // Проверяем различные возможные пути к городу доставки
 1485
 01486            var city = jsonOrder["deliveryRegion"]?["name"]?.ToString()
 01487                       ?? jsonOrder["delivery"]?["region"]?["name"]?.ToString()
 01488                       ?? jsonOrder["delivery"]?["address"]?["city"]?.ToString()
 01489                       ?? string.Empty;
 1490
 01491            if (!string.IsNullOrEmpty(city))
 1492
 1493            {
 01494                order.DeliveryCity = city;
 1495            }
 1496
 1497
 1498            // Категория из offerMapping
 1499
 01500            if (firstItem != null)
 1501
 1502            {
 01503                var categoryName = firstItem["offerMapping"]?["mapping"]?["marketCategoryName"]?.ToString();
 1504
 01505                if (!string.IsNullOrEmpty(categoryName))
 1506
 1507                {
 01508                    order.NewCategory = categoryName;
 1509                }
 1510            }
 1511
 1512
 1513            // 13. Исправление: AppStatus
 1514
 01515            if (order.AppStatus == null)
 1516
 1517            {
 01518                string statusName = "Не указан";
 1519
 1520
 01521                var appStatus = _context.AppStatuses.FirstOrDefault(c => c.Name == statusName);
 1522
 1523
 01524                if (appStatus != null)
 1525
 1526                {
 01527                    order.AppStatus = appStatus;
 1528                }
 1529
 1530                else
 1531
 1532                {
 01533                    var newStatus = new AppStatus { Name = statusName };
 1534
 01535                    _context.AppStatuses.Add(newStatus);
 1536
 01537                    order.AppStatus = newStatus;
 1538                }
 1539            }
 1540
 1541
 1542            // 14. Исправление: Supplier
 1543
 01544            if (order.Supplier == null)
 1545
 1546            {
 01547                string supplierName = "Не указан";
 1548
 1549
 01550                var supplier = _context.Suppliers.FirstOrDefault(c => c.Name == supplierName);
 1551
 1552
 01553                if (supplier != null)
 1554
 1555                {
 01556                    order.Supplier = supplier;
 1557                }
 1558
 1559                else
 1560
 1561                {
 01562                    var newSupplier = new Supplier { Name = supplierName };
 1563
 01564                    _context.Suppliers.Add(newSupplier);
 1565
 01566                    order.Supplier = newSupplier;
 1567                }
 1568            }
 1569
 1570
 01571            order.UpdatedBy = "Yandex";
 1572
 01573            order = SetCorrectProductKey(order);
 1574
 1575
 01576            await _context.SaveChangesAsync();
 1577
 1578
 01579            return order;
 01580        }
 1581
 1582        /// <summary>
 1583        /// Возвращает первое доступное изображение предложения из данных offer-mappings Яндекс.Маркета.
 1584        /// API отдает pictures как массив URL, а в некоторых ответах — как объекты с полем url.
 1585        /// </summary>
 1586        private static string? GetYandexPrimaryImageUrl(JToken? item)
 1587        {
 01588            var offer = item?["offerMapping"]?["offer"];
 1589
 01590            foreach (var mediaCollection in new[] { offer?["pictures"], offer?["mediaFiles"] })
 1591            {
 01592                if (mediaCollection is not JArray mediaItems)
 1593                    continue;
 1594
 01595                foreach (var mediaItem in mediaItems)
 1596                {
 01597                    var url = mediaItem.Type == JTokenType.String
 01598                        ? mediaItem.Value<string>()
 01599                        : mediaItem?["url"]?.Value<string>();
 1600
 01601                    if (Uri.TryCreate(url, UriKind.Absolute, out var imageUri) &&
 01602                        (imageUri.Scheme == Uri.UriSchemeHttp || imageUri.Scheme == Uri.UriSchemeHttps))
 1603                    {
 01604                        return imageUri.ToString();
 1605                    }
 1606                }
 1607            }
 1608
 01609            return null;
 01610        }
 1611
 1612
 1613        private async Task<Order> CastToModelFromExcel(Dictionary<string, string> dataColumn,
 1614            OzonClient selectedClient,
 1615            Manufacturer selectedManufacturer,
 1616            Warehouse selectedWarehouse,
 1617            Supplier selectedSupplier,
 1618            string selectedClientStatus,
 1619            CurrencyCode selectedCurrencyCode,
 1620            DateTime? selectedShippingDate,
 1621            DateTime? selectedProcessingDate)
 1622
 1623        {
 01624            Order order = new();
 1625
 1626
 01627            CultureInfo culture = new("en-US");
 1628
 01629            NumberStyles style = NumberStyles.Number;
 1630
 1631
 01632            if (dataColumn.TryGetValue("Артикул", out var article))
 1633
 1634            {
 01635                order.ProductKey = article?.ToString();
 1636            }
 1637
 01638            else if (dataColumn.TryGetValue("Key", out var key))
 1639
 1640            {
 01641                order.ProductKey = key.ToString();
 1642
 01643                article = key.ToString();
 1644
 01645                selectedManufacturer = new Manufacturer() { Id = -1 };
 1646            }
 1647
 1648
 01649            order.Key = (dataColumn.TryGetValue("Номер заказа", out var shipmentNumber)
 01650                            ? shipmentNumber?.ToString()
 01651                            : null)
 01652                        + (order.ProductKey != null ? order.ProductKey : null);
 1653
 1654
 01655            order.ShipmentNumber = shipmentNumber?.ToString();
 1656
 1657
 01658            if (dataColumn.TryGetValue("Клиент", out var clientName))
 1659
 1660            {
 01661                OzonClient clientByName = _context.OzonClients.FirstOrDefault(o => o.Name == clientName);
 1662
 01663                if (clientByName == null)
 1664
 1665                {
 01666                    OzonClient newOzonClient = new OzonClient()
 01667
 01668                    {
 01669                        Name = clientName,
 01670
 01671                        CurrencyCode = CurrencyCode.USD,
 01672                    };
 1673
 1674
 01675                    _context.OzonClients.Add(newOzonClient);
 1676
 01677                    await _context.SaveChangesAsync();
 1678
 1679
 01680                    order.OzonClient = newOzonClient;
 01681                }
 1682
 1683                else
 1684
 1685                {
 01686                    order.OzonClient = clientByName;
 1687                }
 1688            }
 1689
 01690            else if (selectedClient.Id != 0)
 1691
 1692            {
 01693                order.OzonClient = _context.OzonClients.Find(selectedClient.Id);
 1694            }
 1695
 1696
 01697            if (selectedCurrencyCode != CurrencyCode.NON && order.OzonClient != null)
 1698
 1699            {
 01700                if (order.OzonClient.CurrencyCode != selectedCurrencyCode)
 1701
 1702                {
 01703                    order.OzonClient.CurrencyCode = selectedCurrencyCode;
 1704                }
 1705            }
 1706
 01707            else if (selectedClient.Id != 0)
 1708
 1709            {
 01710                order.OzonClient = _context.OzonClients.Find(selectedClient.Id);
 1711            }
 1712
 1713
 01714            var cultureDateInfo = new CultureInfo("ru-RU");
 1715
 1716
 01717            order.ProcessingDate = dataColumn.TryGetValue("Принят в обработку", out var processingDateStr) &&
 01718                                   DateTime.TryParse(processingDateStr, cultureDateInfo, DateTimeStyles.None,
 01719                                       out var processingDate)
 01720                ? processingDate
 01721                : selectedProcessingDate != null
 01722                    ? selectedProcessingDate
 01723                    : DateTime.Now.AddHours(_releaseManager.GetTimeZone());
 1724
 1725
 01726            order.ShippingDate = dataColumn.TryGetValue("Дата отгрузки", out var shippingDateStr) &&
 01727                                 DateTime.TryParse(shippingDateStr, cultureDateInfo, DateTimeStyles.None,
 01728                                     out var shippingDate)
 01729                ? shippingDate
 01730                : selectedShippingDate;
 1731
 1732
 1733            // Проверяем "Статус клиента" сначала, затем "Статус"
 1734
 01735            if (dataColumn.TryGetValue("Статус клиента", out var clientStatusValue))
 1736
 1737            {
 01738                order.ClientStatus = await GetOrCreateClientStatusAsync(clientStatusValue?.ToString());
 1739            }
 1740
 01741            else if (dataColumn.TryGetValue("Статус", out var status))
 1742
 1743            {
 01744                order.ClientStatus = await GetOrCreateClientStatusAsync(status?.ToString());
 1745            }
 1746
 01747            else if (selectedClientStatus != null)
 1748
 1749            {
 01750                order.ClientStatus = await GetOrCreateClientStatusAsync(selectedClientStatus);
 1751            }
 1752
 1753            else
 1754
 1755            {
 01756                order.ClientStatus = null;
 1757            }
 1758
 1759
 01760            order.ProductName = dataColumn.TryGetValue("Наименование товара", out var productName)
 01761                ? productName?.ToString()
 01762                : null;
 1763
 1764
 01765            order.Quantity = dataColumn.TryGetValue("Кол.", out var quantityStr) &&
 01766                             int.TryParse(DelSubStr(quantityStr), style, culture, out var quantity)
 01767                ? quantity
 01768                : (int?)null;
 1769
 1770
 01771            if (dataColumn.TryGetValue("Склад отгрузки", out var warehouseName))
 1772
 1773            {
 01774                Warehouse shipmentWarehouse = _context.Warehouses.FirstOrDefault(w => w.Name == warehouseName);
 1775
 1776
 01777                if (shipmentWarehouse != null)
 1778
 1779                {
 01780                    order.ShipmentWarehouse = shipmentWarehouse;
 1781                }
 1782
 1783                else
 1784
 1785                {
 01786                    var newWarehouse = new Warehouse { Name = warehouseName };
 1787
 01788                    _context.Warehouses.Add(newWarehouse);
 1789
 01790                    await _context.SaveChangesAsync();
 1791
 01792                    order.ShipmentWarehouse = newWarehouse;
 01793                }
 1794            }
 1795
 01796            else if (selectedWarehouse != null && selectedWarehouse.Id != 0)
 1797
 1798            {
 01799                order.ShipmentWarehouse = _context.Warehouses.FirstOrDefault(w => w.Id == selectedWarehouse.Id);
 1800            }
 1801
 1802
 01803            if (dataColumn.TryGetValue("Производитель", out var manufacturerName))
 1804
 1805            {
 01806                Manufacturer manufacturer = _context.Manufacturers.FirstOrDefault(m => m.Name == manufacturerName);
 1807
 1808
 01809                if (manufacturer != null)
 1810
 1811                {
 01812                    order.Manufacturer = manufacturer;
 1813                }
 1814
 1815                else
 1816
 1817                {
 01818                    var newManufacturer = new Manufacturer
 01819
 01820                    {
 01821                        Code = null,
 01822
 01823                        Name = manufacturerName
 01824                    };
 1825
 1826
 01827                    _context.Manufacturers.Add(newManufacturer);
 1828
 01829                    _context.SaveChanges();
 1830
 1831
 01832                    order.Manufacturer = newManufacturer;
 1833                }
 1834
 1835
 01836                order = await SetEtProducerByName(order, manufacturerName);
 1837            }
 1838
 01839            else if (selectedManufacturer != null && selectedManufacturer.Id != 0)
 1840
 1841            {
 01842                if (selectedManufacturer.Id == -1)
 1843
 1844                {
 01845                    string extractedCode = Regex.Match(article.ToString(), @"=(\w{3})").Groups[1].Value;
 1846
 1847
 01848                    if (string.IsNullOrEmpty(extractedCode))
 1849
 1850                    {
 01851                        extractedCode = Regex.Match(article.ToString(), @"=(\w{2})").Groups[1].Value;
 1852                    }
 1853
 1854
 01855                    Manufacturer manufacturer = _context.Manufacturers
 01856                        .Where(m => m.Code == extractedCode)
 01857                        .FirstOrDefault();
 1858
 1859
 01860                    if (manufacturer != null)
 1861
 1862                    {
 01863                        order.Manufacturer = manufacturer;
 1864                    }
 1865
 1866                    else
 1867
 1868                    {
 01869                        var newManufacturer = new Manufacturer { Code = extractedCode };
 1870
 01871                        _context.Manufacturers.Add(newManufacturer);
 1872
 01873                        _context.SaveChanges();
 1874
 01875                        order.Manufacturer = newManufacturer;
 1876                    }
 1877
 1878
 01879                    order = await SetEtProducerByCode(order, extractedCode);
 1880                }
 1881
 01882                else if (selectedManufacturer.Id == -2)
 1883
 1884                {
 01885                    string tabelManufacturerName = order.ProductName != null ? order.ProductName.Split(' ')[0] : null;
 1886
 01887                    if (tabelManufacturerName != null)
 1888
 1889                    {
 01890                        Manufacturer manufacturer = _context.Manufacturers
 01891                            .Where(m => m.Name.ToLower().Contains(tabelManufacturerName.ToLower())).FirstOrDefault();
 1892
 01893                        if (manufacturer != null)
 1894
 1895                        {
 01896                            order.Manufacturer = manufacturer;
 1897                        }
 1898                    }
 1899                }
 1900
 1901                else
 1902
 1903                {
 01904                    order.Manufacturer = _context.Manufacturers.FirstOrDefault(m => m.Id == selectedManufacturer.Id);
 1905                }
 1906            }
 1907
 1908
 01909            if (dataColumn.TryGetValue("Код валюты отправления", out var currencyName))
 1910
 1911            {
 01912                Currency currency = _context.Currencys.FirstOrDefault(c => c.Name == currencyName);
 1913
 1914
 01915                if (currency != null)
 1916
 1917                {
 01918                    order.Сurrency = currency;
 1919                }
 1920
 1921                else
 1922
 1923                {
 01924                    var newCurrency = new Currency
 01925
 01926                    {
 01927                        Name = currencyName
 01928                    };
 1929
 1930
 01931                    _context.Currencys.Add(newCurrency);
 1932
 01933                    await _context.SaveChangesAsync();
 1934
 01935                    order.Сurrency = newCurrency;
 01936                }
 1937            }
 1938
 1939
 01940            order.Supplier = null;
 1941
 01942            order.PurchasePrice = null;
 1943
 1944
 01945            if (order.ProductInfo == null && dataColumn.TryGetValue("Артикул", out var productArticle))
 1946
 1947            {
 01948                Product product = _context.Products.FirstOrDefault(p => p.Article == productArticle);
 1949
 1950
 01951                if (product != null)
 1952
 1953                {
 01954                    order.ProductInfo = product;
 1955                }
 1956
 1957                else
 1958
 1959                {
 01960                    product = new Product
 01961
 01962                    {
 01963                        Article = productArticle
 01964                    };
 1965
 1966
 01967                    _context.Products.Add(product);
 1968
 01969                    await _context.SaveChangesAsync();
 1970
 01971                    order.ProductInfo = product;
 1972                }
 01973            }
 1974
 01975            else if (order.ProductInfo == null && !dataColumn.TryGetValue("Артикул", out var nullProductArticle))
 1976
 1977            {
 01978                var product = new Product();
 1979
 1980
 01981                _context.Products.Add(product);
 1982
 01983                _context.SaveChanges();
 1984
 1985
 01986                order.ProductInfo = product;
 1987            }
 1988
 1989
 01990            order.DeliveryCity = dataColumn.TryGetValue("Город доставки", out var deliveryCity)
 01991                ? deliveryCity?.ToString()
 01992                : null;
 1993
 1994
 1995            // Проверяем "Статус клиента" сначала, затем "Статус клинта" (для обратной совместимости), затем "Статус"
 1996
 01997            string clientStatusValue1 = null;
 1998
 01999            if (dataColumn.TryGetValue("Статус клиента", out clientStatusValue1))
 2000
 2001            {
 02002                order.ClientStatus = await GetOrCreateClientStatusAsync(clientStatusValue1?.ToString());
 2003            }
 2004
 02005            else if (dataColumn.TryGetValue("Статус клинта", out var clientStauts))
 2006
 2007            {
 02008                order.ClientStatus = await GetOrCreateClientStatusAsync(clientStauts?.ToString());
 2009            }
 2010
 02011            else if (dataColumn.TryGetValue("Статус", out var status))
 2012
 2013            {
 02014                order.ClientStatus = await GetOrCreateClientStatusAsync(status?.ToString());
 2015            }
 2016
 02017            else if (selectedClientStatus != null)
 2018
 2019            {
 02020                order.ClientStatus = await GetOrCreateClientStatusAsync(selectedClientStatus);
 2021            }
 2022
 2023            else
 2024
 2025            {
 02026                order.ClientStatus = null;
 2027            }
 2028
 2029
 02030            if (order.AppStatus == null)
 2031
 2032            {
 02033                string statusName = "Не указан";
 2034
 2035
 02036                AppStatus appStatus = _context.AppStatuses.FirstOrDefault(c => c.Name == statusName);
 2037
 2038
 02039                if (appStatus != null)
 2040
 2041                {
 02042                    order.AppStatus = appStatus;
 2043                }
 2044
 2045                else
 2046
 2047                {
 02048                    var newStatus = new AppStatus { Name = statusName };
 2049
 2050
 02051                    _context.AppStatuses.Add(newStatus);
 2052
 02053                    await _context.SaveChangesAsync();
 2054
 02055                    order.AppStatus = newStatus;
 02056                }
 2057            }
 2058
 2059
 02060            if (order.Supplier == null && selectedSupplier.Id == 0)
 2061
 2062            {
 02063                string supplierName = "Не указан";
 2064
 2065
 02066                Supplier supplier = _context.Suppliers.FirstOrDefault(c => c.Name == supplierName);
 2067
 2068
 02069                if (supplier != null)
 2070
 2071                {
 02072                    order.Supplier = supplier;
 2073                }
 2074
 2075                else
 2076
 2077                {
 02078                    var newSupplier = new Supplier { Name = supplierName };
 2079
 2080
 02081                    _context.Suppliers.Add(newSupplier);
 2082
 02083                    await _context.SaveChangesAsync();
 2084
 02085                    order.Supplier = newSupplier;
 02086                }
 2087            }
 2088
 02089            else if (selectedSupplier.Id != 0)
 2090
 2091            {
 02092                order.Supplier = _context.Suppliers.FirstOrDefault(m => m.Id == selectedSupplier.Id);
 2093            }
 2094
 2095
 02096            order.Price = dataColumn.TryGetValue("Цена", out var priceStr) &&
 02097                          decimal.TryParse(DelSubStr(priceStr), style, culture, out var price)
 02098                ? price
 02099                : (decimal?)null;
 2100
 2101
 02102            if (dataColumn.TryGetValue("Сумма отправления", out var shipmentAmountStr) &&
 02103                decimal.TryParse(DelSubStr(shipmentAmountStr), style, culture, out var shipmentAmount))
 2104
 2105            {
 02106                order.ShipmentAmount = shipmentAmount;
 2107            }
 2108
 02109            else if (order.Price != (decimal?)null && order.Quantity != (int?)null)
 2110
 2111            {
 02112                order.ShipmentAmount = order.Price * order.Quantity;
 2113            }
 2114
 2115
 2116            // Если в файле нет колонки «Цена сайта» (или она пустая), базой расчёта служит «Цена».
 02117            order.ProductInfo.CurrentPriceWithDiscount =
 02118                dataColumn.TryGetValue("Цена сайта", out var priceWithDiscountStr) &&
 02119                decimal.TryParse(priceWithDiscountStr, style, culture, out var priceWithDiscount) &&
 02120                priceWithDiscount > 0
 02121                    ? priceWithDiscount
 02122                    : order.Price ?? 0;
 2123
 2124
 02125            order.MaxOzonCommission = dataColumn.TryGetValue("Максимальная комиссия", out var maxOzonCommissionStr) &&
 02126                                      decimal.TryParse(maxOzonCommissionStr, style, culture, out var maxOzonCommission)
 02127                ? maxOzonCommission
 02128                : 0;
 2129
 2130
 02131            order.MinOzonCommission = dataColumn.TryGetValue("Минимальная комиссия", out var minOzonCommissionStr) &&
 02132                                      decimal.TryParse(minOzonCommissionStr, style, culture, out var minOzonCommission)
 02133                ? minOzonCommission
 02134                : 0;
 2135
 2136
 02137            order.PurchasePrice = dataColumn.TryGetValue("Цена закупки", out var purchasePriceStr) &&
 02138                                  decimal.TryParse(purchasePriceStr, style, culture, out var purchasePrice)
 02139                ? purchasePrice
 02140                : 0;
 2141
 2142
 02143            order = SetCorrectProductKey(order);
 2144
 2145
 02146            order.FromFile = true;
 2147
 02148            order.UpdatedBy = "Excel";
 2149
 2150
 02151            _context.SaveChanges();
 2152
 2153
 02154            order = await _ordersDataServices.CastToModel(order);
 2155
 2156
 02157            return order;
 02158        }
 2159
 2160
 2161        private async Task<Order> CastToModelFromExcelForDropBox(Dictionary<string, string> dataColumn,
 2162            OzonClient selectedClient,
 2163            Manufacturer selectedManufacturer,
 2164            Warehouse selectedWarehouse,
 2165            Supplier selectedSupplier,
 2166            string selectedClientStatus,
 2167            CurrencyCode selectedCurrencyCode,
 2168            DateTime? selectedShippingDate,
 2169            DateTime? selectedProcessingDate,
 2170            string deliveryName)
 2171
 2172        {
 02173            Order order = new();
 2174
 2175
 02176            CultureInfo culture = new("en-US");
 2177
 02178            NumberStyles style = NumberStyles.Number;
 2179
 02180            string regionCode = _releaseManager.GetRegionCode();
 2181
 2182
 02183            if (dataColumn.TryGetValue("Артикул", out var article))
 2184
 2185            {
 02186                order.ProductKey = article?.ToString();
 2187            }
 2188
 02189            else if (dataColumn.TryGetValue("Key", out var key))
 2190
 2191            {
 02192                order.ProductKey = key.ToString();
 2193
 02194                article = key.ToString();
 2195
 02196                selectedManufacturer = new Manufacturer() { Id = -1 };
 2197            }
 2198
 2199
 02200            order.Key = (dataColumn.TryGetValue("Номер заказа", out var shipmentNumber)
 02201                            ? shipmentNumber?.ToString()
 02202                            : null)
 02203                        + (order.ProductKey != null ? order.ProductKey : null);
 2204
 2205
 02206            order.ShipmentNumber = shipmentNumber?.ToString();
 2207
 2208
 02209            if (dataColumn.TryGetValue("Клиент", out var clientName))
 2210
 2211            {
 02212                OzonClient clientByName = _context.OzonClients.FirstOrDefault(o => o.Name == clientName);
 2213
 02214                if (clientByName == null)
 2215
 2216                {
 02217                    OzonClient newOzonClient = new OzonClient()
 02218
 02219                    {
 02220                        Name = clientName,
 02221
 02222                        CurrencyCode = CurrencyCode.USD,
 02223                    };
 2224
 2225
 02226                    _context.OzonClients.Add(newOzonClient);
 2227
 02228                    await _context.SaveChangesAsync();
 2229
 2230
 02231                    order.OzonClient = newOzonClient;
 02232                }
 2233
 2234                else
 2235
 2236                {
 02237                    order.OzonClient = clientByName;
 2238                }
 2239            }
 2240
 02241            else if (selectedClient.Id != 0)
 2242
 2243            {
 02244                order.OzonClient = _context.OzonClients.Find(selectedClient.Id);
 2245            }
 2246
 2247
 02248            if (selectedCurrencyCode != CurrencyCode.NON && order.OzonClient != null)
 2249
 2250            {
 02251                if (order.OzonClient.CurrencyCode != selectedCurrencyCode)
 2252
 2253                {
 02254                    order.OzonClient.CurrencyCode = selectedCurrencyCode;
 2255                }
 2256            }
 2257
 02258            else if (selectedClient.Id != 0)
 2259
 2260            {
 02261                order.OzonClient = _context.OzonClients.Find(selectedClient.Id);
 2262            }
 2263
 2264
 02265            var cultureDateInfo = new CultureInfo("ru-RU");
 2266
 2267
 02268            order.ProcessingDate = dataColumn.TryGetValue("Принят в обработку", out var processingDateStr) &&
 02269                                   DateTime.TryParse(processingDateStr, cultureDateInfo, DateTimeStyles.None,
 02270                                       out var processingDate)
 02271                ? processingDate
 02272                : selectedProcessingDate != null
 02273                    ? selectedProcessingDate
 02274                    : DateTime.Now.AddHours(_releaseManager.GetTimeZone());
 2275
 2276
 02277            order.ShippingDate = dataColumn.TryGetValue("Дата отгрузки", out var shippingDateStr) &&
 02278                                 DateTime.TryParse(shippingDateStr, cultureDateInfo, DateTimeStyles.None,
 02279                                     out var shippingDate)
 02280                ? shippingDate
 02281                : selectedShippingDate;
 2282
 2283
 2284            // Проверяем "Статус клиента" сначала, затем "Статус"
 2285
 02286            if (dataColumn.TryGetValue("Статус клиента", out var clientStatusValue))
 2287
 2288            {
 02289                order.ClientStatus = await GetOrCreateClientStatusAsync(clientStatusValue?.ToString());
 2290            }
 2291
 02292            else if (dataColumn.TryGetValue("Статус", out var status))
 2293
 2294            {
 02295                order.ClientStatus = await GetOrCreateClientStatusAsync(status?.ToString());
 2296            }
 2297
 02298            else if (selectedClientStatus != null)
 2299
 2300            {
 02301                order.ClientStatus = await GetOrCreateClientStatusAsync(selectedClientStatus);
 2302            }
 2303
 2304            else
 2305
 2306            {
 02307                order.ClientStatus = null;
 2308            }
 2309
 2310
 02311            order.ProductName = dataColumn.TryGetValue("Наименование товара", out var productName)
 02312                ? productName?.ToString()
 02313                : null;
 2314
 2315
 02316            order.Quantity = dataColumn.TryGetValue("Кол.", out var quantityStr) &&
 02317                             int.TryParse(DelSubStr(quantityStr), style, culture, out var quantity)
 02318                ? quantity
 02319                : (int?)null;
 2320
 2321
 02322            if (dataColumn.TryGetValue("Склад отгрузки", out var warehouseName))
 2323
 2324            {
 02325                Warehouse shipmentWarehouse = _context.Warehouses.FirstOrDefault(w => w.Name == warehouseName);
 2326
 2327
 02328                if (shipmentWarehouse != null)
 2329
 2330                {
 02331                    order.ShipmentWarehouse = shipmentWarehouse;
 2332                }
 2333
 2334                else
 2335
 2336                {
 02337                    var newWarehouse = new Warehouse { Name = warehouseName };
 2338
 02339                    _context.Warehouses.Add(newWarehouse);
 2340
 02341                    await _context.SaveChangesAsync();
 2342
 02343                    order.ShipmentWarehouse = newWarehouse;
 02344                }
 2345            }
 2346
 02347            else if (selectedWarehouse != null && selectedWarehouse.Id != 0)
 2348
 2349            {
 02350                order.ShipmentWarehouse = _context.Warehouses.FirstOrDefault(w => w.Id == selectedWarehouse.Id);
 2351            }
 2352
 2353
 02354            if (dataColumn.TryGetValue("Производитель", out var manufacturerName))
 2355
 2356            {
 02357                Manufacturer manufacturer = _context.Manufacturers.FirstOrDefault(m => m.Name == manufacturerName);
 2358
 2359
 02360                if (manufacturer != null)
 2361
 2362                {
 02363                    order.Manufacturer = manufacturer;
 2364                }
 2365
 2366                else
 2367
 2368                {
 02369                    var newManufacturer = new Manufacturer
 02370
 02371                    {
 02372                        Code = null,
 02373
 02374                        Name = manufacturerName
 02375                    };
 2376
 2377
 02378                    _context.Manufacturers.Add(newManufacturer);
 2379
 02380                    _context.SaveChanges();
 2381
 2382
 02383                    order.Manufacturer = newManufacturer;
 2384                }
 2385
 2386
 02387                order = await SetEtProducerByName(order, manufacturerName);
 2388            }
 2389
 02390            else if (selectedManufacturer != null && selectedManufacturer.Id != 0)
 2391
 2392            {
 02393                if (selectedManufacturer.Id == -1)
 2394
 2395                {
 02396                    string extractedCode = Regex.Match(article.ToString(), @"=(\w{3})").Groups[1].Value;
 2397
 2398
 02399                    if (string.IsNullOrEmpty(extractedCode))
 2400
 2401                    {
 02402                        extractedCode = Regex.Match(article.ToString(), @"=(\w{2})").Groups[1].Value;
 2403                    }
 2404
 2405
 02406                    Manufacturer manufacturer = _context.Manufacturers
 02407                        .Where(m => m.Code == extractedCode)
 02408                        .FirstOrDefault();
 2409
 2410
 02411                    if (manufacturer != null)
 2412
 2413                    {
 02414                        order.Manufacturer = manufacturer;
 2415                    }
 2416
 2417                    else
 2418
 2419                    {
 02420                        var newManufacturer = new Manufacturer { Code = extractedCode };
 2421
 02422                        _context.Manufacturers.Add(newManufacturer);
 2423
 02424                        _context.SaveChanges();
 2425
 02426                        order.Manufacturer = newManufacturer;
 2427                    }
 2428
 2429
 02430                    order = await SetEtProducerByCode(order, extractedCode);
 2431                }
 2432
 02433                else if (selectedManufacturer.Id == -2)
 2434
 2435                {
 02436                    string tabelManufacturerName = order.ProductName != null ? order.ProductName.Split(' ')[0] : null;
 2437
 02438                    if (tabelManufacturerName != null)
 2439
 2440                    {
 02441                        Manufacturer manufacturer = _context.Manufacturers
 02442                            .Where(m => m.Name.ToLower().Contains(tabelManufacturerName.ToLower())).FirstOrDefault();
 2443
 02444                        if (manufacturer != null)
 2445
 2446                        {
 02447                            order.Manufacturer = manufacturer;
 2448                        }
 2449                    }
 2450                }
 2451
 2452                else
 2453
 2454                {
 02455                    order.Manufacturer = _context.Manufacturers.FirstOrDefault(m => m.Id == selectedManufacturer.Id);
 2456                }
 2457            }
 2458
 2459
 02460            if (dataColumn.TryGetValue("Код валюты отправления", out var currencyName))
 2461
 2462            {
 02463                Currency currency = _context.Currencys.FirstOrDefault(c => c.Name == currencyName);
 2464
 2465
 02466                if (currency != null)
 2467
 2468                {
 02469                    order.Сurrency = currency;
 2470                }
 2471
 2472                else
 2473
 2474                {
 02475                    var newCurrency = new Currency
 02476
 02477                    {
 02478                        Name = currencyName
 02479                    };
 2480
 2481
 02482                    _context.Currencys.Add(newCurrency);
 2483
 02484                    await _context.SaveChangesAsync();
 2485
 02486                    order.Сurrency = newCurrency;
 02487                }
 2488            }
 2489
 2490
 02491            order.Supplier = null;
 2492
 02493            order.PurchasePrice = null;
 2494
 2495
 02496            if (order.ProductInfo == null && dataColumn.TryGetValue("Артикул", out var productArticle))
 2497
 2498            {
 02499                Product product = _context.Products.FirstOrDefault(p => p.Article == productArticle);
 2500
 2501
 02502                if (product != null)
 2503
 2504                {
 02505                    order.ProductInfo = product;
 2506                }
 2507
 2508                else
 2509
 2510                {
 02511                    product = new Product
 02512
 02513                    {
 02514                        Article = productArticle
 02515                    };
 2516
 2517
 02518                    _context.Products.Add(product);
 2519
 02520                    await _context.SaveChangesAsync();
 2521
 02522                    order.ProductInfo = product;
 2523                }
 02524            }
 2525
 02526            else if (order.ProductInfo == null && !dataColumn.TryGetValue("Артикул", out var nullProductArticle))
 2527
 2528            {
 02529                var product = new Product();
 2530
 2531
 02532                _context.Products.Add(product);
 2533
 02534                _context.SaveChanges();
 2535
 2536
 02537                order.ProductInfo = product;
 2538            }
 2539
 2540
 02541            order.DeliveryCity = dataColumn.TryGetValue("Город доставки", out var deliveryCity)
 02542                ? deliveryCity?.ToString()
 02543                : null;
 2544
 2545
 2546            // Проверяем "Статус клиента" сначала, затем "Статус клинта" (для обратной совместимости), затем "Статус"
 2547
 02548            string clientStatusValue1 = null;
 2549
 02550            if (dataColumn.TryGetValue("Статус клиента", out clientStatusValue1))
 2551
 2552            {
 02553                order.ClientStatus = await GetOrCreateClientStatusAsync(clientStatusValue1?.ToString());
 2554            }
 2555
 02556            else if (dataColumn.TryGetValue("Статус клинта", out var clientStauts))
 2557
 2558            {
 02559                order.ClientStatus = await GetOrCreateClientStatusAsync(clientStauts?.ToString());
 2560            }
 2561
 02562            else if (dataColumn.TryGetValue("Статус", out var status))
 2563
 2564            {
 02565                order.ClientStatus = await GetOrCreateClientStatusAsync(status?.ToString());
 2566            }
 2567
 02568            else if (selectedClientStatus != null)
 2569
 2570            {
 02571                order.ClientStatus = await GetOrCreateClientStatusAsync(selectedClientStatus);
 2572            }
 2573
 2574            else
 2575
 2576            {
 02577                order.ClientStatus = null;
 2578            }
 2579
 2580
 02581            if (order.AppStatus == null)
 2582
 2583            {
 02584                string statusName = "Не указан";
 2585
 2586
 02587                AppStatus appStatus = _context.AppStatuses.FirstOrDefault(c => c.Name == statusName);
 2588
 2589
 02590                if (appStatus != null)
 2591
 2592                {
 02593                    order.AppStatus = appStatus;
 2594                }
 2595
 2596                else
 2597
 2598                {
 02599                    var newStatus = new AppStatus { Name = statusName };
 2600
 2601
 02602                    _context.AppStatuses.Add(newStatus);
 2603
 02604                    await _context.SaveChangesAsync();
 2605
 02606                    order.AppStatus = newStatus;
 02607                }
 2608            }
 2609
 2610
 02611            if (order.Supplier == null && selectedSupplier.Id == 0)
 2612
 2613            {
 02614                string supplierName = "Не указан";
 2615
 2616
 02617                Supplier supplier = _context.Suppliers.FirstOrDefault(c => c.Name == supplierName);
 2618
 2619
 02620                if (supplier != null)
 2621
 2622                {
 02623                    order.Supplier = supplier;
 2624                }
 2625
 2626                else
 2627
 2628                {
 02629                    var newSupplier = new Supplier { Name = supplierName };
 2630
 2631
 02632                    _context.Suppliers.Add(newSupplier);
 2633
 02634                    await _context.SaveChangesAsync();
 2635
 02636                    order.Supplier = newSupplier;
 02637                }
 2638            }
 2639
 02640            else if (selectedSupplier.Id != 0)
 2641
 2642            {
 02643                order.Supplier = _context.Suppliers.FirstOrDefault(m => m.Id == selectedSupplier.Id);
 2644            }
 2645
 2646
 02647            var numberStyles = NumberStyles.Number | NumberStyles.AllowCurrencySymbol;
 2648
 2649
 02650            if (order.Price == null &&
 02651                dataColumn.TryGetValue("Целая часть", out var integerPartStr) &&
 02652                dataColumn.TryGetValue("Дробная часть", out var fractionalPartStr))
 2653
 2654            {
 02655                if (int.TryParse(integerPartStr, out var integerPart) &&
 02656                    int.TryParse(fractionalPartStr, out var fractionalPart))
 2657
 2658                {
 02659                    order.Price = integerPart + (decimal)fractionalPart / 100;
 2660                }
 2661            }
 2662
 2663
 02664            if (dataColumn.TryGetValue("Сумма отправления", out var shipmentAmountStr) &&
 02665                decimal.TryParse(DelSubStr(shipmentAmountStr), numberStyles, cultureDateInfo, out var shipmentAmount))
 2666
 2667            {
 02668                order.ShipmentAmount = shipmentAmount;
 2669            }
 2670
 02671            else if (order.Price != null && order.Quantity != null)
 2672
 2673            {
 02674                order.ShipmentAmount = order.Price * order.Quantity;
 2675            }
 2676
 2677
 2678            // Часть поставщиков (Exist и другие) не присылает колонку «Цена сайта» — в таких файлах
 2679            // единственная цена лежит в «Цена», поэтому берём её, иначе цена сайта осталась бы нулевой.
 02680            order.ProductInfo.CurrentPriceWithDiscount =
 02681                dataColumn.TryGetValue("Цена сайта", out var priceWithDiscountStr) &&
 02682                decimal.TryParse(priceWithDiscountStr, numberStyles, cultureDateInfo, out var priceWithDiscount) &&
 02683                priceWithDiscount > 0
 02684                    ? priceWithDiscount
 02685                    : order.Price ?? 0;
 2686
 2687
 02688            order.MaxOzonCommission = dataColumn.TryGetValue("Максимальная комиссия", out var maxOzonCommissionStr)
 02689                ? decimal.TryParse(maxOzonCommissionStr, numberStyles, cultureDateInfo, out var maxOzonCommission)
 02690                    ? maxOzonCommission
 02691                    : 0
 02692                : 0;
 2693
 2694
 02695            order.MinOzonCommission = dataColumn.TryGetValue("Минимальная комиссия", out var minOzonCommissionStr)
 02696                ? decimal.TryParse(minOzonCommissionStr, numberStyles, cultureDateInfo, out var minOzonCommission)
 02697                    ? minOzonCommission
 02698                    : 0
 02699                : 0;
 2700
 2701
 02702            order.PurchasePrice = dataColumn.TryGetValue("Цена закупки", out var purchasePriceStr)
 02703                ? decimal.TryParse(purchasePriceStr, numberStyles, cultureDateInfo, out var purchasePrice)
 02704                    ? purchasePrice
 02705                    : 0
 02706                : 0;
 2707
 2708
 02709            if (!string.IsNullOrWhiteSpace(deliveryName))
 2710
 2711            {
 02712                var delivery = await _deliveryService.GetOrCreateDeliveryAsync(deliveryName, deliveryName);
 2713
 2714
 02715                order.Delivery = delivery;
 2716
 02717                order.DeliveryId = delivery.Id;
 2718            }
 2719
 2720
 02721            if (order.ProductInfo != null && string.IsNullOrEmpty(order.ProductInfo.ImageUrl)
 02722                                          && !string.IsNullOrEmpty(_imageBaseUrl))
 2723            {
 02724                string? manufacturerCode = order.Manufacturer?.Code;
 02725                string? productArticleForImage = article?.ToString();
 2726
 02727                if (!string.IsNullOrEmpty(manufacturerCode) && !string.IsNullOrEmpty(productArticleForImage))
 2728                {
 02729                    var normalizedArticle = NormalizeArticle(productArticleForImage);
 02730                    var normalizedManufacturer = manufacturerCode.ToUpperInvariant();
 2731
 02732                    var imgUrl = $"{_imageBaseUrl}/{normalizedManufacturer}/{normalizedArticle}_1.jpg";
 2733
 02734                    if (await ImageExistsAsync(imgUrl))
 2735                    {
 02736                        order.ProductInfo.ImageUrl = imgUrl;
 2737                    }
 02738                }
 2739            }
 2740
 2741
 02742            order = SetCorrectProductKey(order);
 2743
 2744
 02745            order.FromFile = true;
 2746
 02747            order.UpdatedBy = "Excel";
 2748
 2749
 02750            _context.SaveChanges();
 2751
 2752
 02753            return order;
 02754        }
 2755
 2756        private static string NormalizeArticle(string input)
 2757        {
 02758            if (string.IsNullOrEmpty(input))
 02759                return string.Empty;
 2760
 2761            // Удаляем всё кроме букв и цифр
 02762            var cleaned = Regex.Replace(input, "[^a-zA-Z0-9]", "");
 2763
 02764            return cleaned.ToUpperInvariant();
 2765        }
 2766
 2767        private async Task<bool> ImageExistsAsync(string url)
 2768        {
 2769            try
 2770            {
 02771                using var httpClient = _httpClientFactory.CreateClient();
 02772                using var request = new HttpRequestMessage(HttpMethod.Head, url);
 2773
 02774                var response = await httpClient.SendAsync(request);
 02775                return response.IsSuccessStatusCode;
 2776            }
 02777            catch
 2778            {
 02779                return false;
 2780            }
 02781        }
 2782
 2783        private async Task<Order> CastToModelFromExcelForEmEx(Dictionary<string, string> dataColumn,
 2784            OzonClient client,
 2785            Warehouse warehouse,
 2786            DateTime selectedShippingDate,
 2787            DateTime selectedProcessingDate,
 2788            string deliveryName)
 2789
 2790        {
 02791            var cultureDateInfo = new CultureInfo("ru-RU");
 2792
 02793            var numberStyles = NumberStyles.Number | NumberStyles.AllowCurrencySymbol;
 2794
 02795            Order order = new();
 2796
 2797
 02798            order.OzonClient = client;
 2799
 02800            order.ShipmentWarehouse = warehouse;
 2801
 02802            order.ShippingDate = selectedShippingDate;
 2803
 02804            order.ProcessingDate = selectedProcessingDate;
 2805
 02806            if (!string.IsNullOrWhiteSpace(deliveryName))
 2807
 2808            {
 02809                var delivery = await _deliveryService.GetOrCreateDeliveryAsync(deliveryName, deliveryName);
 2810
 2811
 02812                order.Delivery = delivery;
 2813
 02814                order.DeliveryId = delivery.Id;
 2815            }
 2816
 2817
 02818            order.ShipmentNumber = dataColumn.TryGetValue("5", out var shipmentNumber)
 02819                ? shipmentNumber
 02820                : string.Empty;
 2821
 2822
 02823            order.ProductName = dataColumn.TryGetValue("6", out var productName)
 02824                ? productName
 02825                : string.Empty;
 2826
 2827
 02828            order.ProductKey = dataColumn.TryGetValue("4", out var article)
 02829                ? article
 02830                : string.Empty;
 2831
 2832
 02833            var produserName = dataColumn.TryGetValue("2", out var prodName)
 02834                ? prodName
 02835                : string.Empty;
 2836
 2837
 02838            await SetEtProducerByNameAndCode(order, produserName);
 2839
 02840            SetCorrectProductKey(order);
 2841
 2842
 02843            order.Price = dataColumn.TryGetValue("12", out var priceStr)
 02844                ? decimal.TryParse(priceStr.Replace('.', ','), numberStyles, cultureDateInfo, out var price)
 02845                    ? price
 02846                    : 0
 02847                : 0;
 2848
 2849
 02850            order.ShipmentAmount = dataColumn.TryGetValue("13", out var shipmentAmountStr)
 02851                ? decimal.TryParse(shipmentAmountStr.Replace('.', ','), numberStyles, cultureDateInfo,
 02852                    out var shipmentAmount)
 02853                    ? shipmentAmount
 02854                    : 0
 02855                : 0;
 2856
 2857
 02858            order.Quantity = dataColumn.TryGetValue("8", out var quantityStr) &&
 02859                             int.TryParse(DelSubStr(quantityStr), out var quantity)
 02860                ? quantity
 02861                : (int?)null;
 2862
 2863
 02864            if (order.ProductInfo == null && dataColumn.TryGetValue("4", out var productArticle))
 2865
 2866            {
 02867                Product product = _context.Products.FirstOrDefault(p => p.Article == productArticle);
 2868
 2869
 02870                if (product != null)
 2871
 2872                {
 02873                    order.ProductInfo = product;
 2874                }
 2875
 2876                else
 2877
 2878                {
 02879                    product = new Product
 02880
 02881                    {
 02882                        Article = productArticle
 02883                    };
 2884
 2885
 02886                    _context.Products.Add(product);
 2887
 02888                    await _context.SaveChangesAsync();
 2889
 02890                    order.ProductInfo = product;
 2891                }
 02892            }
 2893
 02894            else if (order.ProductInfo == null && !dataColumn.TryGetValue("4", out var nullProductArticle))
 2895
 2896            {
 02897                var product = new Product();
 2898
 2899
 02900                _context.Products.Add(product);
 2901
 02902                _context.SaveChanges();
 2903
 2904
 02905                order.ProductInfo = product;
 2906            }
 2907
 2908
 02909            if (order.AppStatus == null)
 2910
 2911            {
 02912                string statusName = "Не указан";
 2913
 2914
 02915                AppStatus appStatus = _context.AppStatuses.FirstOrDefault(c => c.Name == statusName);
 2916
 2917
 02918                if (appStatus != null)
 2919
 2920                {
 02921                    order.AppStatus = appStatus;
 2922                }
 2923
 2924                else
 2925
 2926                {
 02927                    var newStatus = new AppStatus { Name = statusName };
 2928
 2929
 02930                    _context.AppStatuses.Add(newStatus);
 2931
 02932                    await _context.SaveChangesAsync();
 2933
 02934                    order.AppStatus = newStatus;
 02935                }
 2936            }
 2937
 2938
 02939            if (order.ProductInfo != null && string.IsNullOrEmpty(order.ProductInfo.ImageUrl)
 02940                && !string.IsNullOrEmpty(_imageBaseUrl))
 2941            {
 02942                string? productArticleForImage = article?.ToString();
 2943
 02944                if (!string.IsNullOrEmpty(produserName) && !string.IsNullOrEmpty(productArticleForImage))
 2945                {
 02946                    var imgUrl = $"{_imageBaseUrl}/{produserName.ToUpper()}/{productArticleForImage}_1.jpg";
 02947                    order.ProductInfo.ImageUrl = imgUrl;
 2948
 2949                }
 2950            }
 2951
 2952
 02953            order.Key = order.ShipmentNumber + order.ProductKey;
 2954
 2955
 02956            order.FromFile = true;
 2957
 02958            order.UpdatedBy = "Excel";
 2959
 2960
 02961            return order;
 02962        }
 2963
 2964
 2965        private Order SetCorrectProductKey(Order order)
 2966
 2967        {
 02968            string pattern = @"^[A-Za-z0-9\-[\]]+=[A-Za-z0-9\[\]]+$";
 2969
 2970
 02971            Regex regex = new Regex(pattern);
 2972
 02973            Match match = regex.Match(order.ProductKey);
 2974
 2975
 02976            if (match.Success)
 2977
 2978            {
 02979                order.Article = Regex.Replace(order.ProductKey.Substring(0, order.ProductKey.IndexOf('=')),
 02980                    "[^A-Za-z0-9]", "");
 2981
 02982                return order;
 2983            }
 2984
 2985
 02986            string cleanedArticle = Regex.Replace(order.ProductKey, "[^A-Za-z0-9]", "");
 2987
 02988            var key = "NON";
 2989
 02990            if (order.EtProducer != null)
 2991
 2992            {
 02993                key = order.EtProducer != null && order.EtProducer.MarketPrefix != null
 02994                    ? $"={order.EtProducer.MarketPrefix}"
 02995                    : "=";
 2996            }
 2997
 2998
 02999            if (order.EtProducerId.HasValue && order.EtProducerId != 0)
 3000
 3001            {
 03002                var etProducer = _etProducerDataServices.GetEtProducer(order.EtProducerId.Value);
 3003
 03004                key = etProducer != null && etProducer.MarketPrefix != null
 03005                    ? $"={etProducer.MarketPrefix}"
 03006                    : "=";
 3007            }
 3008
 3009
 03010            order.Article = cleanedArticle;
 3011
 03012            order.ProductKey = cleanedArticle + key;
 3013
 3014
 03015            return order;
 3016        }
 3017
 3018
 3019        private string SetCorrectStatus(string status)
 3020
 3021        {
 03022            if (status == null)
 3023
 3024            {
 03025                return null;
 3026            }
 3027
 3028
 03029            if (status == "Отменен")
 3030
 3031            {
 03032                status = "Отменён";
 3033            }
 3034
 3035
 03036            return status;
 3037        }
 3038
 3039
 3040        private async Task<ClientStatus?> GetOrCreateClientStatusAsync(string? statusName)
 3041
 3042        {
 03043            if (string.IsNullOrEmpty(statusName))
 3044
 3045            {
 03046                return null;
 3047            }
 3048
 3049
 03050            string correctedStatus = SetCorrectStatus(statusName);
 3051
 03052            if (string.IsNullOrEmpty(correctedStatus))
 3053
 3054            {
 03055                return null;
 3056            }
 3057
 3058
 03059            var clientStatus = await _context.ClientStatuses
 03060                .FirstOrDefaultAsync(cs => cs.Name == correctedStatus);
 3061
 3062
 03063            if (clientStatus == null)
 3064
 3065            {
 03066                clientStatus = new ClientStatus { Name = correctedStatus };
 3067
 03068                await _context.ClientStatuses.AddAsync(clientStatus);
 3069
 03070                await _context.SaveChangesAsync();
 3071            }
 3072
 3073
 03074            return clientStatus;
 03075        }
 3076
 3077
 3078        private (decimal, decimal, string, string, decimal, decimal) CalculateСommissions(JToken jsonOrder, Order order)
 3079
 3080        {
 03081            JToken productPrices = jsonOrder["productPricesWithArticle"];
 3082
 03083            JToken productСommissions = productPrices["commissions"];
 3084
 3085
 3086            // Используем Value<decimal>() вместо .ToString() + ConvertNumberByRegion.
 3087            // JToken.Value<decimal>() читает числовое значение напрямую из JSON без
 3088            // преобразования в строку, поэтому не зависит от локали сервера (ru-RU, de-DE и т.д.)
 3089            // и корректно работает на любом окружении.
 03090            decimal priceWithDiscount = productPrices["price"]?["marketing_seller_price"]?.Value<decimal>() ?? 0;
 3091
 03092            decimal price = productPrices["price"]?["price"]?.Value<decimal>() ?? 0;
 3093
 03094            decimal percentFbs = productСommissions?["sales_percent_fbs"]?.Value<decimal>() ?? 0;
 3095
 03096            decimal acquiring = productPrices["acquiring"]?.Value<decimal>() ?? 0;
 3097
 03098            decimal delivToCustomer = productСommissions?["fbs_deliv_to_customer_amount"]?.Value<decimal>() ?? 0;
 3099
 03100            decimal fbsFirstMileMax = productСommissions?["fbs_first_mile_max_amount"]?.Value<decimal>() ?? 0;
 3101
 03102            decimal fbsDirectFlowTransMax = productСommissions?["fbs_direct_flow_trans_max_amount"]?.Value<decimal>() ??
 3103
 03104            decimal fbsFirstMileMin = productСommissions?["fbs_first_mile_min_amount"]?.Value<decimal>() ?? 0;
 3105
 03106            decimal fbsDirectFlowTransMin = productСommissions?["fbs_direct_flow_trans_min_amount"]?.Value<decimal>() ??
 3107
 3108
 3109            // Комиссия рассчитывается от цены продажи (marketing_seller_price),
 3110            // а не от базовой цены (price) — именно так считает Ozon.
 03111            decimal salesСommission = priceWithDiscount / 100 * percentFbs;
 3112
 03113            decimal standaertSum = acquiring + delivToCustomer + salesСommission;
 3114
 3115
 03116            decimal maxСommission = standaertSum + fbsFirstMileMax + fbsDirectFlowTransMax;
 3117
 03118            decimal minCommission = standaertSum + fbsFirstMileMin + fbsDirectFlowTransMin;
 3119
 3120
 03121            string sb1 = BuildCommissionInfo(
 03122                salesСommission,
 03123                acquiring,
 03124                delivToCustomer,
 03125                fbsFirstMileMax,
 03126                fbsDirectFlowTransMax,
 03127                maxСommission);
 3128
 03129            string sb2 = BuildCommissionInfo(
 03130                salesСommission,
 03131                acquiring,
 03132                delivToCustomer,
 03133                fbsFirstMileMin,
 03134                fbsDirectFlowTransMin,
 03135                minCommission);
 3136
 3137
 03138            var (maxComment, minComment, min, max) = (sb1, sb2, minCommission, maxСommission);
 3139
 3140
 03141            return (price, priceWithDiscount, maxComment, minComment, min, max);
 3142        }
 3143
 3144        private static string BuildCommissionInfo(
 3145            decimal salesCommission,
 3146            decimal acquiring,
 3147            decimal lastMile,
 3148            decimal firstMile,
 3149            decimal directFlow,
 3150            decimal total) =>
 03151            $"Комиссия за продажу: {salesCommission.ToString("0.##", CultureInfo.InvariantCulture)} ₽; " +
 03152            $"Эквайринг: {acquiring.ToString("0.##", CultureInfo.InvariantCulture)} ₽; " +
 03153            $"Последняя миля FBS: {lastMile.ToString("0.##", CultureInfo.InvariantCulture)} ₽; " +
 03154            $"Обработка отправления FBS: {firstMile.ToString("0.##", CultureInfo.InvariantCulture)} ₽; " +
 03155            $"Магистраль FBS: {directFlow.ToString("0.##", CultureInfo.InvariantCulture)} ₽; " +
 03156            $"Итого: {total.ToString("0.##", CultureInfo.InvariantCulture)} ₽";
 3157
 3158
 3159        public decimal ConvertNumberByRegion(string number, string regionCode)
 3160        {
 3161            // API Ozon всегда возвращает числа с точкой (InvariantCulture).
 3162            // Файлы CSV-отчётов в ru-регионе используют запятую (ru-RU).
 3163            //
 3164            // ВАЖНО: нельзя использовать NumberStyles.Number с InvariantCulture —
 3165            // он включает AllowThousands, где запятая является разделителем тысяч.
 3166            // Это приводит к тому, что "25,88" (ru-RU десятичный) парсится как 2588.
 3167            // NumberStyles.Float использует только точку как десятичный разделитель
 3168            // и не содержит AllowThousands.
 03169            if (decimal.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out var invariantResult))
 03170                return invariantResult;
 03171            if (decimal.TryParse(number, NumberStyles.Number, new CultureInfo("ru-RU"), out var ruResult))
 03172                return ruResult;
 03173            return 0;
 3174        }
 3175
 3176
 3177        private string DelSubStr(string str)
 3178
 3179        {
 03180            if (string.IsNullOrEmpty(str))
 3181
 3182            {
 03183                return str;
 3184            }
 3185
 3186
 03187            if (str.StartsWith("'"))
 3188
 3189            {
 03190                return str.Substring(1);
 3191            }
 3192
 3193
 03194            return str;
 3195        }
 3196
 3197
 3198        private async Task<Order> SetEtProducerByCode(Order order, string extractedCode)
 3199
 3200        {
 03201            var producer = await _etProducerDataServices.GetRealIdAsyncByCode(extractedCode, order.ProductName);
 3202
 03203            if (producer != null)
 3204
 3205            {
 03206                order.EtProducerId = producer.RealId;
 3207            }
 3208
 3209
 03210            return order;
 03211        }
 3212
 3213
 3214        private async Task<Order> SetEtProducerByName(Order order, string name)
 3215
 3216        {
 03217            var producer = await _etProducerDataServices.GetRealIdAsyncByName(name);
 3218
 03219            if (producer != null)
 3220
 3221            {
 03222                order.EtProducerId = producer.RealId;
 3223            }
 3224
 3225
 03226            return order;
 03227        }
 3228
 3229
 3230        private async Task<Order> SetEtProducerByNameAndCode(Order order, string name)
 3231
 3232        {
 03233            var producer = await _etProducerDataServices.GetRealIdAsyncByName(name);
 3234
 03235            if (producer == null)
 3236
 3237            {
 03238                producer = await _etProducerDataServices.GetRealIdAsyncByCode(name);
 3239            }
 3240
 3241
 03242            if (producer != null)
 3243
 3244            {
 03245                order.EtProducerId = producer.RealId;
 3246            }
 3247
 3248
 03249            return order;
 03250        }
 3251
 3252
 3253        private async Task CreateOrderPriceAndIndexes(Order order, JToken jsonOrder)
 3254
 3255        {
 03256            await Task.CompletedTask;
 3257            try
 3258
 3259            {
 3260                // Проверяем наличие данных о ценах в JSON
 3261
 03262                var priceData = jsonOrder["productPricesWithArticle"]?["price"];
 3263
 03264                var priceIndexesData = jsonOrder["productPricesWithArticle"]?["price_indexes"];
 3265
 3266
 03267                if (priceData == null || priceIndexesData == null)
 3268
 03269                    return;
 3270
 3271
 3272                // Создаем OrderPrice
 3273
 03274                var orderPrice = new OrderPrice
 03275
 03276                {
 03277                    OrderId = order.Id,
 03278
 03279                    AutoActionEnabled = priceData["auto_action_enabled"]?.Value<bool>() ?? false,
 03280
 03281                    CurrencyCode = priceData["currency_code"]?.ToString() ?? "RUB",
 03282
 03283                    MarketingPrice = priceData["marketing_price"]?.Value<decimal>() ?? 0,
 03284
 03285                    MarketingSellerPrice = priceData["marketing_seller_price"]?.Value<decimal>() ?? 0,
 03286
 03287                    MinPrice = priceData["min_price"]?.Value<decimal>() ?? 0,
 03288
 03289                    OldPrice = priceData["old_price"]?.Value<decimal>() ?? 0,
 03290
 03291                    Price = priceData["price"]?.Value<decimal>() ?? 0,
 03292
 03293                    RetailPrice = priceData["retail_price"]?.Value<decimal>() ?? 0,
 03294
 03295                    Vat = priceData["vat"]?.Value<decimal>() ?? 0,
 03296
 03297                    AutoAddToOzonActionsListEnabled =
 03298                        priceData["auto_add_to_ozon_actions_list_enabled"]?.Value<bool>() ?? false,
 03299
 03300                    NetPrice = priceData["net_price"]?.Value<decimal>() ?? 0
 03301                };
 3302
 3303
 3304                // Создаем PriceIndexes
 3305
 03306                var priceIndexes = new PriceIndexes
 03307
 03308                {
 03309                    OrderPriceId = orderPrice.Id,
 03310
 03311                    // External index data
 03312
 03313                    ExternalMinPrice = priceIndexesData["external_index_data"]?["min_price"]?.Value<decimal>() ?? 0,
 03314
 03315                    ExternalMinPriceCurrency =
 03316                        priceIndexesData["external_index_data"]?["min_price_currency"]?.ToString() ?? "RUB",
 03317
 03318                    ExternalPriceIndexValue =
 03319                        priceIndexesData["external_index_data"]?["price_index_value"]?.Value<decimal>() ?? 0,
 03320
 03321                    // Ozon index data
 03322
 03323                    OzonMinPrice = priceIndexesData["ozon_index_data"]?["min_price"]?.Value<decimal>() ?? 0,
 03324
 03325                    OzonMinPriceCurrency =
 03326                        priceIndexesData["ozon_index_data"]?["min_price_currency"]?.ToString() ?? "RUB",
 03327
 03328                    OzonPriceIndexValue =
 03329                        priceIndexesData["ozon_index_data"]?["price_index_value"]?.Value<decimal>() ?? 0,
 03330
 03331                    // Self marketplaces index data
 03332
 03333                    SelfMarketplacesMinPrice =
 03334                        priceIndexesData["self_marketplaces_index_data"]?["min_price"]?.Value<decimal>() ?? 0,
 03335
 03336                    SelfMarketplacesMinPriceCurrency =
 03337                        priceIndexesData["self_marketplaces_index_data"]?["min_price_currency"]?.ToString() ?? "RUB",
 03338
 03339                    SelfMarketplacesPriceIndexValue =
 03340                        priceIndexesData["self_marketplaces_index_data"]?["price_index_value"]?.Value<decimal>() ?? 0,
 03341
 03342                    ColorIndex = priceIndexesData["color_index"]?.ToString() ?? "WHITE"
 03343                };
 3344
 3345
 03346                orderPrice.PriceIndexes = priceIndexes;
 3347
 03348                order.OrderPrice = orderPrice;
 03349            }
 3350
 03351            catch (Exception ex)
 3352
 3353            {
 3354                // Логируем ошибку, но не прерываем выполнение
 3355
 03356                Console.WriteLine($"Ошибка при создании OrderPrice и PriceIndexes: {ex.Message}");
 03357            }
 03358        }
 3359
 3360        /// <summary>
 3361        /// Преобразует уже полученный пакетный ответ <c>/v5/product/info/prices</c> в снимок.
 3362        /// Здесь находится только транспортное преобразование JSON → доменная модель;
 3363        /// дальнейшая работа с акциями выполняется по типизированным сущностям.
 3364        /// </summary>
 3365        private static OrderOzonPromotionSnapshot? CreateOzonPromotionSnapshot(JToken jsonOrder)
 3366        {
 03367            var payload = jsonOrder["productPricesWithArticle"] as JObject;
 03368            if (payload == null)
 03369                return null;
 3370
 03371            var price = payload["price"];
 03372            var commissions = payload["commissions"];
 03373            var marketingActions = payload["marketing_actions"];
 03374            var priceIndexes = payload["price_indexes"];
 03375            var ozonActionsExist = marketingActions?["ozon_actions_exist"]?.Value<bool>() ?? false;
 3376
 03377            var snapshot = new OrderOzonPromotionSnapshot
 03378            {
 03379                OzonProductId = payload["product_id"]?.Value<long>() ?? 0,
 03380                OfferId = payload["offer_id"]?.ToString() ?? jsonOrder["Артикул"]?.ToString() ?? string.Empty,
 03381                CapturedAt = DateTime.UtcNow,
 03382                OzonActionsExist = ozonActionsExist,
 03383                AutoActionEnabled = price?["auto_action_enabled"]?.Value<bool>() ?? false,
 03384                AutoAddToOzonActionsListEnabled = price?["auto_add_to_ozon_actions_list_enabled"]?.Value<bool>() ?? fals
 03385                CurrencyCode = price?["currency_code"]?.ToString() ?? "RUB",
 03386                MarketingPrice = price?["marketing_price"]?.Value<decimal>() ?? 0,
 03387                MarketingSellerPrice = price?["marketing_seller_price"]?.Value<decimal>() ?? 0,
 03388                MinPrice = price?["min_price"]?.Value<decimal>() ?? 0,
 03389                OldPrice = price?["old_price"]?.Value<decimal>() ?? 0,
 03390                Price = price?["price"]?.Value<decimal>() ?? 0,
 03391                RetailPrice = price?["retail_price"]?.Value<decimal>() ?? 0,
 03392                NetPrice = price?["net_price"]?.Value<decimal>() ?? 0,
 03393                Vat = price?["vat"]?.Value<decimal>() ?? 0,
 03394                Acquiring = payload["acquiring"]?.Value<decimal>() ?? 0,
 03395                VolumeWeight = payload["volume_weight"]?.Value<decimal>() ?? 0,
 03396                FboCommissionPercent = commissions?["sales_percent_fbo"]?.Value<decimal>() ?? 0,
 03397                FbsCommissionPercent = commissions?["sales_percent_fbs"]?.Value<decimal>() ?? 0,
 03398                FbsDeliveryToCustomerAmount = commissions?["fbs_deliv_to_customer_amount"]?.Value<decimal>() ?? 0,
 03399                FbsFirstMileMinAmount = commissions?["fbs_first_mile_min_amount"]?.Value<decimal>() ?? 0,
 03400                FbsFirstMileMaxAmount = commissions?["fbs_first_mile_max_amount"]?.Value<decimal>() ?? 0,
 03401                FbsDirectFlowMinAmount = commissions?["fbs_direct_flow_trans_min_amount"]?.Value<decimal>() ?? 0,
 03402                FbsDirectFlowMaxAmount = commissions?["fbs_direct_flow_trans_max_amount"]?.Value<decimal>() ?? 0,
 03403                ExternalPriceIndex = priceIndexes?["external_index_data"]?["price_index_value"]?.Value<decimal>() ?? 0,
 03404                OzonPriceIndex = priceIndexes?["ozon_index_data"]?["price_index_value"]?.Value<decimal>() ?? 0,
 03405                SelfMarketplacePriceIndex = priceIndexes?["self_marketplaces_index_data"]?["price_index_value"]?.Value<d
 03406                CurrentPeriodFrom = marketingActions?["current_period_from"]?.Value<DateTime?>(),
 03407                CurrentPeriodTo = marketingActions?["current_period_to"]?.Value<DateTime?>(),
 03408                RawPricePayload = payload.ToString(Newtonsoft.Json.Formatting.None)
 03409            };
 3410
 03411            foreach (var action in marketingActions?["actions"]?.OfType<JObject>() ?? [])
 3412            {
 03413                var title = action["title"]?.ToString() ?? string.Empty;
 03414                snapshot.Conditions.Add(new OrderOzonPromotionCondition
 03415                {
 03416                    SourceType = title.StartsWith("РК.", StringComparison.OrdinalIgnoreCase)
 03417                        ? OzonPromotionSourceType.SellerAction
 03418                        : OzonPromotionSourceType.Ozon,
 03419                    Title = title,
 03420                    Value = action["value"]?.Value<decimal?>(),
 03421                    DateFrom = action["date_from"]?.Value<DateTime?>(),
 03422                    DateTo = action["date_to"]?.Value<DateTime?>()
 03423                });
 3424            }
 3425
 03426            if (ozonActionsExist && snapshot.Conditions.All(x => x.SourceType != OzonPromotionSourceType.Ozon))
 3427            {
 03428                snapshot.Conditions.Add(new OrderOzonPromotionCondition
 03429                {
 03430                    SourceType = OzonPromotionSourceType.Ozon,
 03431                    Title = "Акция Ozon",
 03432                    ActionPrice = snapshot.MarketingSellerPrice
 03433                });
 3434            }
 3435
 03436            snapshot.HasAnyPromotion = snapshot.OzonActionsExist || snapshot.Conditions.Count > 0;
 03437            return snapshot;
 3438        }
 3439    }
 3440}

Methods/Properties

.cctor()
.ctor(OzonRepositories.Context.ApplicationDbContext,Services.ReleasServices.ReleaseManager.ReleaseManager,Services.DataServices.OrdersFileMetadataDataService,Services.DataServices.EtProducerDataServices,Services.CacheServices.Cache.AppCache,SlqStudio.Application.Services.EmailService.IEmailService,Services.DataServices.DeliveryDataServices,Services.DataServices.OrdersDataServices,Microsoft.Extensions.Configuration.IConfiguration,System.Net.Http.IHttpClientFactory)
JsonToOrders()
DetachAddedEntries()
IsMariaDbReturningBatchError(System.Exception)
ExcelToOrders()
ExcelToOrdersForDropbox()
YandexToOrders()
XlsToOrdersForDropbox()
SetFileDataAsync()
SafeField(Newtonsoft.Json.Linq.JToken,System.String,System.String,System.Collections.Generic.List`1<System.String>)
SafeOptionalField(Newtonsoft.Json.Linq.JToken,System.String)
CastToModel()
GetYandexOrderItemPrice(Newtonsoft.Json.Linq.JToken)
CastToModelFromYandex()
GetYandexPrimaryImageUrl(Newtonsoft.Json.Linq.JToken)
CastToModelFromExcel()
CastToModelFromExcelForDropBox()
NormalizeArticle(System.String)
ImageExistsAsync()
CastToModelFromExcelForEmEx()
SetCorrectProductKey(OzonDomains.Models.Order)
SetCorrectStatus(System.String)
GetOrCreateClientStatusAsync()
CalculateСommissions(Newtonsoft.Json.Linq.JToken,OzonDomains.Models.Order)
BuildCommissionInfo(System.Decimal,System.Decimal,System.Decimal,System.Decimal,System.Decimal,System.Decimal)
ConvertNumberByRegion(System.String,System.String)
DelSubStr(System.String)
SetEtProducerByCode()
SetEtProducerByName()
SetEtProducerByNameAndCode()
CreateOrderPriceAndIndexes()
CreateOzonPromotionSnapshot(Newtonsoft.Json.Linq.JToken)