A staff-only firm administration tool in the economy explorer to disband and rename a firm from the UI, faithfully reproducing the business-rian plugin's control flow (balance sweep, account archival, firm archive, name rules). It must be a separate, deliberately-gated tool (not a button bolted onto the public firm page) because disband is destructive and moves money, and every action must be audited.
Disband sweeps balances and archives Treasury accounts — high blast radius. Keep it off the public/firm drilldown to avoid accidental/casual invocation: a dedicated /admin/firms surface, admin-role gated, with an explicit confirm step (mirroring the plugin's 60s confirmation token for the user path). Rename can live there too.
Disband — FirmServiceImpl.disbandInternal() (FirmServiceImpl.java:169–198), single @Transactional:
treasury.resolveOrCreatePersonal(proprietorUuid) (creates if missing).firm_accounts WHERE firm_id=? AND removed_at IS NULL.getBalanceByAccountId); if > 0, transfer the full balance → proprietor's personal account via TreasuryApi.transfer(TransferRequest) with reason "Firm disbanded", requester=authorizer=proprietor, source "BusinessPlugin";treasury.archiveAccount(accountId));UPDATE firm_accounts SET removed_at = CURRENT_TIMESTAMP …).UPDATE firm SET is_archived = 1, default_account_id = NULL, updated_at=….Rename — FirmServiceImpl.renameFirm() (FirmServiceImpl.java:270–286), @Transactional:
[A-Za-z0-9 _.\-]{2,32}, no leading digit (NameValidator/validateFirmName); reject MiniMessage/&<> (chat-injection safe).Acme→ACME).UPDATE firm SET display_name=? WHERE firm_id=?. No Treasury calls, no side effects. Note: Treasury account display names ("<name> Corporate Account") are not updated by the plugin either — they intentionally drift. ChestShops bind by account id (B:<base36>), not name, so rename is shop-safe.All money movement and Treasury-account archival must go through the Treasury authoritative path — never raw SQL against accounts/balances. The explorer (Node/Kysely) cannot call the Java TreasuryApi. So the money parts have to be delegated to the Treasury service tier.
Option A — orchestrate in treasury-rest-api, drive from the explorer (recommended).
treasury-rest-api already has DB access to both the Treasury accounts (AccountMapper, incl. resolve-personal-by-player) and the business firm/firm_accounts tables (FirmMapper), plus the idempotent POST /api/v1/transfers and PATCH /firms/me (display-name). Add admin-scoped endpoints there that replicate the plugin faithfully in Java, within proper Treasury transaction/idempotency boundaries:
POST /api/v1/admin/firms/{firmId}/disband → resolve-or-create proprietor personal, per-account transfer (idempotency-keyed) + archive + firm_accounts soft-delete, then firm archive. Returns a per-account result list for the audit log.POST /api/v1/admin/firms/{firmId}/rename (or reuse the firm-update path) with the 2–32 / regex / no-leading-digit / case-insensitive-uniqueness rules.VerifiedToken write endpoints are firm-scoped me; transfer is "from the token's account"). This is a sibling Treasury API issue dependency.
The explorer provides /admin/firms UI + a thin server action that calls these endpoints (server-side, admin-credentialed) and records explorer_audit. This keeps sensitive money logic in one Java tier instead of reimplementing it in TypeScript.Option B — orchestrate in the explorer backend (fallback). Explorer does the business-schema writes directly (it already writes MariaDB for groups/rate-limits/webhooks): firm_accounts soft-delete + firm archive + rename/validation; and calls treasury-rest-api per account for transfer + (new) archive + resolve-personal. More moving parts and a two-system partial-failure surface; only viable once rest-api exposes an admin transfer-from-arbitrary-account + archive.
Partial-failure / idempotency (applies to both): Treasury calls are external IPC and can't share a DB transaction with the firm-table writes — the plugin documents this exact orphan risk. The tool must therefore: process accounts one-by-one, use idempotency keys on every transfer (so a retry can't double-pay), be resumable (re-running disband on a partially-disbanded firm skips already-archived/removed accounts), and log every step so an interrupted run is reconcilable. Order matters: transfer → archive account → soft-delete link → (after all accounts) archive firm.
viewer.role === 'admin' (requireRole/requireAdmin), no anonymous/player variant.explorer_audit row per operation via audit()/auditView() (lib/audit.ts): target_type:'firm', target_id: firm id, method POST, path /admin/firms/{disband|rename}, actor + outcome. For disband, also log the per-account transfer/archive results (amount swept, destination personal account, idempotency key) so the money movement is fully traceable.firm: is_archived→1, default_account_id→NULL, display_name (rename), updated_at.firm_accounts: removed_at (soft-delete per link).accounts: archived (via Treasury service); balances moved via transfer only.explorer_audit: one+ rows per action./admin/firms (admin-only) can rename a firm with the exact plugin validation/uniqueness rules; non-admins get 403; case-only rename allowed.firm_accounts links, and archives the firm — matching the plugin outcome.explorer_audit trail (incl. per-account money movement for disband).Option A needs a Treasury API (treasury-rest-api) issue to add the admin-scoped disband/rename endpoints + an admin credential scope. File that as a sibling and link it.
Dev built + verified end-to-end. economy-explorer 565fbb3: new /admin/firms admin-gated tool (search → rename + type-to-confirm disband) calling the ledger-authoritative treasury admin API via a server-only client, each action audited (explorer_audit, target_type firm). tsc/lint/unit/build green. gitops: wired TREASURY_API_BASE_URL/TREASURY_ADMIN_TOKEN (from economy-explorer-treasury-admin) into the dev deployment (base overlay). Dev deploy rolled to 565fbb3 with the env. Verified from the dev explorer pod's own runtime: disband of a seeded test firm → HTTP 200, swept 320.00 firm→proprietor, account+firm archived (DB confirmed), cleaned up. Shipping to prod next (no prod e2e per request).
Shipped to prod. Release economy-explorer #24 merged → main e665fbd. Both prod instances rolled to production-sha-e665fbd (ArgoCD economy-explorer-production Healthy, 2/2 each) with the treasury admin credential wired (gitops: per-network TREASURY_API_BASE_URL/TREASURY_ADMIN_TOKEN from economy-explorer-{democracycraft,statecraft}-treasury-admin; dev wired via the base overlay). Per request, no e2e test in prod — verified deployment + env wiring + health only. Dev was fully e2e-verified (disband via the explorer's own credential → 200, ledger sweep confirmed). /admin/firms is now live (admin-gated) on dev + both prod networks.
Backend dependency filed: PAR-210 — admin-scoped firm disband/rename endpoints + service credential in
treasury-rest-api(the ledger-authoritative tier for Option A). PAR-209 is the explorer UI + orchestration +explorer_audit.