ποΈ PUT-1732: delete a workspace seat for good, once it is disabled - #3740
Conversation
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
6b96d3e to
2ba9468
Compare
2ba9468 to
de223f1
Compare
Terminology: "workspace" is now "team", everywhereThe feature said Applied to every commit rather than added on top, so each PR shows its own code already named correctly and no intermediate commit references a name a later one changes. All 13 commits still typecheck individually. The contract-bearing renamesThese were free to change now and expensive after release, which is why they went in this pass:
Plus Scoped, and checked"workspace" means other things in this repo. These were deliberately left alone, and verified untouched:
Audited three ways: every changed line mentions team or workspace (no collateral edits), no stutters or mangled words ( One thing not renamedThe git branch names still say |
de223f1 to
1973243
Compare
1973243 to
fbd43ad
Compare
fbd43ad to
1c731f9
Compare
A disabled account persists indefinitely. Removing it is an explicit request, never a timer, and it is refused on a live account with `account_must_be_disabled_first` β which puts a reversible step in front of the only irreversible operation in the feature. The audit row is written before `cascadeDelete` runs. The FKs are ON DELETE SET NULL and the `_keep` columns carry the identifiers, so the record of what was done survives the account it names. No second billing emit here: `cascadeDelete` already captures the seat and fires `team.account.deleted` through UserAccountService, and emitting again would close the storage charge twice. Disabling closed the per-account charge; this closes the storage one, and it is the only thing that does. There is no restore window, and none was wanted: the reversible step already exists earlier at disable, a disabled account costs only the bytes it holds so nothing pressures a hasty delete, and a restore promise means retaining data the team explicitly asked to be rid of. Published in rate-limits-and-quotas.md alongside the team-deletion note, since the two are easy to confuse and only one of them frees a seat. Closes PUT-1732.
1c731f9 to
8c939a9
Compare
Closes PUT-1732. Stacked on #3738.
The gap this closes
Phase 5's scope named hard delete, and it was the one piece missing.
cascadeDeleteexisted but appeared in the team path only as a provisioning rollback β there was no way for a workspace owner to remove a seat they no longer want. A disabled account persisted forever with no route out.The ordering is the substance
Deletion is refused unless the account is already disabled:
That puts a reversible step in front of the only irreversible operation in the feature, and means deletion cannot be reached by a single action on a live account. Disable first, then delete.
The audit row is written before
cascadeDeleteruns. The FKs areON DELETE SET NULLand the_keepcolumns carry the identifiers, so the record of what was done survives the account it names. Written after, it would name nothing.The billing emit that is deliberately absent
deleteMemberdoes not emitteam.account.deleted.UserAccountService.cascadeDeletealready captures the seat before the delete and emits it after β so a second emit here would close the storage charge twice.That was worth checking rather than assuming: the natural-looking implementation is
captureSeatForBillingβcascadeDeleteβemitSeatDeleted, which is exactly what the phase 3 wiring already does one layer down. The test asserts the count is 1, so a future reader who adds the "missing" emit gets told.Disabling closed the per-account charge; this closes the storage one, and it is the only thing that does.
No restore window
Considered and rejected, for three reasons: the reversible step already exists earlier at disable; a disabled account costs only the bytes it holds, so nothing pressures a hasty delete; and a restore promise means retaining data the workspace has explicitly asked to be rid of. Delete means delete. There is no retention timer either β with no file transfer, the account still holds the member's work, so a clock would destroy real data on a schedule nobody revisited.
Published in
rate-limits-and-quotas.mdnext to the workspace-deletion note, because the two are easy to confuse and only one of them frees a seat.Verification
Typecheck clean. Full backend suite 7887 passed | 26 skipped.
Falsified β each piece broken, the failing test confirmed, then restored:
refuses to delete a live accountkeeps the audit trail attributable after the account is goneFour tests total: the two above, one asserting the account is really gone and the deleted event fires exactly once, and an HTTP test covering the route wiring end to end (409 on a live account, 200 after disabling, row gone).
Not done here
The confirmation dialog β PUT-1732 step 2 says "separately confirmed" β is GUI, so it lands in phase 6 with
TabTeams(PUT-1740). The endpoint is unconditional today; the confirmation is a client-side gate, which is the right place for it but does mean the API will delete on a single call until that ships.