92 lines
4.0 KiB
Markdown
92 lines
4.0 KiB
Markdown
# 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.
|