# Customer Management — Existing Architecture Discovery (Phase 5B)

Documented before any schema change. **Increment A makes NO schema changes** — it builds the directory,
profile overview and search on top of the existing structures below. Later increments extend additively.

## Tables

### `customers` (migration `2026_10_27_120000`)
Tenant-authoritative directory; **tenant-owned** (shared across the tenant's locations), separate from any
WordPress/storefront identity.

| Column | Notes |
| --- | --- |
| `id` (uuid, pk) | stable customer UUID — **never reassigned** |
| `tenant_id` (uuid) | from context, never client input |
| `type` | `individual` \| `business` (CHECK) |
| `status` | `active` \| `inactive` \| `blocked` (CHECK) |
| `is_guest` (bool) | anonymous online identity; **excluded from staff search by default** |
| `first_name`, `last_name`, `display_name`, `email`, `phone`, `notes` | optional identity/contact |
| `normalized_email` | **STORED generated** = `lower(email)` for a non-guest with an email, else NULL |
| `created_at`, `updated_at` | |

**Indexes:** `unique(tenant_id, id)` (composite-FK target); `unique(tenant_id, normalized_email)` (dedup —
a second named customer with the same email is impossible; guests/email-less carry NULL and never collide);
`index(tenant_id, status)`; `index(tenant_id, phone)`.

### `customer_addresses` (migration `2026_10_27_120001`)
Many addresses per customer. **International-ready**: `country` is ISO-3166-1 alpha-2; `region`/`postal_code`
are nullable (no Canadian assumptions). At-most-one default billing / default shipping is a **hard DB
guarantee** via STORED generated keys (`default_billing_key`/`default_shipping_key`) + unique indexes.
Composite FK `(tenant_id, customer_id)` → `customers(tenant_id, id)` makes cross-tenant references
structurally impossible. `RESTRICT` on delete (customers are status-managed, never hard-deleted in Phase 1).

### `orders.customer_id` (migration `2026_10_27_120002`)
Nullable FK linking an order to a directory customer. Orders **also** keep a denormalized snapshot
(`customer_name`/`customer_phone`/`customer_email`) — the **order-time truth**, preserved even if the
directory record later changes.

## Services & resources
- **`CustomerService`** — owns identity: `create` (dedup by email → 422), `update`, `find`, `findByEmail`
  (via `normalized_email`), `search` (name/email/phone LIKE; **guests excluded by default**), `findOrCreate`
  (reuse-or-create by email, used by storefront), and `resolveForOrder` (Phase 5A fix — resolve-or-create
  from an order snapshot by email/unique-phone, never name-only). All tenant-scoped, transactional, audited.
- **`CustomerAddressService`** — add/update/delete addresses + default billing/shipping invariants.
- **`CustomerResource`** — safe projection (identity + addresses). Never reachable via the storefront guard.
- **`CustomerController`** — staff API; reads `customers.view`, writes `customers.manage` (route middleware).

## Customer creation paths (all converge on `CustomerService`)
1. **Directory API** `POST /customers` → `create`.
2. **Staff order write** (`OrderWriteController::store`/`update`) → `resolveForOrder` links/creates from the
   snapshot (Phase 5A fix — closes the "returning customer not found" gap).
3. **Storefront checkout** → `findOrCreate` by email (guests create a guest row).
4. **Reconciliation command** `cacart:reconcile-order-customers` → links legacy snapshot-only orders.

## Isolation & safety already in place
- Tenant isolation via `BelongsToTenant` global scope on `Customer`/`CustomerAddress` (+ composite FKs).
- Guest rows isolated from staff search; storefront guard never reaches this controller.
- Permissions: `customers.view` / `customers.manage`. Financial data gated separately (`payments.view`).
- Audit logging on every customer write.

## What Increment A adds (no schema change)
- `GET /customers-directory` — the existing `search` enriched with **per-page** order/money aggregates
  (`order_count`, `last_order_at`, `outstanding_minor`) computed in **one grouped query** (no N+1); the
  money column is withheld server-side unless the caller has `payments.view`.
- `GET /customers/{id}` enriched with the same aggregates as profile `stats`.
- Sort (`display_name`/`created_at`) + `type` filter on the list.
- Frontend: Operations → Customers directory + tabbed profile (Overview + read-only addresses).

## Deferred to later increments (additive, backwards-compatible)
- B: structured multiple emails/phones + verification flags, richer address editor, POS reuse.
- C: order history + reorder (current prices/stock), authorized financial summary.
- D: groups, tags, **configurable custom fields**, business contact relationships, notes/activity/follow-ups,
  and a `reference_number` identity field with directory search.
- E: cross-channel consistency, privacy/export/retention, performance + full regression.
