| | | 1 | | using System.Text.Json; |
| | | 2 | | using Microsoft.EntityFrameworkCore; |
| | | 3 | | using OzonDomains.Models; |
| | | 4 | | using OzonRepositories.Context; |
| | | 5 | | using Services.PriceСlculationServices; |
| | | 6 | | |
| | | 7 | | namespace Services.DataServices; |
| | | 8 | | |
| | | 9 | | /// <summary>Создаёт неизменяемые данные для объяснения расчёта в истории заказа.</summary> |
| | | 10 | | public sealed class OrderCalculationSnapshotService |
| | | 11 | | { |
| | | 12 | | private readonly ApplicationDbContext _context; |
| | | 13 | | private readonly ICurrencyRateFetcher _rates; |
| | | 14 | | |
| | 1 | 15 | | public OrderCalculationSnapshotService(ApplicationDbContext context, ICurrencyRateFetcher rates) |
| | | 16 | | { |
| | 1 | 17 | | _context = context; |
| | 1 | 18 | | _rates = rates; |
| | 1 | 19 | | } |
| | | 20 | | |
| | | 21 | | public async Task CaptureAsync(IEnumerable<int> orderIds, Guid operationId, string source, string? changedBy, Cancel |
| | | 22 | | { |
| | 2 | 23 | | var ids = orderIds.Distinct().Where(id => id > 0).ToList(); |
| | 1 | 24 | | if (ids.Count == 0 || operationId == Guid.Empty) return; |
| | | 25 | | |
| | 1 | 26 | | var existing = await _context.OrderCalculationSnapshots |
| | 1 | 27 | | .Where(x => ids.Contains(x.OrderId) && x.OperationId == operationId) |
| | 1 | 28 | | .Select(x => x.OrderId) |
| | 1 | 29 | | .ToListAsync(cancellationToken); |
| | 1 | 30 | | var missing = ids.Except(existing).ToList(); |
| | 1 | 31 | | if (missing.Count == 0) return; |
| | | 32 | | |
| | | 33 | | // Триггер записывает поля заказа отдельно. После успешного пользовательского |
| | | 34 | | // сохранения объединяем только свежие ещё не связанные строки в одну операцию. |
| | 1 | 35 | | var historyQuery = _context.OrderHistories.Where(x => missing.Contains(x.OrderId) |
| | 1 | 36 | | && x.OperationId == null && x.ChangedAt >= DateTime.Now.AddMinutes(-5)); |
| | 2 | 37 | | if (!string.IsNullOrWhiteSpace(changedBy)) historyQuery = historyQuery.Where(x => x.ChangedBy == changedBy); |
| | 1 | 38 | | await historyQuery.ExecuteUpdateAsync(x => x.SetProperty(h => h.OperationId, operationId), cancellationToken); |
| | | 39 | | |
| | 1 | 40 | | var orders = await _context.Orders.AsNoTracking() |
| | 1 | 41 | | .Include(x => x.Supplier) |
| | 1 | 42 | | .Include(x => x.ProductInfo) |
| | 1 | 43 | | .Include(x => x.ExtraExpenses).ThenInclude(x => x.ExpenseType) |
| | 1 | 44 | | .Where(x => missing.Contains(x.Id)) |
| | 1 | 45 | | .ToListAsync(cancellationToken); |
| | | 46 | | |
| | 1 | 47 | | var usd = await SafeRateAsync(_rates.GetUSDRateAsync()); |
| | 1 | 48 | | var eur = await SafeRateAsync(_rates.GetEURRateAsync()); |
| | 1 | 49 | | var byn = await SafeRateAsync(_rates.GetBYNRateAsync()); |
| | 1 | 50 | | var capturedAt = DateTime.UtcNow; |
| | | 51 | | |
| | 4 | 52 | | foreach (var order in orders) |
| | | 53 | | { |
| | 1 | 54 | | var supplier = order.Supplier; |
| | 1 | 55 | | var quantity = Math.Max(1, order.Quantity ?? 1); |
| | 1 | 56 | | var currency = supplier?.CurrencyCode.ToString() ?? "RUB"; |
| | 1 | 57 | | var weightCurrency = supplier?.WeightFactorCurrencyCode.ToString() ?? "RUB"; |
| | 1 | 58 | | var purchase = order.OriginalPurchasePrice ?? 0m; |
| | 1 | 59 | | var factor = supplier?.CostFactor ?? 1m; |
| | 1 | 60 | | var weightFactor = supplier?.WeightFactor ?? 0m; |
| | 1 | 61 | | var weight = order.ProductInfo?.Weight ?? 0m; |
| | 1 | 62 | | var purchaseRate = Rate(currency, usd, eur, byn); |
| | 1 | 63 | | var weightRate = Rate(weightCurrency, usd, eur, byn); |
| | 1 | 64 | | var purchaseInRub = purchase * factor * purchaseRate; |
| | 1 | 65 | | var weightCost = weight * weightFactor * weightRate; |
| | 1 | 66 | | var expenses = order.ExtraExpenses.Select(x => new |
| | 1 | 67 | | { |
| | 1 | 68 | | amount = x.Amount, |
| | 1 | 69 | | type = x.ExpenseType?.Name ?? "Без типа", |
| | 1 | 70 | | comment = x.Comment ?? string.Empty |
| | 1 | 71 | | }).ToList(); |
| | 1 | 72 | | var extraTotal = expenses.Sum(x => x.amount); |
| | 1 | 73 | | var extrasPerUnit = extraTotal / quantity; |
| | 1 | 74 | | var costPrice = purchaseInRub + weightCost + extrasPerUnit; |
| | 1 | 75 | | var orderPrice = order.Price ?? 0m; |
| | 1 | 76 | | var currentPriceWithDiscount = order.ProductInfo?.CurrentPriceWithDiscount; |
| | 1 | 77 | | var price = currentPriceWithDiscount is > 0 ? currentPriceWithDiscount.Value : orderPrice; |
| | 1 | 78 | | var minCommission = order.MinOzonCommission ?? 0m; |
| | 1 | 79 | | var maxCommission = order.MaxOzonCommission ?? minCommission; |
| | 1 | 80 | | var minProfit = price - costPrice - maxCommission; |
| | 1 | 81 | | var maxProfit = price - costPrice - minCommission; |
| | 1 | 82 | | var payload = new |
| | 1 | 83 | | { |
| | 1 | 84 | | version = 1, |
| | 1 | 85 | | orderId = order.Id, |
| | 1 | 86 | | shipmentNumber = order.ShipmentNumber, |
| | 1 | 87 | | capturedAt, |
| | 1 | 88 | | supplier = supplier is null ? null : new { id = supplier.Id, name = supplier.Name, site = supplier.Site |
| | 1 | 89 | | quantity, price, orderPrice, currentPriceWithDiscount, shipmentAmount = price * quantity, |
| | 1 | 90 | | purchasePrice = purchase, currency, costFactor = factor, currencyRate = purchaseRate, purchaseInRub, |
| | 1 | 91 | | weight, weightFactor, weightCurrency, weightRate, weightCost, |
| | 1 | 92 | | expenses, extraTotal, extrasPerUnit, costPrice, |
| | 1 | 93 | | minCommission, maxCommission, minCommissionInfo = order.MinCommissionInfo ?? string.Empty, |
| | 1 | 94 | | maxCommissionInfo = order.MaxCommissionInfo ?? string.Empty, |
| | 1 | 95 | | minProfit, maxProfit, |
| | 1 | 96 | | minMarkup = costPrice == 0 ? (decimal?)null : minProfit / costPrice * 100m, |
| | 1 | 97 | | maxMarkup = costPrice == 0 ? (decimal?)null : maxProfit / costPrice * 100m |
| | 1 | 98 | | }; |
| | 1 | 99 | | _context.OrderCalculationSnapshots.Add(new OrderCalculationSnapshot |
| | 1 | 100 | | { |
| | 1 | 101 | | OrderId = order.Id, |
| | 1 | 102 | | OperationId = operationId, |
| | 1 | 103 | | Source = source, |
| | 1 | 104 | | CapturedAt = capturedAt, |
| | 1 | 105 | | PayloadJson = JsonSerializer.Serialize(payload) |
| | 1 | 106 | | }); |
| | | 107 | | } |
| | 1 | 108 | | await _context.SaveChangesAsync(cancellationToken); |
| | 1 | 109 | | } |
| | | 110 | | |
| | | 111 | | private static async Task<decimal> SafeRateAsync(Task<decimal> source) |
| | | 112 | | { |
| | 3 | 113 | | try { return await source; } catch { return 1m; } |
| | 3 | 114 | | } |
| | | 115 | | |
| | 2 | 116 | | private static decimal Rate(string currency, decimal usd, decimal eur, decimal byn) => currency.ToUpperInvariant() s |
| | 2 | 117 | | { |
| | 2 | 118 | | "USD" => usd, "EUR" => eur, "BYN" => byn, _ => 1m |
| | 2 | 119 | | }; |
| | | 120 | | } |