food-market/tests/food-market.UnitTests/MoneyMathTests.cs
nns deef65d819
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
test(money): unit (MoneyMath 25) + integration (7 на Postgres) + MoneyMath refactor
- 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>
2026-10-07 22:11:19 +05:00

121 lines
6 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.

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