- Add TransferFulfillModal for fulfilling wagon transfer requests. - Create TransferRequestFormModal for filing new wagon transfer requests. - Introduce TransferCloseShortModal for closing requests that cannot be fully fulfilled. - Develop WagonTransfersPage to manage and display wagon transfer requests. - Implement utility functions for handling wagon transfer request data and UI components. - Enhance UI with Mantine components for better user experience.
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.