< Summary

Information
Class: OzonOrdersWeb.WebServices.OrderHistory.OrderHistoryLabelPrintCell
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/OrderHistory/OrderHistoryGridRows.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 359
Line coverage: 100%
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_FileName()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/OrderHistory/OrderHistoryGridRows.cs

#LineLine coverage
 1using OzonDomains.Models;
 2using OzonOrdersWeb.Areas.Studio2.ViewModels.OrderHistory;
 3using OzonOrdersWeb.TransactionServices;
 4using Services.OneCServices;
 5using Services.SupplierStatusServices;
 6
 7namespace OzonOrdersWeb.WebServices.OrderHistory;
 8
 9/// <summary>Смена статуса у API-поставщика в строке истории (цвета статусов — данные справочника).</summary>
 10public sealed record OrderHistorySupplierStatusCell(
 11    string ProviderName, string? LogoUrl, string? FromName, string? FromColor, string ToName, string? ToColor,
 12    string? Stage, string? Source, string? Comment);
 13
 14/// <summary>Размещение позиции в корзине API-поставщика в строке истории.</summary>
 15public sealed record OrderHistoryBasketCell(
 16    string Tone, string Icon, string Text, string? LogoUrl, string? SupplierOrderNumber, string? ApiItemId,
 17    string? Product, string? Article, string? Quantity, string? ShipmentNumber, string? ClientName,
 18    string? WarehouseName, string? Buyer, string? DeliveryAddress, string? Error, string? CartUrl);
 19
 20/// <summary>Системная проверка заказа у поставщика (повторное чтение API после проведения) в строке истории.</summary>
 21public sealed record OrderHistoryVerificationCell(
 22    string Tone, string Icon, string Title, string ProviderName, string? LogoUrl, string? Stage, string Summary,
 23    string? SupplierOrderNumber, string? SupplierStatus, IReadOnlyList<string> Checks, DateTime? VerifiedAt);
 24
 25/// <summary>Событие создания или выдачи PDF-этикетки в общей ленте заказа.</summary>
 526public sealed record OrderHistoryLabelPrintCell(string FileName, string Source);
 27
 28/// <summary>
 29/// Строка грида «История изменений»: одно изменённое поле. Строки одной операции (OperationId, минута, автор)
 30/// имеют общий <see cref="OpKey"/> — по нему грид объединяет ячейки операции (rowSpan).
 31/// </summary>
 32public sealed record OrderHistoryChangeRow(
 33    int Id,
 34    DateTime ChangedAt,
 35    string OpKey,
 36    int OpIndex,
 37    DateTime OpTime,
 38    string User,
 39    string? UserName,
 40    string? UserAvatar,
 41    string UserKind,
 42    string Field,
 43    string Kind,
 44    string? Old,
 45    string? New,
 46    OrderHistorySupplierStatusCell? SupplierStatus,
 47    OrderHistoryBasketCell? Basket,
 48    OrderHistoryVerificationCell? Verification,
 49    long? SnapshotId,
 50    int OneCCount,
 51    bool OneCOk,
 52    long PointTicks,
 53    string SearchText,
 54    OrderHistoryLabelPrintCell? LabelPrint = null);
 55
 56/// <summary>Операция истории: проведения 1С, показанные по кнопке в строке (разметка — партиал заголовка 1С).</summary>
 57public sealed record OrderHistoryOperation(int Index, string OpKey, IReadOnlyList<OrderHistoryOneCPostingViewModel> OneC
 58
 59public sealed record OrderHistoryChangeGrid(IReadOnlyList<OrderHistoryChangeRow> Rows, IReadOnlyList<OrderHistoryOperati
 60
 61/// <summary>
 62/// Строки вкладок истории заказа для гридов AG Grid. Раньше эти вычисления жили в GetOrderHistory.cshtml —
 63/// теперь по ним работают фильтры и сортировка грида.
 64/// </summary>
 65public static class OrderHistoryGridRows
 66{
 67    public const string UserKindUser = "user";
 68    public const string UserKindSystem = "system";
 69    public const string UserKindRollback = "rollback";
 70
 71    public static OrderHistoryChangeGrid BuildChanges(OrderHistoryViewModel model)
 72    {
 73        var rows = new List<OrderHistoryChangeRow>();
 74        var operations = new List<OrderHistoryOperation>();
 75        var all = model.Histories.Values.SelectMany(x => x).ToList();
 76
 77        var groups = all
 78            .GroupBy(h => new
 79            {
 80                OperationId = h.OperationId?.ToString(),
 81                RoundedTime = new DateTime(h.ChangedAt.Ticks - h.ChangedAt.Ticks % TimeSpan.TicksPerMinute, h.ChangedAt.
 82                h.ChangedBy
 83            })
 84            .OrderByDescending(g => g.Max(h => h.ChangedAt))
 85            .ToList();
 86
 87        foreach (var group in groups)
 88        {
 89            var changes = group.OrderBy(h => h.ColumnDisplayName ?? string.Empty).ToList();
 90            var opTime = group.Max(h => h.ChangedAt);
 91            var opIndex = operations.Count;
 92            var opKey = $"{group.Key.OperationId}|{group.Key.RoundedTime.Ticks}|{group.Key.ChangedBy}";
 93            var changedBy = group.Key.ChangedBy ?? string.Empty;
 94            var isRollback = changedBy.StartsWith("rollback:", StringComparison.OrdinalIgnoreCase);
 95            var displayUser = isRollback ? changedBy["rollback:".Length..] : changedBy;
 96            var isSystem = !isRollback && (string.IsNullOrWhiteSpace(changedBy)
 97                || changedBy.Contains("system", StringComparison.OrdinalIgnoreCase)
 98                || changedBy.Contains("auto", StringComparison.OrdinalIgnoreCase)
 99                || changedBy.Contains("robot", StringComparison.OrdinalIgnoreCase));
 100            model.UserProfiles.TryGetValue(displayUser, out var profile);
 101            var userName = profile?.DisplayName is { } name && !string.Equals(name, profile.LoginOrEmail, StringComparis
 102            var login = profile?.LoginOrEmail ?? displayUser;
 103
 104            var postings = OneCPostingsForGroup(model, changes, group.Key.ChangedBy, opTime);
 105            operations.Add(new OrderHistoryOperation(opIndex, opKey, postings));
 106            long? snapshotId = group.Key.OperationId is { } opId && model.CalculationSnapshotIdsByOperationId.TryGetValu
 107
 108            foreach (var change in changes)
 109            {
 110                var (kind, field, oldValue, newValue, status, basket, verification, filterText) = DescribeChange(model, 
 111                rows.Add(new OrderHistoryChangeRow(
 112                    change.Id,
 113                    change.ChangedAt,
 114                    opKey,
 115                    opIndex,
 116                    opTime,
 117                    string.IsNullOrWhiteSpace(login) ? "Неизвестно" : login,
 118                    userName,
 119                    profile?.AvatarUrl,
 120                    isRollback ? UserKindRollback : isSystem ? UserKindSystem : UserKindUser,
 121                    field,
 122                    kind,
 123                    oldValue,
 124                    newValue,
 125                    status,
 126                    basket,
 127                    verification,
 128                    snapshotId,
 129                    postings.Count,
 130                    postings.All(p => p.Success),
 131                    opTime.Ticks,
 132                    filterText));
 133            }
 134        }
 135
 136        foreach (var label in model.LabelPrints.OrderByDescending(x => x.OccurredAtUtc).ThenByDescending(x => x.Id))
 137        {
 138            var opIndex = operations.Count;
 139            var opKey = $"label|{label.Id}";
 140            var source = label.UsesFileDate
 141                ? "Дата файла"
 142                : label.Source == LabelPrintSource.PartnerApi ? "Partner API" : "Studio2";
 143            var actor = string.IsNullOrWhiteSpace(label.ActorName) ? "Неизвестно" : label.ActorName;
 144            var labelCell = new OrderHistoryLabelPrintCell(label.FileName, source);
 145            operations.Add(new OrderHistoryOperation(opIndex, opKey, []));
 146            rows.Add(new OrderHistoryChangeRow(
 147                -(opIndex + 1),
 148                label.OccurredAtUtc,
 149                opKey,
 150                opIndex,
 151                label.OccurredAtUtc,
 152                actor,
 153                null,
 154                null,
 155                UserKindUser,
 156                "Печать этикетки",
 157                "labelPrint",
 158                null,
 159                source,
 160                null,
 161                null,
 162                null,
 163                null,
 164                0,
 165                true,
 166                label.OccurredAtUtc.Ticks,
 167                string.Join(' ', "Печать этикетки", label.FileName, source, actor),
 168                labelCell));
 169        }
 170
 171        return new OrderHistoryChangeGrid(rows.OrderByDescending(x => x.OpTime).ThenBy(x => x.OpIndex).ToList(), operati
 172    }
 173
 174    private static (string Kind, string Field, string? Old, string? New, OrderHistorySupplierStatusCell? Status,
 175        OrderHistoryBasketCell? Basket, OrderHistoryVerificationCell? Verification, string SearchText) DescribeChange(Or
 176    {
 177        if (change.ColumnName == OrderSupplierStateService.HistoryColumnName)
 178        {
 179            model.SupplierStatusTimelineByHistoryId.TryGetValue(change.Id, out var timeline);
 180            var providerKey = timeline?.ProviderKey;
 181            var cell = new OrderHistorySupplierStatusCell(
 182                providerKey is null ? change.ColumnDisplayName ?? "Статус поставщика" : SupplierProviderName(providerKey
 183                providerKey is null ? null : SupplierProviderLogoUrl(providerKey),
 184                string.IsNullOrWhiteSpace(timeline?.FromStatusName ?? change.OldValue) ? null : timeline?.FromStatusName
 185                timeline?.FromStatusColor,
 186                timeline?.ToStatusName ?? change.NewValue ?? "Статус не указан",
 187                timeline?.ToStatusColor,
 188                timeline is null ? null : $"{SupplierStageName(timeline.FromStage)} → {SupplierStageName(timeline.ToStag
 189                timeline is null ? null : SupplierSourceName(timeline.Source),
 190                timeline?.Comment);
 191            var field = change.ColumnDisplayName ?? "Статус поставщика";
 192            return ("supplierStatus", field, cell.FromName, cell.ToName, cell, null, null,
 193                string.Join(' ', field, cell.ProviderName, cell.FromName, cell.ToName, cell.Comment));
 194        }
 195
 196        if (change.ColumnName == ApiSupplierBasketHistoryEntry.HistoryColumnName)
 197        {
 198            var field = change.ColumnDisplayName ?? "Корзина API-поставщика";
 199            var entry = ApiSupplierBasketHistoryEntry.TryParse(change.NewValue);
 200            if (entry is null) return ("basket", field, null, null, null, null, null, field);
 201            var (tone, icon, text) = DescribeBasketStatus(entry);
 202            var quantity = entry.Price.HasValue || entry.Quantity.HasValue
 203                ? (entry.Quantity?.ToString("0.##") ?? "1") + (entry.Price.HasValue ? $" × {entry.Price.Value:0.##} {ent
 204                : null;
 205            var product = string.IsNullOrWhiteSpace(entry.ProductName) && string.IsNullOrWhiteSpace(entry.Article)
 206                ? null
 207                : string.IsNullOrWhiteSpace(entry.ProductName) ? "Без названия" : entry.ProductName;
 208            var cell = new OrderHistoryBasketCell(tone, icon, text, NullIfEmpty(entry.LogoUrl), NullIfEmpty(entry.Suppli
 209                NullIfEmpty(entry.ApiItemId), product, NullIfEmpty(entry.Article), quantity, NullIfEmpty(entry.ShipmentN
 210                NullIfEmpty(entry.ClientName), NullIfEmpty(entry.WarehouseName), NullIfEmpty(entry.BuyerKunnr),
 211                NullIfEmpty(entry.DeliveryAddressName), NullIfEmpty(entry.ErrorMessage), NullIfEmpty(entry.SupplierCartU
 212            return ("basket", field, null, text, null, cell, null, entry.BuildFilterText());
 213        }
 214
 215        if (change.ColumnName == SupplierVerificationHistoryEntry.HistoryColumnName)
 216        {
 217            var field = change.ColumnDisplayName ?? "Проверка заказа у поставщика";
 218            var entry = SupplierVerificationHistoryEntry.TryParse(change.NewValue);
 219            if (entry is null) return ("verification", field, null, null, null, null, null, field);
 220            var (tone, icon) = DescribeVerificationStatus(entry.Status);
 221            var title = SupplierVerificationStatus.Title(entry.Status);
 222            var cell = new OrderHistoryVerificationCell(tone, icon, title, entry.SupplierDisplayName,
 223                SupplierProviderLogoUrl(entry.ProviderKey), SupplierStageName(entry.Stage), entry.Summary,
 224                NullIfEmpty(entry.SupplierOrderNumber), NullIfEmpty(entry.SupplierStatusName),
 225                entry.Checks ?? Array.Empty<string>(), entry.VerifiedAt);
 226            return ("verification", field, null, title, null, null, cell, entry.BuildFilterText());
 227        }
 228
 229        var display = change.ColumnDisplayName ?? "Не указано";
 230        var oldValue = string.IsNullOrEmpty(change.OldValue) ? null : change.FormattedOldValue;
 231        var newValue = string.IsNullOrEmpty(change.NewValue) ? null : change.FormattedNewValue;
 232        return ("change", display, oldValue, newValue, null, null, null, string.Join(' ', display, change.OldValue, chan
 233    }
 234
 235    /// <summary>Проведения 1С операции: по привязке к строкам истории, ключу пакета или времени (±10 мин, с поправкой н
 236    private static List<OrderHistoryOneCPostingViewModel> OneCPostingsForGroup(
 237        OrderHistoryViewModel model, IReadOnlyCollection<OzonDomains.Models.OrderHistory> changes, string? changedBy, Da
 238    {
 239        if (model.OneCPostings.Count == 0) return [];
 240        var historyIds = changes.Select(c => c.Id).ToHashSet();
 241        var batchKey = OneCPostingSummaryBuilder.BuildHistoryBatchKey(model.OrderId, opTime, changedBy);
 242        return model.OneCPostings
 243            .Where(p => p.LinkedHistoryIds.Any(historyIds.Contains)
 244                        || (!string.IsNullOrEmpty(p.HistoryBatchKey) && p.HistoryBatchKey == batchKey)
 245                        || MinTimeDiffMinutes(p.PostedAt, opTime) <= 10)
 246            .DistinctBy(p => p.Id)
 247            .OrderByDescending(p => p.PostedAt)
 248            .ToList();
 249    }
 250
 251    public static double MinTimeDiffMinutes(DateTime a, DateTime b)
 252    {
 253        var direct = Math.Abs((a - b).TotalMinutes);
 254        var plus3 = Math.Abs((a.AddHours(3) - b).TotalMinutes);
 255        var minus3 = Math.Abs((a.AddHours(-3) - b).TotalMinutes);
 256        return Math.Min(direct, Math.Min(plus3, minus3));
 257    }
 258
 259    private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;
 260
 261    private static string BasketSupplierName(ApiSupplierBasketHistoryEntry entry) => entry.ProviderKey switch
 262    {
 263        "MlAuto" => "ML-Auto BY",
 264        "MlAutoRu" => "ML-Auto RU",
 265        _ => entry.SupplierDisplayName
 266    };
 267
 268    public static (string Tone, string Icon, string Text) DescribeBasketStatus(ApiSupplierBasketHistoryEntry entry) => e
 269    {
 270        { CartStatus: "Ordered" } => ("success", "fa-circle-check", $"Заказ размещён у поставщика {BasketSupplierName(en
 271        { CartStatus: "Failed" } or { Success: false } => ("danger", "fa-circle-xmark", $"Ошибка добавления в корзину {B
 272        { CartStatus: "Pending" } => ("info", "fa-hourglass-half", $"В корзине {BasketSupplierName(entry)} — ждёт оформл
 273        _ => ("success", "fa-cart-shopping", $"Добавлено в корзину {BasketSupplierName(entry)}")
 274    };
 275
 276    public static (string Tone, string Icon) DescribeVerificationStatus(string status) => status switch
 277    {
 278        SupplierVerificationStatus.Confirmed => ("success", "fa-circle-check"),
 279        SupplierVerificationStatus.NotVerifiable => ("muted", "fa-circle-question"),
 280        SupplierVerificationStatus.ApiError => ("warning", "fa-plug-circle-exclamation"),
 281        _ => ("danger", "fa-circle-xmark")
 282    };
 283
 284    public static string SupplierProviderName(string providerKey) => providerKey switch
 285    {
 286        "Armtek" => "Армтек RU",
 287        "ArmtekBy" => "Армтек РБ",
 288        "Avd" => "АВД",
 289        "MlAuto" => "ML-Auto РБ",
 290        "MlAutoRu" => "ML-Auto RU",
 291        "MikadoMskHod20" => "Микадо МскХод20",
 292        "ZZapMoscow" => "ZZap Москва",
 293        "Japarts" => "Japarts",
 294        _ => providerKey
 295    };
 296
 297    public static string? SupplierProviderLogoUrl(string providerKey) => providerKey switch
 298    {
 299        "Armtek" or "ArmtekBy" => "/images/armtek-logo.svg",
 300        "Avd" => "/images/avd-logo.svg",
 301        "MlAuto" => "https://www.ml-auto.by/media/files/settings/inside-placeholder-logo-mobile.png",
 302        "MlAutoRu" => "https://ml-auto.ru/media/files/settings/inside-placeholder-logo-mobile.png",
 303        "MikadoMskHod20" => "https://mikado-parts.ru/img/MikadoLogo.svg",
 304        "ZZapMoscow" => "/images/zzap-logo.svg",
 305        "Japarts" => "/images/japarts-logo.jpg",
 306        _ => null
 307    };
 308
 309    public static string SupplierStageName(string? stage) => stage switch
 310    {
 311        "Cart" => "Корзина",
 312        "Ordered" => "Заказ оформлен",
 313        "Closed" => "Завершён",
 314        _ => "—"
 315    };
 316
 317    public static string SupplierSourceName(string source) => source switch
 318    {
 319        "Checkout" => "Действие пользователя",
 320        "Job" => "Синхронизация с поставщиком",
 321        "Manual" => "Изменено вручную",
 322        _ => source
 323    };
 324
 325    /// <summary>Статус позиции акта возврата: тон бейджа темы, иконка, подпись.</summary>
 326    public static (string Tone, string Icon, string Text) DescribeHandoverStatus(string? status) => status?.ToLowerInvar
 327    {
 328        "accepted" => ("success", "fa-circle-check", "Принято"),
 329        "rejected" => ("danger", "fa-circle-xmark", "Отказ"),
 330        "pending" => ("info", "fa-hourglass-half", "Ожидает"),
 331        _ => ("muted", "fa-question-circle", status ?? "—")
 332    };
 333
 334    /// <summary>Подробности позиции возврата (подпись → значение) — то, что раньше шло строкой под позицией.</summary>
 335    public static List<KeyValuePair<string, string>> HandoverDetails(OrderHistoryHandoverItemViewModel h)
 336    {
 337        var list = new List<KeyValuePair<string, string>>();
 338        void Add(string label, string? value)
 339        {
 340            if (!string.IsNullOrWhiteSpace(value)) list.Add(new(label, value));
 341        }
 342
 343        Add("Комментарий", h.Comment);
 344        Add("Статус Ozon", h.OzonStatusName);
 345        Add("ID возврата", h.ReturnId?.ToString());
 346        Add("Коробка", h.BoxBarcode);
 347        Add("Причина", h.Reason is null ? null : h.Reason + (h.ReasonIsDefect ? " (причина брака)" : string.Empty));
 348        Add("Комментарий клиента", h.ClientComment);
 349        Add("Причина отказа", h.RejectionReason);
 350        Add("Комментарий отказа", h.RejectionComment);
 351        Add("Трек", h.RuPostTracking);
 352        Add("Номер возврата", h.ReturnNumber);
 353        Add("Номер заказа Ozon", h.OrderNumber);
 354        Add("Способ возврата", h.ClientReturnMethod);
 355        Add("Склад Ozon", h.WarehouseId?.ToString());
 356        Add("Фото", h.PhotosError);
 357        return list;
 358    }
 359}

Methods/Properties

get_FileName()