Skip to main content

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

ColumnTypeNotes
idINTEGER PKHelm-internal identifier
account_numberTEXTHuman-friendly account # (e.g., from AIM migration)
first_nameTEXTPII
last_nameTEXTPII
emailTEXTPII; nullable; UNIQUE when present
phoneTEXTPII; normalized to E.164 when possible
address_line_1, address_line_2, city, province, postal_codeTEXTPII
dobDATEPII; optional, used for age-gated services
is_businessINTEGER (0/1)Flag for commercial accounts
business_nameTEXTWhen is_business = 1
marketing_email_opt_inINTEGER (0/1)Default 0
marketing_sms_opt_inINTEGER (0/1)Default 0
ai_optoutINTEGER (0/1)Default 0; see ADR-0010
popup_noteTEXTFree text shown when customer is loaded ("don't accept their cheques")
deletion_requested_atTEXTTimestamp of erasure request
deleted_atTEXTTimestamp of erasure execution
created_at, updated_atTEXTAudit timestamps
  • customer_bikes — the customer's bikes on record. See customer-bike entity.
  • customer_notes — structured notes, separate from popup_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

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 removed to kept
  • Concatenates names/email/phone if kept has nulls
  • Audit-logs the merge with both row IDs
  • Tombstones the removed row (does not DELETE; sets merged_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_tickets not in dropped_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_at timestamp
  • Audit-logs the action

See data ownership.

Migrated from AIM

For Swicked: 2,784 customer records from AIM's cust table:

  • cust.cust_pkcustomers.account_number
  • cust.fname/lnamecustomers.first_name/last_name
  • AIM's general_notes blob is NOT migrated to customers.popup_note — instead, parsed into structured customer_notes rows where possible, with the raw text preserved in customers.legacy_notes for 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

See also