Skip to content

feat: registerPage types, ability registry, member and invoice readers (module_api 1.3.0) - #8

Merged
gdarko merged 4 commits into
masterfrom
feat/page-route-abilities-members
Sep 16, 2026
Merged

gdarko merged 4 commits into
masterfrom
feat/page-route-abilities-members

Conversation

@gdarko

@gdarko gdarko commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Raises the module contract to module_api 1.3.0 with three additions the next batch of official modules needs.

1. registerPage and page route types (frontend/index.d.ts)

New PageRouteMeta, PageChildContribution, and PageContribution interfaces, plus
registerPage(contribution: PageContribution): () => void on InvoiceShelfExtensionApi.

A page mounts at /admin/modules/{module}/{path} where {module} is the module.json slug;
settings stays reserved for the host's schema-rendered settings page. meta.ability carries the
namespaced ability id(s) the host route guard checks, and children declares sub-routes relative to
the page.

2. Registry::registerAbility (src/Registry.php)

A new Registry::$abilities bucket (slug => namespacedId => entry) lets a module contribute its own
permissions to the host ability catalogue instead of reusing host ones. Entries are stored namespaced
as {slug}:{ability} so modules can never collide with host abilities or with each other:

Registry::registerAbility('sales-tax-us', [
    'ability' => 'view-filing',
    'name' => 'View Tax Filings',
    'depends_on' => ['view-invoice', Registry::abilityId('sales-tax-us', 'view-rate')],
]);

The normalised entry is exactly ['ability', 'name', 'model', 'depends_on', 'owner_only'] in that key
order, which is what the host role editor and CompanyService::setupRoles read. Validation follows the
registerAiDriver precedent: validate first, re-registering an identical entry is a no-op, a conflicting
redefinition throws an InvalidArgumentException naming the slug. depends_on accepts host abilities in
plain form and the module's own in namespaced form; model must be absent or null (module abilities are
never model-scoped). flush() now clears abilities alongside the other module-contributed state.

New public API:

  • Registry::registerAbility(string $slug, array $entry): void
  • Registry::abilitiesFor(string $slug): array
  • Registry::allAbilities(): array
  • Registry::abilityId(string $slug, string $ability): string

3. CompanyDataReader::companyMembers / existingInvoiceIds

public function companyMembers(int $companyId): array;
public function existingInvoiceIds(int $companyId, array $invoiceIds): array;

For modules that assign work to company members or stamp their entries against invoices. The interface
docblock no longer claims to be the AI assistant's "twelve built-in queries" — it now describes the
boundary generically and notes the reader also serves other official modules. findCustomer and
searchCustomers now document the currency_id / currency payload they already return.

Settings-page return types

The brief called for fixing registerCompanySettingsPage / registerAdminSettingsPage to return
() => void. No change was needed — both already declare () => void on master (fixed in
0a536f0). The commit message mentions the fix; the declaration was already correct.

Stubs

  • stubs/json.stub: module_api ^1.2.0 -> ^1.3.0, assets -> ["dist/init.js", "dist/style.css"]
  • stubs/composer.stub: php ^8.3 -> ^8.4, invoiceshelf/modules ^3.3 -> ^3.4
  • tests/ManifestTest.php stub assertions updated to match (and now also pin the stub assets value)

Tests

composer run test: OK (145 tests, 378 assertions) — 17 new, all green. composer run lint: pass.

New coverage in RegistryTest: ability round trip (exact stored shape and key order, namespaced id,
abilitiesFor, allAbilities ordering across two slugs), abilityId(), idempotent re-registration,
conflicting duplicate (asserts the slug appears in the message and the first registration survives),
depends_on with both plain and namespaced ids, a 10-case DataProvider of invalid registrations, unknown
slug, and flush(). AiContractsTest's exact-method-list assertion now includes the two new reader methods.

Follow-ups

Host (InvoiceShelf 3.x):

  • Implement registerPage in the extension runtime: mount /admin/modules/{slug}/{path} with children, guard on meta.ability, and reject the reserved settings path.
  • Implement companyMembers() and existingInvoiceIds() on the host's CompanyDataReader (currently the only implementor — this is a breaking interface change for it).
  • Merge Registry::allAbilities() into the ability catalogue the role editor renders and CompanyService::setupRoles consumes.
  • Grant a module's abilities to owner roles on enable and remove them on uninstall.
  • Bump the host's supported module_api range to include 1.3.0.

SDK:

  • Tag 3.4.0 — the stubs now point modules at invoiceshelf/modules: ^3.4, which does not exist yet.
  • Decide whether stubs/json.stub's compatibility.php should move to ^8.4.0. It still says ^8.3.0 while composer.stub now requires ^8.4, so a freshly scaffolded module is internally inconsistent. Left as-is because it was out of scope for this change.
  • RELEASING.md still pins the reusable workflow example at @3.3.0; update when 3.4.0 is tagged.

Modules:

  • First consumer module should adopt registerPage + registerAbility end to end to validate the contract before 3.4.0 ships.

https://claude.ai/code/session_01DCf36XDKprZifej8dc2r1E

…aders

Introduces module_api 1.3.0: PageContribution and registerPage on the
frontend extension API (and fixes the settings-page return types),
Registry::registerAbility with slug-namespaced ability ids that the host
merges into its ability catalogue, and CompanyDataReader::companyMembers
and existingInvoiceIds for modules that assign work to members or stamp
entries against invoices. Stubs now target SDK ^3.4 and dist/init.js.
The scaffold's module.json now declares php ^8.4.0 to match the composer
stub, and the ability registry docblock uses plain punctuation.
registerMenu and registerUserMenu now reject a non-integer priority or an
empty group, and the README explains how group and priority place an
entry among the core sidebar groups.
@gdarko
gdarko force-pushed the feat/page-route-abilities-members branch from 9ca7eab to 84bbfcf Compare September 14, 2026 21:56
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.

1 participant