Files
zero-fucks-given/.hermes/plans/2026-09-01_144533-erp-foundation-next-steps.md
2026-09-02 09:19:43 +03:00

10 KiB
Raw Permalink Blame History

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 is adonis and 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

  1. Write a failing Japa HTTP test first; run it in Compose against adonis_test and confirm the expected failure.
  2. Implement the smallest behavior needed to pass.
  3. Cover success, authentication, authorization, validation, not-found/conflict, tenant isolation, and any idempotency behavior relevant to that endpoint.
  4. Annotate every active controller action with concrete, valid JSON AutoSwagger request/response examples.
  5. Run:
    • docker compose exec -T backend npm run lint
    • docker compose exec -T backend npm run typecheck
    • docker compose exec -T backend npm run format
    • docker compose exec -T -e DB_DATABASE=adonis_test backend npm test -- --files <spec>
  6. Ensure git diff --check passes. For new API routes, inspect generated docs at /swagger or 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:

  1. Decide and document one supported Compose test command, preferably a dedicated test:compose backend script or Compose service/profile.
  2. Ensure it injects NODE_ENV=test and DB_DATABASE=adonis_test without altering the development backend container’s adonis database.
  3. Add a small test/bootstrap assertion that the test command receives an *_test database name.
  4. 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:

  1. Write failing tests for subdomain/header/token tenant resolution, missing tenant rejection, and cross-tenant record invisibility.
  2. Add a forward-only tenants migration and a tenant-owned record fixture.
  3. Implement a request-scoped tenant context that is immutable after middleware resolution.
  4. Add tenant middleware after authentication and before module controllers.
  5. Introduce a reusable tenant query helper/service so every tenant-owned module uses the same scope.
  6. 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:

  1. Write failing endpoint tests for no token (401), membership absence (403), missing permission (403), and valid tenant role access (200/201).
  2. Add tenant membership persistence and tenant-scoped role/permission lookup without breaking the existing administrator model.
  3. Register core permission slugs: tenant.view, tenant.manage, contacts.view, contacts.create, contacts.update, and contacts.delete.
  4. Add an idempotent migration to grant approved core permissions to the Super Admin role on existing installations.
  5. 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:

  1. Start with HTTP tests for list/create/show/update/delete, validation, permissions, and isolation across two tenants.
  2. Create contacts with tenant ID, contact kind, legal/display name, parent organization, email/phone, address payload, status, timestamps, and custom_fields JSONB.
  3. Generate CRUD assets through MakeModuleController only after the table migration exists; then replace generic controller logic with tenant-aware Contacts service calls.
  4. Register the Contacts permission config and ensure menus/routes use contacts.view.
  5. Add React module configuration, API client, list/form pages, and translations for en, de, fr, and nl with identical key sets.
  6. 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_definitions and 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:

  1. Write failing tests for field creation, allowed target entity, invalid values, required-field enforcement, and tenant isolation.
  2. Persist field key, entity type, scalar type, validation rules, required/visibility state, option values, and tenant ownership.
  3. Build one reusable validator that reads metadata definitions and validates the custom_fields JSONB payload before module persistence.
  4. Expose dynamic-form metadata through an authenticated endpoint with concrete AutoSwagger examples.
  5. 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:

  1. Write failing tests proving that transaction rollback creates neither business record nor outbox event, and retries do not process a duplicate event twice.
  2. 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.
  3. Implement an explicit publisher and idempotent consumer registry. The initial delivery mechanism may use the Adonis event emitter only after outbox persistence succeeds.
  4. Define versioned service interfaces for cross-module reads, beginning with ContactService and future ProductService; prohibit SQL joins across module-owned tables.
  5. Add tests and concrete OpenAPI examples for any operational event-monitoring endpoints.

After the foundation

Proceed in this order:

  1. Product Catalog.
  2. CRM with crm:lead_won outbox event.
  3. Sales Orders consuming crm:lead_won and emitting sales:order_confirmed.
  4. Inventory/Logistics consuming confirmed orders and emitting inventory:stock_shipped.
  5. Procurement and reorder rules emitting/consuming receiving events.
  6. 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.