feat(validations): merchant (bar/lavazh) ticket validations end-to-end
In-park merchants discharge customers' parking: a merchant user scans the ticket on their device (/validate; validation:create + program↔user binding) and applies their program — comp / first-N-minutes free / amount-off (capped, typed at scan) / percent. All money stays at the booth: the quote folds live validations in a canonical order (timeCredit → percent → fixed → comp, net floors at 0, Σ lines ≡ gross − net), the payment records gross/discount and CONSUMES the validation ids (an overstay's fresh period never re-applies them), the receipt prints the gross → lines → net story, and the Z/X-report carries discountTotalMinor leakage. Every apply/void is a signed, attributed ledger event (refId = append-only void); program config is /setup/site master data (Bar/Lavazh checkboxes + right-column panel, tabs when both) whose saves sign config_change. Migration 0024 + reset-db drift-guard entries; 8 route integration tests + priceSession fold suite. See wiki/concepts/validation-discounts.md for the full design record. Claude-Session: https://claude.ai/code/session_01YYkpEsLmoQPaize5ec3oUm
This commit is contained in:
@@ -383,6 +383,30 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Merchant validations (bar/lavazh): the gross fee + one line per
|
||||
discount — the Total below is the NET the customer pays. The lines
|
||||
ride the quote (SessionLookup.validationLines) and reprint on the
|
||||
receipt. See wiki/concepts/validation-discounts.md. */}
|
||||
{!isSubscription &&
|
||||
(s.validationLines ?? []).length > 0 &&
|
||||
s.currency != null &&
|
||||
s.amountMinor != null && (
|
||||
<div className="rounded-term bg-term-panel-2 px-3 py-2 text-[0.75rem]">
|
||||
<div className="flex justify-between text-term-text">
|
||||
<span>{t("val.gross")}</span>
|
||||
<span className="tabular-nums">
|
||||
{formatMoney(s.grossMinor ?? s.amountMinor, s.currency)}
|
||||
</span>
|
||||
</div>
|
||||
{(s.validationLines ?? []).map((v, i) => (
|
||||
<div key={i} className="flex justify-between text-term-green">
|
||||
<span>{v.label}</span>
|
||||
<span className="tabular-nums">−{formatMoney(v.discountMinor, s.currency!)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Total — a subscription is prepaid (no amount) UNLESS it owes an
|
||||
out-of-window window charge; then show that amount. For an overstay the
|
||||
amount is the TOP-UP delta, not the whole stay. */}
|
||||
|
||||
Reference in New Issue
Block a user