10 KiB
ERP Foundation Next Steps Implementation Plan
For Hermes: Implement this plan task-by-task using strict TDD.
Goal: Establish a testable, tenant-safe ERP foundation in the existing AdonisJS 7 + React + PostgreSQL modular monolith, then deliver the first reusable core modules.
Architecture: Preserve the existing apps/backend/modules/<Module> and apps/admin/src/modules/<Module> conventions. Core infrastructure becomes the only source of tenant scope, authorization, contacts, custom-field metadata, durable events, and internal service contracts. Business modules must not issue SQL joins into each other’s tables.
Tech Stack: AdonisJS 7, Lucid ORM, PostgreSQL 17, Japa, Bouncer, @adonisjs/auth, adonis-autoswagger, React 19, Vite, React Query, React Hook Form, React Intl, Docker Compose.
Current verified context
- Compose stack is running with
zfg-*container names. - Backend lint, formatting, and typecheck run successfully in
zfg-backend-adonis. - Database-backed tests require
DB_DATABASE=adonis_test; the default Compose development database isadonisand is deliberately rejected by the test bootstrap. - The module generator now generates protected API CRUD routes, policy checks, concrete AutoSwagger response annotations, and CRUD/auth/permission test scaffolding.
- The generator must be used as a scaffolding baseline only. ERP domain logic must add a migration, service contract, tenant scope, event/outbox behavior, endpoint tests, and documentation before the module is complete.
Definition of done for every ERP endpoint
- Write a failing Japa HTTP test first; run it in Compose against
adonis_testand confirm the expected failure. - Implement the smallest behavior needed to pass.
- Cover success, authentication, authorization, validation, not-found/conflict, tenant isolation, and any idempotency behavior relevant to that endpoint.
- Annotate every active controller action with concrete, valid JSON AutoSwagger request/response examples.
- Run:
docker compose exec -T backend npm run lintdocker compose exec -T backend npm run typecheckdocker compose exec -T backend npm run formatdocker compose exec -T -e DB_DATABASE=adonis_test backend npm test -- --files <spec>
- Ensure
git diff --checkpasses. For new API routes, inspect generated docs at/swaggeror run the project’s Swagger command if one is added.
Step 1: Make the Compose test database invocation first-class
Objective: Remove the need for ad-hoc environment overrides when running backend tests in Compose.
Files likely to change:
- Modify:
docker-compose.yml - Modify or create:
apps/backend/.env.test.example - Modify:
apps/backend/package.json - Test:
apps/backend/tests/bootstrap.ts
Tasks:
- Decide and document one supported Compose test command, preferably a dedicated
test:composebackend script or Compose service/profile. - Ensure it injects
NODE_ENV=testandDB_DATABASE=adonis_testwithout altering the development backend container’sadonisdatabase. - Add a small test/bootstrap assertion that the test command receives an
*_testdatabase name. - Verify the generator test runs without manually supplying
-e DB_DATABASE=adonis_test.
Step 2: Design and implement tenant context
Objective: Resolve one authenticated/request-scoped tenant and prevent unscoped tenant data access.
Files likely to change:
- Create:
apps/backend/database/migrations/<timestamp>_create_tenants_table.ts - Create:
apps/backend/app/base_modules/Tenant/model/tenant.ts - Create:
apps/backend/app/services/tenant_context.ts - Create:
apps/backend/app/middleware/resolve_tenant_middleware.ts - Modify:
apps/backend/start/kernel.ts - Modify:
apps/backend/config/database.ts - Test:
apps/backend/tests/functional/tenant/tenant_context.spec.ts
Tasks:
- Write failing tests for subdomain/header/token tenant resolution, missing tenant rejection, and cross-tenant record invisibility.
- Add a forward-only tenants migration and a tenant-owned record fixture.
- Implement a request-scoped tenant context that is immutable after middleware resolution.
- Add tenant middleware after authentication and before module controllers.
- Introduce a reusable tenant query helper/service so every tenant-owned module uses the same scope.
- Keep the initial approach PostgreSQL row isolation with
tenant_id; defer per-schema/per-database selection until a measured SaaS scaling requirement exists.
Step 3: Extend IAM for tenant-aware ERP permissions
Objective: Build authorization around tenant membership and namespaced ERP permissions.
Files likely to change:
- Create migration(s) for tenant memberships and role assignment scope.
- Modify:
apps/backend/app/services/permission_service.ts - Modify:
apps/backend/app/middleware/initialize_bouncer_middleware.ts - Create:
apps/backend/app/base_modules/Tenant/policy/tenant_policy.ts - Test:
apps/backend/tests/functional/iam/tenant_permissions.spec.ts
Tasks:
- Write failing endpoint tests for no token (401), membership absence (403), missing permission (403), and valid tenant role access (200/201).
- Add tenant membership persistence and tenant-scoped role/permission lookup without breaking the existing administrator model.
- Register core permission slugs:
tenant.view,tenant.manage,contacts.view,contacts.create,contacts.update, andcontacts.delete. - Add an idempotent migration to grant approved core permissions to the Super Admin role on existing installations.
- Add concrete AutoSwagger examples for all IAM endpoints.
Step 4: Implement Base Contacts as the first generated ERP module
Objective: Provide the canonical tenant-owned people and organization directory.
Files likely to change:
- Create:
apps/backend/database/migrations/<timestamp>_create_contacts_table.ts - Create:
apps/backend/modules/Contacts/... - Create:
apps/admin/src/modules/Contacts/... - Test:
apps/backend/tests/modules/Contacts/contacts.api.spec.ts
Tasks:
- Start with HTTP tests for list/create/show/update/delete, validation, permissions, and isolation across two tenants.
- Create
contactswith tenant ID, contact kind, legal/display name, parent organization, email/phone, address payload, status, timestamps, andcustom_fields JSONB. - Generate CRUD assets through MakeModuleController only after the table migration exists; then replace generic controller logic with tenant-aware Contacts service calls.
- Register the Contacts permission config and ensure menus/routes use
contacts.view. - Add React module configuration, API client, list/form pages, and translations for
en,de,fr, andnlwith identical key sets. - Add concrete AutoSwagger examples matching the actual serializer wrappers and error shapes.
Step 5: Implement metadata and custom-fields configuration
Objective: Allow tenant administrators to define validated dynamic fields for approved entities without schema migrations.
Files likely to change:
- Create migration(s) for
custom_field_definitionsand optional option records. - Create:
apps/backend/app/base_modules/Metadata/... - Modify:
apps/backend/modules/Contacts/... - Test:
apps/backend/tests/functional/metadata/custom_fields.spec.ts - Create:
apps/admin/src/modules/Metadata/...
Tasks:
- Write failing tests for field creation, allowed target entity, invalid values, required-field enforcement, and tenant isolation.
- Persist field key, entity type, scalar type, validation rules, required/visibility state, option values, and tenant ownership.
- Build one reusable validator that reads metadata definitions and validates the
custom_fieldsJSONB payload before module persistence. - Expose dynamic-form metadata through an authenticated endpoint with concrete AutoSwagger examples.
- Render dynamic controls in the Contacts admin form, including all requested translations.
Step 6: Establish the internal event/outbox contract before commercial modules
Objective: Make CRM, Sales, Inventory, Procurement, Invoicing, and Accounting communication durable and module-boundary-safe.
Files likely to change:
- Create migration:
apps/backend/database/migrations/<timestamp>_create_outbox_events_table.ts - Create:
apps/backend/app/services/outbox/... - Create:
apps/backend/app/services/module_contracts/... - Test:
apps/backend/tests/functional/outbox/outbox_delivery.spec.ts
Tasks:
- Write failing tests proving that transaction rollback creates neither business record nor outbox event, and retries do not process a duplicate event twice.
- Persist event ID, tenant ID, name, version, aggregate ID, correlation ID, payload, occurred time, and processing state in an outbox table within the business transaction.
- Implement an explicit publisher and idempotent consumer registry. The initial delivery mechanism may use the Adonis event emitter only after outbox persistence succeeds.
- Define versioned service interfaces for cross-module reads, beginning with
ContactServiceand futureProductService; prohibit SQL joins across module-owned tables. - Add tests and concrete OpenAPI examples for any operational event-monitoring endpoints.
After the foundation
Proceed in this order:
- Product Catalog.
- CRM with
crm:lead_wonoutbox event. - Sales Orders consuming
crm:lead_wonand emittingsales:order_confirmed. - Inventory/Logistics consuming confirmed orders and emitting
inventory:stock_shipped. - Procurement and reorder rules emitting/consuming receiving events.
- Invoicing and Accounting from immutable operational events.
Risks and decisions
- Do not implement PostgreSQL-per-tenant schemas or separate databases until tenant row isolation, authorization, and test coverage are established; otherwise every migration and test path becomes unnecessarily complex.
- Do not add Vite Module Federation until the existing admin module registry is insufficient. The existing React module convention is the source of truth for the first ERP modules.
- Do not generate migrations from an already-existing table automatically. Migrations must be authored and reviewed before MakeModuleController scaffolds a module from that table.
- Do not generate generic Swagger schema objects. Every response annotation needs a concrete valid JSON example derived from the serializer.