Files
mercadodevida/work/artifacts/F-187/architect.md
2026-08-22 22:23:50 +02:00

4.0 KiB

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.