feat: re-model drawer cash as directional vouchers (Mandat Arkëtimi / Pagese)

Replace the single signed-± cash_movement with two distinct financial
documents — the direction is the event TYPE, not the sign of an amount:

  cash_in  = Mandat Arkëtimi (receipt / pay-IN,  +)  voucher AR-NNNN
  cash_out = Mandat Pagese  (disbursement / pay-OUT, −)  voucher PA-NNNN

Each carries a positive magnitude, voucher number, reason, the operator who
raised it and the admin who authorized it, and prints an Albanian slip.

Authorization changes from admin-only to operator-RAISED / admin-AUTHORIZED:
any shift:create holder raises the voucher, but POST /api/cash-voucher only
commits when authorizedBy is a real admin (shift:cash) re-entering their
password (verified server-side). Keeps the float control while letting the
operator do the booth paperwork.

Legacy cash_movement events are kept — they still verify and still fold into
the drawer (signed-±); the append-only chain is never rewritten. The drawer
fold and the Z-report window now sum all three types.

Verified against a copy of the live DB with the real signing modules:
cash_in 3000 + cash_out 5000 → drawer −2000, hash-chain verifies OK.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-20 16:18:26 +02:00
parent a20400c2c5
commit 2835f78635
13 changed files with 335 additions and 78 deletions
+18 -7
View File
@@ -627,14 +627,25 @@ export function closeShift(): Promise<ShiftReport> {
return apiFetch("/api/shift/close", { method: "POST" });
}
/** Admin loads/removes physical drawer cash. amountMinor signed: + load, − remove. */
export function recordCashMovement(
amountMinor: number,
reason: string,
): Promise<{ amountMinor: number; balanceMinor: number }> {
return apiFetch("/api/cash-movement", {
/** A drawer cash voucher: Mandat Arkëtimi (cash_in / pay-IN) or Mandat Pagese
* (cash_out / pay-OUT). Direction is the TYPE, amountMinor a positive magnitude.
* Operator-raised, admin-authorized (authorizedBy + their password). */
export function recordCashVoucher(args: {
type: "cash_in" | "cash_out";
amountMinor: number;
reason: string;
authorizedBy: string;
authorizerPassword: string;
}): Promise<{
type: "cash_in" | "cash_out";
amountMinor: number;
voucherNo: string;
balanceMinor: number;
printed: boolean;
}> {
return apiFetch("/api/cash-voucher", {
method: "POST",
body: JSON.stringify({ amountMinor, reason }),
body: JSON.stringify(args),
});
}