Skip to content

fix(auth): migrate the six billing controllers onto @RequirePermissions() - #361

Closed
modernitconsultants wants to merge 1 commit into
TelivityAI:mainfrom
modernit-com-au:migrate-billing-controllers-to-permissions
Closed

modernitconsultants wants to merge 1 commit into
TelivityAI:mainfrom
modernit-com-au:migrate-billing-controllers-to-permissions

Conversation

@modernitconsultants

Copy link
Copy Markdown
Collaborator

Finishes the migration #340 started. That PR moved eighteen controllers off the legacy @Roles() decorator and deliberately left six behind — accounting, cashier, folio, house-account, notifications, payment — because they gated every route, read and write alike, with one identical realm-role list (admin, general_manager, front_desk, reservations, night_auditor, accounting), and no existing permission key matched that list without either widening or narrowing who can record a payment or post a charge.

The rule applied

Preserve what the realm-role gate was granting in practice, rather than narrow it silently. Concretely, reservations was already in the allow-list of every one of these six controllers, so the doc comment on that role — "reservations without cashier / folio posting" — described a split the code has never actually enforced. Only the newer permission catalog enforced it, and only by omission. Same for night_auditor and accounting, which were reachable through the old gate but hold none of the corresponding keys.

What's in it

  • Six controllers onto @RequirePermissions(), reusing existing keys (folios.manage, houseaccounts.manage, cashier.access, communications.manage) and adding one new key, accounting.manage, for the deposit / A-R / accounting-code routes, which had no key at all.
  • ROLE_DEFAULT_PERMISSIONS gains the keys front_desk, night_auditor, accounting and reservations were already being granted by the old list. admin and general_manager need nothing — they pick up a new key automatically via ALL_PERMISSIONS.
  • 0023_billing_write_access_grants.sql backfills those grants for properties that already have role_permissions rows, using the same every-property-with-RBAC cross-join as 0015. A property with no RBAC rows gets nothing, by design.
  • payment-legacy-seam.spec.ts asserted PaymentController's legacy @Roles() metadata. It now asserts the permission key — updated in the same commit that changes the controller, not separately.
  • billing-role-controllers-migration.spec.ts covers all six: asserts no @Roles() metadata is left behind and that the expected key is set. 13 cases.

One side effect, disclosed rather than buried

communications.manage is already held by revenue_manager for an unrelated existing route in groups.controller.ts. Reusing that key on notifications.controller.ts therefore also lets revenue_manager send guest SMS/WhatsApp/Telegram, which the old gate did not. Reusing the existing key seemed better than inventing a near-duplicate — happy to split it into its own key if you'd rather.

Validation, and one honest gap

Run against your ci.yml on this branch: pnpm lint (0 errors), pnpm typecheck (clean), the dependency builds, and the affected specs — billing-role-controllers-migration (13), permissions.catalog (12), payment-legacy-seam (4) and your own legacy-role-controllers-migration (31) — 60 passing.

I could not run the full suite: the build host has no Postgres or Redis, so the integration specs can't execute and scripts/sync-test-count.mjs can't produce a real count. Rather than leave the counts stale and have your --check catch it (as happened on #354, which you kindly fixed rather than bounced), I derived them: baseline 2222/266, permissions.catalog.spec.ts 10 → 12, payment-legacy-seam.spec.ts 4 → 4, plus a new 13-case file, giving 2237 tests / 267 files, and applied them with the script's own exported applyCounts and buildStatsDocument so the output is byte-identical to what a real run would write. If your CI disagrees, the --check step will say so and I'll correct it immediately.

Happy to split this into per-controller commits if that reviews better.

…ns()

TelivityAI#340 migrated eighteen controllers off the legacy @roles() decorator and
deliberately left six behind: accounting, cashier, folio, house-account,
notifications and payment. Those six gated every route -- read and write
alike -- with one identical realm-role list, so no existing permission key
matched it without either widening or narrowing who can record a payment or
post a charge.

This finishes the migration on the rule of preserving what the realm-role gate
was granting in practice, rather than narrowing it silently:

- Six controllers move onto @RequirePermissions(), reusing existing keys where
  they exist (folios.manage, houseaccounts.manage, cashier.access,
  communications.manage) and adding one new key, accounting.manage, for the
  deposit / A-R / accounting-code routes, which had none.
- ROLE_DEFAULT_PERMISSIONS gains the keys four system roles were already being
  granted by the old list: front_desk, night_auditor, accounting and
  reservations. In particular `reservations` was in the allow-list of every
  billing controller, so its doc comment ("reservations without cashier /
  folio posting") described a split the code never enforced.
- Migration 0023_billing_write_access_grants.sql backfills those grants for
  properties that already have role_permissions rows, using the same
  every-property-with-RBAC cross-join as 0015. A property with no RBAC rows
  gets nothing, by design.
- payment-legacy-seam.spec.ts asserted PaymentController's legacy @roles()
  metadata; it now asserts the permission key, updated in the same commit that
  changes the controller.
- billing-role-controllers-migration.spec.ts covers all six, asserting both
  that no @roles() metadata is left behind and that the expected key is set.

One side effect worth disclosing rather than burying: communications.manage is
already held by revenue_manager for an unrelated existing route in
groups.controller.ts, so reusing that key on notifications.controller.ts also
lets revenue_manager send guest SMS/WhatsApp/Telegram. Reusing the existing key
seemed better than inventing a near-duplicate, but it is easy to split if you
would rather.

README.md and docs/test-stats.json updated for the 15 added test cases and one
added file.
@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

Hey Charles — thank you again for #361. Really solid work on finishing the billing controller migration, the honest widenings, and the clear write-up.

I took your branch as the base for #362 rather than coming back with another round of small change requests on this PR — didn’t want to keep pinging you for follow-ups. Full credit is yours; 362 only adds a payments.refund split (so night auditor can post overnight without refunds), tightens the PaymentController narrative, and wires seed to the shared catalog so it can’t drift.

Sorry for the extra PR instead of iterating here — appreciated everything you put into this.

telivity-otaip pushed a commit that referenced this pull request Sep 8, 2026
Builds on #361 (Charles / modernitconsultants): billing controllers onto
@RequirePermissions, plus a payments.refund key so night_auditor can post
overnight without void/refund/correct or deposit refund. Seed now reads
ROLE_DEFAULT_PERMISSIONS from @telivityhaip/shared/permissions-catalog so
demo RBAC cannot drift from the API catalog.

Co-authored-by: ModernIT Consultants <charles@modernit.com.au>
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.

2 participants