| | | 1 | | using System.Globalization; |
| | | 2 | | using Microsoft.EntityFrameworkCore; |
| | | 3 | | using OzonDomains.Models; |
| | | 4 | | using OzonRepositories.Context; |
| | | 5 | | |
| | | 6 | | namespace Services.DataServices.OrderRollback; |
| | | 7 | | |
| | | 8 | | /// <inheritdoc /> |
| | | 9 | | public sealed class OrderHistoryRollbackService : IOrderHistoryRollbackService |
| | | 10 | | { |
| | | 11 | | private readonly ApplicationDbContext _context; |
| | | 12 | | |
| | 15 | 13 | | public OrderHistoryRollbackService(ApplicationDbContext context) |
| | | 14 | | { |
| | 15 | 15 | | _context = context; |
| | 15 | 16 | | } |
| | | 17 | | |
| | | 18 | | public async Task<OrderRollbackPreview> BuildPreviewAsync(int orderId, DateTime pointInTime, CancellationToken ct = |
| | | 19 | | { |
| | 7 | 20 | | var order = await _context.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == orderId, ct); |
| | 7 | 21 | | if (order == null) |
| | 1 | 22 | | return new OrderRollbackPreview { OrderId = orderId, PointInTime = pointInTime, OrderFound = false }; |
| | | 23 | | |
| | 6 | 24 | | var history = await LoadHistoryAsync(orderId, ct); |
| | 6 | 25 | | var snapshot = OrderHistorySnapshotReconstructor.Reconstruct(history, pointInTime); |
| | 6 | 26 | | var lookups = await LoadLookupsAsync(NeededLookups(snapshot), ct); |
| | | 27 | | |
| | 6 | 28 | | var changes = BuildChanges(order, snapshot, lookups); |
| | | 29 | | |
| | 6 | 30 | | return new OrderRollbackPreview |
| | 6 | 31 | | { |
| | 6 | 32 | | OrderId = orderId, |
| | 6 | 33 | | PointInTime = pointInTime, |
| | 6 | 34 | | Changes = changes |
| | 6 | 35 | | }; |
| | 7 | 36 | | } |
| | | 37 | | |
| | | 38 | | public async Task<OrderRollbackResult> ApplyAsync(int orderId, DateTime pointInTime, string changedByMarker, Cancell |
| | | 39 | | { |
| | 8 | 40 | | var order = await _context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, ct); |
| | 8 | 41 | | if (order == null) |
| | 1 | 42 | | return OrderRollbackResult.Fail($"Заказ {orderId} не найден."); |
| | | 43 | | |
| | 7 | 44 | | var history = await LoadHistoryAsync(orderId, ct); |
| | 7 | 45 | | var snapshot = OrderHistorySnapshotReconstructor.Reconstruct(history, pointInTime); |
| | 7 | 46 | | var lookups = await LoadLookupsAsync(NeededLookups(snapshot), ct); |
| | | 47 | | |
| | 7 | 48 | | var changes = BuildChanges(order, snapshot, lookups); |
| | 13 | 49 | | var applicable = changes.Where(c => c.Applicable).ToList(); |
| | 7 | 50 | | if (applicable.Count == 0) |
| | 2 | 51 | | return new OrderRollbackResult { Success = true, AppliedCount = 0, Message = "Изменений для восстановления н |
| | | 52 | | |
| | 20 | 53 | | foreach (var change in applicable) |
| | | 54 | | { |
| | 5 | 55 | | var def = OrderRollbackFieldCatalog.Find(change.Column); |
| | 5 | 56 | | if (def == null) continue; |
| | 5 | 57 | | if (!TryResolveTarget(def, snapshot.GetValueOrDefault(def.Column), lookups, out var target, out _, out _, ou |
| | | 58 | | continue; |
| | 5 | 59 | | ApplyValue(order, def, target); |
| | | 60 | | } |
| | | 61 | | |
| | 5 | 62 | | order.UpdatedBy = changedByMarker; |
| | | 63 | | |
| | | 64 | | try |
| | | 65 | | { |
| | 5 | 66 | | await _context.SaveChangesAsync(ct); |
| | 5 | 67 | | } |
| | 0 | 68 | | catch (DbUpdateException ex) |
| | | 69 | | { |
| | 0 | 70 | | return OrderRollbackResult.Fail($"Не удалось сохранить восстановление: {ex.GetBaseException().Message}"); |
| | | 71 | | } |
| | | 72 | | |
| | 5 | 73 | | return new OrderRollbackResult |
| | 5 | 74 | | { |
| | 5 | 75 | | Success = true, |
| | 5 | 76 | | AppliedCount = applicable.Count, |
| | 5 | 77 | | Message = $"Восстановлено полей: {applicable.Count}." |
| | 5 | 78 | | }; |
| | 8 | 79 | | } |
| | | 80 | | |
| | | 81 | | /// <summary>Какие FK-справочники реально нужны для данного снимка (чтобы не грузить лишние таблицы).</summary> |
| | | 82 | | private static IReadOnlySet<RollbackLookup> NeededLookups(IReadOnlyDictionary<string, string?> snapshot) |
| | 13 | 83 | | => OrderRollbackFieldCatalog.Fields |
| | 351 | 84 | | .Where(f => f.Kind == RollbackFieldKind.ForeignKey && snapshot.ContainsKey(f.Column)) |
| | 6 | 85 | | .Select(f => f.Lookup) |
| | 13 | 86 | | .ToHashSet(); |
| | | 87 | | |
| | | 88 | | private async Task<List<OrderHistory>> LoadHistoryAsync(int orderId, CancellationToken ct) |
| | 13 | 89 | | => await _context.OrderHistories.AsNoTracking() |
| | 13 | 90 | | .Where(h => h.OrderId == orderId) |
| | 13 | 91 | | .ToListAsync(ct); |
| | | 92 | | |
| | | 93 | | // ── Построение списка изменений ──────────────────────────────────────────── |
| | | 94 | | |
| | | 95 | | private static List<OrderRollbackFieldChange> BuildChanges( |
| | | 96 | | Order order, |
| | | 97 | | IReadOnlyDictionary<string, string?> snapshot, |
| | | 98 | | LookupBundle lookups) |
| | | 99 | | { |
| | 13 | 100 | | var changes = new List<OrderRollbackFieldChange>(); |
| | | 101 | | |
| | 728 | 102 | | foreach (var def in OrderRollbackFieldCatalog.Fields) |
| | | 103 | | { |
| | | 104 | | // Поля без истории до точки T трогать нельзя — пропускаем. |
| | 351 | 105 | | if (!snapshot.TryGetValue(def.Column, out var snapValue)) |
| | | 106 | | continue; |
| | | 107 | | |
| | 12 | 108 | | var (currentValue, currentDisplay) = GetCurrent(order, def, lookups); |
| | | 109 | | |
| | 12 | 110 | | if (!TryResolveTarget(def, snapValue, lookups, out var target, out var targetDisplay, out var applicable, ou |
| | | 111 | | { |
| | | 112 | | // Неприменимо (FK не разрешился / парс не удался) — но только если значение реально отличается. |
| | 3 | 113 | | if (!ValuesEqual(def, currentValue, snapValue, lookups)) |
| | | 114 | | { |
| | 3 | 115 | | changes.Add(new OrderRollbackFieldChange |
| | 3 | 116 | | { |
| | 3 | 117 | | Column = def.Column, |
| | 3 | 118 | | DisplayName = def.Display, |
| | 3 | 119 | | CurrentDisplay = currentDisplay, |
| | 3 | 120 | | TargetDisplay = targetDisplay, |
| | 3 | 121 | | Applicable = false, |
| | 3 | 122 | | Warning = warning |
| | 3 | 123 | | }); |
| | | 124 | | } |
| | 3 | 125 | | continue; |
| | | 126 | | } |
| | | 127 | | |
| | 9 | 128 | | if (ValuesEqual(def, currentValue, target)) |
| | | 129 | | continue; |
| | | 130 | | |
| | 7 | 131 | | changes.Add(new OrderRollbackFieldChange |
| | 7 | 132 | | { |
| | 7 | 133 | | Column = def.Column, |
| | 7 | 134 | | DisplayName = def.Display, |
| | 7 | 135 | | CurrentDisplay = currentDisplay, |
| | 7 | 136 | | TargetDisplay = targetDisplay, |
| | 7 | 137 | | Applicable = applicable |
| | 7 | 138 | | }); |
| | | 139 | | } |
| | | 140 | | |
| | 13 | 141 | | return changes; |
| | | 142 | | } |
| | | 143 | | |
| | | 144 | | // ── Текущее значение поля ────────────────────────────────────────────────── |
| | | 145 | | |
| | | 146 | | private static (object? value, string display) GetCurrent(Order order, RollbackFieldDef def, LookupBundle lookups) |
| | | 147 | | { |
| | 12 | 148 | | switch (def.Kind) |
| | | 149 | | { |
| | | 150 | | case RollbackFieldKind.ForeignKey: |
| | | 151 | | { |
| | 6 | 152 | | var id = GetFkId(order, def.Column); |
| | 6 | 153 | | var set = lookups.For(def.Lookup); |
| | 6 | 154 | | var name = id.HasValue && set.IdToName.TryGetValue(id.Value, out var n) ? n : null; |
| | 6 | 155 | | return (id, name ?? "—"); |
| | | 156 | | } |
| | | 157 | | case RollbackFieldKind.Bool: |
| | | 158 | | { |
| | 1 | 159 | | var b = GetBool(order, def.Column); |
| | 1 | 160 | | return (b, b ? "Да" : "Нет"); |
| | | 161 | | } |
| | | 162 | | case RollbackFieldKind.Int: |
| | | 163 | | { |
| | 0 | 164 | | var v = order.Quantity; |
| | 0 | 165 | | return (v, v?.ToString(CultureInfo.InvariantCulture) ?? "—"); |
| | | 166 | | } |
| | | 167 | | case RollbackFieldKind.Decimal: |
| | | 168 | | { |
| | 5 | 169 | | var v = GetDecimal(order, def.Column); |
| | 5 | 170 | | return (v, v?.ToString("0.##", CultureInfo.InvariantCulture) ?? "—"); |
| | | 171 | | } |
| | | 172 | | case RollbackFieldKind.DateTime: |
| | | 173 | | { |
| | 0 | 174 | | var v = GetDate(order, def.Column); |
| | 0 | 175 | | return (v, v?.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) ?? "—"); |
| | | 176 | | } |
| | | 177 | | default: |
| | | 178 | | { |
| | 0 | 179 | | var v = GetString(order, def.Column); |
| | 0 | 180 | | 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 | | { |
| | 17 | 196 | | target = null; |
| | 17 | 197 | | display = "—"; |
| | 17 | 198 | | applicable = true; |
| | 17 | 199 | | warning = null; |
| | | 200 | | |
| | 17 | 201 | | switch (def.Kind) |
| | | 202 | | { |
| | | 203 | | case RollbackFieldKind.ForeignKey: |
| | | 204 | | { |
| | 12 | 205 | | if (snapValue == null) { display = "—"; return true; } |
| | 6 | 206 | | display = snapValue; |
| | 6 | 207 | | var set = lookups.For(def.Lookup); |
| | 6 | 208 | | if (!set.NameToIds.TryGetValue(snapValue.Trim(), out var ids) || ids.Count == 0) |
| | | 209 | | { |
| | 1 | 210 | | applicable = false; |
| | 1 | 211 | | warning = $"Значение «{snapValue}» не найдено в справочнике — поле не будет восстановлено."; |
| | 1 | 212 | | return false; |
| | | 213 | | } |
| | 5 | 214 | | if (ids.Count > 1) |
| | | 215 | | { |
| | 2 | 216 | | applicable = false; |
| | 2 | 217 | | warning = $"Значение «{snapValue}» неоднозначно ({ids.Count} совпадений) — поле не будет восстановле |
| | 2 | 218 | | return false; |
| | | 219 | | } |
| | 3 | 220 | | target = ids[0]; |
| | 3 | 221 | | return true; |
| | | 222 | | } |
| | | 223 | | case RollbackFieldKind.Bool: |
| | | 224 | | { |
| | 2 | 225 | | var b = snapValue is "1" or "true" or "True" or "TRUE"; |
| | 2 | 226 | | target = b; |
| | 2 | 227 | | display = b ? "Да" : "Нет"; |
| | 2 | 228 | | return true; |
| | | 229 | | } |
| | | 230 | | case RollbackFieldKind.Int: |
| | | 231 | | { |
| | 0 | 232 | | if (snapValue == null) { return true; } |
| | 0 | 233 | | if (int.TryParse(snapValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var i)) |
| | | 234 | | { |
| | 0 | 235 | | target = i; |
| | 0 | 236 | | display = i.ToString(CultureInfo.InvariantCulture); |
| | 0 | 237 | | return true; |
| | | 238 | | } |
| | 0 | 239 | | applicable = false; |
| | 0 | 240 | | warning = $"Не удалось разобрать число «{snapValue}»."; |
| | 0 | 241 | | return false; |
| | | 242 | | } |
| | | 243 | | case RollbackFieldKind.Decimal: |
| | | 244 | | { |
| | 7 | 245 | | if (snapValue == null) { return true; } |
| | 7 | 246 | | if (decimal.TryParse(snapValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) |
| | | 247 | | { |
| | 7 | 248 | | target = d; |
| | 7 | 249 | | display = d.ToString("0.##", CultureInfo.InvariantCulture); |
| | 7 | 250 | | return true; |
| | | 251 | | } |
| | 0 | 252 | | applicable = false; |
| | 0 | 253 | | warning = $"Не удалось разобрать число «{snapValue}»."; |
| | 0 | 254 | | return false; |
| | | 255 | | } |
| | | 256 | | case RollbackFieldKind.DateTime: |
| | | 257 | | { |
| | 0 | 258 | | if (snapValue == null) { return true; } |
| | 0 | 259 | | if (DateTime.TryParse(snapValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) |
| | | 260 | | { |
| | 0 | 261 | | target = dt; |
| | 0 | 262 | | display = dt.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); |
| | 0 | 263 | | return true; |
| | | 264 | | } |
| | 0 | 265 | | applicable = false; |
| | 0 | 266 | | warning = $"Не удалось разобрать дату «{snapValue}»."; |
| | 0 | 267 | | return false; |
| | | 268 | | } |
| | | 269 | | default: |
| | | 270 | | { |
| | 0 | 271 | | target = snapValue; |
| | 0 | 272 | | display = string.IsNullOrEmpty(snapValue) ? "—" : snapValue!; |
| | 0 | 273 | | return true; |
| | | 274 | | } |
| | | 275 | | } |
| | | 276 | | } |
| | | 277 | | |
| | | 278 | | // ── Сравнение текущего и целевого значения ───────────────────────────────── |
| | | 279 | | |
| | | 280 | | private static bool ValuesEqual(RollbackFieldDef def, object? current, object? target) |
| | | 281 | | { |
| | 9 | 282 | | return def.Kind switch |
| | 9 | 283 | | { |
| | 3 | 284 | | RollbackFieldKind.ForeignKey => (int?)current == (int?)target, |
| | 1 | 285 | | RollbackFieldKind.Bool => (bool)(current ?? false) == (bool)(target ?? false), |
| | 0 | 286 | | RollbackFieldKind.Int => (int?)current == (int?)target, |
| | 5 | 287 | | RollbackFieldKind.Decimal => (decimal?)current == (decimal?)target, |
| | 0 | 288 | | RollbackFieldKind.DateTime => Nullable.Equals((DateTime?)current, (DateTime?)target), |
| | 0 | 289 | | _ => string.Equals((string?)current, (string?)target, StringComparison.Ordinal) |
| | 9 | 290 | | }; |
| | | 291 | | } |
| | | 292 | | |
| | | 293 | | /// <summary>Сравнение, когда целевое значение не распарсилось — приводим текущее к строке снимка грубо.</summary> |
| | | 294 | | private static bool ValuesEqual(RollbackFieldDef def, object? current, string? snapValue, LookupBundle lookups) |
| | | 295 | | { |
| | 3 | 296 | | if (def.Kind == RollbackFieldKind.ForeignKey) |
| | | 297 | | { |
| | 3 | 298 | | var id = (int?)current; |
| | 3 | 299 | | var set = lookups.For(def.Lookup); |
| | 3 | 300 | | var currentName = id.HasValue && set.IdToName.TryGetValue(id.Value, out var n) ? n : null; |
| | 3 | 301 | | return string.Equals(currentName, snapValue, StringComparison.Ordinal); |
| | | 302 | | } |
| | 0 | 303 | | return false; |
| | | 304 | | } |
| | | 305 | | |
| | | 306 | | // ── Применение целевого значения к заказу ────────────────────────────────── |
| | | 307 | | |
| | | 308 | | private static void ApplyValue(Order order, RollbackFieldDef def, object? target) |
| | | 309 | | { |
| | 5 | 310 | | switch (def.Column) |
| | | 311 | | { |
| | 0 | 312 | | case "ShipmentNumber": order.ShipmentNumber = (string?)target; break; |
| | 0 | 313 | | case "ProductName": order.ProductName = (string?)target; break; |
| | 0 | 314 | | case "ProductKey": order.ProductKey = (string?)target; break; |
| | 0 | 315 | | case "Article": order.Article = (string?)target; break; |
| | 0 | 316 | | case "OrderNumberToSupplier": order.OrderNumberToSupplier = (string?)target; break; |
| | 0 | 317 | | case "DeliveryCity": order.DeliveryCity = (string?)target; break; |
| | | 318 | | |
| | 0 | 319 | | case "Quantity": order.Quantity = (int?)target; break; |
| | | 320 | | |
| | 0 | 321 | | case "ShipmentAmount": order.ShipmentAmount = (decimal?)target; break; |
| | 4 | 322 | | case "Price": order.Price = (decimal?)target; break; |
| | 0 | 323 | | case "PurchasePrice": order.PurchasePrice = (decimal?)target; break; |
| | 0 | 324 | | case "OriginalPurchasePrice": order.OriginalPurchasePrice = (decimal?)target; break; |
| | 0 | 325 | | case "MinOzonCommission": order.MinOzonCommission = (decimal?)target; break; |
| | 0 | 326 | | case "MaxOzonCommission": order.MaxOzonCommission = (decimal?)target; break; |
| | 0 | 327 | | case "MinProfit": order.MinProfit = (decimal?)target; break; |
| | 0 | 328 | | case "MaxProfit": order.MaxProfit = (decimal?)target; break; |
| | 0 | 329 | | case "MinDiscount": order.MinDiscount = (decimal?)target; break; |
| | 0 | 330 | | case "MaxDiscount": order.MaxDiscount = (decimal?)target; break; |
| | 0 | 331 | | case "CostPrice": order.CostPrice = (decimal?)target; break; |
| | | 332 | | |
| | 2 | 333 | | case "IsVerified": order.IsVerified = (bool)(target ?? false); break; |
| | 0 | 334 | | case "IsAccepted": order.IsAccepted = (bool)(target ?? false); break; |
| | | 335 | | |
| | 0 | 336 | | case "ProcessingDate": order.ProcessingDate = (DateTime?)target; break; |
| | 0 | 337 | | case "ShippingDate": order.ShippingDate = (DateTime?)target; break; |
| | | 338 | | |
| | 0 | 339 | | case "AppStatusId": order.AppStatusId = (int?)target; break; |
| | 0 | 340 | | case "ManufacturerId": order.ManufacturerId = (int?)target; break; |
| | 0 | 341 | | case "ShipmentWarehouseId": order.ShipmentWarehouseId = (int?)target; break; |
| | 4 | 342 | | case "SupplierId": order.SupplierId = (int?)target; break; |
| | 0 | 343 | | case "OzonClientId": order.OzonClientId = (int?)target; break; |
| | | 344 | | } |
| | 0 | 345 | | } |
| | | 346 | | |
| | | 347 | | // ── Доступ к свойствам по имени колонки ──────────────────────────────────── |
| | | 348 | | |
| | 6 | 349 | | private static int? GetFkId(Order order, string column) => column switch |
| | 6 | 350 | | { |
| | 0 | 351 | | "AppStatusId" => order.AppStatusId, |
| | 0 | 352 | | "ManufacturerId" => order.ManufacturerId, |
| | 0 | 353 | | "ShipmentWarehouseId" => order.ShipmentWarehouseId, |
| | 6 | 354 | | "SupplierId" => order.SupplierId, |
| | 0 | 355 | | "OzonClientId" => order.OzonClientId, |
| | 0 | 356 | | _ => null |
| | 6 | 357 | | }; |
| | | 358 | | |
| | 1 | 359 | | private static bool GetBool(Order order, string column) => column switch |
| | 1 | 360 | | { |
| | 1 | 361 | | "IsVerified" => order.IsVerified, |
| | 0 | 362 | | "IsAccepted" => order.IsAccepted, |
| | 0 | 363 | | _ => false |
| | 1 | 364 | | }; |
| | | 365 | | |
| | 5 | 366 | | private static decimal? GetDecimal(Order order, string column) => column switch |
| | 5 | 367 | | { |
| | 0 | 368 | | "ShipmentAmount" => order.ShipmentAmount, |
| | 5 | 369 | | "Price" => order.Price, |
| | 0 | 370 | | "PurchasePrice" => order.PurchasePrice, |
| | 0 | 371 | | "OriginalPurchasePrice" => order.OriginalPurchasePrice, |
| | 0 | 372 | | "MinOzonCommission" => order.MinOzonCommission, |
| | 0 | 373 | | "MaxOzonCommission" => order.MaxOzonCommission, |
| | 0 | 374 | | "MinProfit" => order.MinProfit, |
| | 0 | 375 | | "MaxProfit" => order.MaxProfit, |
| | 0 | 376 | | "MinDiscount" => order.MinDiscount, |
| | 0 | 377 | | "MaxDiscount" => order.MaxDiscount, |
| | 0 | 378 | | "CostPrice" => order.CostPrice, |
| | 0 | 379 | | _ => null |
| | 5 | 380 | | }; |
| | | 381 | | |
| | 0 | 382 | | private static DateTime? GetDate(Order order, string column) => column switch |
| | 0 | 383 | | { |
| | 0 | 384 | | "ProcessingDate" => order.ProcessingDate, |
| | 0 | 385 | | "ShippingDate" => order.ShippingDate, |
| | 0 | 386 | | _ => null |
| | 0 | 387 | | }; |
| | | 388 | | |
| | 0 | 389 | | private static string? GetString(Order order, string column) => column switch |
| | 0 | 390 | | { |
| | 0 | 391 | | "ShipmentNumber" => order.ShipmentNumber, |
| | 0 | 392 | | "ProductName" => order.ProductName, |
| | 0 | 393 | | "ProductKey" => order.ProductKey, |
| | 0 | 394 | | "Article" => order.Article, |
| | 0 | 395 | | "OrderNumberToSupplier" => order.OrderNumberToSupplier, |
| | 0 | 396 | | "DeliveryCity" => order.DeliveryCity, |
| | 0 | 397 | | _ => null |
| | 0 | 398 | | }; |
| | | 399 | | |
| | | 400 | | // ── Справочники FK (имя ⇄ Id) ────────────────────────────────────────────── |
| | | 401 | | |
| | | 402 | | private async Task<LookupBundle> LoadLookupsAsync(IReadOnlySet<RollbackLookup> needed, CancellationToken ct) |
| | | 403 | | { |
| | 13 | 404 | | var bundle = new LookupBundle(); |
| | 13 | 405 | | if (needed.Count == 0) |
| | 7 | 406 | | return bundle; |
| | | 407 | | |
| | 6 | 408 | | if (needed.Contains(RollbackLookup.AppStatus)) |
| | 0 | 409 | | bundle.AppStatus = await BuildLookupAsync(_context.AppStatuses.AsNoTracking().Select(x => new NameId(x.Id, x |
| | 6 | 410 | | if (needed.Contains(RollbackLookup.Manufacturer)) |
| | 0 | 411 | | bundle.Manufacturer = await BuildLookupAsync(_context.Manufacturers.AsNoTracking().Select(x => new NameId(x. |
| | 6 | 412 | | if (needed.Contains(RollbackLookup.Warehouse)) |
| | 0 | 413 | | bundle.Warehouse = await BuildLookupAsync(_context.Warehouses.AsNoTracking().Select(x => new NameId(x.Id, x. |
| | 6 | 414 | | if (needed.Contains(RollbackLookup.Supplier)) |
| | 6 | 415 | | bundle.Supplier = await BuildLookupAsync(_context.Suppliers.AsNoTracking().Select(x => new NameId(x.Id, x.Na |
| | 6 | 416 | | if (needed.Contains(RollbackLookup.OzonClient)) |
| | 0 | 417 | | bundle.OzonClient = await BuildLookupAsync(_context.OzonClients.AsNoTracking().Select(x => new NameId(x.Id, |
| | | 418 | | |
| | 6 | 419 | | return bundle; |
| | 13 | 420 | | } |
| | | 421 | | |
| | | 422 | | private static async Task<LookupSet> BuildLookupAsync(IQueryable<NameId> query, CancellationToken ct) |
| | | 423 | | { |
| | 6 | 424 | | var rows = await query.ToListAsync(ct); |
| | 6 | 425 | | var nameToIds = rows |
| | 11 | 426 | | .Where(r => !string.IsNullOrWhiteSpace(r.Name)) |
| | 11 | 427 | | .GroupBy(r => r.Name!.Trim(), StringComparer.Ordinal) |
| | 35 | 428 | | .ToDictionary(g => g.Key, g => g.Select(x => x.Id).Distinct().ToList(), StringComparer.Ordinal); |
| | 6 | 429 | | var idToName = rows |
| | 11 | 430 | | .GroupBy(r => r.Id) |
| | 28 | 431 | | .ToDictionary(g => g.Key, g => g.First().Name?.Trim() ?? string.Empty); |
| | 6 | 432 | | return new LookupSet(nameToIds, idToName); |
| | 6 | 433 | | } |
| | | 434 | | |
| | 55 | 435 | | private readonly record struct NameId(int Id, string? Name); |
| | | 436 | | |
| | 7 | 437 | | private sealed record LookupSet( |
| | 6 | 438 | | Dictionary<string, List<int>> NameToIds, |
| | 16 | 439 | | Dictionary<int, string> IdToName); |
| | | 440 | | |
| | | 441 | | private sealed class LookupBundle |
| | | 442 | | { |
| | 13 | 443 | | public LookupSet AppStatus { get; set; } = Empty; |
| | 13 | 444 | | public LookupSet Manufacturer { get; set; } = Empty; |
| | 13 | 445 | | public LookupSet Warehouse { get; set; } = Empty; |
| | 34 | 446 | | public LookupSet Supplier { get; set; } = Empty; |
| | 13 | 447 | | public LookupSet OzonClient { get; set; } = Empty; |
| | | 448 | | |
| | 1 | 449 | | private static readonly LookupSet Empty = new(new(StringComparer.Ordinal), new()); |
| | | 450 | | |
| | 15 | 451 | | public LookupSet For(RollbackLookup lookup) => lookup switch |
| | 15 | 452 | | { |
| | 0 | 453 | | RollbackLookup.AppStatus => AppStatus, |
| | 0 | 454 | | RollbackLookup.Manufacturer => Manufacturer, |
| | 0 | 455 | | RollbackLookup.Warehouse => Warehouse, |
| | 15 | 456 | | RollbackLookup.Supplier => Supplier, |
| | 0 | 457 | | RollbackLookup.OzonClient => OzonClient, |
| | 0 | 458 | | _ => Empty |
| | 15 | 459 | | }; |
| | | 460 | | } |
| | | 461 | | } |