API (api-v2)
Discounts
Catalogue discounts (automatic or by code) are resolved by a pure function at pricing time. Manual till discounts are a separate path, gated by a staff PIN.
Model
Tables:
discountdiscount_scope: rolequalifyingorrewarddiscount_bxgydiscount_localizationorder_discount_application,order_line_discount: snapshots on the order
| Dimension | Values |
|---|---|
| Trigger | automatic, code |
| Class | product, order, buy_x_get_y |
| Value | percentage, fixed amount (BXGY reward: free, percentage, or fixed amount) |
| Scope | category, section, product, variant. Empty means the whole catalogue. |
| Conditions | minimum subtotal, minimum quantity |
| Limits | usage limit, uses per order |
| Window | starts at, ends at. A paused flag overrides it. |
Routes
| Route | Guard |
|---|---|
get-discounts, get-discount, get-discount-localization | perm discounts.view |
create-discount, update-discount, delete-discount | wide discounts.write |
The dashboard derives status client-side: paused beats live, and expired beats paused.
Resolution
discount.resolve.ts is a pure function with no database access, so it's unit-tested directly.
- Product class: the best single discount wins per line. There's no stacking.
- Order class: split proportionally across lines.
- BXGY: competes with product discounts rather than stacking (
docs/adr/0001). - Line discounts apply before tax; the order-level discount applies after. See Pricing rules.
Not supported: free shipping, customer eligibility, one use per customer, combination rules, and loyalty (only a seam exists, in spec §9).
Manual till discounts
A manual discount (percentage or amount, plus a reason of up to 255 characters) needs a staff discount PIN:
| Route | Guard |
|---|---|
/merchant/staff/v1/get-staff-discount-pins, issue-staff-discount-pin, revoke-staff-discount-pin | owner |
/pos/v1/verify-discount-pin | pos.operate |
How the PIN check works:
- PINs are stored as HMACs derived from
JWT_SECRET, so rotating that secret invalidates every PIN. - Failed attempts are counted in
discount_pin_failure. After too many, verification returns 429 withRetry-After. verify-discount-pinreturns the approver's name for the till to display.
At sale time:
- The PIN is sent again with
create-orderand re-verified. - The approver's name is snapshotted onto the order.
On the till:
- The manual discount comes off last and is capped at the basket total.
- Re-applying an unchanged, already-approved discount doesn't prompt again.