# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview **Xpeditis** is a B2B SaaS maritime freight booking and management platform (maritime equivalent of WebCargo). The platform allows freight forwarders to search and compare real-time shipping rates, book containers online, and manage shipments from a centralized dashboard. **MVP Goal**: Deliver a minimal but professional product capable of handling 50-100 bookings/month for 10-20 early adopter freight forwarders within 4-6 months. ## Architecture ### Hexagonal Architecture (Ports & Adapters) The codebase follows hexagonal architecture principles with clear separation of concerns: - **API Layer (External Adapters)**: Controllers, validation, auth middleware - **Application Layer (Ports)**: Use cases (searchRates, createBooking, confirmBooking) - **Domain Layer (Core)**: Entities (Booking, RateQuote, Carrier, Organization, User) - **Infrastructure Adapters**: DB repositories, carrier connectors, email services, storage, Redis cache ### Tech Stack - **Frontend**: Next.js (TypeScript) with SSR/ISR - **Backend**: Node.js API-first (OpenAPI spec) - **Database**: PostgreSQL (TypeORM or Prisma) - **Cache**: Redis (15 min TTL for spot rates) - **Storage**: S3-compatible (AWS S3 or MinIO) - **Auth**: OAuth2 + JWT (access 15min, refresh 7 days) ### Project Structure (Planned Monorepo) ``` apps/ frontend/ # Next.js application backend/ # Node.js API libs/ domain/ # Core domain entities and business logic infra/ # Infrastructure configuration ``` ## Core Domain Entities - **Organization**: `{ id, name, type, scac, address, logo_url, documents[] }` - **User**: `{ id, org_id, role, email, pwd_hash, totp_secret }` - **RateQuote**: `{ id, origin, destination, carrier_id, price_currency, price_value, surcharges[], etd, eta, transit_days, route, availability }` - **Booking**: `{ id, booking_number, user_id, org_id, rate_quote_id, shipper, consignee, containers[], status, created_at, updated_at }` - **Container**: `{ id, booking_id, type, container_number, vgm, temp, seal_number }` ## Key API Endpoints ### Rate Search - `POST /api/v1/rates/search`: Search shipping rates with origin/destination/container specs - Response includes carrier, pricing, surcharges, ETD/ETA, transit time, route, CO2 emissions - Cache TTL: 15 minutes (Redis) - Timeout: 5 seconds per carrier API ### Booking Management - `POST /api/v1/bookings`: Create booking (multi-step workflow) - `GET /api/v1/bookings/:id`: Get booking details - Booking number format: `WCM-YYYY-XXXXXX` (6 alphanumeric chars) ### Authentication - `/auth/register`: Email + password (≥12 chars) - `/auth/login`: JWT-based login + OAuth2 (Google Workspace, Microsoft 365) - `/auth/logout`: Session termination - `/auth/refresh`: Token refresh ## Critical Integration Points ### Carrier Connectors (MVP Priority) Implement connectors for 3-5 carriers using Strategy pattern: - Maersk (required) - MSC - CMA CGM - Hapag-Lloyd - ONE Each connector must: - Normalize data to internal schema - Implement retry logic and circuit breakers - Respect rate limiting - Log detailed metrics - Respond within 5s or fallback gracefully ### Cache Strategy - Preload top 100 trade lanes on startup - 15-minute TTL for spot rates - Cache hit target: >90% for common routes ## Security Requirements - TLS 1.2+ for all traffic - Password hashing: Argon2id or bcrypt (≥12 rounds) - OWASP Top 10 protection (rate limiting, input validation, CSP headers) - Audit logs for sensitive actions - S3 ACLs for compliance documents - Optional TOTP 2FA ## RBAC Roles - **Admin**: Full system access - **Manager**: Manage bookings and users within organization - **User**: Create and view bookings - **Viewer**: Read-only access ## Performance Targets - Rate search: <2s for 90% of requests (with cache) - Dashboard load: <1s for up to 5k bookings - Carrier API timeout: 5s - Email confirmation: Send within 3s of booking - Session auto-logout: 2h inactivity ## Development Workflow ### Testing Requirements - Unit tests: Domain logic and use cases - Integration tests: Carrier connectors and DB repositories - E2E tests: Complete booking workflow (happy path + 3 common error scenarios) ### Email & Notifications - Templates: MJML format - Booking confirmation email on successful booking - Push notifications (if mobile app) ### Document Generation - PDF booking confirmations - Excel/PDF export for rate search results ## Data Requirements - Port autocomplete: 10k+ ports (IATA/UN LOCODE) - Multi-currency support: USD, EUR - Hazmat support: IMO class validation - Container types: 20', 40', 40'HC, etc. ## MVP Roadmap (4-6 months) **Sprint 0 (2 weeks)**: Repo setup, infrastructure, OpenAPI skeleton, ports/adapters scaffolding **Phase 1 (6-8 weeks)**: Rate search API + UI, Redis cache, 1-2 carrier connectors, basic auth **Phase 2 (6-8 weeks)**: Booking workflow, email templates, dashboard, RBAC, organizations **Phase 3 (4-6 weeks)**: Additional carrier integrations, exports, E2E tests, monitoring, security hardening **Go-to-market (2 weeks)**: Early adopter onboarding, support, KPI tracking ## Important Constraints - Pre-fetch top 100 trade lanes on application startup - All carrier API calls must have circuit breakers - Booking workflow: ≤4 steps maximum - Session timeout: 2 hours of inactivity - Rate search pagination: >20 results - SLA: 95% of rate searches <1s (including cache) ## Business KPIs to Track - Active users (DAU/MAU) - Bookings per month - Search-to-booking conversion rate (target ≥3%) - Average time to create booking - Carrier API error rates - Cache hit ratio - Customer retention at 3 months --- # Backend Hexagonal Architecture Guidelines (Node.js/TypeScript) ## Phase 1: Business Domain Analysis ### Domain Identification - **Primary business domain**: Maritime freight booking platform - **Core entities**: Organization, User, RateQuote, Booking, Container, Carrier - **Business rules**: - Rate quotes expire after 15 minutes (Redis cache) - Bookings must validate container availability in real-time - Multi-step booking workflow (≤4 steps) - RBAC enforcement for all operations - Carrier API timeout: 5 seconds with fallback - **Use cases**: searchRates, createBooking, confirmBooking, manageOrganizations, authenticateUser ### Integration Requirements - **External actors**: Freight forwarders (users), carriers (API integrations) - **External services**: PostgreSQL, Redis, S3, Email (MJML templates), Carrier APIs (Maersk, MSC, CMA CGM, etc.) - **Input interfaces**: REST API (OpenAPI), OAuth2 callbacks - **Output interfaces**: Database persistence, email notifications, carrier API calls, S3 document storage ## Phase 2: Architectural Design ### Module Structure ``` backend/ ├── src/ │ ├── domain/ # Pure business logic (NO external dependencies) │ │ ├── entities/ │ │ │ ├── organization.entity.ts │ │ │ ├── user.entity.ts │ │ │ ├── rate-quote.entity.ts │ │ │ ├── booking.entity.ts │ │ │ ├── container.entity.ts │ │ │ ├── carrier.entity.ts │ │ │ └── index.ts │ │ ├── value-objects/ │ │ │ ├── email.vo.ts │ │ │ ├── booking-number.vo.ts │ │ │ ├── port-code.vo.ts │ │ │ └── index.ts │ │ ├── services/ │ │ │ ├── rate-search.service.ts │ │ │ ├── booking.service.ts │ │ │ ├── user.service.ts │ │ │ └── index.ts │ │ ├── ports/ │ │ │ ├── in/ # API Ports (use cases) │ │ │ │ ├── search-rates.port.ts │ │ │ │ ├── create-booking.port.ts │ │ │ │ ├── manage-user.port.ts │ │ │ │ └── index.ts │ │ │ └── out/ # SPI Ports (infrastructure interfaces) │ │ │ ├── rate-quote.repository.ts │ │ │ ├── booking.repository.ts │ │ │ ├── user.repository.ts │ │ │ ├── carrier-connector.port.ts │ │ │ ├── cache.port.ts │ │ │ ├── email.port.ts │ │ │ └── index.ts │ │ └── exceptions/ │ │ ├── booking-not-found.exception.ts │ │ ├── invalid-rate-quote.exception.ts │ │ ├── carrier-timeout.exception.ts │ │ └── index.ts │ │ │ ├── application/ # Controllers and DTOs (depends ONLY on domain) │ │ ├── controllers/ │ │ │ ├── rates.controller.ts │ │ │ ├── bookings.controller.ts │ │ │ ├── auth.controller.ts │ │ │ └── index.ts │ │ ├── dto/ │ │ │ ├── rate-search.dto.ts │ │ │ ├── create-booking.dto.ts │ │ │ ├── booking-response.dto.ts │ │ │ └── index.ts │ │ ├── mappers/ │ │ │ ├── rate-quote.mapper.ts │ │ │ ├── booking.mapper.ts │ │ │ └── index.ts │ │ └── config/ │ │ ├── validation.config.ts │ │ └── swagger.config.ts │ │ │ ├── infrastructure/ # All external integrations (depends ONLY on domain) │ │ ├── persistence/ │ │ │ ├── typeorm/ │ │ │ │ ├── entities/ │ │ │ │ │ ├── organization.orm-entity.ts │ │ │ │ │ ├── user.orm-entity.ts │ │ │ │ │ ├── booking.orm-entity.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── repositories/ │ │ │ │ │ ├── typeorm-booking.repository.ts │ │ │ │ │ ├── typeorm-user.repository.ts │ │ │ │ │ └── index.ts │ │ │ │ └── mappers/ │ │ │ │ ├── booking-orm.mapper.ts │ │ │ │ └── index.ts │ │ │ └── database.module.ts │ │ ├── cache/ │ │ │ ├── redis-cache.adapter.ts │ │ │ └── cache.module.ts │ │ ├── carriers/ │ │ │ ├── maersk/ │ │ │ │ ├── maersk.connector.ts │ │ │ │ ├── maersk.mapper.ts │ │ │ │ └── maersk.types.ts │ │ │ ├── msc/ │ │ │ ├── cma-cgm/ │ │ │ └── carrier.module.ts │ │ ├── email/ │ │ │ ├── mjml-email.adapter.ts │ │ │ └── email.module.ts │ │ ├── storage/ │ │ │ ├── s3-storage.adapter.ts │ │ │ └── storage.module.ts │ │ └── config/ │ │ ├── database.config.ts │ │ ├── redis.config.ts │ │ └── jwt.config.ts │ │ │ ├── main.ts # Application entry point │ └── app.module.ts # Root module (NestJS) │ ├── test/ │ ├── unit/ │ ├── integration/ │ └── e2e/ │ ├── package.json ├── tsconfig.json ├── jest.config.js └── .env.example ``` ### Port Definitions **API Ports (domain/ports/in/)** - Exposed by domain: - `SearchRatesPort`: Interface for searching shipping rates - `CreateBookingPort`: Interface for creating bookings - `ManageUserPort`: Interface for user management - `AuthenticatePort`: Interface for authentication flows **SPI Ports (domain/ports/out/)** - Required by domain: - `RateQuoteRepository`: Persistence interface for rate quotes - `BookingRepository`: Persistence interface for bookings - `UserRepository`: Persistence interface for users - `CarrierConnectorPort`: Interface for carrier API integrations - `CachePort`: Interface for caching (Redis) - `EmailPort`: Interface for sending emails - `StoragePort`: Interface for S3 document storage ### Adapter Design **Driving Adapters (Input)**: - REST controllers (NestJS @Controller) - GraphQL resolvers (future) - CLI commands (future) **Driven Adapters (Output)**: - TypeORM repositories implementing repository ports - Carrier connectors (Maersk, MSC, etc.) implementing CarrierConnectorPort - Redis adapter implementing CachePort - MJML email adapter implementing EmailPort - S3 adapter implementing StoragePort ## Phase 3: Layer Architecture ### Domain Layer Rules - **Zero external dependencies**: No NestJS, TypeORM, Redis, etc. - **Pure TypeScript**: Only type definitions and business logic - **Self-contained**: Must compile independently - **Test without framework**: Jest only, no NestJS testing utilities ### Application Layer Rules - **Depends only on domain**: Import from `@domain/*` only - **Exposes REST API**: Controllers validate input and delegate to domain services - **DTO mapping**: Transform external DTOs to domain entities - **No business logic**: Controllers are thin, logic stays in domain ### Infrastructure Layer Rules - **Implements SPI ports**: All repository and service interfaces - **Framework dependencies**: TypeORM, Redis, AWS SDK, etc. - **Maps external data**: ORM entities ↔ Domain entities - **Circuit breakers**: Carrier connectors must implement retry/fallback logic ## Phase 4: Technical Validation ### Dependency Management **domain/package.json** (if separate): ```json { "dependencies": {}, // NO runtime dependencies "devDependencies": { "typescript": "^5.3.0", "@types/node": "^20.0.0" } } ``` **Root package.json**: ```json { "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/swagger": "^7.0.0", "typeorm": "^0.3.17", "pg": "^8.11.0", "redis": "^4.6.0", "class-validator": "^0.14.0", "class-transformer": "^0.5.1" } } ``` ### tsconfig.json Path Aliases ```json { "compilerOptions": { "strict": true, "baseUrl": "./src", "paths": { "@domain/*": ["domain/*"], "@application/*": ["application/*"], "@infrastructure/*": ["infrastructure/*"] } } } ``` ### Design Patterns - **Domain-Driven Design**: Entities, Value Objects, Aggregates - **SOLID Principles**: Especially DIP (Dependency Inversion) - **Repository Pattern**: Abstraction over data persistence - **Strategy Pattern**: Carrier connectors (Maersk, MSC, etc.) - **Circuit Breaker**: For carrier API calls (5s timeout) ### NestJS Configuration - Use `@Injectable()` in application and infrastructure layers ONLY - Domain services registered manually via `@Module` providers - Avoid `@Component` or decorators in domain layer - Use interceptors for transactions (`@Transactional`) ## Phase 5: Testing Strategy ### Unit Tests (Domain) ```typescript // domain/services/booking.service.spec.ts describe('BookingService', () => { it('should create booking with valid rate quote', () => { // Test without any framework dependencies const service = new BookingService(mockRepository); const result = service.createBooking(validInput); expect(result.bookingNumber).toMatch(/^WCM-\d{4}-[A-Z0-9]{6}$/); }); }); ``` ### Integration Tests (Infrastructure) ```typescript // infrastructure/persistence/typeorm/repositories/booking.repository.spec.ts describe('TypeOrmBookingRepository', () => { let repository: TypeOrmBookingRepository; beforeAll(async () => { // Use testcontainers for real PostgreSQL await setupTestDatabase(); }); it('should persist booking to database', async () => { const booking = createTestBooking(); await repository.save(booking); const found = await repository.findById(booking.id); expect(found).toBeDefined(); }); }); ``` ### E2E Tests (Full API) ```typescript // test/e2e/booking-workflow.e2e-spec.ts describe('Booking Workflow (E2E)', () => { it('should complete full booking flow', async () => { // 1. Search rates const ratesResponse = await request(app.getHttpServer()) .post('/api/v1/rates/search') .send(searchPayload); // 2. Create booking const bookingResponse = await request(app.getHttpServer()) .post('/api/v1/bookings') .send(bookingPayload); // 3. Verify booking confirmation email sent expect(bookingResponse.status).toBe(201); expect(emailSpy).toHaveBeenCalled(); }); }); ``` ### Test Coverage Targets - **Domain**: 90%+ coverage - **Application**: 80%+ coverage - **Infrastructure**: 70%+ coverage (focus on critical paths) ## Phase 6: Naming Conventions ### TypeScript Conventions - **Interfaces**: `UserRepository` (no "I" prefix) - **Ports**: `SearchRatesPort`, `CarrierConnectorPort` - **Adapters**: `TypeOrmBookingRepository`, `MaerskConnectorAdapter` - **Services**: `BookingService`, `RateSearchService` - **Entities**: `Booking`, `RateQuote`, `Organization` - **Value Objects**: `BookingNumber`, `Email`, `PortCode` - **DTOs**: `CreateBookingDto`, `RateSearchRequestDto` ### File Naming - **Entities**: `booking.entity.ts` - **Interfaces**: `booking.repository.ts` (for ports) - **Implementations**: `typeorm-booking.repository.ts` - **Tests**: `booking.service.spec.ts` - **Barrel exports**: `index.ts` ### Import Order ```typescript // 1. External dependencies import { Injectable } from '@nestjs/common'; // 2. Domain imports import { Booking } from '@domain/entities'; import { BookingRepository } from '@domain/ports/out'; // 3. Relative imports import { BookingOrmEntity } from './entities/booking.orm-entity'; ``` ## Phase 7: Validation Checklist ### Critical Questions - ✅ **Domain isolation**: No `import` of NestJS/TypeORM in domain layer? - ✅ **Dependency direction**: All dependencies point inward toward domain? - ✅ **Framework-free testing**: Can domain be tested without NestJS TestingModule? - ✅ **Database agnostic**: Could we switch from TypeORM to Prisma without touching domain? - ✅ **Interface flexibility**: Could we add GraphQL without changing domain? - ✅ **Compilation independence**: Does domain compile without other layers? ### Data Flow Validation - **Inbound**: HTTP Request → Controller → DTO → Mapper → Domain Entity → Use Case - **Outbound**: Use Case → Repository Port → Adapter → ORM Entity → Database - **Carrier Integration**: Use Case → CarrierConnectorPort → MaerskAdapter → Maersk API ## Phase 8: Pre-Coding Checklist ### Setup Tasks - ✅ Node.js v20+ installed - ✅ TypeScript with `strict: true` - ✅ NestJS CLI installed globally - ✅ ESLint + Prettier configured - ✅ Husky pre-commit hooks - ✅ `.env.example` with all required variables ### Architecture Validation - ✅ PRD reviewed and understood - ✅ Domain entities mapped to database schema - ✅ All ports (in/out) identified - ✅ Carrier connector strategy defined - ✅ Cache strategy documented (Redis, 15min TTL) - ✅ Test strategy approved ### Development Order 1. **Domain layer**: Entities → Value Objects → Services → Ports 2. **Infrastructure layer**: TypeORM entities → Repositories → Carrier connectors → Cache/Email adapters 3. **Application layer**: DTOs → Mappers → Controllers → Validation pipes 4. **Bootstrap**: main.ts → app.module.ts → DI configuration 5. **Tests**: Unit (domain) → Integration (repos) → E2E (API) ## Common Pitfalls to Avoid ⚠️ **Critical Mistakes**: - Circular imports (use barrel exports `index.ts`) - Framework decorators in domain (`@Column`, `@Injectable`) - Business logic in controllers or adapters - Using `any` type (always explicit types) - Promises not awaited (use `async/await` properly) - Carrier APIs without circuit breakers - Missing Redis cache for rate queries - Not validating DTOs with `class-validator` ## Recommended Tools - **Validation**: `class-validator` + `class-transformer` - **Mapping**: Manual mappers (avoid heavy libraries) - **API Docs**: `@nestjs/swagger` (OpenAPI) - **Logging**: Winston or Pino - **Config**: `@nestjs/config` with Joi validation - **Testing**: Jest + Supertest + @faker-js/faker - **Circuit Breaker**: `opossum` library - **Redis**: `ioredis` - **Email**: `mjml` + `nodemailer` ## Example: Complete Feature Flow ### Scenario: Search Rates for Rotterdam → Shanghai ```typescript // 1. Controller (application layer) @Controller('api/v1/rates') export class RatesController { constructor(private readonly searchRatesUseCase: SearchRatesPort) {} @Post('search') async searchRates(@Body() dto: RateSearchDto) { const domainInput = RateSearchMapper.toDomain(dto); const quotes = await this.searchRatesUseCase.execute(domainInput); return RateSearchMapper.toDto(quotes); } } // 2. Use Case (domain port in) export interface SearchRatesPort { execute(input: RateSearchInput): Promise; } // 3. Domain Service (domain layer) export class RateSearchService implements SearchRatesPort { constructor( private readonly cache: CachePort, private readonly carriers: CarrierConnectorPort[] ) {} async execute(input: RateSearchInput): Promise { // Check cache first const cached = await this.cache.get(input.cacheKey); if (cached) return cached; // Query carriers in parallel with timeout const results = await Promise.allSettled( this.carriers.map(c => c.searchRates(input)) ); // Filter successful results const quotes = results .filter(r => r.status === 'fulfilled') .flatMap(r => r.value); // Cache results (15 min TTL) await this.cache.set(input.cacheKey, quotes, 900); return quotes; } } // 4. Infrastructure Adapter (infrastructure layer) export class MaerskConnectorAdapter implements CarrierConnectorPort { async searchRates(input: RateSearchInput): Promise { // HTTP call to Maersk API with 5s timeout const response = await this.httpClient.post( 'https://api.maersk.com/rates', this.mapToMaerskFormat(input), { timeout: 5000 } ); // Map Maersk response to domain entities return this.mapToDomainQuotes(response.data); } } ``` This architecture ensures clean separation, testability, and flexibility for the Xpeditis maritime freight platform.