Wallet
Purpose
A customer’s prepaid money at a company. They top it up (via gateway), they spend it on bookings, they sometimes get refunded.
- Wallet — one balance per
(customer, currency). Negative balances are forbidden by a CHECK constraint. - Wallet transaction — append-only ledger of every credit and debit, with
source_typeindicating what caused the movement (top-up, booking pay, refund, manual adjustment). - Refund request — workflow for converting a paid booking back into wallet balance (UNIQUE per booking).
Bonus points (not money) live on Company customer as bonusBalance (integer); that is a CRM concept, not wallet.
Boundaries
- In: balances, transactions, refund-request workflow.
- Out:
Entities
| ID | Entity | Role |
|---|---|---|
| ENT-040 | Wallet | A per-(customer, currency) balance row |
| ENT-041 | Wallet transaction | Append-only ledger entry — every credit and debit |
| ENT-039 | Refund request | A pending or settled refund tied to a single booking |
Backend implementation
- Module:
libs/features/wallet/ - Surface routing: client surface dominates — top-up flow, transaction list, refund request submission. No admin-side wallet CRUD by default (admins can see balances via customer views).
- Drizzle schema:
libs/shared/data-access-db/src/lib/schema/wallet.schema.ts(pgSchema('wallet')).
Cross-context relationships
- This context owns the
wallet.*Postgres schema. - This context references:
- This context is referenced by:
- Bookings only indirectly —
booking.wallet_debitedis a flag, not an FK; the actual wallet movement is inwallet_transaction.
- Bookings only indirectly —
Open questions
- A
CHECK (balance >= 0)invariant prevents direct overdraw — confirm that the booking-pay path always acquires a lock before debiting to avoid concurrent over-spend. wallet_transaction.source_typeis polymorphic — readers must branch on it to resolve the cause.