feat(F-187): completed feature
This commit is contained in:
91
work/artifacts/F-187/architect.md
Normal file
91
work/artifacts/F-187/architect.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user