feat(F-187): completed feature

This commit is contained in:
chattie
2026-08-22 22:23:44 +02:00
parent a3f6edd325
commit ca12f46bff
22 changed files with 1053 additions and 59 deletions

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

View File

@@ -0,0 +1,12 @@
# F-187 — Documentation
Updated `docs/pos/POS_CHECKOUT.md` with:
- cashier active/inactive/deleted semantics;
- session revocation and fresh-login behavior;
- irreversible soft deletion and historical-attribution guarantee;
- mandatory cash-session close before removal;
- admin lifecycle API endpoints and role restrictions;
- audit event behavior.
Removed F-187 from the future-work list now that the lifecycle is implemented.

View File

@@ -0,0 +1,36 @@
# F-187 — Implementer evidence
## Delivered
- Added reversible migration `055_pos_cashier_lifecycle.js` with active/deactivated/deleted account lifecycle, consistency constraint and cashier status index.
- Extended backoffice domain roles to include POS manager/cashier and lifecycle fields.
- Blocked inactive/deleted accounts in both credential lookup and live session authentication.
- Made cash-session opening transactional and serialized against lifecycle mutation using the same `backoffice_users ... FOR UPDATE` row lock.
- Extended admin-only POS user list with lifecycle status.
- Added admin-only cashier activate/deactivate and soft-delete endpoints.
- Lifecycle mutations reject open cash sessions, revoke every live session, preserve the cashier row/UUID and write an atomic security audit event.
- Added TPV admin cashier UI for create, status display, confirmed deactivate/reactivate and irreversible deletion.
- Added real PostgreSQL integration coverage in `pos-cashier-lifecycle.itest.ts`.
## Validation
- Backend typecheck: PASS.
- Admin typecheck: PASS.
- Backend production build: PASS.
- Admin production build: PASS (only pre-existing Turbopack upload tracing warnings).
- Targeted ESLint for every changed TypeScript/TSX file: PASS.
- Prettier for every changed source/migration/test file: PASS.
- POS unit tests: 12/12 PASS.
- Full unit suite without DB: 268/268 PASS.
- F-187 PostgreSQL integration: 4/4 PASS.
- Full real-PostgreSQL sequential suite: 354/354 PASS across 79 files.
- Migration fresh up / second no-op / full down / re-up: 4/4 PASS.
- `git diff --check`: PASS.
- `./scripts/verify.sh`: PASS.
## Repository baseline notes
- Full backend lint remains red on 9 pre-existing errors in thumbnail script, log broadcaster, an old POS test, and reporting files; all F-187 changed files pass targeted ESLint and Prettier.
- Boundary check retains the single pre-existing security-module import violation; F-187 adds no module-boundary violation.
- Full admin lint has zero errors and 23 pre-existing warnings.
- Existing untracked upload JPGs were not touched.

View File

@@ -0,0 +1,20 @@
{
"feature_id": "F-187",
"agent": "leader",
"stage": "close",
"verdict": "APPROVED",
"checks": [
{ "item": "reviewer/security/qa gates APPROVED", "ok": true },
{ "item": "354/354 tests with real PostgreSQL sequential", "ok": true },
{ "item": "migration up/no-op/down/up", "ok": true },
{ "item": "backend and admin typecheck/build", "ok": true },
{ "item": "targeted changed-file lint and formatting", "ok": true },
{ "item": "verify.sh final exit 0", "ok": true },
{ "item": "operator/API documentation updated", "ok": true }
],
"issues": [],
"notes": [
"Unrelated upload JPGs are excluded from the feature commit.",
"Global backend lint debt and one security boundary violation predate F-187; changed files are clean."
]
}

View File

