Customer
The end-customer of the bike shop. Buys parts, services bikes, rents, trades-in, opts in to marketing. Every meaningful action in Helm references a customer (or the implicit "walk-in" customer).
Drafted from planning · v0.1
Table: customers
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | Helm-internal identifier |
account_number | TEXT | Human-friendly account # (e.g., from AIM migration) |
first_name | TEXT | PII |
last_name | TEXT | PII |
email | TEXT | PII; nullable; UNIQUE when present |
phone | TEXT | PII; normalized to E.164 when possible |
address_line_1, address_line_2, city, province, postal_code | TEXT | PII |
dob | DATE | PII; optional, used for age-gated services |
is_business | INTEGER (0/1) | Flag for commercial accounts |
business_name | TEXT | When is_business = 1 |
marketing_email_opt_in | INTEGER (0/1) | Default 0 |
marketing_sms_opt_in | INTEGER (0/1) | Default 0 |
ai_optout | INTEGER (0/1) | Default 0; see ADR-0010 |
popup_note | TEXT | Free text shown when customer is loaded ("don't accept their cheques") |
deletion_requested_at | TEXT | Timestamp of erasure request |
deleted_at | TEXT | Timestamp of erasure execution |
created_at, updated_at | TEXT | Audit timestamps |
Related tables
customer_bikes— the customer's bikes on record. See customer-bike entity.customer_notes— structured notes, separate frompopup_note. One row per note; deletable individually.customer_marketing_prefs— consent records (when they opted in/out, via what channel).customer_tags— free-form tags applied to the customer (e.g., "VIP", "needs follow-up").
Behaviors
Search
Implemented by GET /api/customers?q=.... Smart search:
- Queries < 4 chars match only structured fields: account number, full first or last name
- Queries ≥ 4 chars also match email, phone fragments, address fragments
- Prevents "tom" matching "cusTOMer" or every record with the substring
Merge
Two customer records can be merged into one (POST /api/customers/{kept}/merge?from={removed}). The merge:
- Reassigns all FKs (bikes, tickets, transactions, notes) from
removedtokept - Concatenates names/email/phone if
kepthas nulls - Audit-logs the merge with both row IDs
- Tombstones the
removedrow (does not DELETE; setsmerged_into_id)
Reversibility is via the audit log; an explicit "unmerge" operation is not provided.
Delete
DELETE /api/customers/{id}:
- Blocked if customer has any
service_ticketsnot indropped_off - Blocked if customer has any
transactions - Blocked if customer has any
customer_bikes - If none: hard DELETE row, audit-logs it
- If any blocker: returns 409 with the blocker list; UI shows "Cannot delete; remove these first"
Erasure (data subject right)
Separate from delete. Erasure:
- Scrubs PII fields (name → "Deleted Customer", email/phone/address → NULL)
- Keeps the row and its ID (so references stay valid)
- Updates
deleted_attimestamp - Audit-logs the action
See data ownership.
Migrated from AIM
For Swicked: 2,784 customer records from AIM's cust table:
cust.cust_pk→customers.account_numbercust.fname/lname→customers.first_name/last_name- AIM's
general_notesblob is NOT migrated tocustomers.popup_note— instead, parsed into structuredcustomer_notesrows where possible, with the raw text preserved incustomers.legacy_notesfor the operator to review - Phone/email/address fields mapped 1:1 with normalization
See migration from AIM.
In-situ editing surface
On the customer profile screen, in edit mode:
- Identity fields (name, phone, email) → click to edit inline
- Address → expandable section with each line editable
- Marketing pills → click to toggle opt-in/out (also clickable in view mode for fast change)
- Popup note → click to edit inline
- AI opt-out toggle → small pill, clickable
- Bikes on record → drag to reorder, × to remove, + to add
- Custom fields → add/remove tag chips