Чистая логика вынесена в 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>
59 lines
2.2 KiB
C#
59 lines
2.2 KiB
C#
using FluentAssertions;
|
||
using foodmarket.Application.Inventory;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.UnitTests;
|
||
|
||
/// <summary>Скользящее средневзвешенное себестоимости (логика SuppliesController.Post).</summary>
|
||
public class MovingAverageCostTests
|
||
{
|
||
[Fact]
|
||
public void First_supply_with_no_stock_uses_purchase_price()
|
||
{
|
||
// нет остатка, нет себестоимости → берём цену закупки
|
||
MovingAverageCost.Compute(currentQty: 0m, currentCost: 0m, incomingQty: 10m, incomingUnitPrice: 100m)
|
||
.Should().Be(100m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Weighted_average_of_old_and_new()
|
||
{
|
||
// 10 шт по 100 + 10 шт по 200 → среднее 150
|
||
MovingAverageCost.Compute(currentQty: 10m, currentCost: 100m, incomingQty: 10m, incomingUnitPrice: 200m)
|
||
.Should().Be(150m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Weighted_average_respects_quantities()
|
||
{
|
||
// 30 шт по 100 + 10 шт по 200 → (3000+2000)/40 = 125
|
||
MovingAverageCost.Compute(currentQty: 30m, currentCost: 100m, incomingQty: 10m, incomingUnitPrice: 200m)
|
||
.Should().Be(125m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Result_is_rounded_to_four_decimals()
|
||
{
|
||
// (1*1 + 2*2)/3 = 1.6666... → 1.6667
|
||
MovingAverageCost.Compute(currentQty: 1m, currentCost: 1m, incomingQty: 2m, incomingUnitPrice: 2m)
|
||
.Should().Be(1.6667m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Existing_stock_with_zero_cost_still_averages_when_qty_positive()
|
||
{
|
||
// currentCost==0 но currentQty>0 → НЕ берём цену закупки целиком,
|
||
// считаем среднее: (5*0 + 5*200)/10 = 100
|
||
MovingAverageCost.Compute(currentQty: 5m, currentCost: 0m, incomingQty: 5m, incomingUnitPrice: 200m)
|
||
.Should().Be(100m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Zero_total_quantity_falls_back_to_purchase_price()
|
||
{
|
||
// возврат/сторно может дать totalQty==0 → не делим на ноль
|
||
MovingAverageCost.Compute(currentQty: -10m, currentCost: 50m, incomingQty: 10m, incomingUnitPrice: 70m)
|
||
.Should().Be(70m);
|
||
}
|
||
}
|