Skip to main content

Transaction

A single financial event. The unit of "the till just rang." Holds line items, applied taxes, payment methods, optional refund references.

Drafted from planning · v0.1

Table: transactions

ColumnTypeNotes
idINTEGER PK
transaction_numberTEXTHuman-readable, e.g., S-N00001 for sale, R-N00001 for refund
kindTEXTsale, refund, service_cashout, rental_charge, rental_deposit, gift_card
customer_idINTEGER FK → customersNULL for walk-ins
staff_idINTEGER FK → staffThe cashier
service_ticket_idINTEGER FK → service_ticketsSet when kind='service_cashout'
parent_transaction_idINTEGER FK → transactionsSet for refunds; points to the original sale
statusTEXTpending, paid, voided, refunded_partial, refunded_full
subtotal_centsINTEGERSum of line items before tax/discount
discount_centsINTEGER NOT NULL DEFAULT 0Order-level discount
gst_cents, pst_centsINTEGERCalculated per BC tax rules
total_centsINTEGERFinal amount paid
tendered_centsINTEGERWhat the customer presented (for cash)
change_centsINTEGERtendered - total for cash sales
stripe_pi_idTEXTStripe PaymentIntent ID if card
stripe_idempotency_keyTEXTThe key used for the Stripe call
atTEXTTimestamp of the transaction
notesTEXTFree-text
created_at, updated_atTEXT
  • transaction_lines — line items (SKU/variant or service-cat, qty, unit price, tax category, tax applied)
  • transaction_payments — multiple payment methods on one transaction (split tender)
  • transaction_refunds — refund details when a refund is partial

Behaviors

Create

POST /api/sales:

  • Body: { customer_id?, lines: [...], payment_method, payment_details? }
  • Inserts transactions (status='pending'), transaction_lines
  • If payment_method='card': creates Stripe PaymentIntent, processes on terminal
  • If payment_method='cash': records tendered_cents, computes change_cents
  • Updates inventory_skus.qty_on_hand for each line
  • Writes audit_events entries
  • On success: updates status to 'paid', stores stripe_pi_id

Refund

POST /api/sales/{id}/refund:

  • Body: { amount_cents, reason, lines? }
  • Creates a new transactions row with kind='refund', parent_transaction_id
  • Stripe: creates a refund against the original PaymentIntent
  • Cash: records as cash returned
  • Updates parent's status to refunded_partial or refunded_full depending on amount
  • Audit-logged

Void

POST /api/sales/{id}/void:

  • Only allowed if status is pending (Stripe charge didn't complete)
  • Updates status to voided; releases the held PaymentIntent

GET /api/sales/search?q=...:

  • Numeric: by transaction_number
  • Date range: from, to query params
  • Customer: customer_id
  • Status: status

Receipt generation

Receipts are PDFs generated on demand:

  • GET /api/sales/{id}/receipt.pdf — generates from transaction state
  • Cached for 30 days in R2 (regeneratable always)
  • Receipt template editable in-situ on a receipt preview screen (in-situ editing)

Idempotency

The Stripe call uses an idempotency key derived from the transaction ID + attempt number:

  • {shop_slug}_txn_{id}_v1 on first attempt
  • {shop_slug}_txn_{id}_v2 on operator-initiated retry
  • Network retries inside the Worker keep the same key

See ADR-0015.

Tax application

Per-line tax via src/lib/tax-bc.js:

  • Each line has a tax_category ('bike_new', 'bike_used', 'component', 'service_labour', etc.)
  • Each line also has a bundled_with_bike flag set at sale time
  • Per-line GST + PST computed
  • Order-level discount distributed proportionally before tax

See ADR-0020: BC tax rules.

Migrated from AIM

For Swicked: 17,996 transactions + 18,154 lines + 10,365 payment records from AIM's scsahd (sale header) + scsasld (line items) + payment tables. The migration maps:

  • scsahd.sah_pktransactions.transaction_number
  • scsahd.sah_custcustomers.id via account number
  • Line items 1:1
  • Tax fields recomputed via Helm's tax engine (verified against AIM totals)

In-situ editing surface

On the Sales screen, in edit mode:

  • Tab labels and order editable
  • Tile labels editable (underlying SKU stays mapped)
  • Frequent tab tile contents editable (+ Add SKU)

On a sale detail screen (post-completion), in edit mode (with permission):

  • Notes editable inline
  • Lines NOT editable post-sale (use refund flow for corrections)
  • Customer attachment editable (attach a customer to a walk-in sale post-hoc)

See also