xpeditis2.0/CLAUDE.md
2026-02-10 17:16:35 +01:00

9.8 KiB

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 platform. Freight forwarders search and compare real-time shipping rates, book containers, and manage shipments. Monorepo with NestJS 10 backend (Hexagonal Architecture) and Next.js 14 frontend.

Development Commands

All commands run from repo root unless noted otherwise.

# Infrastructure (PostgreSQL 15 + Redis 7 + MinIO)
docker-compose up -d

# Install all dependencies
npm run install:all

# Environment setup (required on first run)
cp apps/backend/.env.example apps/backend/.env
cp apps/frontend/.env.example apps/frontend/.env

# Database migrations (from apps/backend/)
cd apps/backend && npm run migration:run

# Development servers
npm run backend:dev    # http://localhost:4000, Swagger: /api/docs
npm run frontend:dev   # http://localhost:3000

Testing

# Backend (from apps/backend/)
npm test                              # Unit tests (Jest)
npm test -- booking.entity.spec.ts    # Single file
npm run test:cov                      # With coverage
npm run test:integration              # Integration tests (needs DB/Redis, 30s timeout)
npm run test:e2e                      # E2E tests

# Frontend (from apps/frontend/)
npm test
npm run test:e2e                      # Playwright (chromium, firefox, webkit + mobile)

# From root
npm run backend:test
npm run frontend:test

Backend test config is in apps/backend/package.json (Jest). Integration test config: apps/backend/jest-integration.json (covers infrastructure layer, setup in test/setup-integration.ts). Frontend E2E config: apps/frontend/playwright.config.ts.

Linting, Formatting & Type Checking

npm run backend:lint       # ESLint backend
npm run frontend:lint      # ESLint frontend
npm run format             # Prettier (all files)
npm run format:check       # Check formatting
# From apps/frontend/
npm run type-check         # TypeScript checking (frontend only)

Database Migrations

cd apps/backend
npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName
npm run migration:run
npm run migration:revert

Build

npm run backend:build    # NestJS build with tsc-alias for path resolution
npm run frontend:build   # Next.js production build (standalone output)

Local Infrastructure

Docker-compose defaults (no .env changes needed for local dev):

  • PostgreSQL: xpeditis:xpeditis_dev_password@localhost:5432/xpeditis_dev
  • Redis: password xpeditis_redis_password, port 6379
  • MinIO (S3-compatible storage): minioadmin:minioadmin, API port 9000, console port 9001

Architecture

Hexagonal Architecture (Backend)

apps/backend/src/
├── domain/           # CORE - Pure TypeScript, NO framework imports
│   ├── entities/     # Booking, RateQuote, User, Carrier, Port, Container, CsvBooking, etc.
│   ├── value-objects/ # Money, Email, BookingNumber, BookingStatus, PortCode, ContainerType, Volume, LicenseStatus, SubscriptionPlan, etc.
│   ├── services/     # Pure domain services (rate-search, csv-rate-price-calculator, booking, port-search, etc.)
│   ├── ports/
│   │   ├── in/       # Use case interfaces with execute() method
│   │   └── out/      # Repository/SPI interfaces (token constants like BOOKING_REPOSITORY = 'BookingRepository')
│   └── exceptions/   # Domain-specific exceptions
├── application/      # Controllers, DTOs (class-validator), Guards, Decorators, Mappers
└── infrastructure/   # TypeORM entities/repos/mappers, Redis cache, carrier APIs, MinIO/S3, email (MJML+Nodemailer), Sentry

