< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.Grid.Commit.TransactionCommitService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Commit/TransactionCommitService.cs
Line coverage
93%
Covered lines: 140
Uncovered lines: 9
Coverable lines: 149
Total lines: 269
Line coverage: 93.9%
Branch coverage
71%
Covered branches: 53
Total branches: 74
Branch coverage: 71.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
CommitAsync()75%3232100%
TryAcquire(...)100%66100%
Release(...)100%22100%
ExecutePipelineAsync()100%1010100%
BuildSuccess(...)60%202090.32%
ReportErrorToTelegram(...)33.33%17633.33%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Commit/TransactionCommitService.cs

#LineLine coverage
 1using Services.ApiServices.TelegramBotApi;
 2using System.Collections.Concurrent;
 3using System.Security.Claims;
 4using Microsoft.Extensions.DependencyInjection;
 5using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 6using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 7using OzonOrdersWeb.TransactionServices;
 8using OzonOrdersWeb.WebServices.Transactions.Grid.Contracts;
 9using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions;
 10using Services.CacheServices.Cache.UserCacheService;
 11
 12namespace OzonOrdersWeb.WebServices.Transactions.Grid.Commit;
 13
 14public interface ITransactionCommitService
 15{
 16    Task<TransactionCommitResult> CommitAsync(TransactionDefinition definition, TransactionCommitRequest request, Claims
 17}
 18
 19/// <summary>
 20/// Конвейер проведения грида: шаги по <c>Definition.Steps</c>, прогресс через SignalR, единая обработка ошибок.
 21/// Порядок и семантика шагов повторяют <c>BaseTransactionController.CreateTransactionPostBase</c>.
 22/// </summary>
 23public sealed class TransactionCommitService : ITransactionCommitService
 24{
 25    /// <summary>
 26    /// Заказы, которые проводятся прямо сейчас. Повторный запрос на те же заказы (перезагрузка страницы,
 27    /// двойной клик) не должен стартовать, пока первый не завершён: он повторил бы побочные эффекты
 28    /// у внешних поставщиков, например заказал бы позиции у Армтека второй раз.
 29    /// </summary>
 130    private static readonly ConcurrentDictionary<int, byte> InFlightOrderIds = new();
 31
 32    private readonly IServiceProvider _services;
 33    private readonly IEnumerable<ITransactionCommitStep> _steps;
 34    private readonly IUserCacheService _userCache;
 35    private readonly TransactionProgressReporter _progress;
 36    private readonly ILogger<TransactionCommitService> _logger;
 37
 2038    public TransactionCommitService(
 2039        IServiceProvider services,
 2040        IEnumerable<ITransactionCommitStep> steps,
 2041        IUserCacheService userCache,
 2042        TransactionProgressReporter progress,
 2043        ILogger<TransactionCommitService> logger)
 44    {
 2045        _services = services;
 2046        _steps = steps;
 2047        _userCache = userCache;
 2048        _progress = progress;
 2049        _logger = logger;
 2050    }
 51
 52    public async Task<TransactionCommitResult> CommitAsync(TransactionDefinition definition, TransactionCommitRequest re
 53    {
 2754        var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
 2755        var cached = await _userCache.GetCachedUserAsync(principal);
 2756        var user = new TransactionCommitUser(
 2757            userId,
 2758            principal.Identity?.Name ?? cached?.UserName ?? string.Empty,
 2759            principal.IsInRole("Admin"),
 2760            cached?.AccessProfileId);
 61
 2762        var processIn1C = definition.OneCMode switch
 2763        {
 164            OneCMode.Required => true,
 365            OneCMode.Optional => request.ProcessIn1C,
 2366            _ => false
 2767        };
 2768        if (request.BulkFromFile && definition.SupportsBulkFromFile)
 69        {
 170            processIn1C = false;
 71        }
 72
 2773        var ctx = new TransactionCommitContext
 2774        {
 2775            Definition = definition,
 2776            Request = request,
 2777            User = user,
 2778            Services = _services,
 2779            // Конвейер не отменяется по RequestAborted: после побочных эффектов у внешних поставщиков
 2780            // (создан заказ, записана 1С) обрыв на полпути оставил бы заказы без записи в Studio2,
 2781            // а повтор запроса оформил бы их второй раз. Клиент мог закрыть страницу — проведение дойдёт до конца.
 2782            CancellationToken = CancellationToken.None,
 2783            ProcessIn1C = processIn1C,
 2784            CommentFlow = TransactionCommentAiSchedulingHelper.BeginFlow(request.Comment, definition.Type, user.Name, pr
 2785        };
 86
 2787        var removed = request.RemovedIds.ToHashSet();
 14388        foreach (var edit in request.Orders.Where(e => e.Id > 0 && !removed.Contains(e.Id)))
 89        {
 2990            ctx.Edits[edit.Id] = edit;
 91        }
 92
 2793        if (ctx.Edits.Count == 0)
 94        {
 195            return TransactionCommitResult.Failed(TransactionErrorCode.NoOrdersSelected, "Нет заказов для проведения.");
 96        }
 97
 98        var byKey = _steps.ToDictionary(s => s.Key, StringComparer.Ordinal);
 7299        var missing = definition.Steps.Where(k => !byKey.ContainsKey(k)).ToList();
 26100        if (missing.Count > 0)
 101        {
 1102            _logger.LogError("TransactionsGrid: для {Slug} не зарегистрированы шаги {Steps}", definition.Slug, string.Jo
 1103            return TransactionCommitResult.Failed(TransactionErrorCode.Unknown, $"Не настроены шаги проведения: {string.
 104        }
 105
 25106        cancellationToken.ThrowIfCancellationRequested();
 24107        var orderIds = ctx.Edits.Keys.Order().ToList();
 24108        if (!TryAcquire(orderIds, out var busyIds))
 109        {
 2110            _logger.LogWarning("TransactionsGrid {Slug}: повторный запрос на заказы {Orders}, они уже проводятся", defin
 2111            return TransactionCommitResult.Failed(TransactionErrorCode.AlreadyInProgress,
 2112                $"Эти заказы уже проводятся другим запросом (№ {string.Join(", ", busyIds)}). " +
 2113                "Дождитесь завершения, обновите страницу и проверьте результат — повторное проведение не запущено.");
 114        }
 115
 116        try
 117        {
 22118            return await ExecutePipelineAsync(ctx, byKey);
 119        }
 120        finally
 121        {
 22122            Release(orderIds);
 123        }
 26124    }
 125
 126    private static bool TryAcquire(IReadOnlyCollection<int> orderIds, out List<int> busyIds)
 127    {
 24128        var acquired = new List<int>();
 24129        busyIds = new List<int>();
 102130        foreach (var id in orderIds)
 131        {
 52132            if (InFlightOrderIds.TryAdd(id, 0)) acquired.Add(id);
 2133            else busyIds.Add(id);
 134        }
 135
 46136        if (busyIds.Count == 0) return true;
 137
 138        // Берём всё или ничего: частично занятый набор не проводим.
 2139        Release(acquired);
 2140        return false;
 141    }
 142
 143    private static void Release(IEnumerable<int> orderIds)
 144    {
 123145        foreach (var id in orderIds) InFlightOrderIds.TryRemove(id, out _);
 24146    }
 147
 148    private async Task<TransactionCommitResult> ExecutePipelineAsync(TransactionCommitContext ctx, IReadOnlyDictionary<s
 149    {
 22150        var definition = ctx.Definition;
 22151        var request = ctx.Request;
 22152        var user = ctx.User;
 153
 22154        await _progress.StageAsync(request.ProgressKey, "received", "Данные приняты",
 22155            ctx.Edits.Count == 1 ? "1 заказ" : $"заказов: {ctx.Edits.Count}");
 156
 22157        string? currentStep = null;
 158        try
 159        {
 116160            foreach (var key in definition.Steps)
 161            {
 41162                if (ctx.IsFailed)
 163                {
 164                    break;
 165                }
 166
 34167                currentStep = key;
 34168                await byKey[key].ExecuteAsync(ctx);
 169            }
 19170        }
 1171        catch (TransactionException ex)
 172        {
 1173            _logger.LogWarning(ex, "TransactionsGrid {Slug}: шаг {Step} остановил проведение ({Code})", definition.Slug,
 1174            ctx.Fail(ex.Code, ex.Message);
 1175        }
 2176        catch (Exception ex)
 177        {
 2178            _logger.LogError(ex, "TransactionsGrid {Slug}: ошибка на шаге {Step}", definition.Slug, currentStep);
 2179            ReportErrorToTelegram(definition, currentStep, user, ex);
 2180            ctx.Fail(TransactionErrorCode.Unknown, ex.Message);
 2181        }
 182
 22183        if (ctx.IsFailed)
 184        {
 10185            var (code, message) = ctx.Failure!.Value;
 186            // Транзакция уже записана — ошибка позднего шага не отменяет проведение, а становится предупреждением.
 10187            if (ctx.TransactionId > 0)
 188            {
 1189                ctx.Warnings.Add(message);
 1190                await _progress.CompleteAsync(request.ProgressKey, true, "Проведено с предупреждениями");
 1191                return BuildSuccess(ctx);
 192            }
 193
 9194            await _progress.CompleteAsync(request.ProgressKey, false, message);
 9195            return new TransactionCommitResult
 9196            {
 9197                Success = false,
 9198                ErrorCode = code,
 9199                Errors = { message },
 9200                Warnings = ctx.Warnings.ToList(),
 9201                RowErrors = new Dictionary<int, string>(ctx.RowErrors),
 9202                SupplierOrderConflicts = ctx.SupplierOrderConflicts.ToList()
 9203            };
 204        }
 205
 12206        await _progress.CompleteAsync(request.ProgressKey, true);
 12207        return BuildSuccess(ctx);
 22208    }
 209
 210    private static TransactionCommitResult BuildSuccess(TransactionCommitContext ctx)
 211    {
 13212        var d = ctx.Definition;
 13213        var cancelCount = ctx.OrdersToTransaction.Count(o => o.AppStatus?.Name == "Отменен");
 13214        var statusName = string.Join(", ", ctx.OrdersToTransaction
 0215            .Select(o => o.AppStatus?.Name)
 0216            .Where(n => !string.IsNullOrWhiteSpace(n))
 13217            .Distinct());
 13218        if (string.IsNullOrWhiteSpace(statusName))
 219        {
 13220            statusName = ctx.TargetStatus?.Name ?? d.TargetStatusName ?? d.Title;
 221        }
 222
 13223        var extra = ctx.SummaryBlocks.Count > 0 ? string.Join("<br/>", ctx.SummaryBlocks) : null;
 13224        var html = TransactionConfig.BuildStandardSuccessMessage(
 13225            ctx.ChangedCount, cancelCount, ctx.TransactionDateTime, ctx.User.Name, ctx.UserComment,
 13226            ctx.OrdersToTransaction, statusName, extra);
 227
 13228        var report = ctx.Report;
 13229        report.AddRange(TransactionReportMapper.FromOrders(ctx.OrdersToTransaction, statusName));
 26230        foreach (var block in ctx.SummaryBlocks.Where(b => !ctx.StructuredBlocks.Contains(b)))
 231        {
 0232            report.AddRange(TransactionReportMapper.FromHtml(block, TransactionReportMapper.SectionOther));
 233        }
 234
 13235        report.AddRange(TransactionReportMapper.FromWarnings(ctx.Warnings));
 13236        var reportDto = report.Build(new TransactionReportSummary(
 13237            ctx.ChangedCount, cancelCount, statusName, ctx.TransactionDateTime, ctx.User.Name,
 13238            string.IsNullOrWhiteSpace(ctx.UserComment) ? null : ctx.UserComment));
 239
 13240        return new TransactionCommitResult
 13241        {
 13242            Success = true,
 13243            TransactionId = ctx.TransactionId,
 13244            ChangedCount = ctx.ChangedCount,
 13245            SummaryHtml = html,
 13246            Report = reportDto,
 13247            SupplierVerification = SupplierVerificationDto.From(ctx.SupplierVerification),
 13248            Warnings = ctx.Warnings.ToList(),
 13249            RowErrors = new Dictionary<int, string>(ctx.RowErrors)
 13250        };
 251    }
 252
 253    private void ReportErrorToTelegram(TransactionDefinition d, string? stage, TransactionCommitUser user, Exception ex)
 254    {
 255        try
 256        {
 2257            var notifier = _services.GetService<ITelegramNotifier>();
 4258            if (notifier is not { IsConfigured: true }) return;
 259
 0260            var message = TelegramMessageFactory.Exception("Транзакция (сетка): " + d.Slug, ex,
 0261                new Dictionary<string, string?> { ["Шаг"] = stage ?? "-", ["Пользователь"] = user.Name });
 0262            _ = notifier.SendAsync(message);
 0263        }
 0264        catch
 265        {
 266            // Telegram не должен ломать проведение.
 0267        }
 2268    }
 269}