< Summary

Information
Class: Services.ChannelTaskServices.ChannelTaskExecutor
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ChannelTaskServices/ChannelTaskExecutor.cs
Line coverage
100%
Covered lines: 150
Uncovered lines: 0
Coverable lines: 150
Total lines: 266
Line coverage: 100%
Branch coverage
89%
Covered branches: 52
Total branches: 58
Branch coverage: 89.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
ExecuteAsync()100%44100%
ExecuteBitrixExcelAsync()94.44%1818100%
ExecuteTransactionExcelAsync()91.66%2424100%
SaveFileAsync()100%11100%
SetStage(...)100%22100%
Complete(...)100%22100%
Fail(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ChannelTaskServices/ChannelTaskExecutor.cs

#LineLine coverage
 1using System.Text.Json;
 2using Microsoft.Extensions.DependencyInjection;
 3using Microsoft.Extensions.Logging;
 4using OzonDomains.Models;
 5using Services.BitrixStockServices;
 6using Services.DataServices;
 7using Services.ParserServices.FielParsers;
 8using SlqStudio.Application.Services.EmailService;
 9
 10namespace Services.ChannelTaskServices;
 11
 3912public sealed class ChannelTaskExecutor(
 3913    IServiceScopeFactory scopeFactory,
 3914    ILogger<ChannelTaskExecutor> logger)
 15{
 3916    private readonly ILogger<ChannelTaskExecutor> _logger = logger;
 17    private const string BitrixEmailRecipient = "lex@interparts.ru";
 118    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
 19
 20    public async Task ExecuteAsync(
 21        string taskId,
 22        ChannelTaskType type,
 23        string payloadJson,
 24        Action<string, Action<ChannelTaskState>> update,
 25        Action<string, string> registerFile,
 26        CancellationToken ct)
 27    {
 2328        _logger.LogDebug("Channel task {TaskId} started ({Type})", taskId, type);
 29        switch (type)
 30        {
 31            case ChannelTaskType.BitrixSyncReportExcel:
 1132                await ExecuteBitrixExcelAsync(taskId, payloadJson, sendEmail: false, update, registerFile, ct);
 233                break;
 34            case ChannelTaskType.BitrixSyncReportEmail:
 235                await ExecuteBitrixExcelAsync(taskId, payloadJson, sendEmail: true, update, registerFile, ct);
 136                break;
 37            case ChannelTaskType.TransactionExcel:
 938                await ExecuteTransactionExcelAsync(taskId, payloadJson, update, registerFile, ct);
 239                break;
 40            default:
 141                Fail(taskId, update, $"Неизвестный тип задачи: {type}");
 42                break;
 43        }
 644    }
 45
 46    private async Task ExecuteBitrixExcelAsync(
 47        string taskId,
 48        string payloadJson,
 49        bool sendEmail,
 50        Action<string, Action<ChannelTaskState>> update,
 51        Action<string, string> registerFile,
 52        CancellationToken ct)
 53    {
 1354        var payload = JsonSerializer.Deserialize<BitrixSyncReportTaskPayload>(payloadJson, JsonOptions)
 1355                      ?? throw new InvalidOperationException("Некорректный payload Bitrix.");
 56
 957        if (string.IsNullOrWhiteSpace(payload.ReportId))
 458            throw new InvalidOperationException("reportId обязателен.");
 59
 560        using var scope = scopeFactory.CreateScope();
 561        var reportService = scope.ServiceProvider.GetRequiredService<IBitrixStockSyncReportService>();
 562        IEmailService? emailService = sendEmail
 563            ? scope.ServiceProvider.GetRequiredService<IEmailService>()
 564            : null;
 65
 566        SetStage(taskId, update, ChannelTaskStages.Loading, "Загрузка данных отчёта", 5);
 867        update(taskId, s => s.ReportId = payload.ReportId);
 68
 569        var progress = new Action<BitrixStockExcelBuildProgress>(p =>
 570        {
 2571            update(taskId, s =>
 2572            {
 1873                s.Stage = p.Stage;
 1874                s.Status = p.Stage;
 1875                s.Current = p.Current;
 1876                s.Total = p.Total;
 1877                s.Percent = p.Percent;
 1878                if (!string.IsNullOrWhiteSpace(p.Message))
 1879                    s.Message = p.Message;
 4380            });
 3081        });
 82
 583        var (bytes, filename) = await BitrixStockSyncReportExcelBuilder.BuildAsync(
 584            reportService,
 585            payload.ReportId.Trim(),
 586            payload.Query ?? new BitrixStockSyncReportQuery(),
 587            progress,
 588            ct);
 89
 590        if (bytes == null || bytes.Length == 0)
 191            throw new InvalidOperationException("Отчёт не найден или Excel-пустой.");
 92
 493        SetStage(taskId, update, ChannelTaskStages.Saving, "Сохранение файла", 95);
 494        var safeName = string.IsNullOrWhiteSpace(filename) ? $"sync-{taskId}.xlsx" : filename;
 495        var fullPath = await SaveFileAsync(taskId, safeName, bytes, ct);
 96
 497        if (sendEmail)
 98        {
 299            SetStage(taskId, update, ChannelTaskStages.Sending, "Отправка на почту…", 98);
 2100            var subject = $"Отчёт синхронизации остатков {BitrixStockMskDateTime.FormatNow()}";
 2101            var body = $"Отчёт синхронизации остатков Ozon во вложении.\nID отчёта: {payload.ReportId}";
 2102            var sent = await emailService!.SendEmailWithAttachmentAsync(
 2103                BitrixEmailRecipient,
 2104                subject,
 2105                body,
 2106                bytes,
 2107                safeName,
 2108                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
 109
 2110            if (!sent)
 1111                throw new InvalidOperationException("Не удалось отправить письмо");
 112
 1113            Complete(taskId, update, registerFile, ChannelTaskResultKind.Email, safeName, "Письмо отправлено на lex@inte
 1114            return;
 115        }
 116
 2117        Complete(taskId, update, registerFile, ChannelTaskResultKind.File, safeName, "Файл готов к скачиванию", fullPath
 3118    }
 119
 120    private async Task ExecuteTransactionExcelAsync(
 121        string taskId,
 122        string payloadJson,
 123        Action<string, Action<ChannelTaskState>> update,
 124        Action<string, string> registerFile,
 125        CancellationToken ct)
 126    {
 9127        var payload = JsonSerializer.Deserialize<TransactionExcelTaskPayload>(payloadJson, JsonOptions)
 9128                      ?? throw new InvalidOperationException("Некорректный payload Transaction.");
 129
 9130        using var scope = scopeFactory.CreateScope();
 9131        var orderServices = scope.ServiceProvider.GetRequiredService<OrdersDataServices>();
 9132        var transactionDataServices = scope.ServiceProvider.GetRequiredService<TransactionDataServices>();
 9133        var excelExporter = scope.ServiceProvider.GetRequiredService<ExcelExporter>();
 134
 9135        SetStage(taskId, update, ChannelTaskStages.Loading, "Загрузка заказов…", 10);
 136
 137        List<Order> orders;
 138        string fileName;
 139
 9140        if (payload.TableMode)
 141        {
 4142            var deleted = payload.DeletedOrderIds.ToHashSet();
 10143            var ids = payload.OrderIds.Where(id => !deleted.Contains(id)).Distinct().ToArray();
 4144            if (ids.Length == 0)
 2145                throw new InvalidOperationException("Нет заказов для экспорта");
 146
 2147            orders = new List<Order>();
 10148            foreach (var orderId in ids)
 149            {
 3150                ct.ThrowIfCancellationRequested();
 3151                var order = await orderServices.GetAsyncNoTracking(orderId);
 3152                if (order != null)
 2153                    orders.Add(order);
 154            }
 155
 2156            if (orders.Count == 0)
 1157                throw new InvalidOperationException("Заказы не найдены");
 158
 1159            orders = orders
 2160                .OrderBy(o => o.EtProducer?.Name ?? "")
 2161                .ThenBy(o => o.Article ?? "")
 1162                .ToList();
 163
 1164            fileName = $"Таблица_заказов_{DateTime.Now:dd.MM.yyyy_HHmmss}.xlsx";
 165        }
 166        else
 167        {
 5168            if (payload.TransactionId is not > 0)
 3169                throw new InvalidOperationException("transactionId обязателен");
 170
 2171            var transaction = await transactionDataServices.GetTransaction(payload.TransactionId.Value);
 2172            if (transaction?.Orders == null || transaction.Orders.Count == 0)
 1173                throw new InvalidOperationException("Транзакция не найдена или без заказов");
 174
 1175            orders = transaction.Orders.ToList();
 1176            fileName = $"Transaction_{transaction.CreateBy}_{transaction.CreatedDateTime:dd.MM.yyyy}.xlsx";
 177        }
 178
 2179        SetStage(taskId, update, ChannelTaskStages.Building, "Сборка Excel…", 60, orders.Count, orders.Count);
 180
 181        byte[] bytes;
 2182        if (payload.TableMode)
 183        {
 1184            var columns = new List<string>
 1185            {
 1186                "Номер заказа", "Клиент", "Наименование товара", "Артикул",
 1187                "Производитель", "Количество", "Сумма отправления"
 1188            };
 1189            bytes = excelExporter.ExportToExcel(orders, columns);
 190        }
 191        else
 192        {
 1193            bytes = excelExporter.ExportToExcel(orders);
 194        }
 195
 2196        SetStage(taskId, update, ChannelTaskStages.Saving, "Сохранение файла", 95);
 2197        var fullPath = await SaveFileAsync(taskId, fileName, bytes, ct);
 2198        Complete(taskId, update, registerFile, ChannelTaskResultKind.File, fileName, "Файл готов к скачиванию", fullPath
 2199    }
 200
 201    private static async Task<string> SaveFileAsync(string taskId, string fileName, byte[] bytes, CancellationToken ct)
 202    {
 6203        var dir = ChannelTaskProcessor.ExportDirectory;
 6204        Directory.CreateDirectory(dir);
 6205        var fullPath = Path.Combine(dir, $"{taskId}-{fileName}");
 6206        await File.WriteAllBytesAsync(fullPath, bytes, ct);
 6207        return fullPath;
 6208    }
 209
 210    private static void SetStage(
 211        string taskId,
 212        Action<string, Action<ChannelTaskState>> update,
 213        string stage,
 214        string message,
 215        int percent,
 216        int current = 0,
 217        int total = 0)
 218    {
 24219        update(taskId, s =>
 24220        {
 13221            s.Status = stage;
 13222            s.Stage = stage;
 13223            s.Message = message;
 13224            s.Percent = percent;
 13225            s.Current = current;
 13226            s.Total = total;
 13227            s.StartedAtUtc ??= DateTime.UtcNow;
 37228        });
 24229    }
 230
 231    private static void Complete(
 232        string taskId,
 233        Action<string, Action<ChannelTaskState>> update,
 234        Action<string, string> registerFile,
 235        ChannelTaskResultKind resultKind,
 236        string fileName,
 237        string message,
 238        string? filePath = null)
 239    {
 5240        update(taskId, s =>
 5241        {
 5242            s.Status = ChannelTaskStages.Completed;
 5243            s.Stage = ChannelTaskStages.Completed;
 5244            s.Message = message;
 5245            s.Percent = 100;
 5246            s.FileName = fileName;
 5247            s.ResultKind = resultKind;
 5248            s.CompletedAtUtc = DateTime.UtcNow;
 10249        });
 250
 5251        if (!string.IsNullOrWhiteSpace(filePath))
 4252            registerFile(taskId, filePath);
 5253    }
 254
 255    private static void Fail(string taskId, Action<string, Action<ChannelTaskState>> update, string error)
 256    {
 1257        update(taskId, s =>
 1258        {
 1259            s.Status = ChannelTaskStages.Failed;
 1260            s.Stage = ChannelTaskStages.Failed;
 1261            s.Error = error;
 1262            s.Message = error;
 1263            s.CompletedAtUtc = DateTime.UtcNow;
 2264        });
 1265    }
 266}