< Summary

Information
Class: Services.ApiServices.OzonApi.AssemblyListGenerationOrchestrator
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/AssemblyListGenerationOrchestrator.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 207
Coverable lines: 207
Total lines: 288
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
StartAsync()100%210%
<StartAsync()0%7280%
LoadArticleImagesAsync()100%210%
LoadOrderInfoAsync()100%210%
LoadZasylMapAsync()100%210%
EnrichOneCStockAsync()0%7280%
OneCArticle()0%620%
EnqueuePersistRunAsync(...)100%210%
CompleteRunWithErrorAsync()100%210%

File(s)

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

#LineLine coverage
 1using System.Collections.Concurrent;
 2using Microsoft.EntityFrameworkCore;
 3using Microsoft.Extensions.Caching.Memory;
 4using Microsoft.Extensions.DependencyInjection;
 5using OzonDomains.Models;
 6using OzonRepositories.Context;
 7using Services.DataServices;
 8using SlqStudio.Application.Services.EmailService;
 9
 10namespace Services.ApiServices.OzonApi;
 11
 12public record GenerationRequest(
 13    int OzonClientId,
 14    string OzonClientName,
 15    string DecryptedClientId,
 16    string DecryptedApiKey,
 17    DateTime CutoffFrom,
 18    DateTime CutoffTo,
 19    string Status,
 20    string? WarehouseFilter,
 21    bool PersistItemsInBackground = false
 22);
 23
 24public class AssemblyListGenerationOrchestrator
 25{
 26    private readonly IHttpClientFactory _httpClientFactory;
 27    private readonly IEmailService? _emailService;
 28    private readonly IMemoryCache _cache;
 29    private readonly AssemblyListService _assemblyListService;
 30    private readonly IServiceScopeFactory _scopeFactory;
 31
 032    public AssemblyListGenerationOrchestrator(
 033        IHttpClientFactory httpClientFactory,
 034        IEmailService? emailService,
 035        IMemoryCache cache,
 036        AssemblyListService assemblyListService,
 037        IServiceScopeFactory scopeFactory)
 38    {
 039        _httpClientFactory = httpClientFactory;
 040        _emailService = emailService;
 041        _cache = cache;
 042        _assemblyListService = assemblyListService;
 043        _scopeFactory = scopeFactory;
 044    }
 45
 46    public async Task<(string JobId, long DbRunId)> StartAsync(GenerationRequest req)
 47    {
 048        var jobId = Guid.NewGuid().ToString("N");
 049        var progressQueue = new ConcurrentQueue<AssemblyListProgress>();
 050        _cache.Set($"al_progress_{jobId}", progressQueue, TimeSpan.FromMinutes(30));
 51
 52        long dbRunId;
 053        using (var scope = _scopeFactory.CreateScope())
 54        {
 055            var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 056            dbRunId = await repo.CreateRunAsync(new AssemblyListRun
 057            {
 058                StartedAtUtc = DateTime.UtcNow,
 059                OzonClientId = req.OzonClientId,
 060                OzonClientName = req.OzonClientName,
 061                FilterStatus = req.Status,
 062                CutoffFrom = DateOnly.FromDateTime(req.CutoffFrom),
 063                CutoffTo = DateOnly.FromDateTime(req.CutoffTo),
 064            });
 065        }
 66
 067        var jobIdCopy = jobId;
 068        var dbRunIdCopy = dbRunId;
 069        var reqCopy = req;
 70
 071        _ = Task.Run(async () =>
 072        {
 073            try
 074            {
 075                var manager = new OzonApiDataManager(
 076                    _httpClientFactory, _emailService,
 077                    reqCopy.DecryptedClientId, reqCopy.DecryptedApiKey);
 078
 079                var progress = new Progress<AssemblyListProgress>(p => progressQueue.Enqueue(p));
 080
 081                progressQueue.Enqueue(new AssemblyListProgress
 082                {
 083                    Step = 1, TotalSteps = 5,
 084                    Message = "Получаем список отправлений...",
 085                });
 086
 087                var allPostings = await manager.GetUnfulfilledPostingsAsync(
 088                    reqCopy.CutoffFrom, reqCopy.CutoffTo, reqCopy.Status);
 089
 090                var zasylByPosting = string.IsNullOrWhiteSpace(reqCopy.WarehouseFilter)
 091                    ? new Dictionary<string, string?>()
 092                    : await LoadZasylMapAsync(allPostings
 093                        .Select(p => p.PostingNumber)
 094                        .Where(n => !string.IsNullOrEmpty(n))
 095                        .Distinct()
 096                        .ToList());
 097
 098                var filteredPostings = AssemblyListService.FilterPostingsByWarehouse(
 099                    allPostings, reqCopy.WarehouseFilter, zasylByPosting);
 0100
 0101                var offerIds = filteredPostings
 0102                    .SelectMany(p => p.Products)
 0103                    .Select(pr => pr.OfferId)
 0104                    .Distinct()
 0105                    .ToList();
 0106
 0107                var postingNums = filteredPostings
 0108                    .Select(p => p.PostingNumber)
 0109                    .Where(n => !string.IsNullOrEmpty(n))
 0110                    .Distinct()
 0111                    .ToList();
 0112
 0113                var buildTask = _assemblyListService.BuildAsync(
 0114                    manager, filteredPostings, reqCopy.Status, reqCopy.DecryptedClientId, progress);
 0115
 0116                // Отдельные scope — один DbContext нельзя использовать параллельно.
 0117                var imagesTask = LoadArticleImagesAsync(offerIds);
 0118                var orderInfoTask = LoadOrderInfoAsync(postingNums);
 0119
 0120                await Task.WhenAll(buildTask, imagesTask, orderInfoTask).ConfigureAwait(false);
 0121
 0122                var result = await buildTask.ConfigureAwait(false);
 0123                var articleImages = await imagesTask.ConfigureAwait(false);
 0124                var orderInfo = await orderInfoTask.ConfigureAwait(false);
 0125
 0126                _assemblyListService.ApplyArticleImages(result, articleImages);
 0127
 0128                foreach (var item in result.Items)
 0129                {
 0130                    if (orderInfo.TryGetValue(item.PostingNumber, out var info))
 0131                    {
 0132                        item.SupplierName = info.SupplierName;
 0133                        item.OrderedToSupplierDate = info.OrderedDate;
 0134                    }
 0135                }
 0136
 0137                await EnrichOneCStockAsync(result, progressQueue).ConfigureAwait(false);
 0138
 0139                _cache.Set($"al_result_{jobIdCopy}", result, TimeSpan.FromMinutes(30));
 0140
 0141                progressQueue.Enqueue(new AssemblyListProgress
 0142                {
 0143                    Step = 5, TotalSteps = 5,
 0144                    Message = $"Готово: {result.TotalPostings} отправлений, {result.Items.Count} строк",
 0145                    IsComplete = true,
 0146                });
 0147
 0148                EnqueuePersistRunAsync(dbRunIdCopy, result);
 0149            }
 0150            catch (Exception ex)
 0151            {
 0152                await CompleteRunWithErrorAsync(dbRunIdCopy, ex.Message).ConfigureAwait(false);
 0153                progressQueue.Enqueue(new AssemblyListProgress
 0154                {
 0155                    Step = 0,
 0156                    Message = ex.Message,
 0157                    IsError = true,
 0158                    ErrorMessage = ex.Message,
 0159                });
 0160            }
 0161        });
 162
 0163        return (jobId, dbRunId);
 0164    }
 165
 166    private async Task<Dictionary<string, string>> LoadArticleImagesAsync(List<string> offerIds)
 167    {
 0168        using var scope = _scopeFactory.CreateScope();
 0169        var productsService = scope.ServiceProvider.GetRequiredService<ProductsDataServices>();
 0170        return await productsService.GetImageUrlsByArticlesAsync(offerIds).ConfigureAwait(false);
 0171    }
 172
 173    private async Task<Dictionary<string, (string? SupplierName, DateTime? OrderedDate)>> LoadOrderInfoAsync(
 174        List<string> postingNums)
 175    {
 0176        using var scope = _scopeFactory.CreateScope();
 0177        var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 0178        return await repo.GetOrderInfoByPostingNumbersAsync(postingNums).ConfigureAwait(false);
 0179    }
 180
 181    private async Task<Dictionary<string, string?>> LoadZasylMapAsync(List<string> postingNums)
 182    {
 0183        using var scope = _scopeFactory.CreateScope();
 0184        var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 0185        return await repo.GetZasylValuesByShipmentNumbersAsync(postingNums).ConfigureAwait(false);
 0186    }
 187
 188    private async Task EnrichOneCStockAsync(
 189        AssemblyListResult result,
 190        ConcurrentQueue<AssemblyListProgress> progressQueue)
 191    {
 0192        progressQueue.Enqueue(new AssemblyListProgress
 0193        {
 0194            Step = 4, TotalSteps = 5,
 0195            Message = "Запрашиваем остатки из 1С...",
 0196        });
 197
 198        try
 199        {
 0200            using var scope = _scopeFactory.CreateScope();
 0201            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
 0202            var mappings = await db.WarehouseMappings.ToListAsync().ConfigureAwait(false);
 0203            var oneCDataManager = scope.ServiceProvider.GetRequiredService<OneCDataManager>();
 204
 205            static string OneCArticle(string offerId) =>
 0206                offerId.Contains('=') ? offerId[..offerId.IndexOf('=')] : offerId;
 207
 0208            var stockPairs = result.Items
 0209                .Select(i =>
 0210                {
 0211                    var mapping = mappings.FirstOrDefault(m =>
 0212                        string.Equals(m.OzonWarehouseName, i.WarehouseName,
 0213                            StringComparison.OrdinalIgnoreCase));
 0214                    return (Article: OneCArticle(i.OfferId),
 0215                            OneCWarehouse: mapping?.BitrixWarehouseName ?? string.Empty);
 0216                })
 0217                .Where(p => !string.IsNullOrEmpty(p.OneCWarehouse))
 0218                .Distinct()
 0219                .ToList();
 220
 0221            if (stockPairs.Count == 0)
 0222                return;
 223
 0224            var stockData = await oneCDataManager.GetStockAndLastReceiptBatchAsync(stockPairs)
 0225                .ConfigureAwait(false);
 0226            foreach (var item in result.Items)
 227            {
 0228                var mapping = mappings.FirstOrDefault(m =>
 0229                    string.Equals(m.OzonWarehouseName, item.WarehouseName,
 0230                        StringComparison.OrdinalIgnoreCase));
 0231                if (mapping == null) continue;
 0232                item.OneCWarehouseName = mapping.BitrixWarehouseName;
 0233                if (stockData.TryGetValue((OneCArticle(item.OfferId), mapping.BitrixWarehouseName), out var stock))
 234                {
 0235                    item.OneCBalance = stock.Balance;
 0236                    item.OneCReceiptDoc = stock.LastReceiptNumber;
 0237                    item.OneCReceiptDocKey = stock.LastReceiptRefKey;
 0238                    item.OneCOtherStock = stock.OtherStock;
 239                }
 240            }
 0241        }
 0242        catch (Exception ex)
 243        {
 0244            progressQueue.Enqueue(new AssemblyListProgress
 0245            {
 0246                Step = 4, TotalSteps = 5,
 0247                Message = $"Предупреждение: не удалось загрузить остатки 1С: {ex.Message}",
 0248            });
 0249        }
 0250    }
 251
 252    private void EnqueuePersistRunAsync(long dbRunId, AssemblyListResult result)
 253    {
 0254        var itemsCopy = result.Items.ToList();
 0255        var totalPostings = result.TotalPostings;
 0256        var totalItems = itemsCopy.Count;
 257
 0258        _ = Task.Run(async () =>
 0259        {
 0260            try
 0261            {
 0262                using var saveScope = _scopeFactory.CreateScope();
 0263                var saveRepo = saveScope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 0264                await saveRepo.SaveItemsAsync(dbRunId, itemsCopy).ConfigureAwait(false);
 0265                await saveRepo.CompleteRunAsync(dbRunId, "completed", totalPostings, totalItems)
 0266                    .ConfigureAwait(false);
 0267            }
 0268            catch
 0269            {
 0270                // best-effort persistence
 0271            }
 0272        });
 0273    }
 274
 275    private async Task CompleteRunWithErrorAsync(long dbRunId, string errorMessage)
 276    {
 277        try
 278        {
 0279            using var scope = _scopeFactory.CreateScope();
 0280            var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 0281            await repo.CompleteRunAsync(dbRunId, "error", 0, 0, errorMessage).ConfigureAwait(false);
 0282        }
 0283        catch
 284        {
 285            // best-effort
 0286        }
 0287    }
 288}