< Summary

Information
Class: Services.DataServices.OrderRollback.OrderHistoryRollbackService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/DataServices/OrderRollback/OrderHistoryRollbackService.cs
Line coverage
62%
Covered lines: 173
Uncovered lines: 102
Coverable lines: 275
Total lines: 461
Line coverage: 62.9%
Branch coverage
28%
Covered branches: 94
Total branches: 334
Branch coverage: 28.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
BuildPreviewAsync()100%22100%
ApplyAsync()100%101092.3%
NeededLookups(...)100%22100%
LoadHistoryAsync()100%11100%
BuildChanges(...)100%1010100%
GetCurrent(...)42.85%782860%
TryResolveTarget(...)47.36%1693855.1%
ValuesEqual(...)50%141066.66%
ValuesEqual(...)83.33%6683.33%
ApplyValue(...)10.15%1062612813.79%
GetFkId(...)40%271044.44%
GetBool(...)25%5466.66%
GetDecimal(...)5.76%11195226.66%
GetDate(...)0%2040%
GetString(...)0%156120%
LoadLookupsAsync()66.66%151273.33%
BuildLookupAsync()100%11100%
get_Id()100%11100%
.ctor(...)100%11100%
get_NameToIds()100%11100%
get_IdToName()100%11100%
get_AppStatus()100%11100%
get_Manufacturer()100%11100%
get_Warehouse()100%11100%
get_Supplier()100%11100%
get_OzonClient()100%11100%
.cctor()100%11100%
For(...)16.66%12644.44%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/DataServices/OrderRollback/OrderHistoryRollbackService.cs

