< Summary

Information
Class: OzonOrdersWeb.WebServices.TransactionTransitionRuleService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/TransactionTransitionRuleService.cs
Line coverage
94%
Covered lines: 341
Uncovered lines: 21
Coverable lines: 362
Total lines: 460
Line coverage: 94.1%
Branch coverage
44%
Covered branches: 15
Total branches: 34
Branch coverage: 44.1%
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%
GetRulesAsync()50%22100%
<GetRulesAsync()30%1162037.93%
InvalidateCache()100%210%
ApplyDefinitionsAsync()66.66%121294.11%
BuildDefaultRules()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/TransactionTransitionRuleService.cs

#LineLine coverage
 1using Microsoft.Extensions.Caching.Memory;
 2using OzonDomains;
 3using OzonDomains.Models;
 4using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions;
 5using Services.DataServices;
 6
 7namespace OzonOrdersWeb.WebServices;
 8
 9public class TransactionTransitionRuleService
 10{
 11    private const string CacheKey = "TransactionTransitionRules";
 112    private static readonly TimeSpan CacheSlidingExpiration = TimeSpan.FromMinutes(2);
 13
 14    private readonly StatusTransitionDataServices _statusTransitionDataServices;
 15    private readonly IMemoryCache _cache;
 16    private readonly ITransactionDefinitionProvider? _definitions;
 17
 118    public TransactionTransitionRuleService(
 119        StatusTransitionDataServices statusTransitionDataServices,
 120        IMemoryCache cache,
 121        ITransactionDefinitionProvider? definitions = null)
 22    {
 123        _statusTransitionDataServices = statusTransitionDataServices;
 124        _cache = cache;
 125        _definitions = definitions;
 126    }
 27
 28    public async Task<IReadOnlyList<TransactionTransitionRuleVm>> GetRulesAsync()
 29    {
 130        return await _cache.GetOrCreateAsync(CacheKey, async entry =>
 131        {
 132            entry.SlidingExpiration = CacheSlidingExpiration;
 133            var defaults = BuildDefaultRules();
 134            await ApplyDefinitionsAsync(defaults);
 135            var transitions = await _statusTransitionDataServices.GetActive();
 136
 137            var byType = transitions
 038                .Where(t => t.FromStatus != null && t.ToStatus != null)
 039                .GroupBy(t => t.TransactionType)
 140                .ToDictionary(g => g.Key, g => g.ToList());
 141
 3242            foreach (var rule in defaults)
 143            {
 1544                if (!Enum.TryParse<TransactionType>(rule.TransactionType.ToString(), out var type))
 145                {
 146                    continue;
 147                }
 148
 1549                if (!byType.TryGetValue(type, out var typeTransitions) || typeTransitions.Count == 0)
 150                {
 151                    continue;
 152                }
 153
 054                var toStatuses = typeTransitions
 055                    .Select(t => t.ToStatus?.Name)
 056                    .Where(n => !string.IsNullOrWhiteSpace(n))
 057                    .Distinct(StringComparer.OrdinalIgnoreCase)
 058                    .ToList();
 159
 060                var fromStatuses = typeTransitions
 061                    .Select(t => t.FromStatus?.Name)
 062                    .Where(n => !string.IsNullOrWhiteSpace(n))
 063                    .Distinct(StringComparer.OrdinalIgnoreCase)
 064                    .ToList();
 165
 066                if (toStatuses.Count > 0)
 167                {
 068                    rule.ToStatus = toStatuses[0]!;
 169                }
 170
 071                if (fromStatuses.Count > 0)
 172                {
 073                    rule.AllowedStatuses = fromStatuses.Select(s => s!).ToList();
 174                }
 175
 076                rule.HasRepeatableSelfTransition = typeTransitions
 077                    .Any(t => t.FromStatusId == t.ToStatusId && t.IsRepeatable);
 178            }
 179
 180            return (IReadOnlyList<TransactionTransitionRuleVm>)defaults;
 281        }) ?? Array.Empty<TransactionTransitionRuleVm>();
 182    }
 83
 84    public void InvalidateCache()
 85    {
 086        _cache.Remove(CacheKey);
 087    }
 88
 89    /// <summary>
 90    /// Накладывает имена/цвета/иконки из админки типов транзакций и адрес нового грида.
 91    /// <see cref="TransactionTransitionRuleVm.Endpoint"/> не меняется — его читает классическая страница заказов;
 92    /// OrdersGrid выбирает <see cref="TransactionTransitionRuleVm.GridEndpoint"/> по флагу <see cref="TransactionTransi
 93    /// </summary>
 94    private async Task ApplyDefinitionsAsync(List<TransactionTransitionRuleVm> rules)
 95    {
 196        if (_definitions == null)
 97        {
 098            return;
 99        }
 100
 101        var definitions = (await _definitions.GetAllAsync()).ToDictionary(d => (int)d.Type);
 34102        foreach (var rule in rules)
 103        {
 16104            if (!definitions.TryGetValue(rule.TransactionType, out var d))
 105            {
 106                continue;
 107            }
 108
 16109            rule.Label = d.Title;
 16110            rule.ShortName = d.ShortName ?? d.Title;
 16111            rule.Icon = string.IsNullOrWhiteSpace(d.Icon) ? rule.Icon : d.Icon + " me-2";
 16112            rule.TransactionColor = string.IsNullOrWhiteSpace(d.Color) ? rule.TransactionColor : d.Color;
 16113            rule.Slug = d.Slug;
 16114            rule.GridEndpoint = d.GridEndpoint;
 16115            rule.PermissionEndpoint = d.LegacyEndpoint;
 16116            rule.UseGrid = d.UseGrid;
 16117            rule.IsEnabled = d.IsEnabled;
 16118            rule.Group = d.Group;
 16119            rule.DisplayOrder = d.DisplayOrder;
 16120            rule.Hint = d.Hint;
 121        }
 122
 123        rules.RemoveAll(r => !r.IsEnabled);
 1124    }
 125
 126    private static List<TransactionTransitionRuleVm> BuildDefaultRules()
 127    {
 1128        return new List<TransactionTransitionRuleVm>
 1129        {
 1130            new()
 1131            {
 1132                TransactionType = (int)TransactionType.OrderedToSupplier,
 1133                Endpoint = "/Studio2/OrderToSupplierTransaction/CreateOrderToSupplierTransactionV2",
 1134                Label = "Заказан поставщику",
 1135                Icon = "fas fa-shopping-cart me-2",
 1136                LinkClass = "btn btn-link link-success",
 1137                Action = "createOrderToSupplierTransaction()",
 1138                AllowedStatuses = new List<string> { "Не указан", "Заказан поставщику", "Заказан реализатору" },
 1139                ToStatus = "Заказан поставщику",
 1140                TransactionColor = "#198754",
 1141                IntegrationMode = "none",
 1142                UiConditions = new List<string>()
 1143            },
 1144            new()
 1145            {
 1146                TransactionType = (int)TransactionType.OrderedToSeller,
 1147                Endpoint = "/Studio2/OrderedToSellerTransaction/CreateOrderedToSellerTransaction",
 1148                Label = "Заказан реализатору",
 1149                Icon = "fas fa-handshake me-2",
 1150                LinkClass = "btn btn-link link-secondary",
 1151                Action = "createOrderedToSellerTransaction()",
 1152                AllowedStatuses = new List<string> { "Не указан", "Заказан поставщику", "Заказан реализатору" },
 1153                ToStatus = "Заказан реализатору",
 1154                TransactionColor = "#6c757d",
 1155                IntegrationMode = "none",
 1156                UiConditions = new List<string>()
 1157            },
 1158            new()
 1159            {
 1160                TransactionType = (int)TransactionType.ShippedBySupplier,
 1161                Endpoint = "/Studio2/ShippedBySupplierTransaction/CreateShippedBySupplierTransaction",
 1162                Label = "Отгружен поставщиком",
 1163                Icon = "fas fa-truck-loading me-2",
 1164                LinkClass = "btn btn-link link-info",
 1165                Action = "createShippedBySupplierTransaction()",
 1166                AllowedStatuses = new List<string> { "Заказан поставщику" },
 1167                ToStatus = "Отгружен поставщиком",
 1168                TransactionColor = "#0dcaf0",
 1169                IntegrationMode = "optional",
 1170                OneCDocumentType = "receipt",
 1171                UiConditions = new List<string>
 1172                {
 1173                    "Единый поставщик",
 1174                    "Единый эффективный склад",
 1175                    "Нужен ИНН поставщика (договор 1С)",
 1176                    "Для 1С: заполнить страну происхождения и ГТД"
 1177                }
 1178            },
 1179            new()
 1180            {
 1181                TransactionType = (int)TransactionType.ShippedToSeller,
 1182                Endpoint = "/Studio2/ShippedToSellerTransaction/CreateShippedToSellerTransaction",
 1183                Label = "Отгружен реализатором",
 1184                Icon = "fas fa-truck me-2",
 1185                LinkClass = "btn btn-link link-warning",
 1186                Action = "createShippedToSellerTransaction()",
 1187                AllowedStatuses = new List<string> { "Заказан реализатору" },
 1188                ToStatus = "Отгружен реализатором",
 1189                TransactionColor = "#ffc107",
 1190                IntegrationMode = "optional",
 1191                OneCDocumentType = "transfer",
 1192                UiConditions = new List<string>
 1193                {
 1194                    "Единый поставщик",
 1195                    "Единый эффективный склад",
 1196                    "Склад должен быть разрешен в маппинге"
 1197                }
 1198            },
 1199            new()
 1200            {
 1201                TransactionType = (int)TransactionType.AcceptedToWarehouse,
 1202                Endpoint = "/Studio2/AcceptedToWarehouseTransaction/AcceptedToWarehouseTransaction",
 1203                Label = "Принят на склад",
 1204                Icon = "fas fa-warehouse me-2",
 1205                LinkClass = "btn btn-link link-info",
 1206                Action = "createAcceptedToWarehouseTransaction()",
 1207                AllowedStatuses = new List<string> { "Заказан поставщику", "Не указан", "Отгружен поставщиком" },
 1208                ToStatus = "ПринятНаСкладе",
 1209                TransactionColor = "#0dcaf0",
 1210                IntegrationMode = "required",
 1211                OneCDocumentType = "transfer",
 1212                UiConditions = new List<string>
 1213                {
 1214                    "Обязательно указать склад отправителя и получателя",
 1215                    "Склады не должны совпадать"
 1216                }
 1217            },
 1218            new()
 1219            {
 1220                TransactionType = (int)TransactionType.ShippedToClient,
 1221                Endpoint = "/Studio2/ShippedToClientTransaction/CreateShippedToClientTransaction",
 1222                Label = "Отгружен клиенту",
 1223                Icon = "fas fa-shipping-fast me-2",
 1224                LinkClass = "btn btn-link link-success",
 1225                Action = "createShippedToClientTransaction()",
 1226                AllowedStatuses = new List<string> { "Заказан реализатору" },
 1227                ToStatus = "Отгружен клиенту",
 1228                TransactionColor = "#198754",
 1229                IntegrationMode = "optional",
 1230                OneCDocumentType = "transfer+expense",
 1231                UiConditions = new List<string>()
 1232            },
 1233            new()
 1234            {
 1235                TransactionType = (int)TransactionType.CustomerReturn,
 1236                Endpoint = "/Studio2/CustomerReturnTransaction/CreateCustomerReturnTransaction",
 1237                Label = "Возврат покупателя",
 1238                Icon = "fas fa-undo me-2",
 1239                LinkClass = "btn btn-link link-warning",
 1240                Action = "createCustomerReturnTransaction()",
 1241                AllowedStatuses = new List<string> { "Отгружен клиенту", "Отменен" },
 1242                ToStatus = "Возврат покупателя",
 1243                TransactionColor = "#ffc107",
 1244                IntegrationMode = "optional",
 1245                OneCDocumentType = "transfer",
 1246                UiConditions = new List<string>
 1247                {
 1248                    "Для 1С: выбрать/проверить склады отправителя и получателя",
 1249                    "Для 1С: склад получателя обязателен по каждому заказу",
 1250                    "Склады отправителя и получателя не должны совпадать"
 1251                }
 1252            },
 1253            new()
 1254            {
 1255                TransactionType = (int)TransactionType.ReturnToSupplier,
 1256                Endpoint = "/Studio2/ReturnToSupplierTransaction/CreateReturnToSupplierTransaction",
 1257                Label = "Возвращено поставщику",
 1258                Icon = "fas fa-reply-all me-2",
 1259                LinkClass = "btn btn-link link-warning",
 1260                Action = "createReturnToSupplierTransaction()",
 1261                AllowedStatuses = new List<string> { "Заказан поставщику", "ПринятНаСкладе", "Возврат поставщику", "Возв
 1262                ToStatus = "Возвращено поставщику",
 1263                TransactionColor = "#ffc107",
 1264                IntegrationMode = "optional",
 1265                OneCDocumentType = "transfer",
 1266                UiConditions = new List<string>
 1267                {
 1268                    "Для 1С: обязательны склад отправителя и получателя",
 1269                    "Склады не должны совпадать"
 1270                }
 1271            },
 1272            new()
 1273            {
 1274                TransactionType = (int)TransactionType.ReturnToWarehouse,
 1275                Endpoint = "/Studio2/ReturnToWarehouseTransaction/CreateReturnToWarehouseTransaction",
 1276                Label = "Возврат на склад",
 1277                Icon = "fas fa-undo-alt me-2",
 1278                LinkClass = "btn btn-link link-info",
 1279                Action = "createReturnToWarehouseTransaction()",
 1280                AllowedStatuses = new List<string> { "ПринятНаСкладе", "Отгружен поставщиком", "Отгружен реализатором" }
 1281                ToStatus = "Возвращен на склад",
 1282                TransactionColor = "#0dcaf0",
 1283                IntegrationMode = "required",
 1284                OneCDocumentType = "transfer",
 1285                UiConditions = new List<string>
 1286                {
 1287                    "Заказ должен быть ранее проведен в 1С (GUID в истории)",
 1288                    "Только заказы одного клиента"
 1289                }
 1290            },
 1291            new()
 1292            {
 1293                TransactionType = (int)TransactionType.WarehouseChange,
 1294                Endpoint = "/Studio2/WarehouseChangeTransaction/CreateWarehouseChangeTransaction",
 1295                Label = "Изменение склада",
 1296                Icon = "fas fa-edit me-2",
 1297                LinkClass = "btn btn-link link-primary",
 1298                Action = "createWarehouseChangeTransaction()",
 1299                AllowedStatuses = new List<string>(),
 1300                ToStatus = null,
 1301                TransactionColor = "#0d6efd",
 1302                IntegrationMode = "none",
 1303                UiConditions = new List<string>()
 1304            },
 1305            new()
 1306            {
 1307                TransactionType = (int)TransactionType.WarehouseTransfer,
 1308                Endpoint = "/Studio2/WarehouseTransferTransaction/CreateWarehouseTransferTransaction",
 1309                Label = "Перемещение между складами",
 1310                Icon = "fas fa-exchange-alt me-2",
 1311                LinkClass = "btn btn-link link-primary",
 1312                Action = "createWarehouseTransferTransaction()",
 1313                AllowedStatuses = new List<string>(),
 1314                DisallowMarketplaceStatuses = new List<string> { "Отменен покупателем" },
 1315                ToStatus = "Перемещен между складами",
 1316                TransactionColor = "#0d6efd",
 1317                IntegrationMode = "optional",
 1318                OneCDocumentType = "transfer",
 1319                UiConditions = new List<string>
 1320                {
 1321                    "Для 1С: обязательны склады отправителя и получателя",
 1322                    "Склады не должны совпадать"
 1323                }
 1324            },
 1325            new()
 1326            {
 1327                TransactionType = (int)TransactionType.ShippedToCommissioner,
 1328                Endpoint = "/Studio2/ShippedToCommissionerTransaction/CreateShippedToCommissionerTransaction",
 1329                Label = "Отгружен комиссионеру",
 1330                Icon = "fas fa-dolly me-2",
 1331                LinkClass = "btn btn-link link-primary",
 1332                Action = "createShippedToCommissionerTransaction()",
 1333                AllowedStatuses = new List<string>(),
 1334                DisallowMarketplaceStatuses = new List<string> { "Отменен покупателем" },
 1335                ToStatus = "Отгружен комиссионеру",
 1336                TransactionColor = "#0d6efd",
 1337                IntegrationMode = "optional",
 1338                OneCDocumentType = "transfer",
 1339                UiConditions = new List<string>
 1340                {
 1341                    "Начальный статус не ограничен, если переходы не заданы в StatusTransition",
 1342                    "Для 1С: нужен корректный клиент Ozon и эффективный склад",
 1343                    "Для 1С: у выбранных заказов должен быть единый поставщик и единый эффективный склад"
 1344                }
 1345            },
 1346            new()
 1347            {
 1348                TransactionType = (int)TransactionType.ToCancellation,
 1349                Endpoint = "/Studio2/ToCancellationTransaction/CreateToCancellationTransaction",
 1350                Label = "К отмене",
 1351                Icon = "fas fa-hourglass-half me-2",
 1352                LinkClass = "btn btn-link link-secondary",
 1353                Action = "createToCancellationTransaction()",
 1354                AllowedStatuses = new List<string> { "Заказан поставщику", "Заказан реализатору" },
 1355                ToStatus = "К отмене",
 1356                TransactionColor = "#6c757d",
 1357                IntegrationMode = "none",
 1358                UiConditions = new List<string>()
 1359            },
 1360            new()
 1361            {
 1362                TransactionType = (int)TransactionType.Cancelled,
 1363                Endpoint = "/Studio2/CancelledTransaction/CreateCancelledTransaction",
 1364                Label = "Отменен",
 1365                Icon = "fas fa-times-circle me-2",
 1366                LinkClass = "btn btn-link link-danger",
 1367                Action = "createCancelledTransaction()",
 1368                AllowedStatuses = new List<string> { "Заказан поставщику" },
 1369                ToStatus = "Отменен",
 1370                TransactionColor = "#dc3545",
 1371                IntegrationMode = "none",
 1372                UiConditions = new List<string>()
 1373            },
 1374            new()
 1375            {
 1376                TransactionType = (int)TransactionType.ReturnTransfer,
 1377                Endpoint = "/Studio2/ReturnTransferTransaction/CreateReturnTransferTransaction",
 1378                Label = "Перемещение",
 1379                Icon = "fas fa-forward me-2",
 1380                LinkClass = "btn btn-link link-primary",
 1381                Action = "createReturnTransferTransaction()",
 1382                AllowedStatuses = new List<string> { "Возврат покупателя" },
 1383                ToStatus = "Перемещение",
 1384                TransactionColor = "#0d6efd",
 1385                IntegrationMode = "none",
 1386                UiConditions = new List<string>()
 1387            },
 1388            new()
 1389            {
 1390                TransactionType = (int)TransactionType.LostByCommissioner,
 1391                Endpoint = "/Studio2/LostByCommissionerTransaction/CreateLostByCommissionerTransaction",
 1392                Label = "Утерян реализатором",
 1393                Icon = "fas fa-question-circle me-2",
 1394                LinkClass = "btn btn-link link-danger",
 1395                Action = "createLostByCommissionerTransaction()",
 1396                AllowedStatuses = new List<string>(),
 1397                ToStatus = "Утерян реализатором",
 1398                TransactionColor = "#dc3545",
 1399                IntegrationMode = "optional",
 1400                OneCDocumentType = "transfer",
 1401                UiConditions = new List<string>
 1402                {
 1403                    "Начальный статус не ограничен",
 1404                    "Для 1С: склад-получатель определяется автоматически как «{склад} (transit)»",
 1405                    "Для 1С: транзитный склад должен существовать в 1С"
 1406                }
 1407            }
 1408        };
 409    }
 410}
 411
 412public class TransactionTransitionRuleVm
 413{
 414    public int TransactionType { get; set; }
 415    public string Endpoint { get; set; } = string.Empty;
 416    public string Label { get; set; } = string.Empty;
 417    public string Icon { get; set; } = string.Empty;
 418    public string LinkClass { get; set; } = string.Empty;
 419    public string Action { get; set; } = string.Empty;
 420    public List<string> AllowedStatuses { get; set; } = new();
 421    public List<string> DisallowMarketplaceStatuses { get; set; } = new();
 422    public string? ToStatus { get; set; }
 423    public string TransactionColor { get; set; } = "#6c757d";
 424    /// <summary>
 425    /// Режим интеграции с 1С: none | optional | required
 426    /// </summary>
 427    public string IntegrationMode { get; set; } = "none";
 428    /// <summary>
 429    /// Тип документа 1С: "receipt" | "transfer" | "expense" | "transfer+expense" | ""
 430    /// </summary>
 431    public string OneCDocumentType { get; set; } = "";
 432    public List<string> UiConditions { get; set; } = new();
 433    /// <summary>
 434    /// Есть активный повторяемый самопереход (FromStatus == ToStatus, IsRepeatable) — например, перезаказ.
 435    /// </summary>
 436    public bool HasRepeatableSelfTransition { get; set; }
 437
 438    // ---- Грид транзакций (/Studio2/TransactionsGrid) и админка типов ----
 439
 440    /// <summary>Сегмент URL грида; пусто, если тип не описан в каталоге.</summary>
 441    public string? Slug { get; set; }
 442
 443    /// <summary>Адрес страницы грида; OrdersGrid идёт по нему, когда <see cref="UseGrid"/>.</summary>
 444    public string? GridEndpoint { get; set; }
 445
 446    /// <summary>Адрес для проверки прав на клиенте — всегда классический (права настроены на него).</summary>
 447    public string? PermissionEndpoint { get; set; }
 448
 449    public bool UseGrid { get; set; }
 450
 451    public bool IsEnabled { get; set; } = true;
 452
 453    public string? ShortName { get; set; }
 454
 455    public string? Group { get; set; }
 456
 457    public int DisplayOrder { get; set; }
 458
 459    public string? Hint { get; set; }
 460}