mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Muluhabt ERP modules
This commit is contained in:
104
CLAUDE.md
104
CLAUDE.md
@@ -32,6 +32,10 @@ copying a pattern across:
|
||||
| `edr-passenger-web/portal` | `@edr/passenger-portal` | **Next.js** | 5174 |
|
||||
| `edr-passenger-web/backoffice` | `@edr/passenger-backoffice` | **Next.js** | 5184 |
|
||||
| `edr-payment-api` | `@edr/payment-api` | NestJS + **TypeORM** | 3003 |
|
||||
| `edr-hr-api` | `@edr/hr-api` | NestJS + **TypeORM** | 3005 |
|
||||
| `edr-hr-web` | `@edr/hr-web` | React + **Vite** | 5185 |
|
||||
| `finance-api` | `@edr/finance-api` | NestJS + **TypeORM** | 3004 |
|
||||
| `finance-web` | `@edr/finance-web` | React + **Vite** | 5186 |
|
||||
|
||||
Those are the **fallbacks compiled into the code**, not what you will be running. Every
|
||||
port is overridden by `PORT` in the app's `.env` / `.env.development`; the freight vite
|
||||
@@ -48,6 +52,58 @@ package and is not built, linted, or type-checked. Leave it alone unless asked.
|
||||
|
||||
`apps/edr-gps-tracker/` is a separate service with its own `.env.example`.
|
||||
|
||||
`apps/edr-hr-api/` owns the `hr` schema and is the HR extension of IAM: employee
|
||||
profiles hang off `iam.employees`, "departments" are `iam.units` with an HR
|
||||
satellite (`hr.unit_hr_profiles`), and job positions extend `iam.positions`. HR
|
||||
stores no copy of an employee's name, unit or organization — those are read
|
||||
through and joined at query time.
|
||||
|
||||
IAM access is split in two, and the split is the design:
|
||||
|
||||
- **Reads** go through `IamDirectoryService` — raw SQL projections over `iam.*`,
|
||||
no entities involved.
|
||||
- **Writes** go through `IamOperationsService`, which resolves IAM's own
|
||||
services out of the container (`ModuleRef.get(..., {strict: false})`) and
|
||||
calls them. HR never writes an `iam.*` table itself, so IAM's validation,
|
||||
transactions and audit trail all still apply.
|
||||
|
||||
That means hr-api **does** import `IamModule` (`IamModule.forRoot({...})`) and
|
||||
**does** register `@tria-plc/iamapi-common` entities. It sidesteps the stale
|
||||
`iamEntities` trap below by registering them as **globs over the package's
|
||||
`dist/`** rather than a hand-maintained class list, with `autoLoadEntities: false`
|
||||
— see `apps/edr-hr-api/src/config/database.config.ts`. A package bump cannot
|
||||
leave that list stale, because there is no list.
|
||||
|
||||
One consequence worth knowing before touching lifecycle code: IAM's
|
||||
`deactivateEmployee` ends every position the employee holds, and
|
||||
`activateEmployee` does **not** put them back. HR surfaces this rather than
|
||||
hiding it (`accessAlignment` on the profile response).
|
||||
|
||||
`apps/finance-api/` owns the `finance` schema — general ledger, chart of
|
||||
accounts, receivables, payables, budgets and fixed assets. It follows hr-api's
|
||||
shape exactly (embedded `IamModule.forRoot`, IAM entities registered as globs
|
||||
over both package dists, `autoLoadEntities: false`, migrations in
|
||||
`finance.migrations` run only by `migration:run`).
|
||||
|
||||
Two rules define it, and neither is negotiable:
|
||||
|
||||
- **Finance writes only `finance.*`.** Revenue, cash and payroll are projected
|
||||
**read-only** out of `freight`, `passenger`, `edr_payment` and `hr` with
|
||||
schema-qualified raw SQL, plus payment events off `PAYMENT_EVENTS_EXCHANGE`.
|
||||
No source app writes `finance.*`, and Finance writes none of theirs.
|
||||
- **Money is `numeric(14,2)` in major units.** The column NAMES lie upstream:
|
||||
everything on the payment-intent path is called `*Minor` but holds MAJOR
|
||||
amounts, while passenger's Prisma `Int` columns really are minor. Never infer
|
||||
the unit from the name — `apps/finance-api/src/common/money.ts` is the
|
||||
authority, and it also records which upstream rows are known bad.
|
||||
|
||||
Note both finance-api and hr-api serve `POST /api/v1/auth/login`: embedding
|
||||
`IamModule` brings IAM's auth controller with it. finance-web therefore
|
||||
authenticates against finance-api itself and needs no `x-client-app` header
|
||||
(both verified against the running services). `edr-hr-web` points its login at
|
||||
freight-api instead, on the assumption that an IAM-embedding app has no auth
|
||||
routes — that assumption is wrong, and the coupling is unnecessary.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Location | Purpose |
|
||||
@@ -112,23 +168,44 @@ gate-pass scenarios). Read the script before running one; several write real row
|
||||
|
||||
## Environment & database
|
||||
|
||||
- Postgres is **external**. There is no postgres service in `docker-compose.yaml`, and
|
||||
no port `5433`/`5434` is published anywhere in the repo.
|
||||
- **One database, schema-separated.** Every app connects to the SAME Postgres database
|
||||
and is isolated by schema, not by database — `iam`, `freight`, `passenger`,
|
||||
`edr_payment`, `audit`. This mirrors production, where the Smart Office database holds
|
||||
the `freight` schema alongside the rest (see `dump-smart_office_prod-*.sql`). There is
|
||||
no longer a per-domain database; do not add one.
|
||||
- Postgres is **external** to `docker-compose.yaml` (no service there). For a local one,
|
||||
`infrastructure/docker/docker-compose.db.dev.yml` starts a single `postgres` on 5432
|
||||
(`edr_database`) and creates every schema via `infrastructure/docker/initdb/`.
|
||||
- Freight API connection comes from `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`,
|
||||
`DB_NAME` (defaults: `localhost:5433`, `edr_freight`). Development points these at a
|
||||
remote database.
|
||||
`DB_NAME` (defaults: `localhost:5432`, `edr_database`). Development points these at a
|
||||
remote database. `edr-gps-tracker` and `edr-payment-api` read the same DB_* convention;
|
||||
`edr-passenger-api` uses `DATABASE_URL` (Prisma, `?schema=passenger`) plus
|
||||
`DATABASE_*`/`DATABASE_SCHEMA=iam` for its read-only TypeORM IAM connection — all three
|
||||
must resolve to this one database.
|
||||
- The connection sits behind a **connection pooler**. Do **not** pass
|
||||
`extra.options: '-c search_path=…'` — the pooler rejects it with
|
||||
`08P01 unsupported startup parameter in options: search_path`. `search_path` is applied
|
||||
per-connection in a pool `connect` handler instead. See
|
||||
`apps/edr-freight-api/src/config/database.config.ts` before touching connection options.
|
||||
- Each app owns its own database. **No cross-database joins**; cross-domain data flows
|
||||
through API calls or message queues.
|
||||
- IAM tables live in their own `iam` schema (`iam.users`, `iam.user_credentials`),
|
||||
freight tables in `freight`.
|
||||
- `psql` is not installed on the dev machine. To query the database, use the `edr-db`
|
||||
skill (below) or write a short Node script using `pg` and run it from
|
||||
`apps/edr-freight-api`, where `pg` resolves.
|
||||
- Each app owns its own **schema**, and its own migration history in that schema:
|
||||
`iam.typeorm_migrations` and `freight.migrations` (both applied by freight-api),
|
||||
`passenger._prisma_migrations` (Prisma), and payment-api's own table in `edr_payment`.
|
||||
An app never writes another app's schema.
|
||||
- IAM tables live in the `iam` schema (`iam.users`, `iam.user_credentials`), freight
|
||||
tables in `freight`. **`edr-freight-api` is the authoritative owner of the `iam`
|
||||
schema** — it ships the `iam:migration:run|show|revert` scripts. Passenger's IAM
|
||||
connection is read-only (`synchronize: false`, `migrationsRun: false`).
|
||||
- **Cross-schema references stay soft.** Co-location makes hard FKs possible, but there
|
||||
are zero FKs from `freight.*` into `iam.*` and that is deliberate: references are plain
|
||||
UUID columns plus denormalized display fields, so IAM stays independently deployable and
|
||||
the audit trail outlives a deleted user. Keep it that way in new modules.
|
||||
- The e2e harnesses are the **one exception** and stay hermetic: `edr_freight_e2e` on 5533
|
||||
(`e2e/freight/`) and the passenger test DB on 5544 (`e2e/docker-compose.yml`). Do not
|
||||
point them at the shared database.
|
||||
- `psql` **is** installed (Homebrew, v17.9 — verified 2026-08-19); the earlier claim that
|
||||
it was not is out of date. Either use it directly against the local database, or use the
|
||||
`edr-db` skill (below) / a short Node script using `pg` run from `apps/edr-freight-api`,
|
||||
where `pg` resolves.
|
||||
|
||||
## Hard rules
|
||||
|
||||
@@ -340,6 +417,11 @@ hand; do not assume the hook caught it.
|
||||
| Watch-mode reload | New code, old schema → 500 | Apply the migration to the dev DB or restart fully |
|
||||
| Login 403 from curl | "Missing or unrecognized x-client-app header" | Send `x-client-app: backoffice` or `portal` |
|
||||
| Copying a passenger pattern into freight | Passenger is Prisma + Next.js, freight is TypeORM + Vite | Check which stack you are in first |
|
||||
| Binding `payment.*` on the payment exchange | Silently receives NOTHING — wire keys are three words (`payment.passenger.succeeded`) and AMQP `*` matches exactly one | Bind `payment.<service>.*` for one service, or `payment.#` for all |
|
||||
| Summing money across currencies | Passenger bookings are charged in ETB, DJF **and** USD; adding them overstates ETB revenue (6.3M on the dev replica) | Group by currency; convert only at an explicitly recorded rate |
|
||||
| Reading a `DATE` column in raw SQL | node-postgres parses it to LOCAL midnight, so `toISOString().slice(0,10)` returns the PREVIOUS day east of UTC — a month-end lands in the wrong period | Cast in SQL: `period_end::text`. Never round-trip a DATE through a JS `Date` |
|
||||
| A `CHECK` listing enum values wider than the column | `varchar(16)` accepted every status until the 17-character one was first used, then failed mid-operation | Size the column for the LONGEST permitted value |
|
||||
| A service opening its own transaction inside a caller's | The inner write commits independently; a later failure leaves an orphaned posted row | Pass the caller's `EntityManager` through (see `JournalsService.createPosted`) |
|
||||
|
||||
## Project skills
|
||||
|
||||
|
||||
Reference in New Issue
Block a user