# Customers — profile, addresses, documents

The signed-in customer surface. Every endpoint here requires a **customer
session**: `authorization: Bearer <supabase jwt>` (minted by the
passwordless flow — see [auth.md](auth.md) — or a client-side supabase
password sign-in) **plus** the `x-client-id` tenant header the storefront
always sends. Missing/invalid JWT → `401 {code: "unauthenticated"}` on every
route below.

Lazy registration: `/customers/me` resolves the customer by
(client_id, JWT email) and **creates the row on first authenticated call**
(has_account=true, `account_status` per the store's approval policy) — a
fresh login never 404s on "me".

`customer.account_status` (`pending` | `approved`) is on every customer
payload; when the store's B2B approval policy is on, hide B2B content until
`approved`.

> The executable blocks below prove the **auth boundary** (401 contract) —
> the docs harness is anonymous, so happy paths are shown as `jsonc` shapes
> and are pinned by `tests/contract/customers.contract.test.ts` and
> `tests/store/customer-accounts-*.test.ts` with real sessions.

## POST /api/store/customers — guest-to-registered conversion

- **Purpose**: bind a password sign-up to a customer row. Flow:
  `supabase.auth.signUp({email, password})` client-side → this call with the
  same email. NOT needed for the passwordless code flow (verify already
  lazy-creates the customer).
- **Auth**: Bearer JWT (+ `x-client-id`). Body email MUST equal the JWT
  email.
- **Request**:

```jsonc
// POST /api/store/customers
{
  "email": "maria@example.com",   // must match the session email
  "first_name": "Maria",           // optional
  "last_name": "Petrova",          // optional
  "phone": "+359888123456",        // optional
  "company_name": "Acme OOD"       // optional
}
```

- **Response**: `{customer}` — the full customer with `addresses` embedded
  (empty on first create). See the shape under `/customers/me` below.
- **Errors**: `401 unauthenticated` · `403 email_mismatch` (body email ≠
  session email) · `400 validation_failed`.
- **SDK**: `customers.createCustomer(client, input)`
- **Components**: account registration form (account pages family).
- **Settings**: store approval policy decides `account_status` of a NEW row
  (`pending` when B2B approval is required, else `approved`).

```bash
# No session → the documented 401 contract (the auth boundary, executable).
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/store/customers" \
  -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
  -d '{"email": "doc-'$RUN'@doc.test"}')
test "$STATUS" = 401
```

## GET /api/store/customers/me — the signed-in customer

- **Purpose**: hydrate account state after login / on account pages.
  Lazy-creates the customer row on first call for this (tenant, email).
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Request**: no params.
- **Response**:

```jsonc
{
  "customer": {
    "id": "uuid",
    "client_id": "uuid",
    "email": "maria@example.com",
    "first_name": "Maria",
    "last_name": "Petrova",
    "phone": "+359888123456",
    "company_name": null,
    "company_eik": null,            // Bulgarian company id (ЕИК) — invoice checkout
    "vat_number": null,
    "has_account": true,
    "account_status": "approved",   // "pending" | "approved" — B2B gating
    "tags": [],                      // admin-only labels; read-only here
    "metadata": null,
    "created_by": null,
    "created_at": "ISO-8601",
    "updated_at": "ISO-8601",
    "deleted_at": null,
    "addresses": [ /* CustomerAddress[], created_at asc — shape below */ ]
  }
}
```

- **Errors**: `401 unauthenticated` · `400 missing_client_id`.
- **SDK**: `customers.getMe(client)`
- **Components**: account dashboard / header account state.
- **Settings**: approval policy (`account_status` on lazy create).

```bash
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/customers/me" \
  -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 401
# The error envelope carries the stable code:
curl -s "$BASE/api/store/customers/me" -H "x-client-id: $CLIENT_ID" \
  | grep -q '"code":"unauthenticated"'
```

## POST /api/store/customers/me — update own profile

- **Purpose**: profile edit (names, phone, company/VAT master data).
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Request** (all optional; nullable fields clear with `null`):

```jsonc
{
  "first_name": "Maria",
  "last_name": "Petrova",
  "phone": "+359888123456",
  "company_name": "Acme OOD",
  "company_eik": "123456789",   // loose format — foreign B2B customers exist
  "vat_number": "BG123456789",
  "metadata": {}
  // "tags" are admin-only — the server STRIPS them from this body.
}
```

- **Response**: `{customer}` — the refreshed customer (shape above).
- **Errors**: `401 unauthenticated` · `400 validation_failed`.
- **SDK**: `customers.updateMe(client, input)`
- **Components**: account profile form; invoice-details step in checkout.
- **Settings**: `company_eik`/`vat_number` feed invoice-required checkout
  (order-documents).

## GET /api/store/customers/me/addresses — list addresses

- **Purpose**: the address book. Returns **every** address (created_at asc)
  — the envelope is nominal: `offset` is always 0 and `limit` equals
  `count`; no query params are read.
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Response**:

```jsonc
{
  "addresses": [
    {
      "id": "uuid",
      "client_id": "uuid",
      "customer_id": "uuid",
      "address_name": "Home",
      "first_name": "Maria",
      "last_name": "Petrova",
      "company": null,
      "address_1": "ul. Ivan Vazov 1",
      "address_2": null,
      "city": "Sofia",
      "country_code": "bg",          // stored lower-case
      "province": null,
      "postal_code": "1000",
      "phone": "+359888123456",
      "is_default_billing": false,
      "is_default_shipping": true,
      "metadata": null,
      "created_at": "ISO-8601",
      "updated_at": "ISO-8601",
      "deleted_at": null
    }
  ],
  "count": 1,
  "offset": 0,
  "limit": 1
}
```

- **Errors**: `401 unauthenticated`.
- **SDK**: `customers.listAddresses(client)`
- **Components**: address book; checkout address picker.

## POST /api/store/customers/me/addresses — create address

- **Purpose**: add to the address book. `is_default_billing` /
  `is_default_shipping: true` clears the flag on every sibling first (one
  default per kind).
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Request**: all fields optional — same keys as the address shape above
  (minus id/customer_id/timestamps). `country_code` is lower-cased.
- **Response**: `{customer}` — the **full refreshed customer** (addresses
  embedded), NOT the created address alone; find it in
  `customer.addresses`.
- **Errors**: `401 unauthenticated` · `400 validation_failed`.
- **SDK**: `customers.createAddress(client, input)`

```bash
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
  "$BASE/api/store/customers/me/addresses" \
  -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
  -d '{"city": "Sofia"}')
test "$STATUS" = 401
```

## GET /api/store/customers/me/addresses/:id — one address

- **Purpose**: read one owned address (edit-form hydrate).
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Response**: `{address}` — ONE address object (the only address
  endpoint that returns `{address}` instead of `{customer}`).
- **Errors**: `401 unauthenticated` · `404 not_found` — unknown OR another
  customer's address; ownership violations 404, they never 403.
- **SDK**: `customers.getAddress(client, addressId)`

## POST /api/store/customers/me/addresses/:id — update address

- **Purpose**: partial update; default flags clear siblings.
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Request**: same keys as create, nullable to clear.
- **Response**: `{customer}` — full refreshed customer.
- **Errors**: `401 unauthenticated` · `404 not_found` ·
  `400 validation_failed`.
- **SDK**: `customers.updateAddress(client, addressId, input)`

## DELETE /api/store/customers/me/addresses/:id — delete address

- **Purpose**: soft-delete an owned address.
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Response**: `{customer}` — full refreshed customer (address gone).
- **Errors**: `401 unauthenticated` · `404 not_found`.
- **SDK**: `customers.deleteAddress(client, addressId)`

## GET /api/store/customers/me/documents — my documents (invoices)

- **Purpose**: the account "Invoices" page — the customer's issued
  (non-void) order documents, `issued_at` desc. Ownership is INNER-join
  enforced (session customer id AND client_id) — cross-customer reads are
  impossible (adversarial-tested).
- **Auth**: Bearer JWT (+ `x-client-id`).
- **Request**: query `{limit? (≤200, default 20), offset?, doc_type?}`.
- **Response**:

```jsonc
{
  "documents": [
    {
      "id": "uuid",
      "order_id": "order_…",
      "order_display_id": 1042,
      "doc_type": "invoice",
      "number": "0000000042",
      "issued_at": "ISO-8601",
      "pdf_url": "https://…/invoice.pdf",
      "due_date": null,
      "paid_at": "ISO-8601"
    }
  ],
  "count": 1,
  "offset": 0,
  "limit": 20
}
```

- **Errors**: `401 unauthenticated` · `400 validation_failed`.
- **SDK**: `customers.listMyDocuments(client, query?)`
- **Components**: account documents/invoices list.
- **Settings**: order-documents issuing config (doc types, numbering).

```bash
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/customers/me/documents" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 401
```