@@ -0,0 +1,33 @@
{
"feature_id": "F-187",
"agent": "qa",
"stage": "qa_gate",
"verdict": "APPROVED",
"acceptance": [
{ "id": 1, "criterion": "Admin TPV lists cashier lifecycle status", "ok": true, "evidence": "GET /pos/users real-DB assertion and admin status badges" },
{ "id": 2, "criterion": "Admin creates active cashier", "ok": true, "evidence": "Fixed-role create form and server default/response active" },
{ "id": 3, "criterion": "Deactivate and reactivate non-deleted cashier", "ok": true, "evidence": "PATCH lifecycle integration assertions" },
{ "id": 4, "criterion": "Deactivate revokes and blocks current/future auth", "ok": true, "evidence": "DB revoked_at, old-cookie 401 and login 401 assertions" },
{ "id": 5, "criterion": "Confirmed deletion is irreversible", "ok": true, "evidence": "Explicit UI warning, DELETE 204 and reactivation 409" },
{ "id": 6, "criterion": "Open cash session blocks removal", "ok": true, "evidence": "PATCH and DELETE both return POS_CASHIER_HAS_OPEN_SESSION" },
{ "id": 7, "criterion": "Historical attribution survives deletion", "ok": true, "evidence": "Post-delete join keeps cashier ID through pos_cash_sessions" },
{ "id": 8, "criterion": "Admin-only and cashier-role-only", "ok": true, "evidence": "Cashier list 403 and manager target 404" },
{ "id": 9, "criterion": "Migration reversible with active default", "ok": true, "evidence": "Fresh/no-op/down/re-up 4/4 and inserted accounts default active" },
{ "id": 10, "criterion": "Regression and builds green", "ok": true, "evidence": "354/354 real-DB tests, typechecks, backend/admin builds, verify.sh" }
],
"regression": {
"unit_without_db": "268 passed",
"pos_unit": "12 passed",
"f187_real_postgresql": "4 passed",
"real_postgresql_sequential": "354 passed across 79 files",
"migration_cycle": "4 passed",
"backend_build": "passed",
"admin_build": "passed",
"verify": "passed"
},
"issues": [],
"notes": [
"Production admin UI was compile/type/lint validated; no browser automation harness exists for native confirm dialogs.",
"Backend global lint baseline remains red outside changed files; targeted F-187 lint is green."
]
}

View File

@@ -0,0 +1,23 @@
{
"feature_id": "F-187",
"agent": "reviewer",
"stage": "review_gate",
"verdict": "APPROVED",
"checks": [
{ "item": "Soft deletion preserves backoffice user UUID and historical joins", "ok": true },
{ "item": "Lifecycle check constraint permits only coherent active/inactive/deleted states", "ok": true },
{ "item": "Login lookup and live session authentication both reject unavailable accounts", "ok": true },
{ "item": "Deactivation/deletion revokes sessions in the lifecycle transaction", "ok": true },
{ "item": "Cash-session opening and lifecycle mutations serialize on the same user row lock", "ok": true },
{ "item": "Open cash sessions block deactivation/deletion", "ok": true },
{ "item": "Only admin can mutate and only pos_cashier rows can be targeted", "ok": true },
{ "item": "Lifecycle audit event is atomic with each successful mutation", "ok": true },
{ "item": "Admin UI exposes explicit statuses and confirmations", "ok": true },
{ "item": "Migration, integration regression, typecheck and builds pass", "ok": true }
],
"issues": [],
"notes": [
"Deleted cashier email is intentionally retained and remains unique to keep historical receipts human-readable; recreating the same address is not supported.",
"Manager/editor/admin lifecycle remains out of F-187 scope."
]
}

View File

@@ -0,0 +1,29 @@
{
"feature_id": "F-187",
"agent": "security",
"stage": "security_gate",
"verdict": "APPROVED",
"checks": [
{ "item": "Cashier list/create/status/delete require admin role", "ok": true },
{ "item": "Mutation lookup is restricted to pos_cashier targets", "ok": true },
{ "item": "Every lifecycle input and UUID is schema validated", "ok": true },
{ "item": "All lifecycle SQL uses bound parameters", "ok": true },
{ "item": "Deactivation/deletion revokes all live sessions atomically", "ok": true },
{ "item": "Authenticator independently rejects inactive/deleted users", "ok": true },
{ "item": "Credential lookup preserves generic anti-enumeration failure", "ok": true },
{ "item": "Cash-session opening race is serialized by row lock", "ok": true },
{ "item": "Open-register guard prevents abandoning accountable cash", "ok": true },
{ "item": "Security audit events record actor/action/target atomically", "ok": true },
{ "item": "Changed-diff secret scan", "ok": true },
{ "item": "Backend and admin production dependency audit", "ok": true }
],
"dependency_audit": {
"backend": "0 vulnerabilities",
"admin": "0 vulnerabilities"
},
"issues": [],
"notes": [
"Soft deletion intentionally retains cashier email for human-readable historical attribution; this is account removal, not a personal-data erasure workflow.",
"No user-supplied audit metadata is accepted."
]
}