# F-154 — Architect Evidence

## Feature
F-154 — Admin: separate customers from internal users (fix, high/high).

## Problem
Two admin list endpoints both returned **all** `identity_users`, so the Customers and Users
pages showed storefront customers and backoffice staff mixed together:
- `GET /users` (module `users`, `PgProfileRepository.listCustomers`) → all users, no role filter.
  Consumed by the Customers page (`customersApi.list` → `/api/users`).
- `GET /admin/users` (module `security`) → all users unless `?role=` supplied.
  Consumed by the Users page (`adminUsersApi.list` → `/api/admin/users`).

## Roles (DB `identity_users.role`, `citext NOT NULL DEFAULT 'customer'`)
- `customer` → storefront client.
- `admin` / `editor` / `pos_cashier` / `pos_manager` → internal / backoffice staff.

## Decision
Force the separation in the **backend** (single source of truth), not client-side:
1. `listCustomers` (→ `GET /users`) now always appends `AND iu.role = 'customer'` (code literal,
   no user input → no injection). The `?q` search still applies on `email`. Single-user
   `findCustomerById` (`/users/:id`) is untouched (owner-or-admin, role-agnostic).
2. `GET /admin/users`: base condition `role <> 'customer'` (literal) so the Users list NEVER
   returns storefront customers; `?role=admin|editor` still narrows within internal staff.
   `?role=customer` resolves to an empty intersection (still no leak).
3. Frontend: remove the `customer` option from the Users page role dropdown (UX polish — backend
   already enforces the boundary). Customers page unchanged (already calls `/users`).

## Alternatives considered
- Client-side filtering only: rejected — backend is the trust boundary; the admin API must
  not leak customers through `/admin/users`.
- New `/customers` endpoint: rejected — the frontend already binds Customers↔`/users` and
  Users↔`/admin/users`; splitting now duplicates effort with no behavioral gain.

## Boundary / security
- `users` module references `identity_users` only as a SQL table name (existing pattern in
  `search`); no TypeScript import crosses the identity↔users↔security boundary.
  `lint:boundaries` unaffected.
- No migration (`identity_users.role` already exists, NOT NULL DEFAULT 'customer').
- User input (`q`, `role`) stays parameterized (`$N`); the role constants are code literals.

## Test plan (no DB required → runs in `npm test`)
- `users/infrastructure/pg-profile-repository.test.ts`: mock `pg.Pool`, assert `listCustomers`
  emits `iu.role = 'customer'`, respects `q`, count === select filter, returns only customer rows.
- `security/api/security.routes.test.ts`: register routes on a mock Fastify with a mock pool +
  mocked `authenticate` returning `admin`; assert `GET /admin/users` queries `role <> 'customer'`
  and that `?role=admin` adds `AND role = $1`; response excludes `customer` rows.
- `app/tests/users.itest.ts` AC2/AC3: update assertion — `/users` returns the customer (ben),
  not the promoted admin (ana).

## Risk
Medium-high: changes admin list semantics (`/users` now customer-only). Mitigations:
`/users/:id` (single) unchanged; only the LIST contract changes; existing itest updated;
no migration; boundary/lint verified.
