Soft org delete, Compliance Delete
Epic ask (verbatim): "file an epic to create Customers section where operator can see all customers and fully manage their parameters (move all from Directory/Org) and fully manage all of their billing and products. Operator should be able to add/change/remove any products (without or with billing), see all history, etc. UI must be compliant with best practices and Jaah Standard (feed-like, tabbed pages, no prose on page, etc.)"
Settled inputs: PRD = Task 6049, Architecture = Task 6050 (`task_read` both; architecture §3.2 is your pinned contract). This is child **C2**. Uses C1's `customer_events`, C3's Overview slot (`pages/customers/OverviewTab.tsx`), C4's roster `GET /api/customers` and Customers list.
Requirement (operator decisions D9/D10, verbatim gist): D9 "Nothing is deleted forever. All is soft-deleted." D10 "Make org delete a soft delete" + "Add Compliance Delete on a soft-deleted org — confirmed with 2FA and leaves a record of who authorized it."
Requirement (verbatim, Req 8): "Every operator write is audited and authorized on the operator plane. A tenant can never reach these endpoints."
Acceptance:
- Delete org (`DELETE /api/orgs/{org_id}`) keeps its route, gate, `delete_refusal` preconditions and `_erase_billing`, then sets `deleted_at`/`deleted_by_id` instead of purging; audits `org_deleted`; 204.
- `POST /api/orgs/{org_id}/restore` (superadmin, operator plane) clears `deleted_at`; the org stays disabled; audit `org_restored`.
- `POST /api/orgs/{org_id}/compliance-delete` `{"code","execute"}`: 409 unless soft-deleted; `execute:false` = offboard census dry run; `execute:true` requires `mfa.verify_current_code(session, user, code)` with `allow_recovery=False` (un-enrolled operator refused "Set up 2FA first.") then runs the existing `offboard.execute` → `delete_org` purge unchanged. Surviving record: audit `org_compliance_deleted` in the operator's org carrying `authorized_by` (user id + email), `authorized_at`, TOTP outcome, offboard record. The old `POST /offboard` `execute:true` path folds into this route (one door to the purge); its dry run stays.
- A soft-deleted org is excluded from every org list/selector by default (`GET /api/orgs`, shell Orgs filter, pickers — enumerate `select(Org)` list sites) and refused sign-in/dispatch with an explicit `deleted` reason. Customers lists it only under filter **Deleted** (`status=deleted` on the roster); its page shows only Overview + History with actions Restore and Compliance Delete (TOTP dialog).
- Customer history survives a soft delete; Compliance Delete purges `customer_events` (OrgScoped) and the authorizer record survives.
- AC8.1 the new routes are operator-only; tenant 403/404. AC7.1/7.2 for the dialogs (no help prose, usable at 1440/390). AC8.2 audit rows (`mode: null`).
- ADR entry in `docs/adrs/`: "org delete is soft; Compliance Delete is the only purge, 2FA-authorized".
- Migration: `orgs.deleted_at`, `orgs.deleted_by_id` (additive; only migration in its wave).
Credentials & reach:
- Compliance Delete live check: a dev operator user you create in Directory (operator org, superadmin) and enrol in TOTP yourself, then remove; NEVER enrol the shared operator login (mem-8944) — verified (create + `is_superadmin` routes exist, `app/mfa.py` `verify_current_code` exists).
- Operator browser oracles: `jaah/debug/main/operator-login` on `https://app.dev.jaah.ai` — verified.
- API tests, screenshots, `./dc check full`: worktree only — verified.
Out of scope: Do NOT soft-delete any entity other than orgs (named follow-up only); do NOT change the `offboard.execute`/`delete_org` purge internals; do NOT change other customer tabs' bodies or the History UI (C8); tenant Settings → Organizations and tenant Billing pages unchanged; do NOT enrol the shared operator login in 2FA; no new permission keys for tenant roles; must not require act-as (Task 6047).
Done means:
- pytest: Delete on a disabled org → row remains with `deleted_at` set; it is absent from `GET /api/orgs`, every enumerated list/selector, and present only in `GET /api/customers?status=deleted` (leak test over the list sites).
- pytest: sign-in for a user of a soft-deleted org → refused with reason `deleted`.
- pytest: Restore → `deleted_at` cleared, org still disabled, audit `org_restored`.
- pytest: compliance-delete on a non-deleted org → 409; wrong/absent TOTP or un-enrolled operator → refused, nothing purged; valid TOTP → org purged, its `customer_events` gone, `org_compliance_deleted` row in the operator org with `authorized_by` + `authorized_at`; `POST /offboard` execute:true no longer purges.
- pytest: every new route as tenant Owner → 403/404.
- On dev after Jaah Deploy: create a throwaway org → disable → Delete (soft) → appears under Customers filter Deleted → Restore → Delete again → Compliance Delete with your own TOTP-enrolled temp operator → gone; the authorizer audit row is visible. Remove the temp operator user afterwards.
- AC7.2 — Playwright screenshots of the Deleted filter, the deleted org's page and the TOTP dialog at 1440 and 390 attached to the PR, each judged usable.
- `./dc check full` green; the PR's Jaah Deploy to dev succeeds.
Outcome
Soft org delete, Restore, 2FA Compliance Delete. PR #5286 merged, full gate green; dev Jaah Deploy 38347587542 green; dev oracle passed (soft delete → Restore → Compliance Delete by temp TOTP operator, authorizer row, temp user removed). UX follow-ups in report.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Ready
- Runner started
- Ready
- Runner started
- Runner ended (done)