Backend half of PAR-209 (economy-explorer admin firm tool). treasury-rest-api is the ledger-authoritative tier and already touches both Treasury accounts (AccountMapper) and the business firm/firm_accounts tables (FirmMapper), plus the idempotent POST /api/v1/transfers — so the disband/rename orchestration belongs here, called by the explorer.
JwtTokenVerifier/JwtAuthFilter, signed by jwt-secret, minted by AuthService). The token model is VerifiedToken(keyId, ownerUuid, keyType, accountId, firmId) — there is no admin/scope/role claim, and write endpoints are firm-scoped (/firms/me); POST /transfers moves money only from the token's own account. So no current credential can disband an arbitrary firm.AccountController is read-only: balance / by-player / transactions).lib/jwt.ts only mirrors AuthService.buildJwt to mint user keys). So the explorer→rest-api call path + credential must be built from scratch.scope:["admin"] or a keyType=SERVICE) to the minted JWT + VerifiedToken + verifier, and gate the new endpoints on it (reject normal firm/owner keys). Keep it distinct from PERSONAL/BUSINESS/GOVERNMENT keys.FirmServiceImpl faithfully — see PAR-209 for the exact flow):
POST /api/v1/admin/firms/{firmId}/disband — resolve-or-create proprietor PERSONAL account; per firm account (firm_accounts WHERE removed_at IS NULL): transfer positive balance → proprietor personal (reuse the idempotent transfer service, requester/authorizer = proprietor, reason "Firm disbanded"), archive the Treasury account, soft-delete the firm_accounts link; then UPDATE firm SET is_archived=1, default_account_id=NULL. Return a per-account result (amount swept, dest account, idempotency key) for the caller's audit. Idempotent + resumable (skip already-archived/removed accounts).POST /api/v1/admin/firms/{firmId}/rename — name rules from the plugin: length 2–32, regex [A-Za-z0-9 _.\-]{2,32}, no leading digit, case-insensitive uniqueness (allow case-only self-rename); UPDATE firm SET display_name=?.AccountMapper (mark accounts.is_archived), not raw balance writes.Once (1) lands, provisioning is mechanical and per-env (no secret values need printing):
jwt-secret (application-{uat,prod}.yaml → k8s app-secrets/equivalent in the treasury-rest-api dev & prod namespaces). Mint one long-lived admin-scoped service token per env, signed by that env's jwt-secret, persisted as an api_key row with the admin scope (so it's revocable/rotatable like any key, via the existing AuthService admin rotate).development ns, prod → prod ns), e.g. TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL (the in-cluster rest-api service URL per env). economy-explorer gains a small server-only rest-api client (admin actions only) that sends Authorization: Bearer <token>.AuthService.adminForceRotate(keyId); re-write the k8s secret; restart/rollout the explorer.⚠️ Sequencing: the admin scope must exist and validate first — minting/storing a privileged service token before (1) ships would put an unvalidated prod credential in place with nothing enforcing its scope. Provision the secrets in the same change that adds + gates the endpoints, across dev and prod together.
Sibling: PAR-209 (economy-explorer UI + orchestration + explorer_audit).
Code built, tested, pushed to develop (treasury-rest-api 7915c24, economy-schema 2d78712):
JwtTokenVerifier; admin endpoints gate on it.POST /api/v1/admin/firms/{firmId}/disband and /rename — faithful to the plugin flow; disband is a single atomic DB transaction (no orphan risk), idempotent, 422 if a positive balance has no proprietor personal account.executeTransfer core extracted from TransferService (existing path unchanged).@Hidden → excluded from OpenAPI/Swagger (verified by AdminFirmDocsHiddenIT).SERVICE enum value.AdminFirmServiceIT + AdminFirmDocsHiddenIT; full suite green.Remaining (deferred — the provisioning judgement call):
workflow_dispatch) before the SERVICE scope validates.jwt-secret, api_keys row with key_type='SERVICE'), store as a k8s secret (TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL) consumed by economy-explorer in dev + prod.Dev provisioning blocked (infra): applying V15 to dc-test-server via the Flyway Migrate workflow fails at No database found to handle <url> — the dc-test-server FLYWAY_URL GitHub Environment secret is malformed (no valid jdbc: scheme prefix; confirmed it's not a driver issue — fails with both MySQL+MariaDB drivers present). The jdbc:mysql:// prod targets (democracycraft/statecraft) are unaffected. Fix = correct the dc-test-server FLYWAY_URL env secret to a full JDBC URL (cf. the dev app's working jdbc:mariadb://<host>/economy). Once fixed, re-run Migrate(dc-test-server) then mint the dev SERVICE token + k8s secret.
Dev provisioning complete (dc-test-server). Applied V15 by running Flyway locally against the in-cluster dev DB (mariadb.minecraft) via port-forward (the dc-test-server GitHub-env FLYWAY_URL is still malformed and should be fixed separately — also brought dev current from V10→V15). Minted a SERVICE token (api_keys key_id=3, no acc/firm) signed with the dev JWT_SECRET, stored as k8s secret economy-explorer-treasury-admin (development ns: TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL=http://treasury-rest-api.development.svc.cluster.local). Verified against the live dev rest-api (already on image sha-7915c24): SERVICE token → 404 FIRM_NOT_FOUND (auth+scope OK), no token → 401, and /v3/api-docs contains 0 admin/firms (hidden). Deployment env-var wiring deferred to PAR-209 (no consumer yet). Prod (democracycraft/statecraft) not done — awaiting go-ahead.
Dev end-to-end verified through the deployed rest-api (sha-7915c24) with the provisioned SERVICE token, against a seeded throwaway firm: disband → 200, swept 500.00 firm→proprietor-personal via the ledger (real account_balances_mat trigger applied: firm acct 0.00, personal 500.00), firm account archived, firm↔account link soft-deleted, firm archived; rename → 200 (persisted); duplicate name → 409 NAME_TAKEN; leading-digit name → 400 INVALID_BODY. All test rows cleaned up (0 residual). Dev provisioning + functionality fully confirmed.
Prod rollout complete — all environments provisioned + verified.
develop→main: economy-flyway #6 (V15) and treasury-rest-api #15 (admin endpoints); prod deployed production-sha-ed89ba1 (ArgoCD treasury-rest-api-production Healthy, both networks 2/2).SERVICE enum) live on dev + both prod DBs.economy-explorer-treasury-admin (development), prod economy-explorer-{democracycraft,statecraft}-treasury-admin (production) — each TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL./v3/api-docs 0 admin/firms (hidden). Dev also passed a full disband/rename functional run (balance swept via ledger) with cleanup.Deployment env-var wiring into the economy-explorer Deployments is intentionally left to PAR-209 (the consumer). Separate infra nit: dc-test-server FLYWAY_URL GitHub-env secret is malformed (worked around via local Flyway run) — fix for future dev workflow deploys.
Sibling/parent: PAR-209 — the economy-explorer admin firm-management tool that consumes these endpoints. Full plugin control-flow to replicate is documented there.