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:
@@ -65,6 +65,7 @@ export const en: Catalog = {
|
||||
logs: "Logs",
|
||||
backup: "Backup",
|
||||
profile: "Profile",
|
||||
validate: "Validations",
|
||||
},
|
||||
drawer: {
|
||||
stateTitle: "Drawer now",
|
||||
@@ -230,6 +231,7 @@ export const en: Catalog = {
|
||||
evtCashOut: "PAY-OUT",
|
||||
evtCashReview: "REVIEW",
|
||||
evtConfigChange: "CONFIG",
|
||||
evtValidation: "VALIDATION",
|
||||
decision: { authorize: "authorized", deny: "denied" },
|
||||
evtAnomaly: "ANOMALY",
|
||||
evtRefused: "REFUSED",
|
||||
@@ -735,6 +737,51 @@ export const en: Catalog = {
|
||||
fieldPhone: "Phone",
|
||||
fieldEmail: "Email",
|
||||
},
|
||||
// Merchant validations (bar / lavazh) — the /setup/site panel, the merchant's
|
||||
// /validate screen, and the booth-modal discount lines. See validation-discounts.md.
|
||||
val: {
|
||||
// /setup/site
|
||||
sectionTitle: "Merchant validations",
|
||||
sectionHint: "An in-park merchant (bar / car-wash) scans the customer's ticket and grants a parking discount — payment and the receipt always stay at the booth.",
|
||||
enableBar: "Bar",
|
||||
enableLavazh: "Car wash",
|
||||
labelName: "Receipt label",
|
||||
labelNamePh: "e.g. Car wash — first hour free",
|
||||
mode: "Discount type",
|
||||
modeComp: "Parking fully free",
|
||||
modeTimeCredit: "First minutes free",
|
||||
modeFixed: "Amount off (typed at scan)",
|
||||
modePercent: "Percent off",
|
||||
minutes: "Free minutes",
|
||||
percent: "Percent (%)",
|
||||
maxAmount: "Cap per validation",
|
||||
maxPerDay: "Max validations per day (blank = unlimited)",
|
||||
users: "Validating users",
|
||||
usersHint: "Only the selected users (whose role grants validation:create) can apply this program from their device.",
|
||||
noUsers: "No users in the system — create one under Users.",
|
||||
saved: "Saved.",
|
||||
// /validate (the merchant screen)
|
||||
title: "Ticket validation",
|
||||
scanPrompt: "Scan or type the ticket number",
|
||||
lookup: "Look up",
|
||||
entry: "Entry:",
|
||||
notFound: "No ticket found with this number.",
|
||||
closed: "The ticket is closed (exited or voided).",
|
||||
subscription: "This is a subscriber entry — not validatable.",
|
||||
amountLabel: "Discount amount",
|
||||
amountHint: "max {{max}}",
|
||||
apply: "Apply validation",
|
||||
applied: "Validation applied.",
|
||||
existing: "Validations on this ticket",
|
||||
voided: "voided",
|
||||
used: "used in a payment",
|
||||
void: "Void",
|
||||
confirmVoid: "Void this validation?",
|
||||
noPrograms: "You have no validation program bound to you — contact the administrator.",
|
||||
// booth pay modal / receipts
|
||||
gross: "Fee",
|
||||
discount: "Discount",
|
||||
},
|
||||
users: {
|
||||
title: "Users",
|
||||
add: "+ Add user",
|
||||
|
||||
Reference in New Issue
Block a user