namespace foodmarket.Application.Inventory; /// Чистая логика «разбить результат инвентаризации на строки /// Оприходования (излишки) и Списания (недостача)». Вынесено для юнит-тестов. /// /// Правила (по модели МойСклада): /// - diff = факт − расчётное; строки с diff == 0 НЕ участвуют (непосчитанные/совпавшие /// не трогаются); /// - diff > 0 → излишек → строка Оприходования, количество = diff; /// - diff < 0 → недостача → строка Списания, количество = |diff|; /// - сумма строки = количество × UnitCost (снимок себестоимости). /// Сама инвентаризация остатки НЕ двигает. public static class InventoryResult { public readonly record struct ResultLine(Guid ProductId, decimal Quantity, decimal UnitCost) { public decimal LineTotal => decimal.Round(Quantity * UnitCost, 2); } public readonly record struct Split(IReadOnlyList Surplus, IReadOnlyList Shortage) { public bool HasSurplus => Surplus.Count > 0; public bool HasShortage => Shortage.Count > 0; public decimal SurplusTotal => Surplus.Sum(l => l.LineTotal); public decimal ShortageTotal => Shortage.Sum(l => l.LineTotal); } /// Разбивает строки инвентаризации (productId, diff, unitCost) на излишки/недостачи. /// diff==0 пропускаются. Количество в Списании положительное (|diff|). public static Split Build(IEnumerable<(Guid ProductId, decimal Diff, decimal UnitCost)> lines) { var surplus = new List(); var shortage = new List(); foreach (var l in lines) { if (l.Diff > 0m) surplus.Add(new ResultLine(l.ProductId, l.Diff, l.UnitCost)); else if (l.Diff < 0m) shortage.Add(new ResultLine(l.ProductId, -l.Diff, l.UnitCost)); // diff == 0 — пропускаем } return new Split(surplus, shortage); } }