mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
docs
This commit is contained in:
388
CLAUDE.md
388
CLAUDE.md
@@ -1,102 +1,362 @@
|
|||||||
# EDR Platform — Developer Guide
|
# 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
|
## 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
|
## Apps
|
||||||
|
|
||||||
| App | Package name | Purpose | Port |
|
The two domains are **not built the same way**. Check which stack you are in before
|
||||||
| ------------------------------ | --------------------------- | -------------------------------------------------- | ---- |
|
copying a pattern across:
|
||||||
| `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 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
|
## Packages
|
||||||
|
|
||||||
| Package | Purpose |
|
| Package | Location | Purpose |
|
||||||
| ---------------------- | ---------------------------------------------------------------------------------- |
|
| ----------------------- | ----------------------------- | ------------------------------------------------------------- |
|
||||||
| `@edr/types` | Shared TypeScript interfaces and enums |
|
| `@edr/types` | `packages/types` | Shared TypeScript interfaces and enums |
|
||||||
| `@edr/api-common` | Shared NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository |
|
| `@edr/api-common` | `packages/api-common` | 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` | `packages/ui-common` | Shared React components and theme |
|
||||||
| `@edr/ui-common` | Shared React components and theme |
|
| `@edr/iam-seed` | `packages/iam-seed` | IAM baseline seeder for apps sharing the `iam` schema |
|
||||||
| `@edr/eslint-config` | Shared ESLint configurations (base/nestjs/react) |
|
| `@edr/payment-providers`| `packages/payment-providers` | Payment gateway integrations |
|
||||||
| `@edr/tsconfig` | Shared TypeScript configurations |
|
| `@edr/eslint-config` | `packages/config/eslint-config` | Shared ESLint configs (base/nestjs/react) |
|
||||||
| `@edr/prettier-config` | Shared Prettier configuration |
|
| `@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
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| -------------------- | ---------------------------------- |
|
| ----------------------------- | ---------------------------------------- |
|
||||||
| `pnpm install` | Install all workspace dependencies |
|
| `pnpm install` | Install all workspace dependencies |
|
||||||
| `pnpm dev` | Run every app in dev mode |
|
| `pnpm dev` | Run every app in dev mode |
|
||||||
| `pnpm dev:freight` | Run only freight API + web |
|
| `pnpm dev:freight` | Freight API + portal + backoffice |
|
||||||
| `pnpm dev:passenger` | Run only passenger API + web |
|
| `pnpm dev:freight:api` | Freight API only |
|
||||||
| `pnpm build` | Build every package and app |
|
| `pnpm dev:freight:portal` | Freight portal only |
|
||||||
| `pnpm test` | Run all tests |
|
| `pnpm dev:freight:backoffice` | Freight backoffice only |
|
||||||
| `pnpm lint` | Lint everything |
|
| `pnpm dev:passenger` | Passenger API + web |
|
||||||
| `pnpm type-check` | Type-check every package |
|
| `pnpm dev:payment` | Payment API |
|
||||||
| `pnpm format` | Format all files with Prettier |
|
| `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 format` uses bare `prettier`, which ignores `@edr/prettier-config` — it is wired to
|
||||||
- **pnpm** is the only supported package manager — never run `npm install` or `yarn`.
|
nothing. On the single-quoted passenger apps it will re-quote the whole file. Pass
|
||||||
- **Conventional commits** are enforced via commitlint on every commit.
|
`--config` explicitly there.
|
||||||
- **NestJS modules** follow the 4-layer pattern: `module → controller → service → repository` (entities and DTOs live alongside).
|
|
||||||
|
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** use UUID primary keys (`@PrimaryGeneratedColumn('uuid')`).
|
||||||
- **All entities** have `createdAt`, `updatedAt`, `deletedAt` (soft delete) via `@edr/api-common`'s `BaseEntity`.
|
- **All entities** extend `BaseEntity` from `@edr/api-common` — `createdAt`, `updatedAt`,
|
||||||
- **All columns** use `snake_case` in the database (`@Column({ name: 'snake_case' })`); TypeScript properties use `camelCase`.
|
`deletedAt` (soft delete).
|
||||||
- **Never use `synchronize: true`** in production database config. All schema changes go through TypeORM migrations.
|
- **All columns** are `snake_case` in the database (`@Column({ name: 'snake_case' })`);
|
||||||
- **ESLint + Prettier** run on pre-commit via Husky + lint-staged.
|
TypeScript properties are `camelCase`.
|
||||||
- **Services** never inject TypeORM `Repository<T>` directly — they inject the custom repository class.
|
- **Controllers contain no business logic.** They validate, delegate, and shape the response.
|
||||||
- **Controllers** never contain business logic.
|
- **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`
|
### Data access — the real model
|
||||||
- `// TODO: integrate @edr/auth — replace stub @CurrentUser with real one`
|
|
||||||
|
|
||||||
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<Entity>` from
|
||||||
|
`@edr/api-common`. Services inject the repository class, never `Repository<T>` 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
|
Raw SQL is normal here, not a smell — the warehouse and scheduling modules are built on it.
|
||||||
- `edr-freight-web/portal`: 5173
|
It carries one obligation:
|
||||||
- `edr-freight-web/backoffice`: 5183
|
|
||||||
- `edr-passenger-api`: 3002
|
|
||||||
- `edr-payment-api`: 3003
|
|
||||||
- `edr-passenger-web/portal`: 5174
|
|
||||||
- `edr-passenger-web/backoffice`: 5184
|
|
||||||
|
|
||||||
## 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.
|
Writes inside a transaction use `manager.getRepository(Entity)`, not the injected repository,
|
||||||
- `postgres-passenger` (port 5434): database `edr_passenger` — passenger API only.
|
so they join the caller's transaction.
|
||||||
- `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.
|
**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.<area>.<action>)`.
|
||||||
|
- 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
|
## Adding a new module to a NestJS app
|
||||||
|
|
||||||
1. Create `modules/<feature>/` with `entities/`, `dto/`, and the four `<feature>.{module,controller,service,repository}.ts` files.
|
1. Create `modules/<feature>/` with `entities/`, `dto/`, and the four
|
||||||
|
`<feature>.{module,controller,service,repository}.ts` files.
|
||||||
2. The entity extends `BaseEntity` from `@edr/api-common`.
|
2. The entity extends `BaseEntity` from `@edr/api-common`.
|
||||||
3. The repository extends `BaseRepository<Entity>` from `@edr/api-common`.
|
3. The repository extends `BaseRepository<Entity>` from `@edr/api-common`.
|
||||||
4. The service injects the repository class (not `Repository<T>` directly).
|
4. The service injects the repository class (not `Repository<T>` 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`.
|
6. Register the module in the app's `app.module.ts`.
|
||||||
|
|
||||||
## Adding a new shared component to `@edr/ui-common`
|
## Adding a new shared component to `@edr/ui-common`
|
||||||
|
|
||||||
1. Create `src/components/<Name>/<Name>.tsx` and `src/components/<Name>/index.ts`.
|
1. Create `src/components/<Name>/<Name>.tsx` and `src/components/<Name>/index.ts`.
|
||||||
2. Export from `src/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=<each touched package>` 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.
|
||||||
|
|||||||
313
CLAUDE_NEW.md
313
CLAUDE_NEW.md
@@ -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<Entity>` from
|
|
||||||
`@edr/api-common`. Services inject the repository class, never `Repository<T>` 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.<area>.<action>)`.
|
|
||||||
- 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/<feature>/` with `entities/`, `dto/`, and the four
|
|
||||||
`<feature>.{module,controller,service,repository}.ts` files.
|
|
||||||
2. The entity extends `BaseEntity` from `@edr/api-common`.
|
|
||||||
3. The repository extends `BaseRepository<Entity>` from `@edr/api-common`.
|
|
||||||
4. The service injects the repository class (not `Repository<T>` 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/<Name>/<Name>.tsx` and `src/components/<Name>/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=<each touched package>` 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.
|
|
||||||
96
docs/MAP.md
Normal file
96
docs/MAP.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user