The modal read the booking id only from item.booking?.id, but inventory- workbench rows carry bookingId without the booking relation. Result: the container-weights and assigned-trucks queries never ran, so "Recorded net weight (system)" fell back to the inventory row's weight (often 0), the container MultiSelect never appeared, and the truck dropdown showed "not assigned" even when trucks existed - while the server correctly computed the cargo weight and rejected the mismatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EDR Platform - Ethio-Djibouti Railway Passenger API
Enterprise-grade NestJS REST API for the Ethio-Djibouti Railway passenger booking and management platform. Built with TypeScript, PostgreSQL, and Prisma ORM.
🚀 Features
🆕 NEW: Age-Based Pricing, Verifayda 2.0 & Multi-Currency
Age-Based Pricing
- ADULT (≥5 years): Pay 100% of base fare
- CHILD (<5 years): First child travels FREE, subsequent children pay 100%
- Automatic age calculation from date of birth
- Example: 2 adults + 3 children = 4× base fare (first child free)
Verifayda 2.0 Integration
- Real-time Ethiopian national ID verification
- Retrieves passenger data from government database
- National IDs NOT stored (policy compliant)
- Non-Ethiopians use passport (no verification required)
- Booking fails if verification unsuccessful
Multi-Currency Support
- Transaction Currency: ETB (Ethiopian Birr)
- Display Currencies: ETB, DJF (Djiboutian Franc), USD (US Dollar)
- Real-time exchange rate conversion
- Prices shown in user's preferred currency
- Exchange rates: ETB→DJF=3.25, ETB→USD=0.018
Core Modules
- Authentication & Authorization - Dual authentication system:
- Passenger Auth: JWT-based auth with OTP verification, password reset, account lockout
- Corporate IAM: Integration with @tria-plc corporate identity system for back-office operations (agents, supervisors, admins)
- Role-based access control (RBAC) with granular permissions
- Age-Based Pricing - Smart passenger categorization:
- ADULT (≥5 years): Full fare
- CHILD (<5 years): First child free, subsequent children full fare
- Automatic age calculation from date of birth
- Verifayda 2.0 Integration - Ethiopian national ID verification:
- Real-time verification via government API
- Retrieves passenger data (name, DOB, nationality)
- National IDs NOT stored (policy compliant)
- Non-Ethiopians use passport (no verification)
- Multi-Currency Support - Display prices in multiple currencies:
- ETB (Ethiopian Birr) - Transaction currency
- DJF (Djiboutian Franc) - Display option
- USD (US Dollar) - Display option
- Real-time exchange rate conversion
- Booking Management - Complete booking lifecycle:
- Guest Booking: Book without login, optional account creation
- Saved Passengers: Store passenger details for quick rebooking
- Modification, cancellation, refunds, and fare breakdown
- Multi-segment journey support
- Payment Integration - Multi-provider support (Telebirr, CBE Birr, eBirr, Card, Wallet) with webhook handling
- Seat Management - Real-time seat inventory:
- Seat holds with 5-minute expiry
- Seat releases and blocking with coach/class management
- Segment-based seat availability (partial journey bookings)
- Auto-assign seats with contiguous algorithm
- CSV import/export for seat configurations
- Ticketing - QR code and barcode generation, PDF tickets, gate validation with audit logs
- Agent Operations - Counter booking, shift management, commission tracking, and reconciliation
- Passenger Services - Profile management, traveler profiles, saved routes, and preferences
- Loyalty Program - Points accumulation, tier management (Bronze/Silver/Gold/Platinum), and rewards
- Wallet System - Balance management, top-up, transaction ledger
- Live Tracking - Real-time trip status, location updates, delay notifications, crowd signals
- Notifications - Multi-channel (Email, SMS, Push) with templating engine
- Support System - FAQ management, live chat conversations
- Reports & Analytics - Revenue reports, occupancy analytics, agent sales tracking
- Route Management - Route configuration, stops, fare rules, baggage allowance
Technical Features
- Security - Password hashing (bcrypt), JWT tokens, rate limiting, audit logging
- Validation - Request validation with class-validator, DTO transformation
- Documentation - Auto-generated Swagger/OpenAPI docs at
/api-docs - Error Handling - Global exception filters with standardized error responses
- Database - PostgreSQL with Prisma ORM, migrations, and comprehensive seeding
- Scheduling - Cron jobs for automated tasks (seat release, report generation)
- Event System - Event-driven architecture with @nestjs/event-emitter
📋 Prerequisites
- Node.js >= 20.x
- pnpm >= 9.x (
npm install -g pnpm) - PostgreSQL >= 15.x
- Git
🛠️ Installation & Setup
1. Clone Repository
git clone <repository-url>
cd edr-platform
2. Install Dependencies
EDR Platform
Monorepo for the Ethio-Djibouti Railway digital platform. Hosts two product lines — Freight Management and Passenger Management — each with a NestJS API plus React portal and back-office web apps, sharing TypeScript types, NestJS utilities, and a React component library.
Tech Stack
Backend
| Layer | Tech |
|---|---|
| Runtime | Node.js ≥ 20 |
| Framework | NestJS 11 (modular architecture) |
| Language | TypeScript 5 (strict mode, project-wide) |
| ORM | TypeORM 0.3 (UUID PKs, soft deletes, snake_case columns) |
| Database | PostgreSQL 16 (one DB per domain) |
| Validation | class-validator + class-transformer |
| API docs | Swagger via @nestjs/swagger |
| Messaging | @nestjs/microservices (inter-service ready) |
| Testing | Jest + Supertest |
Frontend
| Layer | Tech |
|---|---|
| Framework | React 18 + Vite 5 |
| Language | TypeScript 5 (strict) |
| Routing | React Router v6 |
| Styling | Tailwind CSS v4 (@tailwindcss/vite) + tailwind-merge + class-variance-authority |
| UI primitives | Radix UI (meta radix-ui package, shadcn-style components) |
| Icons | lucide-react |
| State / Data | Zustand (client state) · TanStack Query (server state) |
| HTTP | Axios |
| Auth UI | @tria-plc/iamui-common (external IAM) |
Shared Packages
| Package | Purpose |
|---|---|
@edr/types |
Shared TypeScript interfaces and enums |
@edr/api-common |
NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository |
@edr/ui-common |
Shared React components (DashboardLayout, Sidebar, Button, Modal, etc.) and theme tokens |
@edr/eslint-config |
Shared ESLint configs (base / nestjs / react) |
@edr/tsconfig |
Shared TypeScript configs |
@edr/prettier-config |
Shared Prettier configuration |
Tooling
- pnpm 9 — workspace package manager (sole supported PM)
- Turborepo 2 — task orchestrator with caching
- Husky + lint-staged + commitlint — pre-commit ESLint/Prettier and conventional-commit enforcement
- Docker Compose — local Postgres instances + production stack (see
infrastructure/) - Nginx — reverse proxy / static asset server in production
Planned Integrations
- MinIO — S3-compatible object storage for documents (object keys already shaped under
edr-freight/{linkedType}/{ref}/{filename})
Architecture
High-level layout
┌──────────────────────────────────────────────────────────────────┐
│ EDR Platform (monorepo) │
├──────────────────────┬───────────────────────────────────────────┤
│ Freight domain │ Passenger domain │
│ ┌────────────────┐ │ ┌────────────────┐ │
│ │ freight-portal │ │ │ passenger- │ │
│ │ (React) │ │ │ portal (React)│ │
│ ├────────────────┤ │ ├────────────────┤ │
│ │ freight- │ │ │ passenger- │ │
│ │ backoffice │ │ │ backoffice │ │
│ └───────┬────────┘ │ └───────┬────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────┐ │ ┌────────────────┐ │
│ │ freight-api │ │ │ passenger-api │ │
│ │ (NestJS) │ │ │ (NestJS) │ │
│ └───────┬────────┘ │ └───────┬────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────┐ │ ┌────────────────┐ │
│ │ postgres- │ │ │ postgres- │ │
│ │ freight │ │ │ passenger │ │
│ └────────────────┘ │ └────────────────┘ │
└──────────────────────┴───────────────────────────────────────────┘
Shared (workspace) packages
@edr/types · @edr/api-common · @edr/ui-common · @edr/{eslint,tsconfig,prettier}-config
Domain isolation
- One database per domain.
postgres-freight(port 5433, dbedr_freight) andpostgres-passenger(port 5434, dbedr_passenger). No cross-database joins. Cross-domain data flows only through API calls or message queues. - Each domain owns its data model. Freight bookings/consignments/shipments/trains/invoices/documents live only in the freight DB; passenger journeys/tickets live only in the passenger DB.
NestJS module pattern (per feature)
modules/<feature>/
entities/<feature>.entity.ts // extends BaseEntity (UUID, timestamps, soft delete)
dto/<verb>-<feature>.dto.ts // class-validator DTOs
<feature>.module.ts // wires controller + service + repository
<feature>.controller.ts // HTTP layer only — no business logic
<feature>.service.ts // business logic
<feature>.repository.ts // extends BaseRepository<Entity>; services inject this, NEVER `Repository<T>` directly
Conventions enforced across the codebase:
- All entities have UUID primary keys (
@PrimaryGeneratedColumn('uuid')). - All entities inherit
createdAt/updatedAt/deletedAtfromBaseEntity(soft delete). - DB columns use
snake_casevia@Column({ name: '...' }); TS properties staycamelCase. - No
synchronize: truein production — schema changes go through TypeORM migrations. - ESLint + Prettier run on pre-commit via Husky + lint-staged.
- Conventional-commits enforced via commitlint.
Frontend application structure
apps/edr-freight-web/portal/src/
App.tsx // routes + sidebar definition
main.tsx // React Router + QueryClient providers
components/
Breadcrumbs.tsx // shared local UI
ui/ // shadcn-style primitives (Button, Input, Dialog, Label, Textarea)
pages/
customers/ // CustomersPage + CustomerDetailPage + NewCustomerPage (dialog)
bookings/ // ... + multimodal Transport Legs editor
consignments/
tracking/ // Shipment grid + table view toggle
trains/ // Fleet roster
billing/ // Invoices
documents/ // MinIO-shaped document library
hooks/ // TanStack Query hooks (per feature)
services/ // Axios clients (per feature)
store/ // Zustand stores
lib/ // utilities (`cn` helper, formatters)
Each feature folder typically contains: *Page.tsx (list), *DetailPage.tsx, New*Page.tsx (create/edit dialog with mode: "create" | "edit"), Delete*Dialog.tsx, and a *.mock.ts seed file used by the current mock UI.
Shared layout (@edr/ui-common)
DashboardLayout provides the sidebar + header shell shared across all freight and passenger web apps:
- Sidebar — brand-tinted, icon-led navigation. Main brand color:
#10B981(rgb(16, 185, 129)— emerald-500). Icon containers use the filled style: brand-color background with a white icon. - Header — language picker, notifications, user dropdown (click-driven, click-outside / Escape close); host apps opt into a light/dark theme toggle via
enableThemeToggle(Tailwind class-based, persisted inlocalStorage).
Auth integration (planned)
Authentication is provided by an external @edr/iamui-common / @tria-plc/iamapi-common package. Do not implement login/JWT/password logic in this repo. Use placeholder TODO comments next to controllers and @CurrentUser decorators (in @edr/api-common) until the integration ships.
Apps & Ports
| App | Package name | Purpose | Port |
|---|---|---|---|
edr-freight-api |
@edr/freight-api |
NestJS API for freight | 3001 |
edr-freight-web/portal |
@edr/freight-portal |
React frontend for freight customers | 5173 |
edr-freight-web/backoffice |
@edr/freight-backoffice |
React frontend for freight employees | 5183 |
edr-passenger-api |
@edr/passenger-api |
NestJS API for passengers | 3002 |
edr-passenger-web/portal |
@edr/passenger-portal |
React frontend for passenger customers | 5174 |
edr-passenger-web/backoffice |
@edr/passenger-backoffice |
React frontend for passenger employees | 5184 |
edr-freight-web and edr-passenger-web are grouping folders, not workspace packages. Each holds independent portal/ and backoffice/ workspace packages declared in pnpm-workspace.yaml.
Getting Started
Prerequisites
- Node.js ≥ 20
- pnpm 9 (
corepack enable && corepack prepare pnpm@9.12.0 --activate) - Docker (for local Postgres)
Install
pnpm install
3. Environment Configuration
# Copy environment template
cp apps/edr-passenger-api/.env.example apps/edr-passenger-api/.env
# Edit .env file with your configuration
Required Environment Variables
| Variable | Description | Example |
|---|---|---|
NODE_ENV |
Environment mode | development |
PORT |
HTTP server port | 4000 |
DATABASE_URL |
PostgreSQL connection string | postgresql://user:pass@localhost:5432/edr_passenger |
JWT_SECRET |
JWT signing secret (change in production) | your-secret-key |
JWT_EXPIRES_IN |
JWT token expiry | 7d |
PORTAL_URL |
Web app CORS origin | http://localhost:3000 |
BACK_OFFICE_URL |
Admin portal CORS origin | http://localhost:3001 |
SENDGRID_API_KEY |
SendGrid API key (optional) | SG.xxx |
SENDGRID_FROM_EMAIL |
Email sender address | noreply@edr-platform.com |
Verifayda 2.0 Configuration (Ethiopian National ID Verification)
| Variable | Description | Example |
|---|---|---|
VERIFAYDA_ENABLED |
Enable Verifayda integration | true or false |
VERIFAYDA_API_URL |
Verifayda API endpoint | https://api.verifayda.gov.et/v2 |
VERIFAYDA_API_KEY |
API key for Verifayda service | your-verifayda-api-key |
Note: When VERIFAYDA_ENABLED=false, verification is skipped (development mode only).
Corporate IAM Configuration (Back-office Authentication)
| Variable | Description | Example |
|---|---|---|
IAM_ENABLED |
Enable corporate IAM integration | true or false |
IAM_API_URL |
Corporate IAM API endpoint | https://iam.tria-plc.com/api |
IAM_API_KEY |
API key for IAM service | your-iam-api-key |
Note: When IAM_ENABLED=false, IAM-protected routes allow access without validation (development mode only).
Optional: Payment Provider Configuration
# Telebirr Configuration
TELEBIRR_BASE_URL=https://api.telebirr.com
TELEBIRR_MERCHANT_CODE=your-merchant-code
TELEBIRR_APP_SECRET=your-app-secret
# ... see .env.example for complete list
4. Database Setup
Start PostgreSQL
# Using Docker (recommended)
docker run --name edr-postgres \
-e POSTGRES_USER=edr \
-e POSTGRES_PASSWORD=edr_secret \
-e POSTGRES_DB=edr_passenger \
-p 5432:5432 \
-d postgres:15
# Or use your local PostgreSQL installation
Generate Prisma Client
pnpm --filter @edr/passenger-api run prisma:generate
Run Migrations
pnpm --filter @edr/passenger-api run prisma:migrate:dev
Seed Database
pnpm --filter @edr/passenger-api run prisma:seed
Seed Data Includes:
- 21 Stations (Complete Ethiopian-Djibouti Railway with country codes)
- 1 Route with 21 stops and fare rules
- 2 Train services with 4 trips
- 360 seats across 12 coaches (Economy Regular, Economy Bed, VIP Bed classes)
- 3 User accounts (Admin, Passenger, Agent)
- Fare rules for ADULT and CHILD passenger categories
- Currency exchange rates (ETB, DJF, USD)
- Luggage allowance rules
- Notification templates
- Promotions and FAQ content
- Menu items and station crowd signals
- Fraud detection rules
- Saved passenger profiles for testing
5. Start Development Server
pnpm --filter @edr/passenger-api run dev
API Server: http://localhost:4000
Swagger Docs: http://localhost:4000/api-docs
🔑 Default Credentials
After seeding, use these credentials to test the API:
| Role | Password | Description | |
|---|---|---|---|
| Admin | admin@edr-platform.com |
admin123 |
Full system access, reports, agent management |
| Passenger | kelemu@email.com |
password123 |
Regular user with loyalty (Silver) and wallet |
| Agent | agent@edr-platform.com |
agent123 |
Counter booking agent with commission tracking |
📚 API Documentation
Swagger UI
Interactive API documentation available at: http://localhost:4000/api-docs
Authentication Methods
The API uses two authentication schemes:
1. JWT Authentication (Passenger-facing)
- Used for: Passenger bookings, profile management, wallet, loyalty
- Header:
Authorization: Bearer <jwt-token> - Obtain token:
POST /auth/loginwith passenger credentials - Swagger Security:
JWT-auth
2. IAM Authentication (Back-office)
- Used for: Agent operations, fraud detection, reports, admin functions
- Header:
Authorization: Bearer <iam-token> - Obtain token: From corporate IAM system (https://iam.tria-plc.com)
- Swagger Security:
IAM-auth - Roles: AGENT, SUPERVISOR, ADMIN, STAFF
API Endpoints Overview
| Module | Base Path | Auth Type | Description |
|---|---|---|---|
| Auth | /auth |
Public/JWT | Register, login, OTP verification, password reset |
| Passengers | /passengers |
Public/JWT | Verifayda verification, international registration, profiles |
| Search | /search |
Public | Trip search, availability, fare quotes |
| Stations | /stations |
Public/JWT | Station directory and information |
| Seats | /seats |
JWT/IAM | Seat maps, holds, releases, blocking |
| Bookings | /bookings |
Public/JWT | Guest booking, create, modify, cancel bookings |
| Payments | /payments |
JWT/Public | Payment initiation, webhooks, refunds |
| Tickets | /tickets |
JWT/IAM | Ticket generation, QR/barcode, validation |
| Notifications | /notifications |
JWT | In-app notifications, preferences |
| Loyalty | /loyalty |
JWT | Points, tiers, rewards redemption |
| Wallet | /wallet |
JWT | Balance, top-up, transaction history |
| Promotions | /promos |
Public/JWT | Active promotions, promo code validation |
| Live Tracking | /live |
Public/JWT | Real-time trip status, crowd signals |
| Support | /support |
Public/JWT | FAQ, chat conversations |
| Dashboard | /dashboard |
JWT | Home screen aggregated data |
| Routes | /routes |
JWT/IAM | Reusable route templates with ordered stops |
| Schedules | /schedules |
JWT/IAM | Trip schedules, fare rules, status updates |
| Fleet | /fleet |
JWT/IAM | Train services, coaches, seat configurations |
| Seat Classes | /seat-classes or /classes |
Public/JWT/IAM | Seat class management and configuration |
| Segment Seats | /segments/seats |
Public/JWT | Segment-based seat availability and booking |
| Agents | /agents |
IAM | Agent booking, shifts, commissions, reconciliation |
| Fraud Detection | /fraud |
IAM | Fraud alerts, rules management, user blocking |
| Reports | /reports |
IAM | Revenue, occupancy, agent sales analytics |
Example API Calls
1. Register Passenger
POST /auth/register
Content-Type: application/json
{
"email": "user@example.com",
"phone": "+251911234567",
"fullName": "John Doe",
"password": "SecurePass123"
}
2. Login (Passenger)
POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123"
}
# Response includes JWT token
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { "id": "uuid", "role": "PASSENGER" }
}
3. Verify Ethiopian National ID (Verifayda)
POST /passengers/verify-fayda
Content-Type: application/json
{
"nationalId": "ET123456789"
}
# Response with verified passenger data
{
"verified": true,
"passengerData": {
"fullName": "Abebe Kebede",
"dateOfBirth": "1985-03-15T00:00:00.000Z",
"gender": "Male",
"nationality": "Ethiopian"
}
}
4. Universal Passenger Registration (NEW)
# Guest Ethiopian with Fayda verification
POST /passengers/register
Content-Type: application/json
{
"passengerName": "Abebe Kebede",
"dateOfBirth": "1985-03-15",
"nationalId": "ET123456789",
"phone": "+251911234567",
"deviceId": "device-uuid-123"
}
# Logged-in user with JWT token
POST /passengers/register
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"passengerName": "Abebe Kebede",
"dateOfBirth": "1985-03-15",
"nationalId": "ET123456789",
"phone": "+251911234567"
}
# International passenger (passport)
POST /passengers/register
Content-Type: application/json
{
"passengerName": "John Smith",
"dateOfBirth": "1990-07-20",
"passportNumber": "P1234567",
"passportCountry": "Kenya",
"nationality": "Kenyan",
"phone": "+254712345678",
"email": "john@example.com",
"deviceId": "device-uuid-123"
}
5. Get User Profile (NEW)
GET /auth/profile
Authorization: Bearer <jwt-token>
# Response includes user, passenger, loyalty, and wallet details
{
"id": "uuid",
"email": "user@example.com",
"phone": "+251911234567",
"fullName": "John Doe",
"role": "PASSENGER",
"nationality": "Ethiopian",
"faydaVerified": true,
"faydaVerifiedAt": "2024-01-15T10:30:00.000Z",
"passenger": {
"id": "uuid",
"loyalty": {
"tier": "SILVER",
"pointsBalance": 1500,
"lifetimePoints": 3000
},
"wallet": {
"balanceMinor": 50000,
"currency": "ETB"
}
}
}
6. Search Trips
POST /search
Content-Type: application/json
{
"originStationId": "uuid",
"destinationStationId": "uuid",
"date": "2026-06-15",
"adultCount": 2,
"childCount": 1
}
7. Get Fare Quote
POST /search/fare-quote
Content-Type: application/json
{
"tripId": "uuid",
"serviceClass": "ECONOMY_REGULAR",
"adultCount": 2,
"childCount": 1,
"displayCurrency": "USD"
}
# Response includes age-based pricing breakdown
{
"baseFareMinor": 35000,
"adultCount": 2,
"adultFareMinor": 70000,
"childCount": 1,
"freeChildrenCount": 1,
"paidChildrenCount": 0,
"childFareMinor": 0,
"totalMinor": 73500,
"currency": "ETB",
"displayCurrency": "USD",
"displayTotalMinor": 1323
}
8. Guest Booking (No Login Required)
POST /bookings/guest
Content-Type: application/json
{
"tripId": "uuid",
"holdId": "uuid",
"serviceClass": "ECONOMY_REGULAR",
"displayCurrency": "ETB",
"passengers": [
{
"seatId": "uuid",
"passengerName": "Abebe Kebede",
"dateOfBirth": "1985-03-15",
"idDocumentType": "NATIONAL_ID",
"idDocumentNumber": "ET123456789"
}
],
"createAccount": false,
"savePassengerDetails": true,
"deviceId": "device-uuid"
}
9. Agent Booking (IAM Auth)
POST /agents/bookings
Authorization: Bearer <iam-token>
Content-Type: application/json
{
"tripId": "uuid",
"seats": [...],
"paymentMethod": "CASH",
"cashReceived": 50000
}
🏗️ Project Structure
apps/edr-passenger-api/
├── prisma/
│ ├── schema.prisma # Database schema (40+ models)
│ ├── seed.ts # Comprehensive seed script
│ └── migrations/ # Database migrations
├── src/
│ ├── common/ # Shared utilities
│ │ ├── filters/ # Exception filters
│ │ ├── interceptors/ # Response interceptors
│ │ ├── pipes/ # Validation pipes
│ │ ├── i18n/ # Internationalization
│ │ ├── jwt.guard.ts # JWT authentication guard (passengers)
│ │ ├── jwt.strategy.ts # Passport JWT strategy
│ │ ├── iam-adapter.ts # Corporate IAM guard (back-office)
│ │ ├── iam.module.ts # IAM module
│ │ ├── roles.guard.ts # RBAC authorization guard
│ │ ├── roles.decorator.ts # Roles decorator
│ │ ├── prisma.service.ts # Prisma client service
│ │ └── prisma.module.ts # Prisma module
│ ├── config/ # Configuration files
│ │ ├── app.config.ts # App configuration
│ │ ├── database.config.ts # Database configuration
│ │ └── telebirr.config.ts # Payment provider config
│ ├── modules/ # Feature modules
│ │ ├── auth/ # Authentication & authorization (JWT)
│ │ ├── agents/ # Agent operations (IAM-protected)
│ │ ├── bookings/ # Booking management (JWT)
│ │ ├── currency/ # Currency conversion service
│ │ ├── dashboard/ # Dashboard aggregations (JWT)
│ │ ├── fleet/ # Train fleet management (JWT/IAM)
│ │ ├── fraud/ # Fraud detection (IAM-protected)
│ │ ├── live/ # Live tracking (JWT)
│ │ ├── loyalty/ # Loyalty program (JWT)
│ │ ├── notifications/ # Notification system (JWT)
│ │ ├── passengers/ # Passenger management (JWT)
│ │ ├── payments/ # Payment processing (JWT/Webhooks)
│ │ ├── promos/ # Promotions (JWT)
│ │ ├── reports/ # Reports & analytics (IAM-protected)
│ │ ├── schedules/ # Trip schedules (JWT/IAM)
│ │ ├── search/ # Trip search (JWT)
│ │ ├── seats/ # Seat management (JWT/IAM)
│ │ ├── segments/ # Journey segments (JWT)
│ │ ├── stations/ # Station management (JWT)
│ │ ├── support/ # Customer support (JWT)
│ │ ├── tickets/ # Ticketing (JWT/IAM)
│ │ ├── verifayda/ # Verifayda 2.0 integration
│ │ └── wallet/ # Wallet system (JWT)
│ ├── app.module.ts # Root application module
│ └── main.ts # Application entry point
├── test/ # E2E tests
├── .env.example # Environment template
├── Dockerfile # Docker configuration
├── nest-cli.json # NestJS CLI configuration
├── package.json # Dependencies & scripts
├── tsconfig.json # TypeScript configuration
└── tsconfig.build.json # Build configuration
🗄️ Database Schema
Key Models (40+ total)
Core Entities:
User,Session,Passenger,AgentStation,Route,RouteStop,RouteFareRuleTrainService,Trip,TripStopTime,Coach,SeatBooking,BookingSeat,TicketPaymentIntent,PaymentRefund,PaymentWebhookEvent
Enhanced Features:
OtpCode,PasswordResetToken(Auth)AgentBooking,AgentShift,AgentCommission(Agents)BookingModification,BookingCancellation(Booking lifecycle)GateValidationLog(Ticket validation)BaggageAllowance,BaggageBooking(Baggage)LoyaltyAccount,LoyaltyLedgerEntry,LoyaltyRewardWalletAccount,WalletLedgerEntryNotification,NotificationTemplateAuditLog,OperationalReportSeatBlock,SeatHoldCurrencyExchangeRate(Multi-currency)VerifaydaVerification(National ID verification)SavedPassengerProfile(Guest booking)SeatClass(Seat class configuration)JourneySegment(Multi-segment journeys)
🔧 Available Scripts
# Development
pnpm --filter @edr/passenger-api run dev # Start with hot-reload
# Build
pnpm --filter @edr/passenger-api run build # Compile TypeScript
# Production
pnpm --filter @edr/passenger-api run start # Run compiled code
# Testing
pnpm --filter @edr/passenger-api run test # Unit tests
pnpm --filter @edr/passenger-api run test:e2e # E2E tests
# Code Quality
pnpm --filter @edr/passenger-api run lint # ESLint
pnpm --filter @edr/passenger-api run type-check # TypeScript check
# Database
pnpm --filter @edr/passenger-api run prisma:generate # Generate Prisma client
pnpm --filter @edr/passenger-api run prisma:migrate:dev # Run migrations (local dev)
pnpm --filter @edr/passenger-api run prisma:seed # Seed database
🐳 Docker Deployment
All six apps build from Dockerfiles: each API has its own (apps/edr-freight-api/Dockerfile, apps/edr-passenger-api/Dockerfile); Vite frontends share infrastructure/docker/Dockerfile.web and are served with nginx. APIs run on Node 22.
Prerequisites
- Docker with BuildKit enabled
- A local
.npmrcwith GitHub Packages auth for@tria-plc/*(required for freight API and web images) - External Postgres for each API (compose does not include databases)
- Copy
apps/edr-freight-api/.env.example→.envandapps/edr-passenger-api/.env.example→.envwith real connection strings
Build and run (all apps)
# From monorepo root
DOCKER_BUILDKIT=1 pnpm docker:build
pnpm docker:up
Or without pnpm scripts:
DOCKER_BUILDKIT=1 docker compose build
docker compose up -d
| Service | URL (default) |
|---|---|
| Freight API | http://localhost:3001 |
| Passenger API | http://localhost:4000 |
| Freight portal | http://localhost:5173 |
| Freight backoffice | http://localhost:5183 |
| Passenger portal | http://localhost:5174 |
| Passenger backoffice | http://localhost:5184 |
Build a single service
docker compose build freight-api
docker compose build passenger-portal
Freight images mount .npmrc as a BuildKit secret during pnpm install. Passenger web images do not require private packages.
VITE_API_URL (frontends)
API URLs are baked in at image build time (import.meta.env.VITE_API_URL). Defaults in docker-compose.yaml use http://localhost:3001/api (freight) and http://localhost:4000 (passenger) for local smoke tests. Override build args for production, e.g.:
docker compose build freight-portal \
--build-arg VITE_API_URL=https://freight-api.example.com/api
Migrations
- Freight API: TypeORM migrations are not run on container startup — apply them separately before deploy.
- Passenger API: On each container start, the entrypoint runs
npm run prisma:migrateandnpm run prisma:seed(samepackage.jsonscripts aspnpm run) before starting the server. EnsureDATABASE_URLin.envpoints at a reachable Postgres instance.
For local development, use pnpm --filter @edr/passenger-api run prisma:migrate:dev instead of prisma:migrate.
GitHub Actions (self-hosted runner)
Two workflows deploy independently on push to main, develop, or staging:
| Workflow | Services | Server env root |
|---|---|---|
.github/workflows/deploy-freight.yml |
freight-api, freight-portal, freight-backoffice | /home/user/environmen/edr-freight/<branch>/ |
.github/workflows/deploy-passenger.yml |
passenger-api, passenger-portal, passenger-backoffice | /home/user/environmen/edr-passenger/<branch>/ |
On the runner, place env files before the first deploy (example for branch main):
/home/user/environmen/edr-freight/main/
freight-api.env
freight-portal.env # optional runtime env for Vite/nginx
freight-backoffice.env
freight-web.build.env # exports FREIGHT_VITE_API_URL=...
/home/user/environmen/edr-passenger/main/
passenger-api.env
passenger-portal.env
passenger-backoffice.env
passenger-web.build.env # exports PASSENGER_VITE_API_URL=...
Example freight-web.build.env:
export FREIGHT_VITE_API_URL=https://freight-api.example.com/api
The workflow copies *.env into each app directory, creates .npmrc from the NPM_TOKEN repository secret, then runs docker compose build and docker compose up -d for that stack.
🔒 Security Best Practices
- Environment Variables - Never commit
.envfiles. Use secrets management in production. - JWT Secret - Use strong, randomly generated secrets (min 32 characters).
- Password Hashing - Bcrypt with salt rounds (default: 10).
- Rate Limiting - Implement rate limiting for auth endpoints.
- CORS - Configure allowed origins in production.
- HTTPS - Always use HTTPS in production.
- Database - Use connection pooling and prepared statements (Prisma handles this).
- Audit Logging - All sensitive operations are logged in
AuditLogtable. - Dual Authentication - Passenger routes use JWT, back-office routes use corporate IAM.
- IAM Integration - Corporate IAM validates tokens against centralized identity service.
- Role-Based Access - Granular permissions enforced via IAM roles (AGENT, SUPERVISOR, ADMIN).
- Token Validation - IAM tokens validated in real-time with 5-second timeout.
📊 Monitoring & Logging
- Application Logs - NestJS built-in logger
- Database Queries - Prisma query logging (enable in development)
- Audit Trail - All user actions logged in
AuditLogtable - Error Tracking - Global exception filters with detailed error responses
🧪 Testing
# Unit tests
pnpm --filter @edr/passenger-api run test
# E2E tests
pnpm --filter @edr/passenger-api run test:e2e
# Test coverage
pnpm --filter @edr/passenger-api run test:cov
🚀 Production Deployment
Pre-deployment Checklist
- Update environment variables (JWT_SECRET, DATABASE_URL, etc.)
- Configure IAM integration (IAM_ENABLED=true, IAM_API_URL, IAM_API_KEY)
- Configure Verifayda integration (VERIFAYDA_ENABLED=true, VERIFAYDA_API_KEY)
- Set up currency exchange rate sync (external API)
- Set NODE_ENV=production
- Configure CORS origins (PORTAL_URL, BACK_OFFICE_URL)
- Set up SSL/TLS certificates
- Configure database connection pooling
- Set up monitoring and logging
- Configure backup strategy
- Test payment provider integrations
- Verify IAM token validation endpoint
- Test Verifayda verification with real national IDs
- Verify currency conversion accuracy
- Test age-based pricing calculations
- Review security settings and audit logs
- Test both JWT and IAM authentication flows
Deployment Steps
# 1. Build application
pnpm --filter @edr/passenger-api run build
# 2. Run migrations
pnpm --filter @edr/passenger-api run prisma:migrate
# 3. Start production server
NODE_ENV=production pnpm --filter @edr/passenger-api run start:prod
🤝 Contributing
- Fork the repository
- Create feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open Pull Request
📝 License
This project is proprietary and confidential.
📧 Support
For technical support or questions:
- Email: support@edr-platform.com
- Documentation: http://localhost:4000/api-docs
Built with ❤️ for Ethio-Djibouti Railway
Start local databases
docker compose -f infrastructure/docker/docker-compose.db.dev.yml up -d
Run apps
pnpm dev # every app
pnpm dev:freight # freight API + portal + backoffice
pnpm dev:passenger # passenger API + portal + backoffice
Common scripts
| Script | Description |
|---|---|
pnpm install |
Install workspace dependencies |
pnpm dev |
Run every app in watch mode |
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 with Prettier |
Repository Layout
.
├── apps/
│ ├── edr-freight-api/ NestJS — freight backend
│ ├── edr-freight-web/
│ │ ├── portal/ React — freight customer portal
│ │ └── backoffice/ React — freight back-office
│ ├── edr-passenger-api/ NestJS — passenger backend
│ └── edr-passenger-web/
│ ├── portal/ React — passenger customer portal
│ └── backoffice/ React — passenger back-office
├── packages/
│ ├── api-common/ Shared NestJS utilities + BaseEntity/Repository
│ ├── types/ Shared TS types/enums
│ ├── ui-common/ Shared React components + theme
│ └── config/
│ ├── eslint/ @edr/eslint-config
│ ├── tsconfig/ @edr/tsconfig
│ └── prettier/ @edr/prettier-config
├── infrastructure/
│ ├── docker/ docker-compose files (db.dev / dev / prod)
│ └── nginx/ Production nginx config
├── CLAUDE.md Developer guide for AI-assisted work
├── turbo.json Turborepo task pipeline
├── pnpm-workspace.yaml Workspace manifest
└── README.md
Standards (recap)
- TypeScript strict mode is enabled in every package.
- pnpm only — never run
npm installoryarn. - Conventional commits — enforced via commitlint on every commit.
- NestJS 4-layer pattern —
module → controller → service → repository. - Repository injection — services inject the custom
*Repositoryclass, notRepository<T>. - Controllers are thin — no business logic; delegate to services.
- Migrations only — never enable TypeORM
synchronizein production. - One DB per domain — no cross-database joins.
See CLAUDE.md for the deeper developer guide used during AI-assisted contributions.