From cb0d44bb348de8e497a2baec1b71d228104a977e Mon Sep 17 00:00:00 2001 From: David Date: Wed, 29 Oct 2025 21:18:53 +0100 Subject: [PATCH] feature csv rates --- .claude/settings.local.json | 6 +- .../controllers/admin/csv-rates.controller.ts | 20 +- .../controllers/rates.controller.ts | 9 + .../application/dto/csv-rate-search.dto.ts | 156 +++++++++++- .../application/mappers/csv-rate.mapper.ts | 15 ++ .../domain/ports/in/search-csv-rates.port.ts | 34 +++ .../csv-rate-price-calculator.service.ts | 231 ++++++++++++++++++ .../services/csv-rate-search.service.ts | 39 +-- .../csv-loader/csv-converter.service.ts | 12 +- .../carriers/csv-loader/csv-rate.module.ts | 16 +- 10 files changed, 510 insertions(+), 28 deletions(-) create mode 100644 apps/backend/src/domain/services/csv-rate-price-calculator.service.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7cb3f26..5e65b00 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -27,7 +27,11 @@ "Bash(PGPASSWORD=xpeditis_dev_password psql -h localhost -p 5432 -U xpeditis -d xpeditis_dev -c \"SELECT id, email, role FROM users LIMIT 5;\")", "Bash(PGPASSWORD=xpeditis_dev_password psql -h localhost -p 5432 -U xpeditis -d xpeditis_dev -c \"UPDATE users SET role = ''ADMIN'' WHERE email = ''test4@xpeditis.com''; SELECT id, email, role FROM users WHERE email = ''test4@xpeditis.com'';\")", "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMzg1MDVkMi1hMmVlLTQ5NmMtOWNjZC1iNjUyN2FjMzcxODgiLCJlbWFpbCI6InRlc3Q0QHhwZWRpdGlzLmNvbSIsInJvbGUiOiJBRE1JTiIsIm9yZ2FuaXphdGlvbklkIjoiYTEyMzQ1NjctMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTc2MTU5MDg0OSwiZXhwIjoxNzYxNTkxNzQ5fQ.CPFhvgASXuklZ81FiuX_XwYZfh8xKG4tNG70JQ4Dv8M\")", - "Bash(PGPASSWORD=xpeditis_dev_password psql -h localhost -p 5432 -U xpeditis -d xpeditis_dev -c \"UPDATE users SET role = ''ADMIN'' WHERE email = ''dharnaud77@hotmail.fr''; SELECT id, email, role FROM users WHERE email = ''dharnaud77@hotmail.fr'';\")" + "Bash(PGPASSWORD=xpeditis_dev_password psql -h localhost -p 5432 -U xpeditis -d xpeditis_dev -c \"UPDATE users SET role = ''ADMIN'' WHERE email = ''dharnaud77@hotmail.fr''; SELECT id, email, role FROM users WHERE email = ''dharnaud77@hotmail.fr'';\")", + "Bash(npm run format:*)", + "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMzg1MDVkMi1hMmVlLTQ5NmMtOWNjZC1iNjUyN2FjMzcxODgiLCJlbWFpbCI6InRlc3Q0QHhwZWRpdGlzLmNvbSIsInJvbGUiOiJBRE1JTiIsIm9yZ2FuaXphdGlvbklkIjoiYTEyMzQ1NjctMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTc2MTU5Njk0MywiZXhwIjoxNzYxNTk3ODQzfQ.cwvInoHK_vR24aRRlkJGBv_VBkgyfpCwpXyrAhulQYI\")", + "Read(//Users/david/Downloads/drive-download-20251023T120052Z-1-001/**)", + "Bash(bash:*)" ], "deny": [], "ask": [] diff --git a/apps/backend/src/application/controllers/admin/csv-rates.controller.ts b/apps/backend/src/application/controllers/admin/csv-rates.controller.ts index ba149bc..36e9a36 100644 --- a/apps/backend/src/application/controllers/admin/csv-rates.controller.ts +++ b/apps/backend/src/application/controllers/admin/csv-rates.controller.ts @@ -29,6 +29,7 @@ import { RolesGuard } from '../../guards/roles.guard'; import { Roles } from '../../decorators/roles.decorator'; import { CurrentUser, UserPayload } from '../../decorators/current-user.decorator'; import { CsvRateLoaderAdapter } from '@infrastructure/carriers/csv-loader/csv-rate-loader.adapter'; +import { CsvConverterService } from '@infrastructure/carriers/csv-loader/csv-converter.service'; import { TypeOrmCsvRateConfigRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-csv-rate-config.repository'; import { CsvRateUploadDto, @@ -54,6 +55,7 @@ export class CsvRatesAdminController { constructor( private readonly csvLoader: CsvRateLoaderAdapter, + private readonly csvConverter: CsvConverterService, private readonly csvConfigRepository: TypeOrmCsvRateConfigRepository, private readonly csvRateMapper: CsvRateMapper ) {} @@ -139,8 +141,18 @@ export class CsvRatesAdminController { } try { - // Validate CSV file structure - const validation = await this.csvLoader.validateCsvFile(file.filename); + // Auto-convert CSV if needed (FOB FRET → Standard format) + const conversionResult = await this.csvConverter.autoConvert(file.path, dto.companyName); + const filePathToValidate = conversionResult.convertedPath; + + if (conversionResult.wasConverted) { + this.logger.log( + `Converted ${conversionResult.rowsConverted} rows from FOB FRET format to standard format` + ); + } + + // Validate CSV file structure using the converted path + const validation = await this.csvLoader.validateCsvFile(filePathToValidate); if (!validation.valid) { this.logger.error( @@ -152,8 +164,8 @@ export class CsvRatesAdminController { }); } - // Load rates to verify parsing - const rates = await this.csvLoader.loadRatesFromCsv(file.filename); + // Load rates to verify parsing using the converted path + const rates = await this.csvLoader.loadRatesFromCsv(filePathToValidate); const ratesCount = rates.length; this.logger.log(`Successfully parsed ${ratesCount} rates from ${file.filename}`); diff --git a/apps/backend/src/application/controllers/rates.controller.ts b/apps/backend/src/application/controllers/rates.controller.ts index 9cd1c8d..3b59528 100644 --- a/apps/backend/src/application/controllers/rates.controller.ts +++ b/apps/backend/src/application/controllers/rates.controller.ts @@ -164,6 +164,15 @@ export class RatesController { palletCount: dto.palletCount ?? 0, containerType: dto.containerType, filters: this.csvRateMapper.mapFiltersDtoToDomain(dto.filters), + + // Service requirements for detailed pricing + hasDangerousGoods: dto.hasDangerousGoods ?? false, + requiresSpecialHandling: dto.requiresSpecialHandling ?? false, + requiresTailgate: dto.requiresTailgate ?? false, + requiresStraps: dto.requiresStraps ?? false, + requiresThermalCover: dto.requiresThermalCover ?? false, + hasRegulatedProducts: dto.hasRegulatedProducts ?? false, + requiresAppointment: dto.requiresAppointment ?? false, }; // Execute CSV rate search diff --git a/apps/backend/src/application/dto/csv-rate-search.dto.ts b/apps/backend/src/application/dto/csv-rate-search.dto.ts index a7b8a37..d0b866d 100644 --- a/apps/backend/src/application/dto/csv-rate-search.dto.ts +++ b/apps/backend/src/application/dto/csv-rate-search.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumber, Min, IsOptional, ValidateNested } from 'class-validator'; +import { IsNotEmpty, IsString, IsNumber, Min, IsOptional, ValidateNested, IsBoolean } from 'class-validator'; import { Type } from 'class-transformer'; import { RateSearchFiltersDto } from './rate-search-filters.dto'; @@ -75,6 +75,70 @@ export class CsvRateSearchDto { @ValidateNested() @Type(() => RateSearchFiltersDto) filters?: RateSearchFiltersDto; + + // Service requirements for detailed price calculation + @ApiPropertyOptional({ + description: 'Cargo contains dangerous goods (DG)', + example: true, + default: false, + }) + @IsOptional() + @IsBoolean() + hasDangerousGoods?: boolean; + + @ApiPropertyOptional({ + description: 'Requires special handling', + example: true, + default: false, + }) + @IsOptional() + @IsBoolean() + requiresSpecialHandling?: boolean; + + @ApiPropertyOptional({ + description: 'Requires tailgate lift', + example: false, + default: false, + }) + @IsOptional() + @IsBoolean() + requiresTailgate?: boolean; + + @ApiPropertyOptional({ + description: 'Requires securing straps', + example: true, + default: false, + }) + @IsOptional() + @IsBoolean() + requiresStraps?: boolean; + + @ApiPropertyOptional({ + description: 'Requires thermal protection cover', + example: false, + default: false, + }) + @IsOptional() + @IsBoolean() + requiresThermalCover?: boolean; + + @ApiPropertyOptional({ + description: 'Contains regulated products requiring special documentation', + example: false, + default: false, + }) + @IsOptional() + @IsBoolean() + hasRegulatedProducts?: boolean; + + @ApiPropertyOptional({ + description: 'Requires delivery appointment', + example: true, + default: false, + }) + @IsOptional() + @IsBoolean() + requiresAppointment?: boolean; } /** @@ -115,6 +179,90 @@ export class CsvRateSearchResponseDto { appliedFilters: RateSearchFiltersDto; } +/** + * Surcharge Item DTO + */ +export class SurchargeItemDto { + @ApiProperty({ + description: 'Surcharge code', + example: 'DG_FEE', + }) + code: string; + + @ApiProperty({ + description: 'Surcharge description', + example: 'Dangerous goods fee', + }) + description: string; + + @ApiProperty({ + description: 'Surcharge amount in currency', + example: 65.0, + }) + amount: number; + + @ApiProperty({ + description: 'Type of surcharge calculation', + enum: ['FIXED', 'PER_UNIT', 'PERCENTAGE'], + example: 'FIXED', + }) + type: 'FIXED' | 'PER_UNIT' | 'PERCENTAGE'; +} + +/** + * Price Breakdown DTO + */ +export class PriceBreakdownDto { + @ApiProperty({ + description: 'Base price before any charges', + example: 0, + }) + basePrice: number; + + @ApiProperty({ + description: 'Charge based on volume (CBM)', + example: 150.0, + }) + volumeCharge: number; + + @ApiProperty({ + description: 'Charge based on weight (KG)', + example: 25.0, + }) + weightCharge: number; + + @ApiProperty({ + description: 'Charge for pallets', + example: 125.0, + }) + palletCharge: number; + + @ApiProperty({ + description: 'List of all surcharges', + type: [SurchargeItemDto], + }) + surcharges: SurchargeItemDto[]; + + @ApiProperty({ + description: 'Total of all surcharges', + example: 242.0, + }) + totalSurcharges: number; + + @ApiProperty({ + description: 'Total price including all charges', + example: 542.0, + }) + totalPrice: number; + + @ApiProperty({ + description: 'Currency of the pricing', + enum: ['USD', 'EUR'], + example: 'USD', + }) + currency: string; +} + /** * Single CSV Rate Result DTO */ @@ -162,6 +310,12 @@ export class CsvRateResultDto { }) primaryCurrency: string; + @ApiProperty({ + description: 'Detailed price breakdown with all charges', + type: PriceBreakdownDto, + }) + priceBreakdown: PriceBreakdownDto; + @ApiProperty({ description: 'Whether this rate has separate surcharges', example: true, diff --git a/apps/backend/src/application/mappers/csv-rate.mapper.ts b/apps/backend/src/application/mappers/csv-rate.mapper.ts index e43996a..53ea98a 100644 --- a/apps/backend/src/application/mappers/csv-rate.mapper.ts +++ b/apps/backend/src/application/mappers/csv-rate.mapper.ts @@ -60,6 +60,21 @@ export class CsvRateMapper { priceUSD: result.calculatedPrice.usd, priceEUR: result.calculatedPrice.eur, primaryCurrency: result.calculatedPrice.primaryCurrency, + priceBreakdown: { + basePrice: result.priceBreakdown.basePrice, + volumeCharge: result.priceBreakdown.volumeCharge, + weightCharge: result.priceBreakdown.weightCharge, + palletCharge: result.priceBreakdown.palletCharge, + surcharges: result.priceBreakdown.surcharges.map(s => ({ + code: s.code, + description: s.description, + amount: s.amount, + type: s.type, + })), + totalSurcharges: result.priceBreakdown.totalSurcharges, + totalPrice: result.priceBreakdown.totalPrice, + currency: result.priceBreakdown.currency, + }, hasSurcharges: rate.hasSurcharges(), surchargeDetails: rate.hasSurcharges() ? rate.getSurchargeDetails() : null, transitDays: rate.transitDays, diff --git a/apps/backend/src/domain/ports/in/search-csv-rates.port.ts b/apps/backend/src/domain/ports/in/search-csv-rates.port.ts index 88aa46c..d76b57d 100644 --- a/apps/backend/src/domain/ports/in/search-csv-rates.port.ts +++ b/apps/backend/src/domain/ports/in/search-csv-rates.port.ts @@ -50,6 +50,39 @@ export interface CsvRateSearchInput { palletCount?: number; // Number of pallets (0 if none) containerType?: string; // Optional container type filter filters?: RateSearchFilters; // Advanced filters + + // Service requirements for price calculation + hasDangerousGoods?: boolean; + requiresSpecialHandling?: boolean; + requiresTailgate?: boolean; + requiresStraps?: boolean; + requiresThermalCover?: boolean; + hasRegulatedProducts?: boolean; + requiresAppointment?: boolean; +} + +/** + * Surcharge Item - Individual fee or charge + */ +export interface SurchargeItem { + code: string; + description: string; + amount: number; + type: 'FIXED' | 'PER_UNIT' | 'PERCENTAGE'; +} + +/** + * Price Breakdown - Detailed pricing calculation + */ +export interface PriceBreakdown { + basePrice: number; + volumeCharge: number; + weightCharge: number; + palletCharge: number; + surcharges: SurchargeItem[]; + totalSurcharges: number; + totalPrice: number; + currency: string; } /** @@ -64,6 +97,7 @@ export interface CsvRateSearchResult { eur: number; primaryCurrency: string; }; + priceBreakdown: PriceBreakdown; // Detailed price calculation source: 'CSV'; matchScore: number; // 0-100, how well it matches filters } diff --git a/apps/backend/src/domain/services/csv-rate-price-calculator.service.ts b/apps/backend/src/domain/services/csv-rate-price-calculator.service.ts new file mode 100644 index 0000000..4073001 --- /dev/null +++ b/apps/backend/src/domain/services/csv-rate-price-calculator.service.ts @@ -0,0 +1,231 @@ +import { CsvRate } from '../entities/csv-rate.entity'; + +export interface PriceCalculationParams { + volumeCBM: number; + weightKG: number; + palletCount: number; + hasDangerousGoods: boolean; + requiresSpecialHandling: boolean; + requiresTailgate: boolean; + requiresStraps: boolean; + requiresThermalCover: boolean; + hasRegulatedProducts: boolean; + requiresAppointment: boolean; +} + +export interface PriceBreakdown { + basePrice: number; + volumeCharge: number; + weightCharge: number; + palletCharge: number; + surcharges: SurchargeItem[]; + totalSurcharges: number; + totalPrice: number; + currency: string; +} + +export interface SurchargeItem { + code: string; + description: string; + amount: number; + type: 'FIXED' | 'PER_UNIT' | 'PERCENTAGE'; +} + +/** + * Service de calcul de prix pour les tarifs CSV + * Calcule le prix total basé sur le volume, poids, palettes et services additionnels + */ +export class CsvRatePriceCalculatorService { + /** + * Calcule le prix total pour un tarif CSV donné + */ + calculatePrice( + rate: CsvRate, + params: PriceCalculationParams, + ): PriceBreakdown { + // 1. Prix de base + const basePrice = rate.pricing.basePriceUSD.getAmount(); + + // 2. Frais au volume (USD par CBM) + const volumeCharge = rate.pricing.pricePerCBM * params.volumeCBM; + + // 3. Frais au poids (USD par KG) + const weightCharge = rate.pricing.pricePerKG * params.weightKG; + + // 4. Frais de palettes (25 USD par palette) + const palletCharge = params.palletCount * 25; + + // 5. Surcharges standard du CSV + const standardSurcharges = this.parseStandardSurcharges( + rate.getSurchargeDetails(), + params, + ); + + // 6. Surcharges additionnelles basées sur les services + const additionalSurcharges = this.calculateAdditionalSurcharges(params); + + // 7. Total des surcharges + const allSurcharges = [...standardSurcharges, ...additionalSurcharges]; + const totalSurcharges = allSurcharges.reduce( + (sum, s) => sum + s.amount, + 0, + ); + + // 8. Prix total + const totalPrice = + basePrice + volumeCharge + weightCharge + palletCharge + totalSurcharges; + + return { + basePrice, + volumeCharge, + weightCharge, + palletCharge, + surcharges: allSurcharges, + totalSurcharges, + totalPrice: Math.round(totalPrice * 100) / 100, // Arrondi à 2 décimales + currency: rate.currency || 'USD', + }; + } + + /** + * Parse les surcharges standard du format CSV + * Format: "DOC:10 | ISPS:7 | HANDLING:20 W | DG_FEE:65" + */ + private parseStandardSurcharges( + surchargeDetails: string | null, + params: PriceCalculationParams, + ): SurchargeItem[] { + if (!surchargeDetails) { + return []; + } + + const surcharges: SurchargeItem[] = []; + const items = surchargeDetails.split('|').map((s) => s.trim()); + + for (const item of items) { + const match = item.match(/^([A-Z_]+):(\d+(?:\.\d+)?)\s*([WP%]?)$/); + if (!match) continue; + + const [, code, amountStr, type] = match; + let amount = parseFloat(amountStr); + let surchargeType: 'FIXED' | 'PER_UNIT' | 'PERCENTAGE' = 'FIXED'; + + // Calcul selon le type + if (type === 'W') { + // Par poids (W = Weight) + amount = amount * params.weightKG; + surchargeType = 'PER_UNIT'; + } else if (type === 'P') { + // Par palette + amount = amount * params.palletCount; + surchargeType = 'PER_UNIT'; + } else if (type === '%') { + // Pourcentage (sera appliqué sur le total) + surchargeType = 'PERCENTAGE'; + } + + // Certaines surcharges ne s'appliquent que si certaines conditions sont remplies + if (code === 'DG_FEE' && !params.hasDangerousGoods) { + continue; // Skip DG fee si pas de marchandises dangereuses + } + + surcharges.push({ + code, + description: this.getSurchargeDescription(code), + amount: Math.round(amount * 100) / 100, + type: surchargeType, + }); + } + + return surcharges; + } + + /** + * Calcule les surcharges additionnelles basées sur les services demandés + */ + private calculateAdditionalSurcharges( + params: PriceCalculationParams, + ): SurchargeItem[] { + const surcharges: SurchargeItem[] = []; + + if (params.requiresSpecialHandling) { + surcharges.push({ + code: 'SPECIAL_HANDLING', + description: 'Manutention particulière', + amount: 75, + type: 'FIXED', + }); + } + + if (params.requiresTailgate) { + surcharges.push({ + code: 'TAILGATE', + description: 'Hayon élévateur', + amount: 50, + type: 'FIXED', + }); + } + + if (params.requiresStraps) { + surcharges.push({ + code: 'STRAPS', + description: 'Sangles de sécurité', + amount: 30, + type: 'FIXED', + }); + } + + if (params.requiresThermalCover) { + surcharges.push({ + code: 'THERMAL_COVER', + description: 'Couverture thermique', + amount: 100, + type: 'FIXED', + }); + } + + if (params.hasRegulatedProducts) { + surcharges.push({ + code: 'REGULATED_PRODUCTS', + description: 'Produits réglementés', + amount: 80, + type: 'FIXED', + }); + } + + if (params.requiresAppointment) { + surcharges.push({ + code: 'APPOINTMENT', + description: 'Livraison sur rendez-vous', + amount: 40, + type: 'FIXED', + }); + } + + return surcharges; + } + + /** + * Retourne la description d'un code de surcharge standard + */ + private getSurchargeDescription(code: string): string { + const descriptions: Record = { + DOC: 'Documentation fee', + ISPS: 'ISPS Security', + HANDLING: 'Handling charges', + SOLAS: 'SOLAS VGM', + CUSTOMS: 'Customs clearance', + AMS_ACI: 'AMS/ACI filing', + DG_FEE: 'Dangerous goods fee', + BAF: 'Bunker Adjustment Factor', + CAF: 'Currency Adjustment Factor', + THC: 'Terminal Handling Charges', + BL_FEE: 'Bill of Lading fee', + TELEX_RELEASE: 'Telex release', + ORIGIN_CHARGES: 'Origin charges', + DEST_CHARGES: 'Destination charges', + }; + + return descriptions[code] || code.replace(/_/g, ' '); + } +} diff --git a/apps/backend/src/domain/services/csv-rate-search.service.ts b/apps/backend/src/domain/services/csv-rate-search.service.ts index 98e19d0..b9eb01a 100644 --- a/apps/backend/src/domain/services/csv-rate-search.service.ts +++ b/apps/backend/src/domain/services/csv-rate-search.service.ts @@ -11,6 +11,7 @@ import { RateSearchFilters, } from '../ports/in/search-csv-rates.port'; import { CsvRateLoaderPort } from '../ports/out/csv-rate-loader.port'; +import { CsvRatePriceCalculatorService } from './csv-rate-price-calculator.service'; /** * CSV Rate Search Service @@ -21,7 +22,11 @@ import { CsvRateLoaderPort } from '../ports/out/csv-rate-loader.port'; * Pure domain logic - no framework dependencies. */ export class CsvRateSearchService implements SearchCsvRatesPort { - constructor(private readonly csvRateLoader: CsvRateLoaderPort) {} + private readonly priceCalculator: CsvRatePriceCalculatorService; + + constructor(private readonly csvRateLoader: CsvRateLoaderPort) { + this.priceCalculator = new CsvRatePriceCalculatorService(); + } async execute(input: CsvRateSearchInput): Promise { const searchStartTime = new Date(); @@ -53,29 +58,35 @@ export class CsvRateSearchService implements SearchCsvRatesPort { // Calculate prices and create results const results: CsvRateSearchResult[] = matchingRates.map(rate => { - const priceUSD = rate.getPriceInCurrency(volume, 'USD'); - const priceEUR = rate.getPriceInCurrency(volume, 'EUR'); + // Calculate detailed price breakdown + const priceBreakdown = this.priceCalculator.calculatePrice(rate, { + volumeCBM: input.volumeCBM, + weightKG: input.weightKG, + palletCount: input.palletCount ?? 0, + hasDangerousGoods: input.hasDangerousGoods ?? false, + requiresSpecialHandling: input.requiresSpecialHandling ?? false, + requiresTailgate: input.requiresTailgate ?? false, + requiresStraps: input.requiresStraps ?? false, + requiresThermalCover: input.requiresThermalCover ?? false, + hasRegulatedProducts: input.hasRegulatedProducts ?? false, + requiresAppointment: input.requiresAppointment ?? false, + }); return { rate, calculatedPrice: { - usd: priceUSD.getAmount(), - eur: priceEUR.getAmount(), - primaryCurrency: rate.currency, + usd: priceBreakdown.totalPrice, + eur: priceBreakdown.totalPrice, // TODO: Add currency conversion + primaryCurrency: priceBreakdown.currency, }, + priceBreakdown, source: 'CSV' as const, matchScore: this.calculateMatchScore(rate, input), }; }); - // Sort by price (ascending) in primary currency - results.sort((a, b) => { - const priceA = - a.calculatedPrice.primaryCurrency === 'USD' ? a.calculatedPrice.usd : a.calculatedPrice.eur; - const priceB = - b.calculatedPrice.primaryCurrency === 'USD' ? b.calculatedPrice.usd : b.calculatedPrice.eur; - return priceA - priceB; - }); + // Sort by total price (ascending) + results.sort((a, b) => a.priceBreakdown.totalPrice - b.priceBreakdown.totalPrice); return { results, diff --git a/apps/backend/src/infrastructure/carriers/csv-loader/csv-converter.service.ts b/apps/backend/src/infrastructure/carriers/csv-loader/csv-converter.service.ts index 5d71698..40d36c4 100644 --- a/apps/backend/src/infrastructure/carriers/csv-loader/csv-converter.service.ts +++ b/apps/backend/src/infrastructure/carriers/csv-loader/csv-converter.service.ts @@ -264,14 +264,18 @@ export class CsvConverterService { outputLines.push(values.join(',')); }); - // Écrire le fichier converti + // Écrire le fichier converti (garder le chemin absolu) const outputPath = inputPath.replace('.csv', '-converted.csv'); - await fs.writeFile(outputPath, outputLines.join('\n'), 'utf-8'); + const absoluteOutputPath = path.isAbsolute(outputPath) + ? outputPath + : path.resolve(process.cwd(), outputPath); - this.logger.log(`Conversion completed: ${outputPath} (${convertedRows.length} rows)`); + await fs.writeFile(absoluteOutputPath, outputLines.join('\n'), 'utf-8'); + + this.logger.log(`Conversion completed: ${absoluteOutputPath} (${convertedRows.length} rows)`); return { - outputPath, + outputPath: absoluteOutputPath, rowsConverted: convertedRows.length, }; } catch (error) { diff --git a/apps/backend/src/infrastructure/carriers/csv-loader/csv-rate.module.ts b/apps/backend/src/infrastructure/carriers/csv-loader/csv-rate.module.ts index 8da33ff..aa6c6c7 100644 --- a/apps/backend/src/infrastructure/carriers/csv-loader/csv-rate.module.ts +++ b/apps/backend/src/infrastructure/carriers/csv-loader/csv-rate.module.ts @@ -6,6 +6,7 @@ import { CsvRateSearchService } from '@domain/services/csv-rate-search.service'; // Infrastructure Adapters import { CsvRateLoaderAdapter } from './csv-rate-loader.adapter'; +import { CsvConverterService } from './csv-converter.service'; import { TypeOrmCsvRateConfigRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-csv-rate-config.repository'; // Application Layer @@ -33,13 +34,20 @@ import { CsvRateConfigOrmEntity } from '@infrastructure/persistence/typeorm/enti TypeOrmModule.forFeature([CsvRateConfigOrmEntity]), ], providers: [ - // Domain Services - CsvRateSearchService, - - // Infrastructure Adapters + // Infrastructure Adapters (must be before services that depend on them) CsvRateLoaderAdapter, + CsvConverterService, TypeOrmCsvRateConfigRepository, + // Domain Services (with factory to inject dependencies) + { + provide: CsvRateSearchService, + useFactory: (csvRateLoader: CsvRateLoaderAdapter) => { + return new CsvRateSearchService(csvRateLoader); + }, + inject: [CsvRateLoaderAdapter], + }, + // Application Mappers CsvRateMapper, ],