Skip to content

fix: make ESP delete work and explain why a delete is blocked (#12) - #163

Merged
clbotdev merged 3 commits into
mainfrom
fix/12-esp-delete
Oct 29, 2026
Merged

clbotdev merged 3 commits into
mainfrom
fix/12-esp-delete

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Closes #12. Part of #152.

Root cause

The original redirect. When #12 was filed, needsTeamSelection() in apps/web/lib/tokens.ts treated every 409 as "pick a team". So when the API refused a delete with 409 delivery_source_in_use, the browser was sent to /teams. 81c0d72 narrowed that check to no_team/team_required, and tokens.test.ts already covers 409 delivery_source_in_use. On main the redirect no longer happens. I confirmed this in a browser before changing any code.

Delete was still broken in other ways, all reproduced on main:

  • Organization page: no shared mailbox could be deleted. The menu offered Retire for drafts, which the API always refused with invalid_lifecycle_transition. It offered Delete only for retired mailboxes, which the API also always refused (its documented rule is that only a never-activated draft is deleted). The toast then said the mailbox was "still assigned to a team" even when it was not.
  • Team settings: the team's live default ESP was deleted without a warning. If no other ESP existed, the team was left with no delivery source.
  • Every blocked delete returned the same code, delivery_source_in_use. That included an ESP with old, already-sent transactional email, which can never be deleted, and one with a delivery-feedback connection. The reason appeared in a banner at the top of the page, and the confirmation dialog closed.

Fix

A blocked delete now returns a 409 with a code that says why. Delivery history is never deleted.

code when what the user is told to do
esp_is_default the team's live (not retired) default pick another default, or retire it first
delivery_source_in_use a sending sequence or broadcast, or queued outbound/transactional email let the work finish
esp_has_delivery_history grants (usage history), a feedback connection (its delivery events cascade) or any transactional email retire instead
esp_retire_instead an organization ESP that has been activated retire instead (existing rule)
  • Retiring a draft: an ESP that was activated and later went back to draft (a credential edit does this) can now be retired. A never-activated draft is deleted instead.
  • Dashboard, team settings: the confirmation dialog stays open and shows the reason. There is a new Retire action (shadcn AlertDialog).
  • Dashboard, organization page: never-activated drafts offer Delete, anything that has been live offers Retire, and retired mailboxes have no menu.
  • MCP: new retire_esp tool, added to the parity manifest. delete_esp and delete_esp_config return the same reasons as the API.
  • OpenAPI: the descriptions of both delete operations list the 409 codes.
  • PostHog: new events esp_config_delete_blocked (with reason) and organization_esp_deleted. I added esp_id to PROPERTY_ALLOWLIST. It was missing, so esp_config_removed had been dropping it.
  • Docs: the API README and docs/organizations.md.

Tests

  • bun run lint, format:check and typecheck all pass.
  • API: 910 tests pass (--parallel). check:drift and test:conformance pass.
  • Web: 99 tests pass. Each package's tests pass.
  • New regression tests:
    • queries: default blocked, retired default deleted and the default moved, transactional history blocked, queued transactional blocked, organization draft vs activated, retiring an activated draft
    • routes: the body for each 409, plus the blocked-delete event
    • MCP: blocker messages and retire_esp
    • web: blocker messages, and the organization menu deletes drafts and retires live mailboxes

Browser e2e (Playwright, fresh DB, API :5150 / web :3150)

All 17 checks passed, with no navigation away from the page at any point:

  • Team: an unused draft ESP was removed.
  • Team: removing the default showed the inline "default delivery source" reason.
  • Team: an ESP with queued mail showed the "queued email" reason.
  • Team: an ESP with transactional history showed the "delivery history" reason. After Retire it was retired and its transactional row was kept.
  • Team: after retiring the default, Remove worked and the default moved to another active ESP.
  • Organization: a draft offered Delete and was deleted.
  • Organization: a live mailbox offered only Suspend and Retire, and was retired.
  • Organization: a retired mailbox had no menu, and the API returns esp_retire_instead for it.

…ory (#12)

Delete now answers 409 with a reason code (esp_is_default,
esp_has_delivery_history, esp_retire_instead, delivery_source_in_use)
instead of silently deleting a team's live default or reporting every
blocker as delivery_source_in_use. Activated drafts can be retired, MCP
gains retire_esp, and blocked/organization deletes go to PostHog.
#12)

Settings keeps the confirm dialog open with the reason a delete is
blocked and adds Retire. The organization page offered Retire for drafts
and Delete for retired mailboxes, both of which the API always refused;
it now offers Delete for never-activated drafts and Retire otherwise.
@clbotdev
clbotdev merged commit 18cf98d into main Oct 29, 2026
5 checks passed
@clbotdev
clbotdev deleted the fix/12-esp-delete branch October 29, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ESP delete button does not work

2 participants