Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- MoneyMath: вынесены чистые формулы (статус оплаты, ожидаемая наличность, разница смены, остаток счёта, долг поставщика, защита от переплаты); сервис/SuppliesController/MoneyReportsController теперь зовут их. - UnitTests/MoneyMathTests: 25 кейсов на формулы + money-флаги RolePermissions. - IntegrationTests/MoneyModuleTests (реальный Postgres, 7 [Fact]): мультитенантность (Б не видит счета А), инвариант остатка, частичная→полная оплата приёмки, переплата→409, долг поставщика, продажа без смены→409 + после open→ok, расчёт expected/difference при закрытии смены. - ApiActor: helpers CreateRetailPoint/CreateMoneyAccount/CreatePostedSupply. - tests/e2e/scrape-moysklad.ts — скрейпер для docs/money-moysklad-reference.md. Прогон: UnitTests 25/25 ✓, IntegrationTests money 7/7 ✓ (33s, Testcontainers). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
6 KiB
C#
121 lines
6 KiB
C#
using FluentAssertions;
|
||
using foodmarket.Application.Money;
|
||
using foodmarket.Domain.Organizations;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.UnitTests;
|
||
|
||
/// <summary>Чистые денежные формулы модуля «Деньги» (<see cref="MoneyMath"/>):
|
||
/// статус оплаты приёмки, ожидаемая наличность смены, инвариант остатка счёта,
|
||
/// долг поставщика, защита от переплаты. Эти же формулы использует сервис,
|
||
/// контроллеры и отчёты — тест фиксирует их поведение без БД.</summary>
|
||
public class MoneyMathTests
|
||
{
|
||
// ── Статус оплаты приёмки ────────────────────────────────────────────
|
||
[Theory]
|
||
[InlineData(1000, 0, "unpaid")]
|
||
[InlineData(1000, 400, "partial")]
|
||
[InlineData(1000, 999, "partial")]
|
||
[InlineData(1000, 999.9999, "paid")] // в пределах ε считаем оплаченным
|
||
[InlineData(1000, 1000, "paid")]
|
||
[InlineData(1000, 1200, "paid")] // переплату отображаем как «оплачено»
|
||
[InlineData(0, 0, "paid")] // платить нечего
|
||
[InlineData(-5, 0, "paid")]
|
||
public void PaymentStatus_classifies(decimal total, decimal payed, string expected)
|
||
=> MoneyMath.PaymentStatus(total, payed).Should().Be(expected);
|
||
|
||
// ── Ожидаемая наличность смены ───────────────────────────────────────
|
||
[Fact]
|
||
public void ExpectedCash_is_float_plus_cash_sales_minus_returns_plus_in_minus_out()
|
||
{
|
||
// разменник 20 000 + продажи нал 50 000 − возвраты нал 3 000
|
||
// + внесения 1 000 − выплаты (инкассация) 40 000 = 28 000
|
||
MoneyMath.ExpectedCash(20_000m, 50_000m, 3_000m, 1_000m, 40_000m)
|
||
.Should().Be(28_000m);
|
||
}
|
||
|
||
[Fact]
|
||
public void ExpectedCash_empty_shift_equals_opening_float()
|
||
=> MoneyMath.ExpectedCash(15_000m, 0, 0, 0, 0).Should().Be(15_000m);
|
||
|
||
[Theory]
|
||
[InlineData(10_000, 10_000, 0)] // ровно сошлось
|
||
[InlineData(9_500, 10_000, -500)] // недостача
|
||
[InlineData(10_300, 10_000, 300)] // излишек
|
||
public void CashDifference_is_actual_minus_expected(decimal actual, decimal expected, decimal diff)
|
||
=> MoneyMath.CashDifference(actual, expected).Should().Be(diff);
|
||
|
||
// ── Инвариант остатка счёта ──────────────────────────────────────────
|
||
[Fact]
|
||
public void AccountBalance_is_opening_plus_in_minus_out()
|
||
=> MoneyMath.AccountBalance(100_000m, 75_000m, 40_000m).Should().Be(135_000m);
|
||
|
||
[Fact]
|
||
public void AccountBalance_can_go_negative()
|
||
=> MoneyMath.AccountBalance(0m, 0m, 5_000m).Should().Be(-5_000m);
|
||
|
||
// ── Долг поставщика ──────────────────────────────────────────────────
|
||
[Fact]
|
||
public void SupplierDebt_supplies_minus_paid()
|
||
{
|
||
// 3 приёмки на 300 000, оплачено 100 000, возвратов/корректировок нет.
|
||
MoneyMath.SupplierDebt(300_000m, 100_000m, 0, 0, 0, 0)
|
||
.Should().Be(200_000m);
|
||
}
|
||
|
||
[Fact]
|
||
public void SupplierDebt_returns_reduce_debt()
|
||
{
|
||
// приёмки 300 000 (оплачено 300 000), возврат на 50 000 (деньги назад не получили)
|
||
// → поставщик должен нам 50 000 → долг −50 000.
|
||
MoneyMath.SupplierDebt(300_000m, 300_000m, 50_000m, 0m, 0, 0)
|
||
.Should().Be(-50_000m);
|
||
}
|
||
|
||
[Fact]
|
||
public void SupplierDebt_starting_adjustment_adds_debt()
|
||
{
|
||
// стартовый долг «мы должны» 500 000 при переходе на систему.
|
||
MoneyMath.SupplierDebt(0, 0, 0, 0, 500_000m, 0)
|
||
.Should().Be(500_000m);
|
||
}
|
||
|
||
[Fact]
|
||
public void SupplierDebt_full_formula()
|
||
{
|
||
// приёмки 1 000 000 (оплачено 600 000) − возвраты 100 000 (вернули деньги 100 000)
|
||
// + корр.(мы должны 50 000 − они должны 20 000)
|
||
// = 400 000 − 0 + 30 000 = 430 000
|
||
MoneyMath.SupplierDebt(1_000_000m, 600_000m, 100_000m, 100_000m, 50_000m, 20_000m)
|
||
.Should().Be(430_000m);
|
||
}
|
||
|
||
// ── Защита от переплаты ──────────────────────────────────────────────
|
||
[Theory]
|
||
[InlineData(0, 1000, 1000, true)] // первая полная оплата
|
||
[InlineData(400, 600, 1000, true)] // добивка ровно до total
|
||
[InlineData(400, 601, 1000, false)] // чуть больше остатка — переплата
|
||
[InlineData(1000, 0.00005, 1000, true)] // в пределах ε
|
||
public void CanApplyPayment_guards_overpay(decimal payed, decimal add, decimal total, bool ok)
|
||
=> MoneyMath.CanApplyPayment(payed, add, total).Should().Be(ok);
|
||
|
||
// ── Права роли ───────────────────────────────────────────────────────
|
||
[Fact]
|
||
public void RolePermissions_All_includes_money_flags()
|
||
{
|
||
var p = RolePermissions.All();
|
||
p.MoneyView.Should().BeTrue();
|
||
p.MoneyEdit.Should().BeTrue();
|
||
p.CashShiftOperate.Should().BeTrue();
|
||
}
|
||
|
||
[Fact]
|
||
public void RolePermissions_default_denies_money()
|
||
{
|
||
var p = new RolePermissions();
|
||
p.MoneyView.Should().BeFalse();
|
||
p.MoneyEdit.Should().BeFalse();
|
||
p.CashShiftOperate.Should().BeFalse();
|
||
}
|
||
}
|