#LineLine coverage
 1using System.Globalization;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models;
 4using OzonRepositories.Context;
 5
 6namespace Services.DataServices.OrderRollback;
 7
 8/// <inheritdoc />
 9public sealed class OrderHistoryRollbackService : IOrderHistoryRollbackService
 10{
 11    private readonly ApplicationDbContext _context;
 12
 1513    public OrderHistoryRollbackService(ApplicationDbContext context)
 14    {
 1515        _context = context;
 1516    }
 17
 18    public async Task<OrderRollbackPreview> BuildPreviewAsync(int orderId, DateTime pointInTime, CancellationToken ct = 
 19    {
 720        var order = await _context.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == orderId, ct);
 721        if (order == null)
 122            return new OrderRollbackPreview { OrderId = orderId, PointInTime = pointInTime, OrderFound = false };
 23
 624        var history = await LoadHistoryAsync(orderId, ct);
 625        var snapshot = OrderHistorySnapshotReconstructor.Reconstruct(history, pointInTime);
 626        var lookups = await LoadLookupsAsync(NeededLookups(snapshot), ct);
 27
 628        var changes = BuildChanges(order, snapshot, lookups);
 29
 630        return new OrderRollbackPreview
 631        {
 632            OrderId = orderId,
 633            PointInTime = pointInTime,
 634            Changes = changes
 635        };
 736    }
 37
 38    public async Task<OrderRollbackResult> ApplyAsync(int orderId, DateTime pointInTime, string changedByMarker, Cancell
 39    {
 840        var order = await _context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, ct);
 841        if (order == null)
 142            return OrderRollbackResult.Fail($"Заказ {orderId} не найден.");
 43
 744        var history = await LoadHistoryAsync(orderId, ct);
 745        var snapshot = OrderHistorySnapshotReconstructor.Reconstruct(history, pointInTime);
 746        var lookups = await LoadLookupsAsync(NeededLookups(snapshot), ct);
 47
 748        var changes = BuildChanges(order, snapshot, lookups);
 1349        var applicable = changes.Where(c => c.Applicable).ToList();
 750        if (applicable.Count == 0)
 251            return new OrderRollbackResult { Success = true, AppliedCount = 0, Message = "Изменений для восстановления н
 52
 2053        foreach (var change in applicable)
 54        {
 555            var def = OrderRollbackFieldCatalog.Find(change.Column);
 556            if (def == null) continue;
 557            if (!TryResolveTarget(def, snapshot.GetValueOrDefault(def.Column), lookups, out var target, out _, out _, ou
 58                continue;
 559            ApplyValue(order, def, target);
 60        }
 61
 562        order.UpdatedBy = changedByMarker;
 63
 64        try
 65        {
 566            await _context.SaveChangesAsync(ct);
 567        }
 068        catch (DbUpdateException ex)
 69        {
 070            return OrderRollbackResult.Fail($"Не удалось сохранить восстановление: {ex.GetBaseException().Message}");
 71        }
 72
 573        return new OrderRollbackResult
 574        {
 575            Success = true,
 576            AppliedCount = applicable.Count,
 577            Message = $"Восстановлено полей: {applicable.Count}."
 578        };
 879    }
 80
 81    /// <summary>Какие FK-справочники реально нужны для данного снимка (чтобы не грузить лишние таблицы).</summary>
 82    private static IReadOnlySet<RollbackLookup> NeededLookups(IReadOnlyDictionary<string, string?> snapshot)
 1383        => OrderRollbackFieldCatalog.Fields
 35184            .Where(f => f.Kind == RollbackFieldKind.ForeignKey && snapshot.ContainsKey(f.Column))
 685            .Select(f => f.Lookup)
 1386            .ToHashSet();
 87
 88    private async Task<List<OrderHistory>> LoadHistoryAsync(int orderId, CancellationToken ct)
 1389        => await _context.OrderHistories.AsNoTracking()
 1390            .Where(h => h.OrderId == orderId)
 1391            .ToListAsync(ct);
 92
 93    // ── Построение списка изменений ────────────────────────────────────────────
 94
 95    private static List<OrderRollbackFieldChange> BuildChanges(
 96        Order order,
 97        IReadOnlyDictionary<string, string?> snapshot,
 98        LookupBundle lookups)
 99    {
 13100        var changes = new List<OrderRollbackFieldChange>();
 101
 728102        foreach (var def in OrderRollbackFieldCatalog.Fields)
 103        {
 104            // Поля без истории до точки T трогать нельзя — пропускаем.
 351105            if (!snapshot.TryGetValue(def.Column, out var snapValue))
 106                continue;
 107
 12108            var (currentValue, currentDisplay) = GetCurrent(order, def, lookups);
 109
 12110            if (!TryResolveTarget(def, snapValue, lookups, out var target, out var targetDisplay, out var applicable, ou
 111            {
 112                // Неприменимо (FK не разрешился / парс не удался) — но только если значение реально отличается.
 3113                if (!ValuesEqual(def, currentValue, snapValue, lookups))
 114                {
 3115                    changes.Add(new OrderRollbackFieldChange
 3116                    {
 3117                        Column = def.Column,
 3118                        DisplayName = def.Display,
 3119                        CurrentDisplay = currentDisplay,
 3120                        TargetDisplay = targetDisplay,
 3121                        Applicable = false,
 3122                        Warning = warning
 3123                    });
 124                }
 3125                continue;
 126            }
 127
 9128            if (ValuesEqual(def, currentValue, target))
 129                continue;
 130
 7131            changes.Add(new OrderRollbackFieldChange
 7132            {
 7133                Column = def.Column,
 7134                DisplayName = def.Display,
 7135                CurrentDisplay = currentDisplay,
 7136                TargetDisplay = targetDisplay,
 7137                Applicable = applicable
 7138            });
 139        }
 140
 13141        return changes;
 142    }
 143
 144    // ── Текущее значение поля ──────────────────────────────────────────────────
 145
 146    private static (object? value, string display) GetCurrent(Order order, RollbackFieldDef def, LookupBundle lookups)
 147    {
 12148        switch (def.Kind)
 149        {
 150            case RollbackFieldKind.ForeignKey:
 151            {
 6152                var id = GetFkId(order, def.Column);
 6153                var set = lookups.For(def.Lookup);
 6154                var name = id.HasValue && set.IdToName.TryGetValue(id.Value, out var n) ? n : null;
 6155                return (id, name ?? "—");
 156            }
 157            case RollbackFieldKind.Bool:
 158            {
 1159                var b = GetBool(order, def.Column);
 1160                return (b, b ? "Да" : "Нет");
 161            }
 162            case RollbackFieldKind.Int:
 163            {
 0164                var v = order.Quantity;
 0165                return (v, v?.ToString(CultureInfo.InvariantCulture) ?? "—");
 166            }
 167            case RollbackFieldKind.Decimal:
 168            {
 5169                var v = GetDecimal(order, def.Column);
 5170                return (v, v?.ToString("0.##", CultureInfo.InvariantCulture) ?? "—");
 171            }
 172            case RollbackFieldKind.DateTime:
 173            {
 0174                var v = GetDate(order, def.Column);
 0175                return (v, v?.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) ?? "—");
 176            }
 177            default:
 178            {
 0179                var v = GetString(order, def.Column);
 0180                return (v, string.IsNullOrEmpty(v) ? "—" : v);
 181            }
 182        }
 183    }
 184
 185    // ── Разбор целевого значения из снимка ─────────────────────────────────────
 186
 187    private static bool TryResolveTarget(
 188        RollbackFieldDef def,
 189        string? snapValue,
 190        LookupBundle lookups,
 191        out object? target,
 192        out string display,
 193        out bool applicable,
 194        out string? warning)
 195    {
 17196        target = null;
 17197        display = "—";
 17198        applicable = true;
 17199        warning = null;
 200
 17201        switch (def.Kind)
 202        {
 203            case RollbackFieldKind.ForeignKey:
 204            {
 12205                if (snapValue == null) { display = "—"; return true; }
 6206                display = snapValue;
 6207                var set = lookups.For(def.Lookup);
 6208                if (!set.NameToIds.TryGetValue(snapValue.Trim(), out var ids) || ids.Count == 0)
 209                {
 1210                    applicable = false;
 1211                    warning = $"Значение «{snapValue}» не найдено в справочнике — поле не будет восстановлено.";
 1212                    return false;
 213                }
 5214                if (ids.Count > 1)
 215                {
 2216                    applicable = false;
 2217                    warning = $"Значение «{snapValue}» неоднозначно ({ids.Count} совпадений) — поле не будет восстановле
 2218                    return false;
 219                }
 3220                target = ids[0];
 3221                return true;
 222            }
 223            case RollbackFieldKind.Bool:
 224            {
 2225                var b = snapValue is "1" or "true" or "True" or "TRUE";
 2226                target = b;
 2227                display = b ? "Да" : "Нет";
 2228                return true;
 229            }
 230            case RollbackFieldKind.Int:
 231            {
 0232                if (snapValue == null) { return true; }
 0233                if (int.TryParse(snapValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var i))
 234                {
 0235                    target = i;
 0236                    display = i.ToString(CultureInfo.InvariantCulture);
 0237                    return true;
 238                }
 0239                applicable = false;
 0240                warning = $"Не удалось разобрать число «{snapValue}».";
 0241                return false;
 242            }
 243            case RollbackFieldKind.Decimal:
 244            {
 7245                if (snapValue == null) { return true; }
 7246                if (decimal.TryParse(snapValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
 247                {
 7248                    target = d;
 7249                    display = d.ToString("0.##", CultureInfo.InvariantCulture);
 7250                    return true;
 251                }
 0252                applicable = false;
 0253                warning = $"Не удалось разобрать число «{snapValue}».";
 0254                return false;
 255            }
 256            case RollbackFieldKind.DateTime:
 257            {
 0258                if (snapValue == null) { return true; }
 0259                if (DateTime.TryParse(snapValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt))
 260                {
 0261                    target = dt;
 0262                    display = dt.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture);
 0263                    return true;
 264                }
 0265                applicable = false;
 0266                warning = $"Не удалось разобрать дату «{snapValue}».";
 0267                return false;
 268            }
 269            default:
 270            {
 0271                target = snapValue;
 0272                display = string.IsNullOrEmpty(snapValue) ? "—" : snapValue!;
 0273                return true;
 274            }
 275        }
 276    }
 277
 278    // ── Сравнение текущего и целевого значения ─────────────────────────────────
 279
 280    private static bool ValuesEqual(RollbackFieldDef def, object? current, object? target)
 281    {
 9282        return def.Kind switch
 9283        {
 3284            RollbackFieldKind.ForeignKey => (int?)current == (int?)target,
 1285            RollbackFieldKind.Bool => (bool)(current ?? false) == (bool)(target ?? false),
 0286            RollbackFieldKind.Int => (int?)current == (int?)target,
 5287            RollbackFieldKind.Decimal => (decimal?)current == (decimal?)target,
 0288            RollbackFieldKind.DateTime => Nullable.Equals((DateTime?)current, (DateTime?)target),
 0289            _ => string.Equals((string?)current, (string?)target, StringComparison.Ordinal)
 9290        };
 291    }
 292
 293    /// <summary>Сравнение, когда целевое значение не распарсилось — приводим текущее к строке снимка грубо.</summary>
 294    private static bool ValuesEqual(RollbackFieldDef def, object? current, string? snapValue, LookupBundle lookups)
 295    {
 3296        if (def.Kind == RollbackFieldKind.ForeignKey)
 297        {
 3298            var id = (int?)current;
 3299            var set = lookups.For(def.Lookup);
 3300            var currentName = id.HasValue && set.IdToName.TryGetValue(id.Value, out var n) ? n : null;
 3301            return string.Equals(currentName, snapValue, StringComparison.Ordinal);
 302        }
 0303        return false;
 304    }
 305
 306    // ── Применение целевого значения к заказу ──────────────────────────────────
 307
 308    private static void ApplyValue(Order order, RollbackFieldDef def, object? target)
 309    {
 5310        switch (def.Column)
 311        {
 0312            case "ShipmentNumber": order.ShipmentNumber = (string?)target; break;
 0313            case "ProductName": order.ProductName = (string?)target; break;
 0314            case "ProductKey": order.ProductKey = (string?)target; break;
 0315            case "Article": order.Article = (string?)target; break;
 0316            case "OrderNumberToSupplier": order.OrderNumberToSupplier = (string?)target; break;
 0317            case "DeliveryCity": order.DeliveryCity = (string?)target; break;
 318
 0319            case "Quantity": order.Quantity = (int?)target; break;
 320
 0321            case "ShipmentAmount": order.ShipmentAmount = (decimal?)target; break;
 4322            case "Price": order.Price = (decimal?)target; break;
 0323            case "PurchasePrice": order.PurchasePrice = (decimal?)target; break;
 0324            case "OriginalPurchasePrice": order.OriginalPurchasePrice = (decimal?)target; break;
 0325            case "MinOzonCommission": order.MinOzonCommission = (decimal?)target; break;
 0326            case "MaxOzonCommission": order.MaxOzonCommission = (decimal?)target; break;
 0327            case "MinProfit": order.MinProfit = (decimal?)target; break;
 0328            case "MaxProfit": order.MaxProfit = (decimal?)target; break;
 0329            case "MinDiscount": order.MinDiscount = (decimal?)target; break;
 0330            case "MaxDiscount": order.MaxDiscount = (decimal?)target; break;
 0331            case "CostPrice": order.CostPrice = (decimal?)target; break;
 332
 2333            case "IsVerified": order.IsVerified = (bool)(target ?? false); break;
 0334            case "IsAccepted": order.IsAccepted = (bool)(target ?? false); break;
 335
 0336            case "ProcessingDate": order.ProcessingDate = (DateTime?)target; break;
 0337            case "ShippingDate": order.ShippingDate = (DateTime?)target; break;
 338
 0339            case "AppStatusId": order.AppStatusId = (int?)target; break;
 0340            case "ManufacturerId": order.ManufacturerId = (int?)target; break;
 0341            case "ShipmentWarehouseId": order.ShipmentWarehouseId = (int?)target; break;
 4342            case "SupplierId": order.SupplierId = (int?)target; break;
 0343            case "OzonClientId": order.OzonClientId = (int?)target; break;
 344        }
 0345    }
 346
 347    // ── Доступ к свойствам по имени колонки ────────────────────────────────────
 348
 6349    private static int? GetFkId(Order order, string column) => column switch
 6350    {
 0351        "AppStatusId" => order.AppStatusId,
 0352        "ManufacturerId" => order.ManufacturerId,
 0353        "ShipmentWarehouseId" => order.ShipmentWarehouseId,
 6354        "SupplierId" => order.SupplierId,
 0355        "OzonClientId" => order.OzonClientId,
 0356        _ => null
 6357    };
 358
 1359    private static bool GetBool(Order order, string column) => column switch
 1360    {
 1361        "IsVerified" => order.IsVerified,
 0362        "IsAccepted" => order.IsAccepted,
 0363        _ => false
 1364    };
 365
 5366    private static decimal? GetDecimal(Order order, string column) => column switch
 5367    {
 0368        "ShipmentAmount" => order.ShipmentAmount,
 5369        "Price" => order.Price,
 0370        "PurchasePrice" => order.PurchasePrice,
 0371        "OriginalPurchasePrice" => order.OriginalPurchasePrice,
 0372        "MinOzonCommission" => order.MinOzonCommission,
 0373        "MaxOzonCommission" => order.MaxOzonCommission,
 0374        "MinProfit" => order.MinProfit,
 0375        "MaxProfit" => order.MaxProfit,
 0376        "MinDiscount" => order.MinDiscount,
 0377        "MaxDiscount" => order.MaxDiscount,
 0378        "CostPrice" => order.CostPrice,
 0379        _ => null
 5380    };
 381
 0382    private static DateTime? GetDate(Order order, string column) => column switch
 0383    {
 0384        "ProcessingDate" => order.ProcessingDate,
 0385        "ShippingDate" => order.ShippingDate,
 0386        _ => null
 0387    };
 388
 0389    private static string? GetString(Order order, string column) => column switch
 0390    {
 0391        "ShipmentNumber" => order.ShipmentNumber,
 0392        "ProductName" => order.ProductName,
 0393        "ProductKey" => order.ProductKey,
 0394        "Article" => order.Article,
 0395        "OrderNumberToSupplier" => order.OrderNumberToSupplier,
 0396        "DeliveryCity" => order.DeliveryCity,
 0397        _ => null
 0398    };
 399
 400    // ── Справочники FK (имя ⇄ Id) ──────────────────────────────────────────────
 401
 402    private async Task<LookupBundle> LoadLookupsAsync(IReadOnlySet<RollbackLookup> needed, CancellationToken ct)
 403    {
 13404        var bundle = new LookupBundle();
 13405        if (needed.Count == 0)
 7406            return bundle;
 407
 6408        if (needed.Contains(RollbackLookup.AppStatus))
 0409            bundle.AppStatus = await BuildLookupAsync(_context.AppStatuses.AsNoTracking().Select(x => new NameId(x.Id, x
 6410        if (needed.Contains(RollbackLookup.Manufacturer))
 0411            bundle.Manufacturer = await BuildLookupAsync(_context.Manufacturers.AsNoTracking().Select(x => new NameId(x.
 6412        if (needed.Contains(RollbackLookup.Warehouse))
 0413            bundle.Warehouse = await BuildLookupAsync(_context.Warehouses.AsNoTracking().Select(x => new NameId(x.Id, x.
 6414        if (needed.Contains(RollbackLookup.Supplier))
 6415            bundle.Supplier = await BuildLookupAsync(_context.Suppliers.AsNoTracking().Select(x => new NameId(x.Id, x.Na
 6416        if (needed.Contains(RollbackLookup.OzonClient))
 0417            bundle.OzonClient = await BuildLookupAsync(_context.OzonClients.AsNoTracking().Select(x => new NameId(x.Id, 
 418
 6419        return bundle;
 13420    }
 421
 422    private static async Task<LookupSet> BuildLookupAsync(IQueryable<NameId> query, CancellationToken ct)
 423    {
 6424        var rows = await query.ToListAsync(ct);
 6425        var nameToIds = rows
 11426            .Where(r => !string.IsNullOrWhiteSpace(r.Name))
 11427            .GroupBy(r => r.Name!.Trim(), StringComparer.Ordinal)
 35428            .ToDictionary(g => g.Key, g => g.Select(x => x.Id).Distinct().ToList(), StringComparer.Ordinal);
 6429        var idToName = rows
 11430            .GroupBy(r => r.Id)
 28431            .ToDictionary(g => g.Key, g => g.First().Name?.Trim() ?? string.Empty);
 6432        return new LookupSet(nameToIds, idToName);
 6433    }
 434
 55435    private readonly record struct NameId(int Id, string? Name);
 436
 7437    private sealed record LookupSet(
 6438        Dictionary<string, List<int>> NameToIds,
 16439        Dictionary<int, string> IdToName);
 440
 441    private sealed class LookupBundle
 442    {
 13443        public LookupSet AppStatus { get; set; } = Empty;
 13444        public LookupSet Manufacturer { get; set; } = Empty;
 13445        public LookupSet Warehouse { get; set; } = Empty;
 34446        public LookupSet Supplier { get; set; } = Empty;
 13447        public LookupSet OzonClient { get; set; } = Empty;
 448
 1449        private static readonly LookupSet Empty = new(new(StringComparer.Ordinal), new());
 450
 15451        public LookupSet For(RollbackLookup lookup) => lookup switch
 15452        {
 0453            RollbackLookup.AppStatus => AppStatus,
 0454            RollbackLookup.Manufacturer => Manufacturer,
 0455            RollbackLookup.Warehouse => Warehouse,
 15456            RollbackLookup.Supplier => Supplier,
 0457            RollbackLookup.OzonClient => OzonClient,
 0458            _ => Empty
 15459        };
 460    }
 461}

Methods/Properties

.ctor(OzonRepositories.Context.ApplicationDbContext)
BuildPreviewAsync()
ApplyAsync()
NeededLookups(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
LoadHistoryAsync()
BuildChanges(OzonDomains.Models.Order,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>,Services.DataServices.OrderRollback.OrderHistoryRollbackService/LookupBundle)
GetCurrent(OzonDomains.Models.Order,Services.DataServices.OrderRollback.RollbackFieldDef,Services.DataServices.OrderRollback.OrderHistoryRollbackService/LookupBundle)
TryResolveTarget(Services.DataServices.OrderRollback.RollbackFieldDef,System.String,Services.DataServices.OrderRollback.OrderHistoryRollbackService/LookupBundle,System.Object&,System.String&,System.Boolean&,System.String&)
ValuesEqual(Services.DataServices.OrderRollback.RollbackFieldDef,System.Object,System.Object)
ValuesEqual(Services.DataServices.OrderRollback.RollbackFieldDef,System.Object,System.String,Services.DataServices.OrderRollback.OrderHistoryRollbackService/LookupBundle)
ApplyValue(OzonDomains.Models.Order,Services.DataServices.OrderRollback.RollbackFieldDef,System.Object)
GetFkId(OzonDomains.Models.Order,System.String)
GetBool(OzonDomains.Models.Order,System.String)
GetDecimal(OzonDomains.Models.Order,System.String)
GetDate(OzonDomains.Models.Order,System.String)
GetString(OzonDomains.Models.Order,System.String)
LoadLookupsAsync()
BuildLookupAsync()
get_Id()
.ctor(System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<System.Int32>>,System.Collections.Generic.Dictionary`2<System.Int32,System.String>)
get_NameToIds()
get_IdToName()
get_AppStatus()
get_Manufacturer()
get_Warehouse()
get_Supplier()
get_OzonClient()
.cctor()
For(Services.DataServices.OrderRollback.RollbackLookup)