Раньше продажи/возвраты/внесения/выплаты/расхождение смены не влияли на остаток кассы (касса уходила в −, эквайринг оставался 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>
71 lines
3.5 KiB
C#
71 lines
3.5 KiB
C#
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);
|