# 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.