food-market/tests/food-market.UnitTests/MovingAverageCostTests.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

59 lines
2.2 KiB
C#
Raw Permalink 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.

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);
}
}