< Summary

Information
Class: Services.ApiServices.OzonApi.AssemblyListProgress
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/AssemblyListService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 6
Coverable lines: 6
Total lines: 279
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Step()100%210%
get_TotalSteps()100%210%
get_Message()100%210%
get_IsComplete()100%210%
get_IsError()100%210%
get_ErrorMessage()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    {
 08        public int Step { get; init; }
 09        public int TotalSteps { get; init; } = 4;
 010        public string Message { get; init; } = "";
 011        public bool IsComplete { get; init; }
 012        public bool IsError { get; init; }
 013        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;
 50        private static readonly TimeSpan WarehouseListCacheTtl = TimeSpan.FromMinutes(10);
 51
 52        private static readonly Dictionary<string, string> StatusLabels = new()
 53        {
 54            { "awaiting_registration", "Ожидает регистрации" },
 55            { "acceptance_in_progress", "Идёт приёмка" },
 56            { "awaiting_approve", "Ожидает подтверждения" },
 57            { "awaiting_packaging", "Ожидает упаковки" },
 58            { "awaiting_deliver", "Ожидает отгрузки" },
 59            { "arbitration", "Арбитраж" },
 60            { "client_arbitration", "Клиентский арбитраж" },
 61            { "delivering", "Доставляется" },
 62            { "driver_pickup", "У водителя" },
 63            { "cancelled", "Отменено" },
 64            { "not_accepted", "Не принято на СЦ" },
 65        };
 66
 67        private readonly IMemoryCache _cache;
 68
 69        public AssemblyListService(IMemoryCache cache)
 70        {
 71            _cache = cache;
 72        }
 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        {
 84            if (string.IsNullOrWhiteSpace(warehouseFilter))
 85                return postings.ToList();
 86
 87            return postings
 88                .Where(p =>
 89                {
 90                    var zasyl = zasylByPosting != null && zasylByPosting.TryGetValue(p.PostingNumber, out var z) ? z : n
 91                    var effectiveWarehouse = !string.IsNullOrWhiteSpace(zasyl) ? zasyl : p.WarehouseName;
 92                    return string.Equals(effectiveWarehouse, warehouseFilter, StringComparison.OrdinalIgnoreCase);
 93                })
 94                .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        {
 104            progress?.Report(new AssemblyListProgress { Step = 2, Message = $"Загрузка данных ({postings.Count} отправле
 105
 106            var filtered = postings
 107                .Where(p => string.IsNullOrEmpty(status) || p.Status == status)
 108                .ToList();
 109
 110            var warehouseMap = await GetWarehouseMapAsync(manager, sellerCacheKey).ConfigureAwait(false);
 111
 112            progress?.Report(new AssemblyListProgress
 113            {
 114                Step = 3,
 115                Message = "Создаём задания на этикетки...",
 116            });
 117
 118            var deliverPostings = filtered.Where(p => p.Status == "awaiting_deliver").ToList();
 119            var labelsByPosting = await GenerateLabelsAsync(manager, deliverPostings, progress)
 120                .ConfigureAwait(false);
 121
 122            progress?.Report(new AssemblyListProgress
 123            {
 124                Step = 4,
 125                Message = "Формируем лист подбора...",
 126            });
 127
 128            var items = new List<AssemblyListItem>();
 129            var idx = 1;
 130
 131            foreach (var posting in filtered)
 132            {
 133                var warehouseName = posting.WarehouseName;
 134                if (string.IsNullOrEmpty(warehouseName) && posting.WarehouseId > 0)
 135                    warehouseMap.TryGetValue(posting.WarehouseId, out warehouseName);
 136                if (string.IsNullOrEmpty(warehouseName))
 137                    warehouseName = posting.WarehouseId > 0 ? posting.WarehouseId.ToString() : "—";
 138
 139                var labelInfo = labelsByPosting.GetValueOrDefault(posting.PostingNumber);
 140
 141                foreach (var product in posting.Products)
 142                {
 143                    items.Add(new AssemblyListItem
 144                    {
 145                        Index = idx++,
 146                        PostingNumber = posting.PostingNumber,
 147                        Status = posting.Status,
 148                        StatusLabel = StatusLabels.GetValueOrDefault(posting.Status, posting.Status),
 149                        WarehouseId = posting.WarehouseId,
 150                        WarehouseName = warehouseName,
 151                        OfferId = product.OfferId,
 152                        ProductName = product.Name,
 153                        Quantity = product.Quantity,
 154                        CutoffDate = posting.CutoffDate,
 155                        LabelUrl = labelInfo?.FileUrl,
 156                        LabelError = labelInfo?.Error,
 157                    });
 158                }
 159            }
 160
 161            return new AssemblyListResult
 162            {
 163                Items = items,
 164                TotalPostings = filtered.Count,
 165                GeneratedAt = DateTime.Now,
 166            };
 167        }
 168
 169        public void ApplyArticleImages(AssemblyListResult result, Dictionary<string, string> articleImages)
 170        {
 171            foreach (var item in result.Items)
 172            {
 173                if (articleImages.TryGetValue(item.OfferId, out var imageUrl))
 174                    item.ImageUrl = imageUrl;
 175            }
 176        }
 177
 178        private async Task<Dictionary<long, string>> GetWarehouseMapAsync(OzonApiDataManager manager, string sellerCache
 179        {
 180            var cacheKey = $"assembly_ozon_warehouses_{sellerCacheKey}";
 181            if (_cache.TryGetValue(cacheKey, out Dictionary<long, string>? cached) && cached != null)
 182                return cached;
 183
 184            var warehouses = await manager.GetWarehouseList().ConfigureAwait(false);
 185            var map = warehouses
 186                .Where(w => w.WarehouseId > 0 && !string.IsNullOrEmpty(w.Name))
 187                .ToDictionary(w => w.WarehouseId, w => w.Name!);
 188
 189            _cache.Set(cacheKey, map, WarehouseListCacheTtl);
 190            return map;
 191        }
 192
 193        private static async Task<Dictionary<string, LabelInfo>> GenerateLabelsAsync(
 194            OzonApiDataManager manager,
 195            List<UnfulfilledPosting> deliverPostings,
 196            IProgress<AssemblyListProgress>? progress)
 197        {
 198            var result = new Dictionary<string, LabelInfo>(StringComparer.Ordinal);
 199            if (deliverPostings.Count == 0)
 200                return result;
 201
 202            var chunks = deliverPostings
 203                .Select(p => p.PostingNumber)
 204                .Where(n => !string.IsNullOrEmpty(n))
 205                .Chunk(LabelBatchSize)
 206                .Select(c => c.ToArray())
 207                .ToList();
 208
 209            progress?.Report(new AssemblyListProgress
 210            {
 211                Step = 3,
 212                Message = $"Этикетки: создаём {chunks.Count} заданий для {deliverPostings.Count} отправлений...",
 213            });
 214
 215            var createResults = await Task.WhenAll(chunks.Select(async postingNumbers =>
 216            {
 217                try
 218                {
 219                    var createResp = await manager.CreatePackageLabelTask(postingNumbers).ConfigureAwait(false);
 220                    var taskId = createResp.PickTask()?.TaskId ?? 0;
 221                    return (TaskId: taskId, PostingNumbers: postingNumbers, Error: (string?)null);
 222                }
 223                catch (Exception ex)
 224                {
 225                    return (TaskId: 0L, PostingNumbers: postingNumbers, Error: ex.Message);
 226                }
 227            })).ConfigureAwait(false);
 228
 229            var taskIdToPostings = new Dictionary<long, string[]>();
 230            foreach (var created in createResults)
 231            {
 232                if (created.TaskId > 0)
 233                {
 234                    taskIdToPostings[created.TaskId] = created.PostingNumbers;
 235                    continue;
 236                }
 237
 238                var error = created.Error ?? "Не удалось создать задание";
 239                foreach (var pn in created.PostingNumbers)
 240                    result[pn] = new LabelInfo { Error = error };
 241            }
 242
 243            if (taskIdToPostings.Count == 0)
 244                return result;
 245
 246            progress?.Report(new AssemblyListProgress
 247            {
 248                Step = 3,
 249                Message = $"Этикетки: ожидаем {taskIdToPostings.Count} заданий...",
 250            });
 251
 252            var pollResults = await PackageLabelPollingHelper.PollTasksParallelForFileUrlsAsync(
 253                taskId => manager.GetPackageLabelFile(taskId),
 254                taskIdToPostings).ConfigureAwait(false);
 255
 256            foreach (var (taskId, postings) in taskIdToPostings)
 257            {
 258                if (!pollResults.TryGetValue(taskId, out var poll) || !poll.IsSuccess || string.IsNullOrEmpty(poll.FileU
 259                {
 260                    var error = poll?.Error ?? "Не удалось получить этикетку";
 261                    foreach (var pn in postings)
 262                        result[pn] = new LabelInfo { Error = error };
 263                    continue;
 264                }
 265
 266                foreach (var pn in postings)
 267                    result[pn] = new LabelInfo { FileUrl = poll.FileUrl };
 268            }
 269
 270            return result;
 271        }
 272
 273        private class LabelInfo
 274        {
 275            public string? FileUrl { get; init; }
 276            public string? Error { get; init; }
 277        }
 278    }
 279}