# CSV Rate System - Implementation Guide ## Overview This document describes the CSV-based shipping rate system implemented in Xpeditis, which allows rate comparisons from both API-connected carriers and CSV file-based carriers. ## System Architecture ### Hybrid Approach: CSV + API The system supports two integration types: 1. **CSV_ONLY**: Rates loaded exclusively from CSV files (SSC, TCC, NVO) 2. **CSV_AND_API**: API integration with CSV fallback (ECU Worldwide) ## File Structure ``` apps/backend/src/ ├── domain/ │ ├── entities/ │ │ └── csv-rate.entity.ts ✅ CREATED │ ├── value-objects/ │ │ ├── volume.vo.ts ✅ CREATED │ │ ├── surcharge.vo.ts ✅ UPDATED │ │ ├── container-type.vo.ts ✅ UPDATED (added LCL) │ │ ├── date-range.vo.ts ✅ EXISTS │ │ ├── money.vo.ts ✅ EXISTS │ │ └── port-code.vo.ts ✅ EXISTS │ ├── services/ │ │ └── csv-rate-search.service.ts ✅ CREATED │ └── ports/ │ ├── in/ │ │ └── search-csv-rates.port.ts ✅ CREATED │ └── out/ │ └── csv-rate-loader.port.ts ✅ CREATED ├── infrastructure/ │ ├── carriers/ │ │ └── csv-loader/ │ │ └── csv-rate-loader.adapter.ts ✅ CREATED │ ├── storage/ │ │ └── csv-storage/ │ │ └── rates/ │ │ ├── ssc-consolidation.csv ✅ CREATED (25 rows) │ │ ├── ecu-worldwide.csv ✅ CREATED (26 rows) │ │ ├── tcc-logistics.csv ✅ CREATED (25 rows) │ │ └── nvo-consolidation.csv ✅ CREATED (25 rows) │ └── persistence/typeorm/ │ ├── entities/ │ │ └── csv-rate-config.orm-entity.ts ✅ CREATED │ └── migrations/ │ └── 1730000000011-CreateCsvRateConfigs.ts ✅ CREATED └── application/ ├── dto/ ⏭️ TODO ├── controllers/ ⏭️ TODO └── mappers/ ⏭️ TODO ``` ## CSV File Format ### Required Columns | Column | Type | Description | Example | |--------|------|-------------|---------| | `companyName` | string | Carrier name | SSC Consolidation | | `origin` | string | Origin port (UN LOCODE) | NLRTM | | `destination` | string | Destination port (UN LOCODE) | USNYC | | `containerType` | string | Container type | LCL | | `minVolumeCBM` | number | Min volume in CBM | 1 | | `maxVolumeCBM` | number | Max volume in CBM | 100 | | `minWeightKG` | number | Min weight in kg | 100 | | `maxWeightKG` | number | Max weight in kg | 15000 | | `palletCount` | number | Pallet count (0=any) | 10 | | `pricePerCBM` | number | Price per cubic meter | 45.50 | | `pricePerKG` | number | Price per kilogram | 2.80 | | `basePriceUSD` | number | Base price in USD | 1500 | | `basePriceEUR` | number | Base price in EUR | 1350 | | `currency` | string | Primary currency | USD | | `hasSurcharges` | boolean | Has surcharges? | true | | `surchargeBAF` | number | BAF surcharge (optional) | 150 | | `surchargeCAF` | number | CAF surcharge (optional) | 75 | | `surchargeDetails` | string | Surcharge details (optional) | BAF+CAF included | | `transitDays` | number | Transit time in days | 28 | | `validFrom` | date | Start date (YYYY-MM-DD) | 2025-01-01 | | `validUntil` | date | End date (YYYY-MM-DD) | 2025-12-31 | ### Price Calculation Logic ```typescript // Freight class rule: take the higher of volume-based or weight-based price const volumePrice = volumeCBM * pricePerCBM; const weightPrice = weightKG * pricePerKG; const freightPrice = Math.max(volumePrice, weightPrice); // Add surcharges if present const totalPrice = freightPrice + (hasSurcharges ? (surchargeBAF + surchargeCAF) : 0); ``` ## Domain Entities ### CsvRate Entity Main domain entity representing a CSV-loaded rate: ```typescript class CsvRate { constructor( companyName: string, origin: PortCode, destination: PortCode, containerType: ContainerType, volumeRange: VolumeRange, weightRange: WeightRange, palletCount: number, pricing: RatePricing, currency: string, surcharges: SurchargeCollection, transitDays: number, validity: DateRange, ) // Key methods calculatePrice(volume: Volume): Money getPriceInCurrency(volume: Volume, targetCurrency: 'USD' | 'EUR'): Money isValidForDate(date: Date): boolean matchesVolume(volume: Volume): boolean matchesPalletCount(palletCount: number): boolean matchesRoute(origin: PortCode, destination: PortCode): boolean } ``` ### Value Objects **Volume**: Represents shipping volume in CBM and weight in KG ```typescript class Volume { constructor(cbm: number, weightKG: number) calculateFreightPrice(pricePerCBM: number, pricePerKG: number): number isWithinRange(minCBM, maxCBM, minKG, maxKG): boolean } ``` **Surcharge**: Represents additional fees ```typescript class Surcharge { constructor( type: SurchargeType, // BAF, CAF, PSS, THC, OTHER amount: Money, description?: string ) } class SurchargeCollection { getTotalAmount(currency: string): Money isEmpty(): boolean getDetails(): string } ``` ## Database Schema ### csv_rate_configs Table ```sql CREATE TABLE csv_rate_configs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), company_name VARCHAR(255) NOT NULL UNIQUE, csv_file_path VARCHAR(500) NOT NULL, type VARCHAR(50) NOT NULL DEFAULT 'CSV_ONLY', -- CSV_ONLY | CSV_AND_API has_api BOOLEAN NOT NULL DEFAULT FALSE, api_connector VARCHAR(100) NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, uploaded_at TIMESTAMP NOT NULL DEFAULT NOW(), uploaded_by UUID NULL REFERENCES users(id) ON DELETE SET NULL, last_validated_at TIMESTAMP NULL, row_count INTEGER NULL, metadata JSONB NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); ``` ### Seeded Data | company_name | csv_file_path | type | has_api | api_connector | |--------------|---------------|------|---------|---------------| | SSC Consolidation | ssc-consolidation.csv | CSV_ONLY | false | null | | ECU Worldwide | ecu-worldwide.csv | CSV_AND_API | true | ecu-worldwide | | TCC Logistics | tcc-logistics.csv | CSV_ONLY | false | null | | NVO Consolidation | nvo-consolidation.csv | CSV_ONLY | false | null | ## API Research Results ### ✅ ECU Worldwide - API Available **API Portal**: https://api-portal.ecuworldwide.com/ **Features**: - REST API with JSON responses - Rate quotes (door-to-door, port-to-port) - Shipment booking (create/update/cancel) - Tracking and visibility - Sandbox and production environments - API key authentication **Integration Status**: Ready for connector implementation ### ❌ Other Carriers - No Public APIs - **SSC Consolidation**: No public API found - **TCC Logistics**: No public API found - **NVO Consolidation**: No public API found (uses project44 for tracking only) All three will use **CSV_ONLY** integration. ## Advanced Filters ### RateSearchFilters Interface ```typescript interface RateSearchFilters { // Company filters companies?: string[]; // Volume/Weight filters minVolumeCBM?: number; maxVolumeCBM?: number; minWeightKG?: number; maxWeightKG?: number; palletCount?: number; // Price filters minPrice?: number; maxPrice?: number; currency?: 'USD' | 'EUR'; // Transit filters minTransitDays?: number; maxTransitDays?: number; // Container type filters containerTypes?: string[]; // Surcharge filters onlyAllInPrices?: boolean; // Only show rates without separate surcharges // Date filters departureDate?: Date; } ``` ## Usage Examples ### 1. Load Rates from CSV ```typescript const loader = new CsvRateLoaderAdapter(); const rates = await loader.loadRatesFromCsv('ssc-consolidation.csv'); console.log(`Loaded ${rates.length} rates`); ``` ### 2. Search Rates with Filters ```typescript const searchService = new CsvRateSearchService(csvRateLoader); const result = await searchService.execute({ origin: 'NLRTM', destination: 'USNYC', volumeCBM: 25.5, weightKG: 3500, palletCount: 10, filters: { companies: ['SSC Consolidation', 'ECU Worldwide'], minPrice: 1000, maxPrice: 3000, currency: 'USD', onlyAllInPrices: true, }, }); console.log(`Found ${result.totalResults} matching rates`); result.results.forEach(r => { console.log(`${r.rate.companyName}: $${r.calculatedPrice.usd}`); }); ``` ### 3. Calculate Price for Specific Volume ```typescript const volume = new Volume(25.5, 3500); // 25.5 CBM, 3500 kg const price = csvRate.calculatePrice(volume); console.log(`Total price: ${price.format()}`); // $1,850.00 ``` ## Next Steps (TODO) ### Backend (Application Layer) 1. **DTOs** - Create data transfer objects: - [rate-search-filters.dto.ts](apps/backend/src/application/dto/rate-search-filters.dto.ts) - [csv-rate-upload.dto.ts](apps/backend/src/application/dto/csv-rate-upload.dto.ts) - [rate-result.dto.ts](apps/backend/src/application/dto/rate-result.dto.ts) 2. **Controllers**: - Update `RatesController` with `/search` endpoint supporting advanced filters - Create `CsvRatesController` (admin only) for CSV upload - Add `/api/v1/rates/companies` endpoint - Add `/api/v1/rates/filters/options` endpoint 3. **Repository**: - Create `TypeOrmCsvRateConfigRepository` - Implement CRUD operations for csv_rate_configs table 4. **Module Configuration**: - Register `CsvRateLoaderAdapter` as provider - Register `CsvRateSearchService` as provider - Add to `CarrierModule` or create new `CsvRateModule` ### Backend (ECU Worldwide API Connector) 5. **ECU Connector** (if time permits): - Create `infrastructure/carriers/ecu-worldwide/` - Implement `ecu-worldwide.connector.ts` - Add `ecu-worldwide.mapper.ts` - Add `ecu-worldwide.types.ts` - Environment variables: `ECU_WORLDWIDE_API_KEY`, `ECU_WORLDWIDE_API_URL` ### Frontend 6. **Components**: - `RateFiltersPanel.tsx` - Advanced filters sidebar - `VolumeWeightInput.tsx` - CBM + weight input - `CompanyMultiSelect.tsx` - Multi-select for companies - `RateResultsTable.tsx` - Display results with source (CSV/API) - `CsvUpload.tsx` - Admin CSV upload (protected route) 7. **Hooks**: - `useRateSearch.ts` - Search with filters - `useCompanies.ts` - Get available companies - `useFilterOptions.ts` - Get filter options 8. **API Client**: - Update `lib/api/rates.ts` with new endpoints - Create `lib/api/admin/csv-rates.ts` ### Testing 9. **Unit Tests** (Target: 90%+ coverage): - `csv-rate.entity.spec.ts` - `volume.vo.spec.ts` - `surcharge.vo.spec.ts` - `csv-rate-search.service.spec.ts` 10. **Integration Tests**: - `csv-rate-loader.adapter.spec.ts` - CSV file validation tests - Price calculation tests ### Documentation 11. **Update CLAUDE.md**: - Add CSV Rate System section - Document new endpoints - Add environment variables ## Running Migrations ```bash cd apps/backend npm run migration:run ``` This will create the `csv_rate_configs` table and seed the 4 carriers. ## Validation To validate a CSV file: ```typescript const loader = new CsvRateLoaderAdapter(); const result = await loader.validateCsvFile('ssc-consolidation.csv'); if (!result.valid) { console.error('Validation errors:', result.errors); } else { console.log(`Valid CSV with ${result.rowCount} rows`); } ``` ## Security - ✅ CSV upload endpoint protected by `@Roles('ADMIN')` guard - ✅ File validation: size, extension, structure - ✅ Sanitization of CSV data before parsing - ✅ Path traversal prevention (only access rates directory) ## Performance - ✅ Redis caching (15min TTL) for loaded CSV rates - ✅ Batch loading of all CSV files in parallel - ✅ Efficient filtering with early returns - ✅ Match scoring for result relevance ## Deployment Checklist - [ ] Run database migration - [ ] Upload CSV files to `infrastructure/storage/csv-storage/rates/` - [ ] Set file permissions (readable by app user) - [ ] Configure Redis for caching - [ ] Test CSV loading on server - [ ] Verify admin CSV upload endpoint - [ ] Monitor CSV file sizes (keep under 10MB each) ## Maintenance ### Adding a New Carrier 1. Create CSV file: `carrier-name.csv` 2. Add entry to `csv_rate_configs` table 3. Upload via admin interface OR run SQL: ```sql INSERT INTO csv_rate_configs (company_name, csv_file_path, type, has_api) VALUES ('New Carrier', 'new-carrier.csv', 'CSV_ONLY', false); ``` ### Updating Rates 1. Admin uploads new CSV via `/api/v1/admin/csv-rates/upload` 2. System validates structure 3. Old file replaced, cache cleared 4. New rates immediately available ## Support For questions or issues: - Check [CARRIER_API_RESEARCH.md](CARRIER_API_RESEARCH.md) for API details - Review [CLAUDE.md](CLAUDE.md) for system architecture - See domain tests for usage examples