< Summary

Information
Class: OzonOrdersWeb.WebServices.TransactionTransitionRuleVm
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/TransactionTransitionRuleService.cs
Line coverage
95%
Covered lines: 22
Uncovered lines: 1
Coverable lines: 23
Total lines: 460
Line coverage: 95.6%
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
get_TransactionType()100%11100%
get_Endpoint()100%11100%
get_Label()100%11100%
get_Icon()100%11100%
get_LinkClass()100%11100%
get_Action()100%11100%
get_AllowedStatuses()100%11100%
get_DisallowMarketplaceStatuses()100%11100%
get_ToStatus()100%11100%
get_TransactionColor()100%11100%
get_IntegrationMode()100%11100%
get_OneCDocumentType()100%11100%
get_UiConditions()100%11100%
get_HasRepeatableSelfTransition()100%210%
get_Slug()100%11100%
get_GridEndpoint()100%11100%
get_PermissionEndpoint()100%11100%
get_UseGrid()100%11100%
get_IsEnabled()100%11100%
get_ShortName()100%11100%
get_Group()100%11100%
get_DisplayOrder()100%11100%
get_Hint()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";
 12    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
 18    public TransactionTransitionRuleService(
 19        StatusTransitionDataServices statusTransitionDataServices,
 20        IMemoryCache cache,
 21        ITransactionDefinitionProvider? definitions = null)
 22    {
 23        _statusTransitionDataServices = statusTransitionDataServices;
 24        _cache = cache;
 25        _definitions = definitions;
 26    }
 27
 28    public async Task<IReadOnlyList<TransactionTransitionRuleVm>> GetRulesAsync()
 29    {
 30        return await _cache.GetOrCreateAsync(CacheKey, async entry =>
 31        {
 32            entry.SlidingExpiration = CacheSlidingExpiration;
 33            var defaults = BuildDefaultRules();
 34            await ApplyDefinitionsAsync(defaults);
 35            var transitions = await _statusTransitionDataServices.GetActive();
 36
 37            var byType = transitions
 38                .Where(t => t.FromStatus != null && t.ToStatus != null)
 39                .GroupBy(t => t.TransactionType)
 40                .ToDictionary(g => g.Key, g => g.ToList());
 41
 42            foreach (var rule in defaults)
 43            {
 44                if (!Enum.TryParse<TransactionType>(rule.TransactionType.ToString(), out var type))
 45                {
 46                    continue;
 47                }
 48
 49                if (!byType.TryGetValue(type, out var typeTransitions) || typeTransitions.Count == 0)
 50                {
 51                    continue;
 52                }
 53
 54                var toStatuses = typeTransitions
 55                    .Select(t => t.ToStatus?.Name)
 56                    .Where(n => !string.IsNullOrWhiteSpace(n))
 57                    .Distinct(StringComparer.OrdinalIgnoreCase)
 58                    .ToList();
 59
 60                var fromStatuses = typeTransitions
 61                    .Select(t => t.FromStatus?.Name)
 62                    .Where(n => !string.IsNullOrWhiteSpace(n))
 63                    .Distinct(StringComparer.OrdinalIgnoreCase)
 64                    .ToList();
 65
 66                if (toStatuses.Count > 0)
 67                {
 68                    rule.ToStatus = toStatuses[0]!;
 69                }
 70
 71                if (fromStatuses.Count > 0)
 72                {
 73                    rule.AllowedStatuses = fromStatuses.Select(s => s!).ToList();
 74                }
 75
 76                rule.HasRepeatableSelfTransition = typeTransitions
 77                    .Any(t => t.FromStatusId == t.ToStatusId && t.IsRepeatable);
 78            }
 79
 80            return (IReadOnlyList<TransactionTransitionRuleVm>)defaults;
 81        }) ?? Array.Empty<TransactionTransitionRuleVm>();
 82    }
 83
 84    public void InvalidateCache()
 85    {
 86        _cache.Remove(CacheKey);
 87    }
 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    {
 96        if (_definitions == null)
 97        {
 98            return;
 99        }
 100
 101        var definitions = (await _definitions.GetAllAsync()).ToDictionary(d => (int)d.Type);
 102        foreach (var rule in rules)
 103        {
 104            if (!definitions.TryGetValue(rule.TransactionType, out var d))
 105            {
 106                continue;
 107            }
 108
 109            rule.Label = d.Title;
 110            rule.ShortName = d.ShortName ?? d.Title;
 111            rule.Icon = string.IsNullOrWhiteSpace(d.Icon) ? rule.Icon : d.Icon + " me-2";
 112            rule.TransactionColor = string.IsNullOrWhiteSpace(d.Color) ? rule.TransactionColor : d.Color;
 113            rule.Slug = d.Slug;
 114            rule.GridEndpoint = d.GridEndpoint;
 115            rule.PermissionEndpoint = d.LegacyEndpoint;
 116            rule.UseGrid = d.UseGrid;
 117            rule.IsEnabled = d.IsEnabled;
 118            rule.Group = d.Group;
 119            rule.DisplayOrder = d.DisplayOrder;
 120            rule.Hint = d.Hint;
 121        }
 122
 123        rules.RemoveAll(r => !r.IsEnabled);
 124    }
 125
 126    private static List<TransactionTransitionRuleVm> BuildDefaultRules()
 127    {
 128        return new List<TransactionTransitionRuleVm>
 129        {
 130            new()
 131            {
 132                TransactionType = (int)TransactionType.OrderedToSupplier,
 133                Endpoint = "/Studio2/OrderToSupplierTransaction/CreateOrderToSupplierTransactionV2",
 134                Label = "Заказан поставщику",
 135                Icon = "fas fa-shopping-cart me-2",
 136                LinkClass = "btn btn-link link-success",
 137                Action = "createOrderToSupplierTransaction()",
 138                AllowedStatuses = new List<string> { "Не указан", "Заказан поставщику", "Заказан реализатору" },
 139                ToStatus = "Заказан поставщику",
 140                TransactionColor = "#198754",
 141                IntegrationMode = "none",
 142                UiConditions = new List<string>()
 143            },
 144            new()
 145            {
 146                TransactionType = (int)TransactionType.OrderedToSeller,
 147                Endpoint = "/Studio2/OrderedToSellerTransaction/CreateOrderedToSellerTransaction",
 148                Label = "Заказан реализатору",
 149                Icon = "fas fa-handshake me-2",
 150                LinkClass = "btn btn-link link-secondary",
 151                Action = "createOrderedToSellerTransaction()",
 152                AllowedStatuses = new List<string> { "Не указан", "Заказан поставщику", "Заказан реализатору" },
 153                ToStatus = "Заказан реализатору",
 154                TransactionColor = "#6c757d",
 155                IntegrationMode = "none",
 156                UiConditions = new List<string>()
 157            },
 158            new()
 159            {
 160                TransactionType = (int)TransactionType.ShippedBySupplier,
 161                Endpoint = "/Studio2/ShippedBySupplierTransaction/CreateShippedBySupplierTransaction",
 162                Label = "Отгружен поставщиком",
 163                Icon = "fas fa-truck-loading me-2",
 164                LinkClass = "btn btn-link link-info",
 165                Action = "createShippedBySupplierTransaction()",
 166                AllowedStatuses = new List<string> { "Заказан поставщику" },
 167                ToStatus = "Отгружен поставщиком",
 168                TransactionColor = "#0dcaf0",
 169                IntegrationMode = "optional",
 170                OneCDocumentType = "receipt",
 171                UiConditions = new List<string>
 172                {
 173                    "Единый поставщик",
 174                    "Единый эффективный склад",
 175                    "Нужен ИНН поставщика (договор 1С)",
 176                    "Для 1С: заполнить страну происхождения и ГТД"
 177                }
 178            },
 179            new()
 180            {
 181                TransactionType = (int)TransactionType.ShippedToSeller,
 182                Endpoint = "/Studio2/ShippedToSellerTransaction/CreateShippedToSellerTransaction",
 183                Label = "Отгружен реализатором",
 184                Icon = "fas fa-truck me-2",
 185                LinkClass = "btn btn-link link-warning",
 186                Action = "createShippedToSellerTransaction()",
 187                AllowedStatuses = new List<string> { "Заказан реализатору" },
 188                ToStatus = "Отгружен реализатором",
 189                TransactionColor = "#ffc107",
 190                IntegrationMode = "optional",
 191                OneCDocumentType = "transfer",
 192                UiConditions = new List<string>
 193                {
 194                    "Единый поставщик",
 195                    "Единый эффективный склад",
 196                    "Склад должен быть разрешен в маппинге"
 197                }
 198            },
 199            new()
 200            {
 201                TransactionType = (int)TransactionType.AcceptedToWarehouse,
 202                Endpoint = "/Studio2/AcceptedToWarehouseTransaction/AcceptedToWarehouseTransaction",
 203                Label = "Принят на склад",
 204                Icon = "fas fa-warehouse me-2",
 205                LinkClass = "btn btn-link link-info",
 206                Action = "createAcceptedToWarehouseTransaction()",
 207                AllowedStatuses = new List<string> { "Заказан поставщику", "Не указан", "Отгружен поставщиком" },
 208                ToStatus = "ПринятНаСкладе",
 209                TransactionColor = "#0dcaf0",
 210                IntegrationMode = "required",
 211                OneCDocumentType = "transfer",
 212                UiConditions = new List<string>
 213                {
 214                    "Обязательно указать склад отправителя и получателя",
 215                    "Склады не должны совпадать"
 216                }
 217            },
 218            new()
 219            {
 220                TransactionType = (int)TransactionType.ShippedToClient,
 221                Endpoint = "/Studio2/ShippedToClientTransaction/CreateShippedToClientTransaction",
 222                Label = "Отгружен клиенту",
 223                Icon = "fas fa-shipping-fast me-2",
 224                LinkClass = "btn btn-link link-success",
 225                Action = "createShippedToClientTransaction()",
 226                AllowedStatuses = new List<string> { "Заказан реализатору" },
 227                ToStatus = "Отгружен клиенту",
 228                TransactionColor = "#198754",
 229                IntegrationMode = "optional",
 230                OneCDocumentType = "transfer+expense",
 231                UiConditions = new List<string>()
 232            },
 233            new()
 234            {
 235                TransactionType = (int)TransactionType.CustomerReturn,
 236                Endpoint = "/Studio2/CustomerReturnTransaction/CreateCustomerReturnTransaction",
 237                Label = "Возврат покупателя",
 238                Icon = "fas fa-undo me-2",
 239                LinkClass = "btn btn-link link-warning",
 240                Action = "createCustomerReturnTransaction()",
 241                AllowedStatuses = new List<string> { "Отгружен клиенту", "Отменен" },
 242                ToStatus = "Возврат покупателя",
 243                TransactionColor = "#ffc107",
 244                IntegrationMode = "optional",
 245                OneCDocumentType = "transfer",
 246                UiConditions = new List<string>
 247                {
 248                    "Для 1С: выбрать/проверить склады отправителя и получателя",
 249                    "Для 1С: склад получателя обязателен по каждому заказу",
 250                    "Склады отправителя и получателя не должны совпадать"
 251                }
 252            },
 253            new()
 254            {
 255                TransactionType = (int)TransactionType.ReturnToSupplier,
 256                Endpoint = "/Studio2/ReturnToSupplierTransaction/CreateReturnToSupplierTransaction",
 257                Label = "Возвращено поставщику",
 258                Icon = "fas fa-reply-all me-2",
 259                LinkClass = "btn btn-link link-warning",
 260                Action = "createReturnToSupplierTransaction()",
 261                AllowedStatuses = new List<string> { "Заказан поставщику", "ПринятНаСкладе", "Возврат поставщику", "Возв
 262                ToStatus = "Возвращено поставщику",
 263                TransactionColor = "#ffc107",
 264                IntegrationMode = "optional",
 265                OneCDocumentType = "transfer",
 266                UiConditions = new List<string>
 267                {
 268                    "Для 1С: обязательны склад отправителя и получателя",
 269                    "Склады не должны совпадать"
 270                }
 271            },
 272            new()
 273            {
 274                TransactionType = (int)TransactionType.ReturnToWarehouse,
 275                Endpoint = "/Studio2/ReturnToWarehouseTransaction/CreateReturnToWarehouseTransaction",
 276                Label = "Возврат на склад",
 277                Icon = "fas fa-undo-alt me-2",
 278                LinkClass = "btn btn-link link-info",
 279                Action = "createReturnToWarehouseTransaction()",
 280                AllowedStatuses = new List<string> { "ПринятНаСкладе", "Отгружен поставщиком", "Отгружен реализатором" }
 281                ToStatus = "Возвращен на склад",
 282                TransactionColor = "#0dcaf0",
 283                IntegrationMode = "required",
 284                OneCDocumentType = "transfer",
 285                UiConditions = new List<string>
 286                {
 287                    "Заказ должен быть ранее проведен в 1С (GUID в истории)",
 288                    "Только заказы одного клиента"
 289                }
 290            },
 291            new()
 292            {
 293                TransactionType = (int)TransactionType.WarehouseChange,
 294                Endpoint = "/Studio2/WarehouseChangeTransaction/CreateWarehouseChangeTransaction",
 295                Label = "Изменение склада",
 296                Icon = "fas fa-edit me-2",
 297                LinkClass = "btn btn-link link-primary",
 298                Action = "createWarehouseChangeTransaction()",
 299                AllowedStatuses = new List<string>(),
 300                ToStatus = null,
 301                TransactionColor = "#0d6efd",
 302                IntegrationMode = "none",
 303                UiConditions = new List<string>()
 304            },
 305            new()
 306            {
 307                TransactionType = (int)TransactionType.WarehouseTransfer,
 308                Endpoint = "/Studio2/WarehouseTransferTransaction/CreateWarehouseTransferTransaction",
 309                Label = "Перемещение между складами",
 310                Icon = "fas fa-exchange-alt me-2",
 311                LinkClass = "btn btn-link link-primary",
 312                Action = "createWarehouseTransferTransaction()",
 313                AllowedStatuses = new List<string>(),
 314                DisallowMarketplaceStatuses = new List<string> { "Отменен покупателем" },
 315                ToStatus = "Перемещен между складами",
 316                TransactionColor = "#0d6efd",
 317                IntegrationMode = "optional",
 318                OneCDocumentType = "transfer",
 319                UiConditions = new List<string>
 320                {
 321                    "Для 1С: обязательны склады отправителя и получателя",
 322                    "Склады не должны совпадать"
 323                }
 324            },
 325            new()
 326            {
 327                TransactionType = (int)TransactionType.ShippedToCommissioner,
 328                Endpoint = "/Studio2/ShippedToCommissionerTransaction/CreateShippedToCommissionerTransaction",
 329                Label = "Отгружен комиссионеру",
 330                Icon = "fas fa-dolly me-2",
 331                LinkClass = "btn btn-link link-primary",
 332                Action = "createShippedToCommissionerTransaction()",
 333                AllowedStatuses = new List<string>(),
 334                DisallowMarketplaceStatuses = new List<string> { "Отменен покупателем" },
 335                ToStatus = "Отгружен комиссионеру",
 336                TransactionColor = "#0d6efd",
 337                IntegrationMode = "optional",
 338                OneCDocumentType = "transfer",
 339                UiConditions = new List<string>
 340                {
 341                    "Начальный статус не ограничен, если переходы не заданы в StatusTransition",
 342                    "Для 1С: нужен корректный клиент Ozon и эффективный склад",
 343                    "Для 1С: у выбранных заказов должен быть единый поставщик и единый эффективный склад"
 344                }
 345            },
 346            new()
 347            {
 348                TransactionType = (int)TransactionType.ToCancellation,
 349                Endpoint = "/Studio2/ToCancellationTransaction/CreateToCancellationTransaction",
 350                Label = "К отмене",
 351                Icon = "fas fa-hourglass-half me-2",
 352                LinkClass = "btn btn-link link-secondary",
 353                Action = "createToCancellationTransaction()",
 354                AllowedStatuses = new List<string> { "Заказан поставщику", "Заказан реализатору" },
 355                ToStatus = "К отмене",
 356                TransactionColor = "#6c757d",
 357                IntegrationMode = "none",
 358                UiConditions = new List<string>()
 359            },
 360            new()
 361            {
 362                TransactionType = (int)TransactionType.Cancelled,
 363                Endpoint = "/Studio2/CancelledTransaction/CreateCancelledTransaction",
 364                Label = "Отменен",
 365                Icon = "fas fa-times-circle me-2",
 366                LinkClass = "btn btn-link link-danger",
 367                Action = "createCancelledTransaction()",
 368                AllowedStatuses = new List<string> { "Заказан поставщику" },
 369                ToStatus = "Отменен",
 370                TransactionColor = "#dc3545",
 371                IntegrationMode = "none",
 372                UiConditions = new List<string>()
 373            },
 374            new()
 375            {
 376                TransactionType = (int)TransactionType.ReturnTransfer,
 377                Endpoint = "/Studio2/ReturnTransferTransaction/CreateReturnTransferTransaction",
 378                Label = "Перемещение",
 379                Icon = "fas fa-forward me-2",
 380                LinkClass = "btn btn-link link-primary",
 381                Action = "createReturnTransferTransaction()",
 382                AllowedStatuses = new List<string> { "Возврат покупателя" },
 383                ToStatus = "Перемещение",
 384                TransactionColor = "#0d6efd",
 385                IntegrationMode = "none",
 386                UiConditions = new List<string>()
 387            },
 388            new()
 389            {
 390                TransactionType = (int)TransactionType.LostByCommissioner,
 391                Endpoint = "/Studio2/LostByCommissionerTransaction/CreateLostByCommissionerTransaction",
 392                Label = "Утерян реализатором",
 393                Icon = "fas fa-question-circle me-2",
 394                LinkClass = "btn btn-link link-danger",
 395                Action = "createLostByCommissionerTransaction()",
 396                AllowedStatuses = new List<string>(),
 397                ToStatus = "Утерян реализатором",
 398                TransactionColor = "#dc3545",
 399                IntegrationMode = "optional",
 400                OneCDocumentType = "transfer",
 401                UiConditions = new List<string>
 402                {
 403                    "Начальный статус не ограничен",
 404                    "Для 1С: склад-получатель определяется автоматически как «{склад} (transit)»",
 405                    "Для 1С: транзитный склад должен существовать в 1С"
 406                }
 407            }
 408        };
 409    }
 410}
 411
 412public class TransactionTransitionRuleVm
 413{
 92414    public int TransactionType { get; set; }
 34415    public string Endpoint { get; set; } = string.Empty;
 49416    public string Label { get; set; } = string.Empty;
 48417    public string Icon { get; set; } = string.Empty;
 32418    public string LinkClass { get; set; } = string.Empty;
 32419    public string Action { get; set; } = string.Empty;
 32420    public List<string> AllowedStatuses { get; set; } = new();
 18421    public List<string> DisallowMarketplaceStatuses { get; set; } = new();
 16422    public string? ToStatus { get; set; }
 48423    public string TransactionColor { get; set; } = "#6c757d";
 424    /// <summary>
 425    /// Режим интеграции с 1С: none | optional | required
 426    /// </summary>
 32427    public string IntegrationMode { get; set; } = "none";
 428    /// <summary>
 429    /// Тип документа 1С: "receipt" | "transfer" | "expense" | "transfer+expense" | ""
 430    /// </summary>
 26431    public string OneCDocumentType { get; set; } = "";
 32432    public List<string> UiConditions { get; set; } = new();
 433    /// <summary>
 434    /// Есть активный повторяемый самопереход (FromStatus == ToStatus, IsRepeatable) — например, перезаказ.
 435    /// </summary>
 0436    public bool HasRepeatableSelfTransition { get; set; }
 437
 438    // ---- Грид транзакций (/Studio2/TransactionsGrid) и админка типов ----
 439
 440    /// <summary>Сегмент URL грида; пусто, если тип не описан в каталоге.</summary>
 16441    public string? Slug { get; set; }
 442
 443    /// <summary>Адрес страницы грида; OrdersGrid идёт по нему, когда <see cref="UseGrid"/>.</summary>
 17444    public string? GridEndpoint { get; set; }
 445
 446    /// <summary>Адрес для проверки прав на клиенте — всегда классический (права настроены на него).</summary>
 17447    public string? PermissionEndpoint { get; set; }
 448
 18449    public bool UseGrid { get; set; }
 450
 48451    public bool IsEnabled { get; set; } = true;
 452
 16453    public string? ShortName { get; set; }
 454
 16455    public string? Group { get; set; }
 456
 16457    public int DisplayOrder { get; set; }
 458
 16459    public string? Hint { get; set; }
 460}