< Summary

Information
Class: Services.ApiServices.OzonApi.AssemblyListService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/AssemblyListService.cs
Line coverage
6%
Covered lines: 10
Uncovered lines: 139
Coverable lines: 149
Total lines: 279
Line coverage: 6.7%
Branch coverage
10%
Covered branches: 8
Total branches: 76
Branch coverage: 10.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%210%
FilterPostingsByWarehouse(...)100%88100%
BuildAsync()0%506220%
ApplyArticleImages(...)0%2040%
GetWarehouseMapAsync()0%2040%
GenerateLabelsAsync()0%1056320%
<GenerateLabelsAsync()0%620%
get_FileUrl()100%210%
get_Error()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/AssemblyListService.cs

#LineLine coverage
 1using Microsoft.Extensions.Caching.Memory;
 2using Services.ApiServices.OzonApi.Filters;
 3
 4namespace Services.ApiServices.OzonApi
 5{
 6    public class AssemblyListProgress
 7    {
 8        public int Step { get; init; }
 9        public int TotalSteps { get; init; } = 4;
 10        public string Message { get; init; } = "";
 11        public bool IsComplete { get; init; }
 12        public bool IsError { get; init; }
 13        public string? ErrorMessage { get; init; }
 14    }
 15
 16    public class AssemblyListItem
 17    {
 18        public int Index { get; set; }
 19        public string PostingNumber { get; set; } = "";
 20        public string Status { get; set; } = "";
 21        public string StatusLabel { get; set; } = "";
 22        public long WarehouseId { get; set; }
 23        public string WarehouseName { get; set; } = "";
 24        public string OfferId { get; set; } = "";
 25        public string ProductName { get; set; } = "";
 26        public string? ImageUrl { get; set; }
 27        public int Quantity { get; set; }
 28        public DateTime? CutoffDate { get; set; }
 29        public string? LabelUrl { get; set; }
 30        public string? LabelError { get; set; }
 31        public string? SupplierName { get; set; }
 32        public DateTime? OrderedToSupplierDate { get; set; }
 33        public decimal? OneCBalance { get; set; }
 34        public string? OneCReceiptDoc { get; set; }
 35        public Guid? OneCReceiptDocKey { get; set; }
 36        public string? OneCOtherStock { get; set; }
 37        public string? OneCWarehouseName { get; set; }
 38    }
 39
 40    public class AssemblyListResult
 41    {
 42        public List<AssemblyListItem> Items { get; set; } = new();
 43        public int TotalPostings { get; set; }
 44        public DateTime GeneratedAt { get; set; } = DateTime.Now;
 45    }
 46
 47    public class AssemblyListService
 48    {
 49        internal const int LabelBatchSize = 50;
 050        private static readonly TimeSpan WarehouseListCacheTtl = TimeSpan.FromMinutes(10);
 51
 052        private static readonly Dictionary<string, string> StatusLabels = new()
 053        {
 054            { "awaiting_registration", "Ожидает регистрации" },
 055            { "acceptance_in_progress", "Идёт приёмка" },
 056            { "awaiting_approve", "Ожидает подтверждения" },
 057            { "awaiting_packaging", "Ожидает упаковки" },
 058            { "awaiting_deliver", "Ожидает отгрузки" },
 059            { "arbitration", "Арбитраж" },
 060            { "client_arbitration", "Клиентский арбитраж" },
 061            { "delivering", "Доставляется" },
 062            { "driver_pickup", "У водителя" },
 063            { "cancelled", "Отменено" },
 064            { "not_accepted", "Не принято на СЦ" },
 065        };
 66
 67        private readonly IMemoryCache _cache;
 68
 069        public AssemblyListService(IMemoryCache cache)
 70        {
 071            _cache = cache;
 072        }
 73
 74        /// <summary>
 75        /// Фильтрует отправления по эффективному складу (для раннего отсечения до этикеток/1С).
 76        /// Эффективный склад — Засыл заказа, если он задан, иначе склад отгрузки Ozon
 77        /// (то же правило, что и TransactionWarehouseHelper.GetEffectiveWarehouseName).
 78        /// </summary>
 79        public static List<UnfulfilledPosting> FilterPostingsByWarehouse(
 80            IEnumerable<UnfulfilledPosting> postings,
 81            string? warehouseFilter,
 82            IReadOnlyDictionary<string, string?>? zasylByPosting = null)
 83        {
 684            if (string.IsNullOrWhiteSpace(warehouseFilter))
 185                return postings.ToList();
 86
 587            return postings
 588                .Where(p =>
 589                {
 790                    var zasyl = zasylByPosting != null && zasylByPosting.TryGetValue(p.PostingNumber, out var z) ? z : n
 791                    var effectiveWarehouse = !string.IsNullOrWhiteSpace(zasyl) ? zasyl : p.WarehouseName;
 792                    return string.Equals(effectiveWarehouse, warehouseFilter, StringComparison.OrdinalIgnoreCase);
 593                })
 594                .ToList();
 95        }
 96
 97        public async Task<AssemblyListResult> BuildAsync(
 98            OzonApiDataManager manager,
 99            IReadOnlyList<UnfulfilledPosting> postings,
 100            string status,
 101            string sellerCacheKey,
 102            IProgress<AssemblyListProgress>? progress = null)
 103        {
 0104            progress?.Report(new AssemblyListProgress { Step = 2, Message = $"Загрузка данных ({postings.Count} отправле
 105
 0106            var filtered = postings
 0107                .Where(p => string.IsNullOrEmpty(status) || p.Status == status)
 0108                .ToList();
 109
 0110            var warehouseMap = await GetWarehouseMapAsync(manager, sellerCacheKey).ConfigureAwait(false);
 111
 0112            progress?.Report(new AssemblyListProgress
 0113            {
 0114                Step = 3,
 0115                Message = "Создаём задания на этикетки...",
 0116            });
 117
 0118            var deliverPostings = filtered.Where(p => p.Status == "awaiting_deliver").ToList();
 0119            var labelsByPosting = await GenerateLabelsAsync(manager, deliverPostings, progress)
 0120                .ConfigureAwait(false);
 121
 0122            progress?.Report(new AssemblyListProgress
 0123            {
 0124                Step = 4,
 0125                Message = "Формируем лист подбора...",
 0126            });
 127
 0128            var items = new List<AssemblyListItem>();
 0129            var idx = 1;
 130
 0131            foreach (var posting in filtered)
 132            {
 0133                var warehouseName = posting.WarehouseName;
 0134                if (string.IsNullOrEmpty(warehouseName) && posting.WarehouseId > 0)
 0135                    warehouseMap.TryGetValue(posting.WarehouseId, out warehouseName);
 0136                if (string.IsNullOrEmpty(warehouseName))
 0137                    warehouseName = posting.WarehouseId > 0 ? posting.WarehouseId.ToString() : "—";
 138
 0139                var labelInfo = labelsByPosting.GetValueOrDefault(posting.PostingNumber);
 140
 0141                foreach (var product in posting.Products)
 142                {
 0143                    items.Add(new AssemblyListItem
 0144                    {
 0145                        Index = idx++,
 0146                        PostingNumber = posting.PostingNumber,
 0147                        Status = posting.Status,
 0148                        StatusLabel = StatusLabels.GetValueOrDefault(posting.Status, posting.Status),
 0149                        WarehouseId = posting.WarehouseId,
 0150                        WarehouseName = warehouseName,
 0151                        OfferId = product.OfferId,
 0152                        ProductName = product.Name,
 0153                        Quantity = product.Quantity,
 0154                        CutoffDate = posting.CutoffDate,
 0155                        LabelUrl = labelInfo?.FileUrl,
 0156                        LabelError = labelInfo?.Error,
 0157                    });
 158                }
 159            }
 160
 0161            return new AssemblyListResult
 0162            {
 0163                Items = items,
 0164                TotalPostings = filtered.Count,
 0165                GeneratedAt = DateTime.Now,
 0166            };
 0167        }
 168
 169        public void ApplyArticleImages(AssemblyListResult result, Dictionary<string, string> articleImages)
 170        {
 0171            foreach (var item in result.Items)
 172            {
 0173                if (articleImages.TryGetValue(item.OfferId, out var imageUrl))
 0174                    item.ImageUrl = imageUrl;
 175            }
 0176        }
 177
 178        private async Task<Dictionary<long, string>> GetWarehouseMapAsync(OzonApiDataManager manager, string sellerCache
 179        {
 0180            var cacheKey = $"assembly_ozon_warehouses_{sellerCacheKey}";
 0181            if (_cache.TryGetValue(cacheKey, out Dictionary<long, string>? cached) && cached != null)
 0182                return cached;
 183
 0184            var warehouses = await manager.GetWarehouseList().ConfigureAwait(false);
 0185            var map = warehouses
 0186                .Where(w => w.WarehouseId > 0 && !string.IsNullOrEmpty(w.Name))
 0187                .ToDictionary(w => w.WarehouseId, w => w.Name!);
 188
 0189            _cache.Set(cacheKey, map, WarehouseListCacheTtl);
 0190            return map;
 0191        }
 192
 193        private static async Task<Dictionary<string, LabelInfo>> GenerateLabelsAsync(
 194            OzonApiDataManager manager,
 195            List<UnfulfilledPosting> deliverPostings,
 196            IProgress<AssemblyListProgress>? progress)
 197        {
 0198            var result = new Dictionary<string, LabelInfo>(StringComparer.Ordinal);
 0199            if (deliverPostings.Count == 0)
 0200                return result;
 201
 0202            var chunks = deliverPostings
 0203                .Select(p => p.PostingNumber)
 0204                .Where(n => !string.IsNullOrEmpty(n))
 0205                .Chunk(LabelBatchSize)
 0206                .Select(c => c.ToArray())
 0207                .ToList();
 208
 0209            progress?.Report(new AssemblyListProgress
 0210            {
 0211                Step = 3,
 0212                Message = $"Этикетки: создаём {chunks.Count} заданий для {deliverPostings.Count} отправлений...",
 0213            });
 214
 0215            var createResults = await Task.WhenAll(chunks.Select(async postingNumbers =>
 0216            {
 0217                try
 0218                {
 0219                    var createResp = await manager.CreatePackageLabelTask(postingNumbers).ConfigureAwait(false);
 0220                    var taskId = createResp.PickTask()?.TaskId ?? 0;
 0221                    return (TaskId: taskId, PostingNumbers: postingNumbers, Error: (string?)null);
 0222                }
 0223                catch (Exception ex)
 0224                {
 0225                    return (TaskId: 0L, PostingNumbers: postingNumbers, Error: ex.Message);
 0226                }
 0227            })).ConfigureAwait(false);
 228
 0229            var taskIdToPostings = new Dictionary<long, string[]>();
 0230            foreach (var created in createResults)
 231            {
 0232                if (created.TaskId > 0)
 233                {
 0234                    taskIdToPostings[created.TaskId] = created.PostingNumbers;
 0235                    continue;
 236                }
 237
 0238                var error = created.Error ?? "Не удалось создать задание";
 0239                foreach (var pn in created.PostingNumbers)
 0240                    result[pn] = new LabelInfo { Error = error };
 241            }
 242
 0243            if (taskIdToPostings.Count == 0)
 0244                return result;
 245
 0246            progress?.Report(new AssemblyListProgress
 0247            {
 0248                Step = 3,
 0249                Message = $"Этикетки: ожидаем {taskIdToPostings.Count} заданий...",
 0250            });
 251
 0252            var pollResults = await PackageLabelPollingHelper.PollTasksParallelForFileUrlsAsync(
 0253                taskId => manager.GetPackageLabelFile(taskId),
 0254                taskIdToPostings).ConfigureAwait(false);
 255
 0256            foreach (var (taskId, postings) in taskIdToPostings)
 257            {
 0258                if (!pollResults.TryGetValue(taskId, out var poll) || !poll.IsSuccess || string.IsNullOrEmpty(poll.FileU
 259                {
 0260                    var error = poll?.Error ?? "Не удалось получить этикетку";
 0261                    foreach (var pn in postings)
 0262                        result[pn] = new LabelInfo { Error = error };
 0263                    continue;
 264                }
 265
 0266                foreach (var pn in postings)
 0267                    result[pn] = new LabelInfo { FileUrl = poll.FileUrl };
 268            }
 269
 0270            return result;
 0271        }
 272
 273        private class LabelInfo
 274        {
 0275            public string? FileUrl { get; init; }
 0276            public string? Error { get; init; }
 277        }
 278    }
 279}