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
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | |
transaction_number | TEXT | Human-readable, e.g., S-N00001 for sale, R-N00001 for refund |
kind | TEXT | sale, refund, service_cashout, rental_charge, rental_deposit, gift_card |
customer_id | INTEGER FK → customers | NULL for walk-ins |
staff_id | INTEGER FK → staff | The cashier |
service_ticket_id | INTEGER FK → service_tickets | Set when kind='service_cashout' |
parent_transaction_id | INTEGER FK → transactions | Set for refunds; points to the original sale |
status | TEXT | pending, paid, voided, refunded_partial, refunded_full |
subtotal_cents | INTEGER | Sum of line items before tax/discount |
discount_cents | INTEGER NOT NULL DEFAULT 0 | Order-level discount |
gst_cents, pst_cents | INTEGER | Calculated per BC tax rules |
total_cents | INTEGER | Final amount paid |
tendered_cents | INTEGER | What the customer presented (for cash) |
change_cents | INTEGER | tendered - total for cash sales |
stripe_pi_id | TEXT | Stripe PaymentIntent ID if card |
stripe_idempotency_key | TEXT | The key used for the Stripe call |
at | TEXT | Timestamp of the transaction |
notes | TEXT | Free-text |
created_at, updated_at | TEXT |
Related tables
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': recordstendered_cents, computeschange_cents - Updates
inventory_skus.qty_on_handfor each line - Writes
audit_eventsentries - On success: updates status to 'paid', stores
stripe_pi_id
Refund
POST /api/sales/{id}/refund:
- Body:
{ amount_cents, reason, lines? } - Creates a new
transactionsrow withkind='refund',parent_transaction_id - Stripe: creates a refund against the original PaymentIntent
- Cash: records as cash returned
- Updates parent's status to
refunded_partialorrefunded_fulldepending 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
Search
GET /api/sales/search?q=...:
- Numeric: by
transaction_number - Date range:
from,toquery 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}_v1on first attempt{shop_slug}_txn_{id}_v2on 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_bikeflag set at sale time - Per-line GST + PST computed
- Order-level discount distributed proportionally before tax
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_pk→transactions.transaction_numberscsahd.sah_cust→customers.idvia 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)