# F-187 — Architecture

## Decision

Model cashier removal as an account lifecycle on `backoffice_users`; never delete the row referenced by POS history.

Migration `055_pos_cashier_lifecycle.js` adds:

- `active boolean NOT NULL DEFAULT true`
- `deactivated_at timestamptz NULL`
- `deleted_at timestamptz NULL`
- consistency check: deleted implies inactive; active implies no lifecycle timestamps
- index over POS cashier role/status

Existing accounts remain active. Down removes only lifecycle columns/index/check.

## Semantics

| Action | Result | Reversible | Sessions |
|---|---|---:|---|
| Deactivate | `active=false`, `deactivated_at=now()` | yes | revoke all |
| Reactivate | `active=true`, timestamps null | yes, unless deleted | remain revoked |
| Delete | `active=false`, `deleted_at=now()` | no | revoke all |

Delete is a tombstone rather than physical SQL deletion. The UUID and email remain available to historical receipt/session/reporting joins. Deleted cashiers are returned by the admin list with status `deleted`, but cannot be mutated again.

Only rows whose current role is `pos_cashier` can be targeted. Managers, editors and administrators remain out of scope.

## API

All endpoints use backoffice authentication and `requireRole(admin)`.

- `GET /pos/users`: existing endpoint gains `active`, `deactivatedAt`, `deletedAt`, `status`; remains POS staff list-compatible.
- `POST /pos/users`: existing creation contract; lifecycle defaults active.
- `PATCH /pos/users/:id/status` body `{ "active": boolean }`: deactivate/reactivate cashier.
- `DELETE /pos/users/:id`: irreversible soft deletion, HTTP 204.

Errors:

- `POS_CASHIER_NOT_FOUND` (404): target absent or not `pos_cashier`.
- `POS_CASHIER_HAS_OPEN_SESSION` (409): close register first.
- `POS_CASHIER_DELETED` (409): attempted status change on tombstone.
- `POS_CASHIER_ALREADY_DELETED` (409): repeat deletion.

Mutations use a transaction and lock the cashier row `FOR UPDATE`. They check open cash sessions before lifecycle mutation, revoke `backoffice_sessions`, and append `pos.cashier.deactivated`, `pos.cashier.reactivated` or `pos.cashier.deleted` to `security_audit_log` in the same transaction.

## Race safety

`PgCashSessionRepository.open` must also lock the target `backoffice_users` row inside its transaction and require `active=true AND deleted_at IS NULL` before inserting. This serializes cash-session opening against deactivation/deletion:

- open wins: lifecycle mutation sees the open session and returns 409;
- lifecycle wins: opening sees inactive/deleted and fails.

## Authentication

Defense in depth at both entry paths:

- `PgBackofficeUserRepository.findByEmail` only returns active, non-deleted accounts, so login gives the existing generic invalid-credentials response.
- `createBackofficeSessionAuthenticator` includes the same lifecycle predicate, so sessions are invalid even before revocation completes and after database restore/races.
- combined authentication inherits the backoffice check.

No account-state detail is exposed by login.

## Admin UI

Add a **Cajeros** section to the TPV admin page:

- create form (email/password) fixed to `pos_cashier`;
- table with email, status and creation date;
- active: Deactivate + Delete;
- inactive: Reactivate + Delete;
- deleted: no mutation actions;
- native explicit confirmations name the cashier and explain open-register/history behavior.

The admin page reloads cashier state independently from store-scoped terminal/payment configuration.

## Tests

PostgreSQL integration coverage:

1. migration defaults existing cashier active;
2. non-admin receives 403;
3. deactivation revokes sessions and blocks authentication/login lookup;
4. reactivation works without restoring revoked sessions;
5. open cash session blocks deactivation and deletion;
6. deletion keeps the same cashier row and historical `pos_cash_sessions.user_id` join;
7. deleted cashier cannot reactivate;
8. non-cashier target behaves as not found;
9. migration up/no-op/down/up remains green.

Targeted typecheck/build covers admin UI contract.
