food-market/src/food-market.application/Money/IMoneyService.cs
nns 1e6d7fb2ab fix(money): движения смены попадают в остатки счетов + запрет отрицательного остатка
Раньше продажи/возвраты/внесения/выплаты/расхождение смены не влияли на остаток
кассы (касса уходила в −, эквайринг оставался 0). Теперь при закрытии смены
MoneyService.MaterializeShiftMovementsAsync создаёт Posted-движения money_transactions
по типам (Category): продажи нал/карта, возвраты нал/карта, внесение, выплата,
излишек/недостача — привязанные к смене, идемпотентно. Остаток кассы/эквайринга и
отчёт «Деньги» теперь верны автоматически (всё читает money_transactions).

Отрицательный остаток: расход (CashOut/PaymentOut/TransferOut) больше остатка счёта
и выплата из смены больше наличных смены → 409 insufficient-funds; настройка
Organization.AllowNegativeBalance (по умолч. выкл., меняет только Admin).

Миграция Phase34 (idempotent): money_transactions.Category/.CashShiftId,
organizations.AllowNegativeBalance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 03:11:09 +05:00

71 lines
3.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.

using foodmarket.Domain.Money;
namespace foodmarket.Application.Money;
/// <summary>Единый API модуля «Деньги». Контроллеры дёргают только эти
/// методы — внутри сервиса сидит вся бизнес-логика: пересчёт
/// <c>Supply.PayedSum</c>, парная проводка переводов, расчёт expected
/// cash для смены, применение статусов. Все методы Serializable-safe
/// (транзакция внутри — одна на операцию).</summary>
public interface IMoneyService
{
/// <summary>Провести готовый документ: смена Status Draft→Posted,
/// пересчёт PayedSum на связанных документах. Пустой список
/// <c>Links</c> допустим (аналог noClosingDocs=true). Для TransferOut
/// — атомарно создаёт/проводит парный TransferIn.</summary>
Task<MoneyServiceResult> PostAsync(Guid transactionId, Guid currentUserId, CancellationToken ct);
/// <summary>Отменить проведение. Пересчёт PayedSum в сторону уменьшения.
/// После Unpost документ можно удалить или снова отредактировать и
/// провести заново.</summary>
Task<MoneyServiceResult> UnpostAsync(Guid transactionId, Guid currentUserId, CancellationToken ct);
/// <summary>Открыть смену на точке. Падает 409 если на RetailPoint
/// уже есть открытая смена.</summary>
Task<CashShiftOpenResult> OpenShiftAsync(Guid retailPointId, Guid cashAccountId,
Guid? acquiringAccountId, decimal openingFloat, Guid cashierUserId, CancellationToken ct);
/// <summary>Закрыть смену: вычислить snapshot'ы продаж/возвратов,
/// expected cash, разницу. Запрещено если уже закрыта.</summary>
Task<CashShiftCloseResult> CloseShiftAsync(Guid shiftId, decimal actualCash,
Guid closedByUserId, string? notes, CancellationToken ct);
/// <summary>Добавить внесение/выплату внутри открытой смены.</summary>
Task<CashDrawerOperationResult> AddDrawerOperationAsync(Guid shiftId,
CashDrawerDirection direction, decimal amount, string reason,
Guid byUserId, CancellationToken ct);
}
public enum MoneyServiceStatus
{
Ok = 0,
NotFound = 1,
AlreadyPosted = 2,
AlreadyDraft = 3,
ValidationFailed = 4,
Conflict = 5,
ShiftNotOpen = 6,
ShiftAlreadyOpen = 7,
OverpaidDocument = 8,
LinkedSumMismatch = 9,
WrongCurrency = 10,
AccountInactive = 11,
InsufficientFunds = 12,
}
public record MoneyServiceResult(MoneyServiceStatus Status, string? ErrorCode = null, string? Message = null)
{
public static MoneyServiceResult Ok() => new(MoneyServiceStatus.Ok);
public static MoneyServiceResult Fail(MoneyServiceStatus s, string code, string msg)
=> new(s, code, msg);
}
public record CashShiftOpenResult(MoneyServiceStatus Status, Guid? ShiftId, string? Number,
string? ErrorCode, string? Message);
public record CashShiftCloseResult(MoneyServiceStatus Status, decimal ExpectedCash,
decimal CashDifference, string? ErrorCode, string? Message);
public record CashDrawerOperationResult(MoneyServiceStatus Status, Guid? OperationId,
string? ErrorCode, string? Message);