| | | 1 | | using System.Text.RegularExpressions; |
| | | 2 | | using Microsoft.EntityFrameworkCore; |
| | | 3 | | using OzonDomains.Models; |
| | | 4 | | using OzonOrdersWeb.Areas.Studio2.Controllers.Base; |
| | | 5 | | using OzonOrdersWeb.Areas.Studio2.Models.Transactions; |
| | | 6 | | using Services.DataServices; |
| | | 7 | | |
| | | 8 | | namespace OzonOrdersWeb.WebServices.Transactions.Grid.Commit.Steps; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Проверка исходных статусов (StatusTransition или дефолт определения), статусов маркетплейса, |
| | | 12 | | /// единого поставщика/склада и наличия GUID 1С в истории — те же правила, что на GET классических страниц. |
| | | 13 | | /// Заказы в <c>StatusOnlyWhenAlreadyIn</c> помечаются как «только статус». |
| | | 14 | | /// </summary> |
| | | 15 | | public sealed class ValidateSourceStatusStep : ITransactionCommitStep |
| | | 16 | | { |
| | 0 | 17 | | private static readonly Regex OneCGuid = new( |
| | 0 | 18 | | "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", |
| | 0 | 19 | | RegexOptions.IgnoreCase | RegexOptions.Compiled); |
| | | 20 | | |
| | | 21 | | private readonly StatusTransitionDataServices _transitions; |
| | | 22 | | private readonly TransactionDataServices _transactions; |
| | | 23 | | private readonly AppStatusLookup _statuses; |
| | | 24 | | |
| | 0 | 25 | | public ValidateSourceStatusStep(StatusTransitionDataServices transitions, TransactionDataServices transactions, AppS |
| | | 26 | | { |
| | 0 | 27 | | _transitions = transitions; |
| | 0 | 28 | | _transactions = transactions; |
| | 0 | 29 | | _statuses = statuses; |
| | 0 | 30 | | } |
| | | 31 | | |
| | 0 | 32 | | public string Key => TransactionCommitStepKeys.ValidateSourceStatus; |
| | | 33 | | |
| | | 34 | | public async Task ExecuteAsync(TransactionCommitContext ctx) |
| | | 35 | | { |
| | 0 | 36 | | var d = ctx.Definition; |
| | 0 | 37 | | ctx.Transitions = d.UseStatusTransitions |
| | 0 | 38 | | ? await _transitions.GetByTransactionType(d.Type) |
| | 0 | 39 | | : new List<StatusTransition>(); |
| | | 40 | | |
| | | 41 | | // Список на странице строится из обеих сторон переходов, целевого и дополнительных статусов. |
| | | 42 | | // Проверка должна принимать ровно те же варианты, а не только исходную сторону перехода. |
| | 0 | 43 | | var selectableIds = ctx.Transitions.SelectMany(t => new[] { t.FromStatusId, t.ToStatusId }).ToHashSet(); |
| | 0 | 44 | | var selectableNames = d.AllowedSourceStatuses |
| | 0 | 45 | | .Concat(d.ExtraStatusNames) |
| | 0 | 46 | | .Append(d.TargetStatusName) |
| | 0 | 47 | | .Where(name => !string.IsNullOrWhiteSpace(name)) |
| | 0 | 48 | | .Distinct(StringComparer.OrdinalIgnoreCase); |
| | 0 | 49 | | foreach (var name in selectableNames) |
| | | 50 | | { |
| | 0 | 51 | | var status = await _statuses.FindAsync(name!); |
| | 0 | 52 | | if (status != null) |
| | | 53 | | { |
| | 0 | 54 | | selectableIds.Add(status.Id); |
| | | 55 | | } |
| | | 56 | | } |
| | 0 | 57 | | var statusOnly = d.StatusOnlyWhenAlreadyIn.ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | 0 | 58 | | var disallowMarket = d.DisallowMarketplaceStatuses.ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | | 59 | | |
| | 0 | 60 | | foreach (var order in ctx.Orders.ToList()) |
| | | 61 | | { |
| | 0 | 62 | | var statusName = order.AppStatus?.Name ?? string.Empty; |
| | 0 | 63 | | string? problem = null; |
| | | 64 | | |
| | 0 | 65 | | if (!string.IsNullOrEmpty(order.Status) && disallowMarket.Contains(order.Status)) |
| | | 66 | | { |
| | 0 | 67 | | problem = $"Статус маркетплейса «{order.Status}» — проведение недоступно."; |
| | | 68 | | } |
| | 0 | 69 | | else if (statusOnly.Contains(statusName)) |
| | | 70 | | { |
| | 0 | 71 | | ctx.StatusOnlyOrderIds.Add(order.Id); |
| | | 72 | | } |
| | 0 | 73 | | else if (selectableIds.Count > 0) |
| | | 74 | | { |
| | 0 | 75 | | if (!selectableIds.Contains(order.AppStatusId ?? 0)) |
| | | 76 | | { |
| | 0 | 77 | | var names = string.Join(", ", ctx.Transitions |
| | 0 | 78 | | .SelectMany(t => new[] { t.FromStatus?.Name, t.ToStatus?.Name }) |
| | 0 | 79 | | .Where(n => !string.IsNullOrEmpty(n)) |
| | 0 | 80 | | .Distinct()); |
| | 0 | 81 | | problem = $"Статус «{statusName}» не входит в допустимые: {names}."; |
| | | 82 | | } |
| | | 83 | | } |
| | | 84 | | |
| | 0 | 85 | | if (problem == null && d.RequirePriorOneCGuid && !await HasOneCGuidAsync(order.Id, ctx.CancellationToken)) |
| | | 86 | | { |
| | 0 | 87 | | problem = "У заказа нет GUID операции 1С в истории транзакций."; |
| | | 88 | | } |
| | | 89 | | |
| | 0 | 90 | | if (problem == null && !TransactionConfig.StandardOrderFilter(order) && d.TargetStatusName != null && !d.Pri |
| | | 91 | | { |
| | 0 | 92 | | problem = "Заказ отменён маркетплейсом."; |
| | | 93 | | } |
| | | 94 | | |
| | 0 | 95 | | if (problem != null) |
| | | 96 | | { |
| | 0 | 97 | | ctx.RowErrors[order.Id] = problem; |
| | | 98 | | } |
| | 0 | 99 | | } |
| | | 100 | | |
| | 0 | 101 | | if (ctx.RowErrors.Count > 0) |
| | | 102 | | { |
| | 0 | 103 | | var first = ctx.Orders.First(o => ctx.RowErrors.ContainsKey(o.Id)); |
| | 0 | 104 | | ctx.Fail(TransactionErrorCode.InvalidSourceStatus, $"Заказ {first.ShipmentNumber}: {ctx.RowErrors[first.Id]} |
| | 0 | 105 | | return; |
| | | 106 | | } |
| | | 107 | | |
| | 0 | 108 | | if (d.RequireSingleSupplierAndWarehouse) |
| | | 109 | | { |
| | 0 | 110 | | var inns = ctx.Orders.Select(o => o.Supplier?.INNCode?.Trim()).Where(i => !string.IsNullOrEmpty(i)).Distinct |
| | 0 | 111 | | if (inns.Count > 1) |
| | | 112 | | { |
| | 0 | 113 | | ctx.Fail(TransactionErrorCode.MixedSuppliers, "Все заказы должны быть от одного поставщика (по ИНН)."); |
| | 0 | 114 | | return; |
| | | 115 | | } |
| | | 116 | | |
| | 0 | 117 | | var warehouses = ctx.Orders.Select(o => TransactionWarehouseHelper.GetEffectiveWarehouseName(o)).Where(w => |
| | 0 | 118 | | if (warehouses.Count > 1) |
| | | 119 | | { |
| | 0 | 120 | | ctx.Fail(TransactionErrorCode.MixedWarehouses, $"Заказы с разных складов: {string.Join(", ", warehouses) |
| | | 121 | | } |
| | | 122 | | } |
| | 0 | 123 | | } |
| | | 124 | | |
| | | 125 | | private async Task<bool> HasOneCGuidAsync(int orderId, CancellationToken ct) |
| | | 126 | | { |
| | 0 | 127 | | var comments = await _transactions.GetTransactions() |
| | 0 | 128 | | .Where(t => t.Orders.Any(o => o.Id == orderId) && t.Comment != null && t.Comment != "") |
| | 0 | 129 | | .Select(t => t.Comment!) |
| | 0 | 130 | | .ToListAsync(ct); |
| | 0 | 131 | | return comments.Any(c => OneCGuid.IsMatch(c)); |
| | 0 | 132 | | } |
| | | 133 | | } |