Skip to content

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_type indicating 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:
    • Gateway-side payment records (LiqPay/Mono) — Payments. Wallet transaction is the in-app ledger; the gateway payment is the external bookkeeping; they coexist.
    • The booking being refundedBookings (refund_request.booking_id references back).

Entities

IDEntityRole
ENT-040WalletA per-(customer, currency) balance row
ENT-041Wallet transactionAppend-only ledger entry — every credit and debit
ENT-039Refund requestA 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:
    • Companies via wallet.customer_idcompany_customer.id.
    • Bookings via refund_request.booking_idbooking.id (UNIQUE).
  • This context is referenced by:
    • Bookings only indirectly — booking.wallet_debited is a flag, not an FK; the actual wallet movement is in wallet_transaction.

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_type is polymorphic — readers must branch on it to resolve the cause.