< Summary

Information
Class: Services.ApiServices.OzonApi.GenerationRequest
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/AssemblyListGenerationOrchestrator.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 11
Coverable lines: 11
Total lines: 288
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
.ctor(...)100%210%
get_OzonClientId()100%210%
get_OzonClientName()100%210%
get_DecryptedClientId()100%210%
get_DecryptedApiKey()100%210%
get_CutoffFrom()100%210%
get_CutoffTo()100%210%
get_Status()100%210%
get_WarehouseFilter()100%210%
get_PersistItemsInBackground()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
 012public record GenerationRequest(
 013    int OzonClientId,
 014    string OzonClientName,
 015    string DecryptedClientId,
 016    string DecryptedApiKey,
 017    DateTime CutoffFrom,
 018    DateTime CutoffTo,
 019    string Status,
 020    string? WarehouseFilter,
 021    bool PersistItemsInBackground = false
 022);
 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
 32    public AssemblyListGenerationOrchestrator(
 33        IHttpClientFactory httpClientFactory,
 34        IEmailService? emailService,
 35        IMemoryCache cache,
 36        AssemblyListService assemblyListService,
 37        IServiceScopeFactory scopeFactory)
 38    {
 39        _httpClientFactory = httpClientFactory;
 40        _emailService = emailService;
 41        _cache = cache;
 42        _assemblyListService = assemblyListService;
 43        _scopeFactory = scopeFactory;
 44    }
 45
 46    public async Task<(string JobId, long DbRunId)> StartAsync(GenerationRequest req)
 47    {
 48        var jobId = Guid.NewGuid().ToString("N");
 49        var progressQueue = new ConcurrentQueue<AssemblyListProgress>();
 50        _cache.Set($"al_progress_{jobId}", progressQueue, TimeSpan.FromMinutes(30));
 51
 52        long dbRunId;
 53        using (var scope = _scopeFactory.CreateScope())
 54        {
 55            var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 56            dbRunId = await repo.CreateRunAsync(new AssemblyListRun
 57            {
 58                StartedAtUtc = DateTime.UtcNow,
 59                OzonClientId = req.OzonClientId,
 60                OzonClientName = req.OzonClientName,
 61                FilterStatus = req.Status,
 62                CutoffFrom = DateOnly.FromDateTime(req.CutoffFrom),
 63                CutoffTo = DateOnly.FromDateTime(req.CutoffTo),
 64            });
 65        }
 66
 67        var jobIdCopy = jobId;
 68        var dbRunIdCopy = dbRunId;
 69        var reqCopy = req;
 70
 71        _ = Task.Run(async () =>
 72        {
 73            try
 74            {
 75                var manager = new OzonApiDataManager(
 76                    _httpClientFactory, _emailService,
 77                    reqCopy.DecryptedClientId, reqCopy.DecryptedApiKey);
 78
 79                var progress = new Progress<AssemblyListProgress>(p => progressQueue.Enqueue(p));
 80
 81                progressQueue.Enqueue(new AssemblyListProgress
 82                {
 83                    Step = 1, TotalSteps = 5,
 84                    Message = "Получаем список отправлений...",
 85                });
 86
 87                var allPostings = await manager.GetUnfulfilledPostingsAsync(
 88                    reqCopy.CutoffFrom, reqCopy.CutoffTo, reqCopy.Status);
 89
 90                var zasylByPosting = string.IsNullOrWhiteSpace(reqCopy.WarehouseFilter)
 91                    ? new Dictionary<string, string?>()
 92                    : await LoadZasylMapAsync(allPostings
 93                        .Select(p => p.PostingNumber)
 94                        .Where(n => !string.IsNullOrEmpty(n))
 95                        .Distinct()
 96                        .ToList());
 97
 98                var filteredPostings = AssemblyListService.FilterPostingsByWarehouse(
 99                    allPostings, reqCopy.WarehouseFilter, zasylByPosting);
 100
 101                var offerIds = filteredPostings
 102                    .SelectMany(p => p.Products)
 103                    .Select(pr => pr.OfferId)
 104                    .Distinct()
 105                    .ToList();
 106
 107                var postingNums = filteredPostings
 108                    .Select(p => p.PostingNumber)
 109                    .Where(n => !string.IsNullOrEmpty(n))
 110                    .Distinct()
 111                    .ToList();
 112
 113                var buildTask = _assemblyListService.BuildAsync(
 114                    manager, filteredPostings, reqCopy.Status, reqCopy.DecryptedClientId, progress);
 115
 116                // Отдельные scope — один DbContext нельзя использовать параллельно.
 117                var imagesTask = LoadArticleImagesAsync(offerIds);
 118                var orderInfoTask = LoadOrderInfoAsync(postingNums);
 119
 120                await Task.WhenAll(buildTask, imagesTask, orderInfoTask).ConfigureAwait(false);
 121
 122                var result = await buildTask.ConfigureAwait(false);
 123                var articleImages = await imagesTask.ConfigureAwait(false);
 124                var orderInfo = await orderInfoTask.ConfigureAwait(false);
 125
 126                _assemblyListService.ApplyArticleImages(result, articleImages);
 127
 128                foreach (var item in result.Items)
 129                {
 130                    if (orderInfo.TryGetValue(item.PostingNumber, out var info))
 131                    {
 132                        item.SupplierName = info.SupplierName;
 133                        item.OrderedToSupplierDate = info.OrderedDate;
 134                    }
 135                }
 136
 137                await EnrichOneCStockAsync(result, progressQueue).ConfigureAwait(false);
 138
 139                _cache.Set($"al_result_{jobIdCopy}", result, TimeSpan.FromMinutes(30));
 140
 141                progressQueue.Enqueue(new AssemblyListProgress
 142                {
 143                    Step = 5, TotalSteps = 5,
 144                    Message = $"Готово: {result.TotalPostings} отправлений, {result.Items.Count} строк",
 145                    IsComplete = true,
 146                });
 147
 148                EnqueuePersistRunAsync(dbRunIdCopy, result);
 149            }
 150            catch (Exception ex)
 151            {
 152                await CompleteRunWithErrorAsync(dbRunIdCopy, ex.Message).ConfigureAwait(false);
 153                progressQueue.Enqueue(new AssemblyListProgress
 154                {
 155                    Step = 0,
 156                    Message = ex.Message,
 157                    IsError = true,
 158                    ErrorMessage = ex.Message,
 159                });
 160            }
 161        });
 162
 163        return (jobId, dbRunId);
 164    }
 165
 166    private async Task<Dictionary<string, string>> LoadArticleImagesAsync(List<string> offerIds)
 167    {
 168        using var scope = _scopeFactory.CreateScope();
 169        var productsService = scope.ServiceProvider.GetRequiredService<ProductsDataServices>();
 170        return await productsService.GetImageUrlsByArticlesAsync(offerIds).ConfigureAwait(false);
 171    }
 172
 173    private async Task<Dictionary<string, (string? SupplierName, DateTime? OrderedDate)>> LoadOrderInfoAsync(
 174        List<string> postingNums)
 175    {
 176        using var scope = _scopeFactory.CreateScope();
 177        var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 178        return await repo.GetOrderInfoByPostingNumbersAsync(postingNums).ConfigureAwait(false);
 179    }
 180
 181    private async Task<Dictionary<string, string?>> LoadZasylMapAsync(List<string> postingNums)
 182    {
 183        using var scope = _scopeFactory.CreateScope();
 184        var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 185        return await repo.GetZasylValuesByShipmentNumbersAsync(postingNums).ConfigureAwait(false);
 186    }
 187
 188    private async Task EnrichOneCStockAsync(
 189        AssemblyListResult result,
 190        ConcurrentQueue<AssemblyListProgress> progressQueue)
 191    {
 192        progressQueue.Enqueue(new AssemblyListProgress
 193        {
 194            Step = 4, TotalSteps = 5,
 195            Message = "Запрашиваем остатки из 1С...",
 196        });
 197
 198        try
 199        {
 200            using var scope = _scopeFactory.CreateScope();
 201            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
 202            var mappings = await db.WarehouseMappings.ToListAsync().ConfigureAwait(false);
 203            var oneCDataManager = scope.ServiceProvider.GetRequiredService<OneCDataManager>();
 204
 205            static string OneCArticle(string offerId) =>
 206                offerId.Contains('=') ? offerId[..offerId.IndexOf('=')] : offerId;
 207
 208            var stockPairs = result.Items
 209                .Select(i =>
 210                {
 211                    var mapping = mappings.FirstOrDefault(m =>
 212                        string.Equals(m.OzonWarehouseName, i.WarehouseName,
 213                            StringComparison.OrdinalIgnoreCase));
 214                    return (Article: OneCArticle(i.OfferId),
 215                            OneCWarehouse: mapping?.BitrixWarehouseName ?? string.Empty);
 216                })
 217                .Where(p => !string.IsNullOrEmpty(p.OneCWarehouse))
 218                .Distinct()
 219                .ToList();
 220
 221            if (stockPairs.Count == 0)
 222                return;
 223
 224            var stockData = await oneCDataManager.GetStockAndLastReceiptBatchAsync(stockPairs)
 225                .ConfigureAwait(false);
 226            foreach (var item in result.Items)
 227            {
 228                var mapping = mappings.FirstOrDefault(m =>
 229                    string.Equals(m.OzonWarehouseName, item.WarehouseName,
 230                        StringComparison.OrdinalIgnoreCase));
 231                if (mapping == null) continue;
 232                item.OneCWarehouseName = mapping.BitrixWarehouseName;
 233                if (stockData.TryGetValue((OneCArticle(item.OfferId), mapping.BitrixWarehouseName), out var stock))
 234                {
 235                    item.OneCBalance = stock.Balance;
 236                    item.OneCReceiptDoc = stock.LastReceiptNumber;
 237                    item.OneCReceiptDocKey = stock.LastReceiptRefKey;
 238                    item.OneCOtherStock = stock.OtherStock;
 239                }
 240            }
 241        }
 242        catch (Exception ex)
 243        {
 244            progressQueue.Enqueue(new AssemblyListProgress
 245            {
 246                Step = 4, TotalSteps = 5,
 247                Message = $"Предупреждение: не удалось загрузить остатки 1С: {ex.Message}",
 248            });
 249        }
 250    }
 251
 252    private void EnqueuePersistRunAsync(long dbRunId, AssemblyListResult result)
 253    {
 254        var itemsCopy = result.Items.ToList();
 255        var totalPostings = result.TotalPostings;
 256        var totalItems = itemsCopy.Count;
 257
 258        _ = Task.Run(async () =>
 259        {
 260            try
 261            {
 262                using var saveScope = _scopeFactory.CreateScope();
 263                var saveRepo = saveScope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 264                await saveRepo.SaveItemsAsync(dbRunId, itemsCopy).ConfigureAwait(false);
 265                await saveRepo.CompleteRunAsync(dbRunId, "completed", totalPostings, totalItems)
 266                    .ConfigureAwait(false);
 267            }
 268            catch
 269            {
 270                // best-effort persistence
 271            }
 272        });
 273    }
 274
 275    private async Task CompleteRunWithErrorAsync(long dbRunId, string errorMessage)
 276    {
 277        try
 278        {
 279            using var scope = _scopeFactory.CreateScope();
 280            var repo = scope.ServiceProvider.GetRequiredService<AssemblyListRunRepository>();
 281            await repo.CompleteRunAsync(dbRunId, "error", 0, 0, errorMessage).ConfigureAwait(false);
 282        }
 283        catch
 284        {
 285            // best-effort
 286        }
 287    }
 288}