From 339996d27fb1fa116e9fd23d806b2cace672f807 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Mon, 17 Aug 2026 07:49:12 +0000 Subject: [PATCH] docs --- CLAUDE.md | 388 +++++++++++++++++++++++++++++++++++++++++--------- CLAUDE_NEW.md | 313 ---------------------------------------- docs/MAP.md | 96 +++++++++++++ 3 files changed, 420 insertions(+), 377 deletions(-) delete mode 100644 CLAUDE_NEW.md create mode 100644 docs/MAP.md diff --git a/CLAUDE.md b/CLAUDE.md index b90d3b1dd..a20fbaee7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,102 +1,362 @@ # EDR Platform — Developer Guide +> This file is the contract. If something here contradicts the code, the code is the +> truth and this file is a bug — fix it in the same PR. + +**Looking for where something lives? Read [`docs/MAP.md`](docs/MAP.md) first.** It routes +you to the right module or page without a repo-wide grep. + ## Overview -Monorepo for the Ethio Djibouti Railway (EDR) digital platform. Contains the Freight Management and Passenger Management applications, plus shared types, NestJS utilities, and React component libraries. +Monorepo for the Ethio Djibouti Railway (EDR) digital platform. Contains the Freight +Management and Passenger Management applications, a payment microservice, plus shared +types, NestJS utilities, and React component libraries. + +The freight domain is the largest and most active area. Its core flow is: +**booking → receive to warehouse → store → load onto train → dispatch → arrive → unload +→ customer truck (self-haul) or EDR last mile → handover → exit paper → delivered.** +Fees (storage, demurrage, double handling, truck detention) and allocation rules +(warehouse/yard/zone) hang off the warehouse stage. ## Apps -| App | Package name | Purpose | Port | -| ------------------------------ | --------------------------- | -------------------------------------------------- | ---- | -| `edr-freight-api` | `@edr/freight-api` | NestJS API for freight management | 3001 | -| `edr-freight-web/portal` | `@edr/freight-portal` | React frontend for freight customer/portal users | 5173 | -| `edr-freight-web/backoffice` | `@edr/freight-backoffice` | React frontend for freight backoffice employees | 5183 | -| `edr-passenger-api` | `@edr/passenger-api` | NestJS API for passenger management | 3002 | -| `edr-payment-api` | `@edr/payment-api` | NestJS payment microservice (intents, webhooks) | 3003 | -| `edr-passenger-web/portal` | `@edr/passenger-portal` | React frontend for passenger customer/portal users | 5174 | -| `edr-passenger-web/backoffice` | `@edr/passenger-backoffice` | React frontend for passenger backoffice employees | 5184 | +The two domains are **not built the same way**. Check which stack you are in before +copying a pattern across: -`edr-freight-web` and `edr-passenger-web` are grouping folders, not workspace packages. Each holds a `portal/` and `backoffice/` sub-app, both of which are independent pnpm workspace packages (declared in `pnpm-workspace.yaml`). The existing `pnpm dev:freight` / `pnpm dev:passenger` turbo filters (`@edr/freight-*` / `@edr/passenger-*`) cover all four web apps + their APIs. +| App | Package name | Stack | Default port | +| ------------------------------ | --------------------------- | ---------------------- | ------------ | +| `edr-freight-api` | `@edr/freight-api` | NestJS + **TypeORM** | 3001 | +| `edr-freight-web/portal` | `@edr/freight-portal` | React + **Vite** | 5273 | +| `edr-freight-web/backoffice` | `@edr/freight-backoffice` | React + **Vite** | 5283 | +| `edr-passenger-api` | `@edr/passenger-api` | NestJS + **Prisma** | 4000 | +| `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 | + +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 +apps read it in `vite.config.ts` (`Number(env.PORT) || 5273`). This machine is shared by +the whole team and the low ports are contested — see the workspace root `CLAUDE.md` and +`./wt ports` for who currently holds what. + +`edr-freight-web` and `edr-passenger-web` are grouping folders, not workspace packages. +Each holds a `portal/` and `backoffice/` sub-app, both independent pnpm workspace +packages (see `pnpm-workspace.yaml`). + +`apps/edr-landing/` exists on disk but has **no `package.json`** — it is not a workspace +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`. ## Packages -| Package | Purpose | -| ---------------------- | ---------------------------------------------------------------------------------- | -| `@edr/types` | Shared TypeScript interfaces and enums | -| `@edr/api-common` | Shared NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository | -| `@edr/iam-seed` | IAM baseline seeder for the apps sharing the `iam` schema (freight + passenger) | -| `@edr/ui-common` | Shared React components and theme | -| `@edr/eslint-config` | Shared ESLint configurations (base/nestjs/react) | -| `@edr/tsconfig` | Shared TypeScript configurations | -| `@edr/prettier-config` | Shared Prettier configuration | +| Package | Location | Purpose | +| ----------------------- | ----------------------------- | ------------------------------------------------------------- | +| `@edr/types` | `packages/types` | Shared TypeScript interfaces and enums | +| `@edr/api-common` | `packages/api-common` | NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository | +| `@edr/ui-common` | `packages/ui-common` | Shared React components and theme | +| `@edr/iam-seed` | `packages/iam-seed` | IAM baseline seeder for apps sharing the `iam` schema | +| `@edr/payment-providers`| `packages/payment-providers` | Payment gateway integrations | +| `@edr/eslint-config` | `packages/config/eslint-config` | Shared ESLint configs (base/nestjs/react) | +| `@edr/tsconfig` | `packages/config/tsconfig` | Shared TypeScript configs | +| `@edr/prettier-config` | `packages/config/prettier-config` | Shared Prettier config | + +The three `config/*` packages are nested one level deeper than the rest — `packages/config` +itself is not a package. + +**`@edr/types` is consumed as its built `dist/`** (`main: ./dist/index.js`). Editing a +type in `packages/types/src` changes nothing for consumers until you rebuild: + +```bash +pnpm turbo build --filter=@edr/types +``` + +If a type-check fails on a field you just added to `@edr/types`, this is why. ## Commands -| Command | Description | -| -------------------- | ---------------------------------- | -| `pnpm install` | Install all workspace dependencies | -| `pnpm dev` | Run every app in dev mode | -| `pnpm dev:freight` | Run only freight API + web | -| `pnpm dev:passenger` | Run only passenger API + web | -| `pnpm build` | Build every package and app | -| `pnpm test` | Run all tests | -| `pnpm lint` | Lint everything | -| `pnpm type-check` | Type-check every package | -| `pnpm format` | Format all files with Prettier | +| Command | Description | +| ----------------------------- | ---------------------------------------- | +| `pnpm install` | Install all workspace dependencies | +| `pnpm dev` | Run every app in dev mode | +| `pnpm dev:freight` | Freight API + portal + backoffice | +| `pnpm dev:freight:api` | Freight API only | +| `pnpm dev:freight:portal` | Freight portal only | +| `pnpm dev:freight:backoffice` | Freight backoffice only | +| `pnpm dev:passenger` | Passenger API + web | +| `pnpm dev:payment` | Payment API | +| `pnpm build` | Build every package and app | +| `pnpm test` | Run all tests (turbo) | +| `pnpm type-check` | Type-check every package | +| `pnpm format` | Format all files with Prettier | +| `pnpm lint` | **Does not work** — see below | -## Standards +**`pnpm lint` fails.** `eslint` is not installed anywhere in the workspace, so +`turbo run lint` dies with `eslint: not found` even though every package declares a +`lint` script and `@edr/eslint-config` exists. Until someone adds the dependency, +tsc's `noUnusedLocals` is the only working unused-code check. Do not claim a change is +"lint clean". -- **TypeScript strict mode** is enabled in every package and app. -- **pnpm** is the only supported package manager — never run `npm install` or `yarn`. -- **Conventional commits** are enforced via commitlint on every commit. -- **NestJS modules** follow the 4-layer pattern: `module → controller → service → repository` (entities and DTOs live alongside). +`pnpm format` uses bare `prettier`, which ignores `@edr/prettier-config` — it is wired to +nothing. On the single-quoted passenger apps it will re-quote the whole file. Pass +`--config` explicitly there. + +Prefer targeted turbo filters over whole-repo runs — they are minutes faster: + +```bash +pnpm turbo type-check --filter=@edr/freight-api --filter=@edr/freight-backoffice +``` + +`apps/edr-freight-api` also carries many `seed:*` scripts (demo bookings, wagons, trains, +gate-pass scenarios). Read the script before running one; several write real rows. + +## 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. +- 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. +- 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. + +## Hard rules + +These are non-negotiable. Everything else is a strong default. + +- **pnpm only.** Never run `npm install` or `yarn`. +- **TypeScript strict mode** is on in every package and app. Do not weaken it, and do not + reach for `any` to make an error go away. +- **Never `synchronize: true`.** Not in production, not anywhere. It is currently `false` + in every config and it has already corrupted this database twice (see *Migrations*). + All schema changes go through migrations. - **All entities** use UUID primary keys (`@PrimaryGeneratedColumn('uuid')`). -- **All entities** have `createdAt`, `updatedAt`, `deletedAt` (soft delete) via `@edr/api-common`'s `BaseEntity`. -- **All columns** use `snake_case` in the database (`@Column({ name: 'snake_case' })`); TypeScript properties use `camelCase`. -- **Never use `synchronize: true`** in production database config. All schema changes go through TypeORM migrations. -- **ESLint + Prettier** run on pre-commit via Husky + lint-staged. -- **Services** never inject TypeORM `Repository` directly — they inject the custom repository class. -- **Controllers** never contain business logic. +- **All entities** extend `BaseEntity` from `@edr/api-common` — `createdAt`, `updatedAt`, + `deletedAt` (soft delete). +- **All columns** are `snake_case` in the database (`@Column({ name: 'snake_case' })`); + TypeScript properties are `camelCase`. +- **Controllers contain no business logic.** They validate, delegate, and shape the response. +- **Conventional commits.** `fix(warehouses): …`, `feat(bookings): …`. +- **Do not commit or push unless asked.** Propose the change; let the human decide when it lands. +- **Do not break working behaviour to add new behaviour.** When a fix is risky, say so and + offer the safe version. -## Auth +## Architecture -Authentication is handled by an external package (`@edr/iamui-common` or equivalent) that will be integrated later. **Do not** implement any auth, login, logout, JWT verification, password hashing, or user management code in this repo. +### NestJS module shape -When auth integration is needed, use placeholder TODO comments: +`module → controller → service → repository`, with `entities/` and `dto/` alongside. +`docs/MAP.md` lists the ~60 freight modules grouped by domain. -- `// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth` -- `// TODO: integrate @edr/auth — replace stub @CurrentUser with real one` +### Data access — the real model -The `@CurrentUser`, `@Roles`, and `@Public` decorators in `@edr/api-common` are bare metadata setters with no guard wiring — they exist so controllers can be annotated correctly without depending on auth infrastructure yet. +There are two sanctioned ways to read and write, and you must pick the right one: -## Port Assignments +1. **Entity CRUD → the custom repository class.** Extends `BaseRepository` from + `@edr/api-common`. Services inject the repository class, never `Repository` directly. +2. **Read projections, queue endpoints, cross-table reports → raw SQL** via + `this.dataSource.query(...)` or `manager.query(...)` inside a transaction. -- `edr-freight-api`: 3001 -- `edr-freight-web/portal`: 5173 -- `edr-freight-web/backoffice`: 5183 -- `edr-passenger-api`: 3002 -- `edr-payment-api`: 3003 -- `edr-passenger-web/portal`: 5174 -- `edr-passenger-web/backoffice`: 5184 +Raw SQL is normal here, not a smell — the warehouse and scheduling modules are built on it. +It carries one obligation: -## Database Layout +> **HARD RULE — validate every raw SQL statement against a real database before you ship it.** +> A typo'd column name is a runtime 500 that no type-checker will catch. Run it through +> `EXPLAIN` against the dev database. Column drift is real (see *Migrations*). -- `postgres-freight` (port 5433): database `edr_freight` — freight API only. -- `postgres-passenger` (port 5434): database `edr_passenger` — passenger API only. -- `edr_payment` schema — lives in the same Postgres database as the domain system (whatever the passenger `DATABASE_URL` points at) but is owned exclusively by `apps/edr-payment-api`. Dedicated DB user, no cross-schema FKs, domain apps have no grants on it (see `docs/payment-service/`). -- Each app owns its own DB. No cross-database joins; cross-domain data flows through API calls or message queues. +Writes inside a transaction use `manager.getRepository(Entity)`, not the injected repository, +so they join the caller's transaction. + +**Never do slow I/O inside a database transaction.** Queue the work and fan it out after +commit. An SMS awaited inside a transaction once held capacity locks open for the whole +gateway timeout. Any outbound HTTP call must set an explicit `timeout` — axios defaults to +no timeout and will wait forever. + +### Migrations + +Migrations are the most dangerous surface in this repo. Two production-grade incidents have +already come from it. **Freight and payment use TypeORM migrations; passenger uses Prisma** +(`apps/edr-passenger-api/prisma/migrations`) — the rules below are about the TypeORM side. + +- `migrationsRun: false` — **migrations do NOT run on API boot.** They run as a separate + one-shot step, via the Dockerfile's `migration` build target (`docker build --target + migration`), with `migrationsTransactionMode: 'each'`. + - CI: `.github/workflows/deploy.yml` builds the `migration` image and runs it + (`docker run --rm --env-file ...`) *before* building/deploying the app image. + - e2e: `docker-compose.e2e.yaml`'s `freight-migration-e2e` service runs once and + `freight-api-e2e` depends on it (`condition: service_completed_successfully`). + - Local dev (`docker-compose.yaml`) has no equivalent migration service yet — run + migrations yourself before `docker compose up freight-api`, e.g. + `docker build --target migration -f apps/edr-freight-api/Dockerfile -t freight-migration .` + then `docker run --rm --env-file apps/edr-freight-api/.env freight-migration`. Don't + use `pnpm run migrate` for this — it runs via `ts-node`, which never writes compiled + output to `dist/`, and the freight migrations glob only matches `dist/migrations/*.js`. + It silently applies zero freight migrations while exiting 0. +- Consequences you must design for: + - A watch-mode hot reload does **not** re-run migrations. If you add a column that new + code reads, apply it to the dev database yourself (idempotently) or fully restart. + - `apps/edr-freight-api/src/config/database.config.ts`'s `iamEntities` array is a + hand-maintained list of `@tria-plc/iamapi-common` entity classes. The live app never + notices when it's stale (`autoLoadEntities: true` papers over gaps via IAM's own + `forFeature()` registrations), but the standalone migration `DataSource` + (`data-source.ts`, no `autoLoadEntities`) does not have that fallback — a missing + entity throws `Entity metadata for X#y was not found` at `initialize()`, before a + single migration runs. **Every `@tria-plc/iamapi-common` version bump is a candidate + for this to break again** — diff the package's entity classes against `iamEntities` + when bumping it. +- **Give every migration a unique timestamp.** `apps/edr-freight-api/src/migrations` holds + 39 files, and 8 timestamps are shared by two or more of them. TypeORM orders by timestamp + and breaks ties non-deterministically. Check before adding one: + + ```bash + ls apps/edr-freight-api/src/migrations | grep -oE '^[0-9]+' | sort | uniq -d + ``` + + The prefix must be unused *and* higher than the newest recorded row. Note the + `freight.migrations` table has far more rows (~309) than this folder has files — most + come from `@tria-plc/iamapi-common`'s own migrations, which run from the same data source. +- **Write idempotent DDL**: `ADD COLUMN IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, and + backfills guarded by `WHERE col IS NULL`. +- **Never assume a recorded migration actually applied.** `AddGrnNumberToWarehouseInventory` + was recorded in `migrations` while its column was absent — it had been dropped out of band. + TypeORM will never re-run a recorded migration, so the fix is a *new repair migration*. +- **A repair migration's `down()` should be a no-op.** Reverting a repair must not + re-introduce the outage it fixed. + +### Auth + +Auth **is implemented in this repo.** Do not add TODO stubs, and do not write your own. + +- `@CurrentUser()` (`@edr/api-common`) is a real `createParamDecorator`, not a metadata stub. +- Route protection uses `@UseGuards(JwtGuard)` and `@UseGuards(PermissionGuard([...]))`. +- Freight-domain checks use `hasFreightPermission(user, FREIGHT_PERMS..)`. +- Permissions are declared in `apps/edr-freight-api/src/seed/freight-permissions.registry.ts`. + Add a permission there before referencing it. +- Login is freight-api's own `POST /api/auth/login` (SharedAuthModule from + `@tria-plc/api-common`). Every login call needs an **`x-client-app` header** — + `backoffice` for employees, `portal` for customers. Without it the API 403s with + "Missing or unrecognized x-client-app header". Browsers send it; curl must add it. +- IAM has its own migrations, run ahead of freight migrations from the same data source, and + its own CLI scripts (`iam:migration:run`, `iam:seed:run`). + +Ownership checks are separate from permission checks. A staff user passes +`hasFreightPermission`; a customer must additionally pass an ownership assertion such as +`assertCustomerCanAccessBooking`. Do not drop the ownership check because the permission check passed. + +## Frontend conventions + +- The **freight** web apps use **Mantine v9** (`^9.3.0`). Its APIs differ from v6/v7 — + check the installed version before copying a snippet. +- `@edr/ui-common` holds shared components and theme; it is imported in ~94 files across the + freight web apps. Prefer it over re-implementing a component. +- **Blob downloads need the async error decoder.** A request with `responseType: 'blob'` + delivers the JSON error body as a `Blob`, so the synchronous `extractErrorMessage` finds no + `.message` and degrades to `"Request failed with status code 400"`. Use + `await extractDownloadErrorMessage(error)` in every PDF/blob catch block. Mutation catches + keep the synchronous version — their bodies are already parsed JSON. +- Server-side guards must be reflected in the UI. If the API will reject the action, the + button should be disabled, hidden, or explain the blocker — not fire and surface a 400. +- Prefer disabling a control with a visible reason over silently hiding it. + +## Notifications + +In-app notifications resolve recipients from the company's **linked portal users**. If a +company has none, `notify()` logs `0 recipients — skipped` and stores nothing, with no error. +SMS and email still send, because they address the company's phone and email directly. Check +this before debugging a "missing notification". + +## PDF generation + +Chromium is not installed in every environment. PDF paths must fall back to the hand-rolled +generators (`styled-pdf.util.ts`, `buildFallbackPdf`, `buildTabularFallbackPdf`) rather than +assume a headless browser exists. ## Adding a new module to a NestJS app -1. Create `modules//` with `entities/`, `dto/`, and the four `.{module,controller,service,repository}.ts` files. +1. Create `modules//` with `entities/`, `dto/`, and the four + `.{module,controller,service,repository}.ts` files. 2. The entity extends `BaseEntity` from `@edr/api-common`. 3. The repository extends `BaseRepository` from `@edr/api-common`. 4. The service injects the repository class (not `Repository` directly). -5. The controller uses `@ApiTags()` + `@ApiOperation()` for Swagger. +5. The controller uses `@ApiTags()` + `@ApiOperation()` for Swagger, and guards the route. 6. Register the module in the app's `app.module.ts`. ## Adding a new shared component to `@edr/ui-common` 1. Create `src/components//.tsx` and `src/components//index.ts`. 2. Export from `src/index.ts`. -3. Component is a functional component with a `ComponentNameProps` interface (named-exported alongside the default). +3. Component is a functional component with a `ComponentNameProps` interface + (named-exported alongside the default). + +## Definition of done + +A change is done when **all** of these hold. State explicitly which you ran. + +1. **It type-checks.** `pnpm turbo type-check --filter=` passes. + If you edited `packages/types`, you ran `pnpm turbo build --filter=@edr/types` first. +2. **Raw SQL is verified.** Every new or edited SQL statement ran under `EXPLAIN` against the + dev database without error. +3. **Migrations are safe.** Unique timestamp, idempotent DDL, and — if the migration adds + something the new code reads — applied to the dev database, since watch mode will not run it. +4. **No new test failures.** `pnpm test` for `@edr/freight-api` has been red on `dev`, so a + fully green suite is not the bar — but confirm that for yourself rather than assuming it, + then run the specs covering what you touched and confirm you introduced no new failure. +5. **Formatting is clean** for the files you touched. Git hooks do **not** run automatically + (see below), and `pnpm lint` does not work at all, so `noUnusedLocals` from the + type-check is your only unused-code signal. +6. **The behaviour was actually observed**, not merely compiled — you drove the flow, hit the + endpoint, or ran the query. If you could not, say so plainly. +7. **Report honestly.** If a check was skipped, tests failed, or a fix is unverified, say it in + the summary. Never describe unverified work as done. + +### Hooks do not run + +`commitlint.config.js` and a `lint-staged` config both exist, and husky's shims are installed +at `.husky/_/`. But there are **no user hook scripts** (`.husky/pre-commit`, +`.husky/commit-msg`), so husky's shim exits 0 and **neither lint-staged nor commitlint ever +fire.** Nothing validates your commit message or formats your staged files. Run the checks by +hand; do not assume the hook caught it. + +## Known traps + +| Trap | What happens | What to do | +| --- | --- | --- | +| Schema drift | A recorded migration's column is missing; queries and inserts 500 | Write a new repair migration; never edit the recorded one | +| Duplicate migration timestamps | Non-deterministic ordering; a migration can be skipped | Pick a fresh, higher timestamp | +| `@edr/types` not rebuilt | Consumers can't see your new field | `pnpm turbo build --filter=@edr/types` | +| Slow I/O in a transaction | Locks held for the gateway timeout | Queue it; fan out after commit; always set an HTTP timeout | +| Blob error bodies | Real 400 message replaced by "Request failed with status code 400" | `await extractDownloadErrorMessage(error)` | +| Company with no portal user | In-app notification silently vanishes | Check portal users before debugging | +| 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 | + +## Project skills + +Reusable workflows live in `.claude/skills/`. Use them instead of re-deriving the steps: + +| Skill | Use for | +| --- | --- | +| `edr-db` | Query / `EXPLAIN`-validate / inspect the remote dev DB (`node .claude/skills/edr-db/query.cjs …`). psql is not installed — this is the sanctioned path. Also carries the 400/500 diagnosis loop. | +| `verify` | The definition-of-done runner: targeted type-check, `@edr/types` rebuild, SQL validation, migration checklist, honest test bar. Run before calling anything finished. | +| `standup` | "What did I do today / this week" reports for tickets, grounded in `git log` — including the check that commit subjects match their contents. | + +## Working style + +- **Verify before asserting.** Read the code or query the database. Do not infer behaviour + from a filename. +- **Investigate, then propose.** For anything risky or wide-reaching, present the plan and the + trade-off before changing files. +- **Small, reviewable commits**, one logical change each, conventional message. +- **Branch from `dev`; PRs target `dev`.** +- When a finding turns out to be wrong, say so and retract it. A rejected finding is a result. diff --git a/CLAUDE_NEW.md b/CLAUDE_NEW.md deleted file mode 100644 index a99f64d0a..000000000 --- a/CLAUDE_NEW.md +++ /dev/null @@ -1,313 +0,0 @@ -# EDR Platform — Developer Guide - -> This file is the contract. If something here contradicts the code, the code is the -> truth and this file is a bug — fix it in the same PR. - -## Overview - -Monorepo for the Ethio Djibouti Railway (EDR) digital platform. Contains the Freight -Management and Passenger Management applications, a payment microservice, plus shared -types, NestJS utilities, and React component libraries. - -The freight domain is the largest and most active area. Its core flow is: -**booking → receive to warehouse → store → load onto train → dispatch → arrive → unload -→ customer truck (self-haul) or EDR last mile → handover → exit paper → delivered.** -Fees (storage, demurrage, double handling, truck detention) and allocation rules -(warehouse/yard/zone) hang off the warehouse stage. - -## Apps - -| App | Package name | Purpose | Default port | -| ------------------------------ | --------------------------- | -------------------------------------------------- | ------------ | -| `edr-freight-api` | `@edr/freight-api` | NestJS API for freight management | 3001 | -| `edr-freight-web/portal` | `@edr/freight-portal` | React frontend for freight customer/portal users | 5173 | -| `edr-freight-web/backoffice` | `@edr/freight-backoffice` | React frontend for freight backoffice employees | 5183 | -| `edr-passenger-api` | `@edr/passenger-api` | NestJS API for passenger management | 3002 | -| `edr-payment-api` | `@edr/payment-api` | NestJS payment microservice (intents, webhooks) | 3003 | -| `edr-passenger-web/portal` | `@edr/passenger-portal` | React frontend for passenger customer/portal users | 5174 | -| `edr-passenger-web/backoffice` | `@edr/passenger-backoffice` | React frontend for passenger backoffice employees | 5184 | - -`edr-freight-web` and `edr-passenger-web` are grouping folders, not workspace packages. -Each holds a `portal/` and `backoffice/` sub-app, both independent pnpm workspace -packages (see `pnpm-workspace.yaml`). - -`apps/edr-landing/` exists on disk but has **no `package.json`** — it is not a workspace -package and is not built, linted, or type-checked. Leave it alone unless asked. - -## Packages - -| Package | Purpose | -| ---------------------- | ---------------------------------------------------------------------------------- | -| `@edr/types` | Shared TypeScript interfaces and enums | -| `@edr/api-common` | Shared NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository | -| `@edr/ui-common` | Shared React components and theme | -| `@edr/eslint-config` | Shared ESLint configurations (base/nestjs/react) | -| `@edr/tsconfig` | Shared TypeScript configurations | -| `@edr/prettier-config` | Shared Prettier configuration | - -**`@edr/types` is consumed as its built `dist/`** (`main: ./dist/index.js`). Editing a -type in `packages/types/src` changes nothing for consumers until you rebuild: - -```bash -pnpm turbo build --filter=@edr/types -``` - -If a type-check fails on a field you just added to `@edr/types`, this is why. - -## Commands - -| Command | Description | -| --------------------------- | ---------------------------------------- | -| `pnpm install` | Install all workspace dependencies | -| `pnpm dev` | Run every app in dev mode | -| `pnpm dev:freight` | Freight API + portal + backoffice | -| `pnpm dev:freight:api` | Freight API only | -| `pnpm dev:freight:portal` | Freight portal only | -| `pnpm dev:freight:backoffice` | Freight backoffice only | -| `pnpm dev:passenger` | Passenger API + web | -| `pnpm dev:payment` | Payment API | -| `pnpm build` | Build every package and app | -| `pnpm test` | Run all tests (turbo) | -| `pnpm lint` | Lint everything | -| `pnpm type-check` | Type-check every package | -| `pnpm format` | Format all files with Prettier | - -Prefer targeted turbo filters over whole-repo runs — they are minutes faster: - -```bash -pnpm turbo type-check --filter=@edr/freight-api --filter=@edr/freight-backoffice -``` - -`apps/edr-freight-api` also carries many `seed:*` scripts (demo bookings, wagons, trains, -gate-pass scenarios). Read the script before running one; several write real rows. - -## 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. -- 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. -- 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. -- `psql` is not installed on the dev machine. To query the database, write a short Node - script using the `pg` client and run it from `apps/edr-freight-api` (where `pg` resolves). - -## Hard rules - -These are non-negotiable. Everything else is a strong default. - -- **pnpm only.** Never run `npm install` or `yarn`. -- **TypeScript strict mode** is on in every package and app. Do not weaken it, and do not - reach for `any` to make an error go away. -- **Never `synchronize: true`.** Not in production, not anywhere. It is currently `false` - in every config and it has already corrupted this database twice (see *Migrations*). - All schema changes go through TypeORM migrations. -- **All entities** use UUID primary keys (`@PrimaryGeneratedColumn('uuid')`). -- **All entities** extend `BaseEntity` from `@edr/api-common` — `createdAt`, `updatedAt`, - `deletedAt` (soft delete). -- **All columns** are `snake_case` in the database (`@Column({ name: 'snake_case' })`); - TypeScript properties are `camelCase`. -- **Controllers contain no business logic.** They validate, delegate, and shape the response. -- **Conventional commits.** `fix(warehouses): …`, `feat(bookings): …`. -- **Do not commit or push unless asked.** Propose the change; let the human decide when it lands. -- **Do not break working behaviour to add new behaviour.** When a fix is risky, say so and - offer the safe version. - -## Architecture - -### NestJS module shape - -`module → controller → service → repository`, with `entities/` and `dto/` alongside. - -### Data access — the real model - -There are two sanctioned ways to read and write, and you must pick the right one: - -1. **Entity CRUD → the custom repository class.** Extends `BaseRepository` from - `@edr/api-common`. Services inject the repository class, never `Repository` directly. -2. **Read projections, queue endpoints, cross-table reports → raw SQL** via - `this.dataSource.query(...)` or `manager.query(...)` inside a transaction. - -Raw SQL is normal here, not a smell — the warehouse and scheduling modules are built on it. -It carries one obligation: - -> **HARD RULE — validate every raw SQL statement against a real database before you ship it.** -> A typo'd column name is a runtime 500 that no type-checker will catch. Run it through -> `EXPLAIN` against the dev database. Column drift is real (see *Migrations*). - -Writes inside a transaction use `manager.getRepository(Entity)`, not the injected repository, -so they join the caller's transaction. - -**Never do slow I/O inside a database transaction.** Queue the work and fan it out after -commit. An SMS awaited inside a transaction once held capacity locks open for the whole -gateway timeout. Any outbound HTTP call must set an explicit `timeout` — axios defaults to -no timeout and will wait forever. - -### Migrations - -Migrations are the most dangerous surface in this repo. Two production-grade incidents have -already come from it. - -- `migrationsRun: false` — **migrations do NOT run on API boot.** They run as a separate - one-shot step, via the Dockerfile's `migration` build target (`docker build --target - migration`), with `migrationsTransactionMode: 'each'`. - - CI: `.github/workflows/deploy.yml` builds the `migration` image and runs it - (`docker run --rm --env-file ...`) *before* building/deploying the app image. - - e2e: `docker-compose.e2e.yaml`'s `freight-migration-e2e` service runs once and - `freight-api-e2e` depends on it (`condition: service_completed_successfully`). - - Local dev (`docker-compose.yaml`) has no equivalent migration service yet — run - migrations yourself before `docker compose up freight-api`, e.g. - `docker build --target migration -f apps/edr-freight-api/Dockerfile -t freight-migration .` - then `docker run --rm --env-file apps/edr-freight-api/.env freight-migration`. Don't - use `pnpm run migrate` for this — it runs via `ts-node`, which never writes compiled - output to `dist/`, and the freight migrations glob only matches `dist/migrations/*.js`. - It silently applies zero freight migrations while exiting 0. -- Consequences you must design for: - - A watch-mode hot reload does **not** re-run migrations. If you add a column that new - code reads, apply it to the dev database yourself (idempotently) or fully restart. - - `apps/edr-freight-api/src/config/database.config.ts`'s `iamEntities` array is a - hand-maintained list of `@tria-plc/iamapi-common` entity classes. The live app never - notices when it's stale (`autoLoadEntities: true` papers over gaps via IAM's own - `forFeature()` registrations), but the standalone migration `DataSource` - (`data-source.ts`, no `autoLoadEntities`) does not have that fallback — a missing - entity throws `Entity metadata for X#y was not found` at `initialize()`, before a - single migration runs. **Every `@tria-plc/iamapi-common` version bump is a candidate - for this to break again** — diff the package's entity classes against `iamEntities` - when bumping it. -- **Give every migration a unique timestamp.** 34 timestamps are currently shared by two or - more migrations. TypeORM orders by timestamp and breaks ties non-deterministically. Before - adding one, check the filename prefix is unused *and* higher than the newest recorded row. -- **Write idempotent DDL**: `ADD COLUMN IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, and - backfills guarded by `WHERE col IS NULL`. -- **Never assume a recorded migration actually applied.** `AddGrnNumberToWarehouseInventory` - was recorded in `migrations` while its column was absent — it had been dropped out of band. - TypeORM will never re-run a recorded migration, so the fix is a *new repair migration*. -- **A repair migration's `down()` should be a no-op.** Reverting a repair must not - re-introduce the outage it fixed. - -### Auth - -Auth **is implemented in this repo.** Do not add TODO stubs, and do not write your own. - -- `@CurrentUser()` (`@edr/api-common`) is a real `createParamDecorator`, not a metadata stub. -- Route protection uses `@UseGuards(JwtGuard)` and `@UseGuards(PermissionGuard([...]))`. -- Freight-domain checks use `hasFreightPermission(user, FREIGHT_PERMS..)`. -- Permissions are declared in `apps/edr-freight-api/src/seed/freight-permissions.registry.ts`. - Add a permission there before referencing it. -- IAM has its own migrations, run ahead of freight migrations from the same data source, and - its own CLI scripts (`iam:migration:run`, `iam:seed:run`). - -Ownership checks are separate from permission checks. A staff user passes -`hasFreightPermission`; a customer must additionally pass an ownership assertion such as -`assertCustomerCanAccessBooking`. Do not drop the ownership check because the permission check passed. - -## Frontend conventions - -- The web apps use **Mantine v9**. Its APIs differ from v6/v7 — check the installed version - before copying a snippet. -- `@edr/ui-common` holds shared components and theme; it is imported in ~94 files across the - freight web apps. Prefer it over re-implementing a component. -- **Blob downloads need the async error decoder.** A request with `responseType: 'blob'` - delivers the JSON error body as a `Blob`, so the synchronous `extractErrorMessage` finds no - `.message` and degrades to `"Request failed with status code 400"`. Use - `await extractDownloadErrorMessage(error)` in every PDF/blob catch block. Mutation catches - keep the synchronous version — their bodies are already parsed JSON. -- Server-side guards must be reflected in the UI. If the API will reject the action, the - button should be disabled, hidden, or explain the blocker — not fire and surface a 400. -- Prefer disabling a control with a visible reason over silently hiding it. - -## Notifications - -In-app notifications resolve recipients from the company's **linked portal users**. If a -company has none, `notify()` logs `0 recipients — skipped` and stores nothing, with no error. -SMS and email still send, because they address the company's phone and email directly. Check -this before debugging a "missing notification". - -## PDF generation - -Chromium is not installed in every environment. PDF paths must fall back to the hand-rolled -generators (`styled-pdf.util.ts`, `buildFallbackPdf`, `buildTabularFallbackPdf`) rather than -assume a headless browser exists. - -## Adding a new module to a NestJS app - -1. Create `modules//` with `entities/`, `dto/`, and the four - `.{module,controller,service,repository}.ts` files. -2. The entity extends `BaseEntity` from `@edr/api-common`. -3. The repository extends `BaseRepository` from `@edr/api-common`. -4. The service injects the repository class (not `Repository` directly). -5. The controller uses `@ApiTags()` + `@ApiOperation()` for Swagger, and guards the route. -6. Register the module in the app's `app.module.ts`. - -## Adding a new shared component to `@edr/ui-common` - -1. Create `src/components//.tsx` and `src/components//index.ts`. -2. Export from `src/index.ts`. -3. Component is a functional component with a `ComponentNameProps` interface - (named-exported alongside the default). - -## Definition of done - -A change is done when **all** of these hold. State explicitly which you ran. - -1. **It type-checks.** `pnpm turbo type-check --filter=` passes. - If you edited `packages/types`, you ran `pnpm turbo build --filter=@edr/types` first. -2. **Raw SQL is verified.** Every new or edited SQL statement ran under `EXPLAIN` against the - dev database without error. -3. **Migrations are safe.** Unique timestamp, idempotent DDL, and — if the migration adds - something the new code reads — applied to the dev database, since watch mode will not run it. -4. **No new test failures.** `pnpm test` for `@edr/freight-api` is **currently red on `dev`**, - so a fully green suite is not the bar. Run the specs covering what you touched and confirm - you introduced no new failure. -5. **Lint and format are clean** for the files you touched. Git hooks do **not** run these - automatically (see below), so run them yourself. -6. **The behaviour was actually observed**, not merely compiled — you drove the flow, hit the - endpoint, or ran the query. If you could not, say so plainly. -7. **Report honestly.** If a check was skipped, tests failed, or a fix is unverified, say it in - the summary. Never describe unverified work as done. - -### Hooks do not run - -`commitlint.config.js` and a `lint-staged` config both exist, and husky's shims are installed -at `.husky/_/`. But there are **no user hook scripts** (`.husky/pre-commit`, -`.husky/commit-msg`), so husky's shim exits 0 and **neither lint-staged nor commitlint ever -fire.** Nothing validates your commit message or formats your staged files. Run the checks by -hand; do not assume the hook caught it. - -## Known traps - -| Trap | What happens | What to do | -| --- | --- | --- | -| Schema drift | A recorded migration's column is missing; queries and inserts 500 | Write a new repair migration; never edit the recorded one | -| Duplicate migration timestamps | Non-deterministic ordering; a migration can be skipped | Pick a fresh, higher timestamp | -| `@edr/types` not rebuilt | Consumers can't see your new field | `pnpm turbo build --filter=@edr/types` | -| Slow I/O in a transaction | Locks held for the gateway timeout | Queue it; fan out after commit; always set an HTTP timeout | -| Blob error bodies | Real 400 message replaced by "Request failed with status code 400" | `await extractDownloadErrorMessage(error)` | -| Company with no portal user | In-app notification silently vanishes | Check portal users before debugging | -| Watch-mode reload | New code, old schema → 500 | Apply the migration to the dev DB or restart fully | - -## Project skills - -Reusable workflows live in `.claude/skills/`. Use them instead of re-deriving the steps: - -| Skill | Use for | -| --- | --- | -| `edr-db` | Query / `EXPLAIN`-validate / inspect the remote dev DB (`node .claude/skills/edr-db/query.cjs …`). psql is not installed — this is the sanctioned path. Also carries the 400/500 diagnosis loop. | -| `verify` | The definition-of-done runner: targeted type-check, `@edr/types` rebuild, SQL validation, migration checklist, honest test bar. Run before calling anything finished. | -| `standup` | "What did I do today / this week" reports for tickets, grounded in `git log` — including the check that commit subjects match their contents. | - -## Working style - -- **Verify before asserting.** Read the code or query the database. Do not infer behaviour - from a filename. -- **Investigate, then propose.** For anything risky or wide-reaching, present the plan and the - trade-off before changing files. -- **Small, reviewable commits**, one logical change each, conventional message. -- **Branch from `dev`; PRs target `dev`.** -- When a finding turns out to be wrong, say so and retract it. A rejected finding is a result. diff --git a/docs/MAP.md b/docs/MAP.md new file mode 100644 index 000000000..d6aa76322 --- /dev/null +++ b/docs/MAP.md @@ -0,0 +1,96 @@ +# Repo map — start here + +Routing table for "where does X live". Read this before a repo-wide grep. Paths are from +the repo root. The rules and traps are in [`../CLAUDE.md`](../CLAUDE.md); this file only +answers *where*. + +## Pick your stack first + +| If you are working on… | Code lives in | Stack | +| --------------------------------- | --------------------------------- | -------------------- | +| Freight API / business logic | `apps/edr-freight-api/src` | NestJS + TypeORM | +| Freight customer UI | `apps/edr-freight-web/portal` | React + Vite + Mantine v9 | +| Freight staff UI | `apps/edr-freight-web/backoffice` | React + Vite + Mantine v9 | +| Passenger API | `apps/edr-passenger-api` | NestJS + **Prisma** | +| Passenger UI | `apps/edr-passenger-web/*` | **Next.js** | +| Payments (intents, webhooks) | `apps/edr-payment-api` | NestJS + TypeORM | +| Gateway integrations | `packages/payment-providers` | — | +| A shared type or enum | `packages/types/src` | rebuild after editing | +| A shared React component | `packages/ui-common/src` | — | +| A Nest decorator/filter/base class| `packages/api-common/src` | — | + +## Freight API entry points + +| File | What it is | +| --- | --- | +| `src/main.ts` | Boot, port (`PORT`, falls back to 3001), global pipes | +| `src/app.module.ts` | Every module is registered here — the index of the API | +| `src/config/database.config.ts` | Connection, pooler `search_path` handling, `iamEntities` list | +| `src/data-source.ts` | Standalone DataSource used by migrations only (no `autoLoadEntities`) | +| `src/migrations/` | TypeORM migrations (39 files; check for timestamp clashes) | +| `src/seed/freight-permissions.registry.ts` | Every freight permission; declare before use | +| `src/scripts/` | One-off and `seed:*` scripts — several write real rows | + +## Freight modules by domain + +All under `apps/edr-freight-api/src/modules/`. + +| Domain | Modules | +| --- | --- | +| **Booking & commercial** | `bookings` `contracts` `contract-templates` `consignments` `cargoes` `companies` `user-trade-access` `transit-agents` `shipping-lines` | +| **Warehouse & yard** | `warehouses` `facilities` `container-management` | +| **Rail operations** | `trains` `train-schedules` `train-scheduling` `train-sets` `wagons` `wagon-types` `locomotives` `routes` `scheduling` `scheduling-reschedule` `interchange-documents` | +| **Road / first & last mile** | `first-mile` `last-mile` `last-mile-requests` `drivers` `vehicles` `truck-types` `fleet-history` `fuel` `maintenance` `gps-tracking` `tracking` | +| **Money** | `billing` `payment` `exchange-settings` | +| **Identity & access** | `auth` `otp` `verifayda` `audit` | +| **Documents & files** | `files` `file-upload-settings` `signatures` `stamp-settings` `logo-settings` `minio` | +| **Comms** | `notifications` `notification-inbox` `support-chat` `support-content` | +| **Ops & admin** | `backoffice` `overview` `reports` `dropdown-settings` `rule-engine` `compliance` `procurement` `incidents` `import-operations` `eims` `ai` `health` | + +Each module follows `module → controller → service → repository`, with `entities/` and +`dto/` alongside. + +## Freight web + +Pages live in `src/pages/`, roughly mirroring the API domains. + +- **portal** (customer): `bookings` `contracts` `consignments` `billing` `payments` + `tracking` `accounts` `customers` `shipping-line` `support` `settings`, plus + `MyPortalPage/`, `MySignaturePage.tsx`, `EDRFreightLandingPage.tsx`. +- **backoffice** (staff): `bookings` `contracts` `contract_templates` `consignments` + `customers` `billing` `invoices` `documents` `fleet` `dashboard` `admin` `configuration` + `auth` `ai`, plus many single-file pages (`AuditLogsPage.tsx`, `ActivityLogPage.tsx`, + `BulkUploadPage.tsx`, `ContentManagementPage.tsx`, …). + +Shared components and theme come from `@edr/ui-common` — check there before writing one. + +## Tests + +| Suite | Location | Run with | +| --- | --- | --- | +| Unit / spec | beside the code, `*.spec.ts` | `pnpm --filter @edr/freight-api test` | +| Freight e2e (Cypress, containerized) | `e2e/freight` | `pnpm e2e:freight:ci` | +| Passenger e2e | `e2e/` + `e2e/run.sh` | `pnpm test:e2e:passenger` | +| UI e2e (Playwright) | `e2e-ui/` | `pnpm test:e2e:ui` | +| Integration | `integration/` | `pnpm it:up`, `pnpm it:test` | + +The freight e2e stack has no host Xvfb — use `ci` (containerized), not `run`/`open`. +Its compose project name is fixed (`edr-freight-e2e`), so only one can run on this +machine at a time. + +## Documents + +| Doc | Covers | Trust | +| --- | --- | --- | +| `../CLAUDE.md` | The contract: rules, traps, definition of done | Current — fix in the same PR if wrong | +| `docs/TESTING.md` | Test strategy | — | +| `docs/e2e-test-matrix.md`, `docs/ui-e2e-test-matrix.md` | Coverage matrices | — | +| `docs/ISSUES.md`, `docs/SOLUTIONS.md` | Running log of problems and fixes | Historical | +| `docs/uploads.md` | File upload handling | — | +| `docs/qa/edr-freight-qa-test-plan.md` | QA test plan | — | +| `../DEPLOYMENT.md` | Deploy process | — | +| `../ITMLS_DB_Design.md`, `../orgstructure.md` | Design notes | Historical | +| `../E2E_TEST_REPORT.md`, `../checkpoint.md` | Point-in-time snapshots | **Stale by design — dated artifacts, not references** | + +Root-level `*.sql` and `*.dump` files are ad-hoc data snapshots, not part of the schema. +Migrations are the only source of truth for schema.