food-market/src/food-market.application/Inventory/MovingAverageCost.cs
nns f3d517f257 test(unit): xUnit-проект food-market.UnitTests, 23 теста (P1-20)
Чистая логика вынесена в Application для тестируемости и используется контроллерами:
- MovingAverageCost.Compute (скользящее среднее себестоимости) ← SuppliesController.Post
- RetailPaymentValidator.IsSufficient (достаточность оплаты) ← RetailSalesController.Post

Тесты:
- MovingAverageCost: первая приёмка, средневзвешенное, округление до 4 знаков, totalQty=0.
- RetailPaymentValidator: ровно/переплата/недоплата, округление до 2 знаков.
- StockService.ApplyMovement (SQLite in-memory): материализация Stock+движение,
  инкремент, отрицательное списание, throw без tenant.
- Мультитенантный query-filter AppDbContext: tenant видит своё; чужой не видит;
  SuperAdmin без override — всё; с override — только выбранную оргу.

Все 23 зелёные. EF8 SQLite поддерживает ToJson (EmployeeRole.Permissions).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 03:01:56 +05:00

26 lines
1.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace foodmarket.Application.Inventory;
/// <summary>Скользящее средневзвешенное себестоимости товара при приёмке.
///
/// Себестоимость пересчитывается так, чтобы отражать средневзвешенную цену
/// всех закупленных единиц: <c>(остаток·текущаяСебестоимость + приход·ценаЗакупки)
/// / (остаток + приход)</c>. На первой приёмке (нет остатка и себестоимости)
/// берётся цена закупки. Результат округляется до 4 знаков.
///
/// Вынесено из <c>SuppliesController.Post</c> для юнит-тестируемости —
/// контроллер вызывает этот метод.</summary>
public static class MovingAverageCost
{
public static decimal Compute(
decimal currentQty, decimal currentCost, decimal incomingQty, decimal incomingUnitPrice)
{
var totalQty = currentQty + incomingQty;
// Порядок важен: && связывает крепче ||, т.е.
// (totalQty==0) || ((currentCost==0) && (currentQty==0)).
var newCost = totalQty == 0m || currentCost == 0m && currentQty == 0m
? incomingUnitPrice
: (currentQty * currentCost + incomingQty * incomingUnitPrice) / totalQty;
return Math.Round(newCost, 4, MidpointRounding.AwayFromZero);
}
}