Critical dependency rules:

  • Domain layer: zero imports from NestJS, TypeORM, Redis, or any framework
  • Dependencies flow inward only: Infrastructure → Application → Domain
  • Path aliases: @domain/*, @application/*, @infrastructure/* (defined in apps/backend/tsconfig.json)
  • Domain tests run without NestJS TestingModule
  • Backend has strict TypeScript: strict: true, strictNullChecks: true (but strictPropertyInitialization: false)

NestJS Modules (app.module.ts)

Global guards: JwtAuthGuard (all routes protected by default), CustomThrottlerGuard.

Feature modules: Auth, Rates, Ports, Bookings, CsvBookings, Organizations, Users, Dashboard, Audit, Notifications, Webhooks, GDPR, Admin, Subscriptions.

Infrastructure modules: CacheModule, CarrierModule, SecurityModule, CsvRateModule.

Swagger plugin enabled in nest-cli.json — DTOs auto-documented.

Frontend (Next.js 14 App Router)

apps/frontend/
├── app/              # App Router pages
│   ├── dashboard/    # Protected routes (bookings, admin, settings)
│   └── carrier/      # Carrier portal (magic link auth)
└── src/
    ├── components/   # React components (shadcn/ui in ui/, layout/, bookings/, admin/)
    ├── hooks/        # useBookings, useNotifications, useCsvRateSearch, useCompanies, useFilterOptions
    ├── lib/
    │   ├── api/      # Fetch-based API client with auto token refresh (client.ts + per-module files)
    │   ├── context/  # Auth context
    │   └── fonts.ts  # Manrope (headings) + Montserrat (body)
    ├── types/        # TypeScript type definitions
    └── utils/        # Export utilities (Excel, PDF)

Path aliases: @/*./src/*, @/components/*, @/lib/*, @/app/*./app/*, @/types/*, @/hooks/*, @/utils/*

Note: Frontend tsconfig has strict: false, noImplicitAny: false, strictNullChecks: false (unlike backend which is strict).

Brand Design

Colors: Navy #10183A (primary), Turquoise #34CCCD (accent), Green #067224 (success), Gray #F2F2F2. Fonts: Manrope (headings), Montserrat (body). Landing page is in French.

Key Patterns

Entity Pattern (Domain)

Private constructor + static create() factory. Immutable — mutation methods return new instances. Some entities also have fromPersistence() for reconstitution and toObject() for serialization.

export class Booking {
  private readonly props: BookingProps;
  static create(props: Omit<BookingProps, 'bookingNumber' | 'status'>): Booking { ... }
  updateStatus(newStatus: BookingStatus): Booking { // Returns new instance
    return new Booking({ ...this.props, status: newStatus });
  }
}

Value Object Pattern

Immutable, self-validating via static create(). E.g. Money supports USD, EUR, GBP, CNY, JPY with arithmetic and formatting methods.

Repository Pattern

  • Interface in domain/ports/out/ with token constant (e.g. BOOKING_REPOSITORY = 'BookingRepository')
  • Implementation in infrastructure/persistence/typeorm/repositories/
  • ORM entities: infrastructure/persistence/typeorm/entities/*.orm-entity.ts
  • Separate mapper classes (infrastructure/persistence/typeorm/mappers/) with static toOrm(), toDomain(), toDomainMany() methods

Frontend API Client

Custom Fetch wrapper in src/lib/api/client.ts — exports get(), post(), patch(), del(), upload(), download(). Auto-refreshes JWT on 401. Tokens stored in localStorage. Per-module files (auth.ts, bookings.ts, rates.ts, etc.) import from client.

Application Decorators

  • @Public() — skip JWT auth
  • @Roles() — role-based access control
  • @CurrentUser() — inject authenticated user

Carrier Connectors

Five carrier connectors (Maersk, MSC, CMA CGM, Hapag-Lloyd, ONE) extending base-carrier.connector.ts, each with request/response mappers. Circuit breaker via opossum (5s timeout).

Caching

Redis with 15-min TTL for rate quotes. Key format: rate:{origin}:{destination}:{containerType}.

Business Rules

  • Booking number format: WCM-YYYY-XXXXXX
  • Booking status flow: draft → confirmed → shipped → delivered
  • Rate quotes expire after 15 minutes
  • Multi-currency: USD, EUR, GBP, CNY, JPY
  • RBAC Roles: ADMIN, MANAGER, USER, VIEWER, CARRIER
  • JWT: access token 15min, refresh token 7d
  • Password hashing: Argon2

Carrier Portal Workflow

  1. Admin creates CSV booking → assigns carrier
  2. Email with magic link sent (1-hour expiry)
  3. Carrier auto-login → accept/reject booking
  4. Activity logged in carrier_activities table

Common Pitfalls

  • Never import NestJS/TypeORM in domain layer
  • Never use any type in backend (strict mode enabled)
  • Never modify applied migrations — create new ones
  • Always validate DTOs with class-validator decorators
  • Always create separate mappers for Domain ↔ ORM conversions
  • ORM entity files must match pattern *.orm-entity.{ts,js} (auto-discovered by data-source)
  • Migration files must be in infrastructure/persistence/typeorm/migrations/
  • Database synchronize is hard-coded to false — always use migrations

Adding a New Feature

  1. Domain Entitydomain/entities/*.entity.ts (pure TS, unit tests)
  2. Value Objectsdomain/value-objects/*.vo.ts (immutable)
  3. Port Interfacedomain/ports/out/*.repository.ts (with token constant)
  4. ORM Entityinfrastructure/persistence/typeorm/entities/*.orm-entity.ts
  5. Migrationnpm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName
  6. Repository Implinfrastructure/persistence/typeorm/repositories/
  7. Mapperinfrastructure/persistence/typeorm/mappers/ (static toOrm/toDomain/toDomainMany)
  8. DTOsapplication/dto/ (with class-validator decorators)
  9. Controllerapplication/controllers/ (with Swagger decorators)
  10. Module → Register and import in app.module.ts

Documentation

  • API Docs: http://localhost:4000/api/docs (Swagger, when running)
  • Setup guide: docs/installation/START-HERE.md
  • Carrier Portal API: apps/backend/docs/CARRIER_PORTAL_API.md
  • Full docs index: docs/README.md