diff --git a/apps/backend/src/application/dto/csv-booking.dto.ts b/apps/backend/src/application/dto/csv-booking.dto.ts index d553dbf..775f562 100644 --- a/apps/backend/src/application/dto/csv-booking.dto.ts +++ b/apps/backend/src/application/dto/csv-booking.dto.ts @@ -6,6 +6,7 @@ import { Min, IsOptional, IsEnum, + IsObject, MinLength, MaxLength, } from 'class-validator'; @@ -169,6 +170,16 @@ export class CreateCsvBookingDto { @MaxLength(1000) notes?: string; + @ApiPropertyOptional({ + description: + 'Selected options & services as a JSON string (multipart), e.g. {"insurance":true}', + example: '{"insurance":true,"customsStop":true}', + }) + @IsOptional() + @IsString() + @MaxLength(2000) + options?: string; + // Documents will be handled via file upload interceptor // Not included in DTO validation but processed separately } @@ -346,6 +357,14 @@ export class UpdateCsvBookingRateDto { @IsString() @MaxLength(2000) notes?: string; + + @ApiPropertyOptional({ + description: 'Selected options & services (customs, insurance, DG, handling…)', + example: { insurance: true, customsStop: true }, + }) + @IsOptional() + @IsObject() + options?: Record; } /** @@ -602,6 +621,12 @@ export class CsvBookingResponseDto { example: 'EUR', }) fobCurrency?: string; + + @ApiPropertyOptional({ + description: 'Selected options & services (customs, insurance, DG, handling…)', + example: { insurance: true }, + }) + options?: Record; } /** diff --git a/apps/backend/src/application/services/csv-booking.service.ts b/apps/backend/src/application/services/csv-booking.service.ts index f4abcd6..3bb52a8 100644 --- a/apps/backend/src/application/services/csv-booking.service.ts +++ b/apps/backend/src/application/services/csv-booking.service.ts @@ -172,6 +172,16 @@ export class CsvBookingService { : CsvBookingStatus.PENDING; // Create domain entity (no email sent yet when a payment is required) + let parsedOptions: Record = {}; + if (dto.options) { + try { + const parsed = JSON.parse(dto.options); + if (parsed && typeof parsed === 'object') parsedOptions = parsed; + } catch { + this.logger.warn(`Ignoring invalid options JSON for booking creation`); + } + } + const booking = new CsvBooking( bookingId, userId, @@ -202,7 +212,8 @@ export class CsvBookingService { dto.freightTotal, dto.freightCurrency, dto.fobTotal, - dto.fobCurrency + dto.fobCurrency, + parsedOptions ); // Save to database @@ -1118,6 +1129,7 @@ export class CsvBookingService { fobTotal: dto.fobTotal, fobCurrency: dto.fobCurrency, notes: dto.notes, + options: dto.options, }); } catch (err) { throw new BadRequestException(err instanceof Error ? err.message : 'Invalid booking rate'); @@ -1571,6 +1583,7 @@ export class CsvBookingService { freightCurrency: booking.freightCurrency, fobTotal: booking.fobTotal, fobCurrency: booking.fobCurrency, + options: booking.options ?? {}, }; } diff --git a/apps/backend/src/domain/entities/csv-booking.entity.ts b/apps/backend/src/domain/entities/csv-booking.entity.ts index d85e6dc..c6c3ee1 100644 --- a/apps/backend/src/domain/entities/csv-booking.entity.ts +++ b/apps/backend/src/domain/entities/csv-booking.entity.ts @@ -95,7 +95,10 @@ export class CsvBooking { public freightTotal?: number, public freightCurrency?: string, public fobTotal?: number, - public fobCurrency?: string + public fobCurrency?: string, + // Options & services selected in the search form (customs, insurance, DG, + // handling…). Stored as a flat map of enabled flags. Editable before payment. + public options: Record = {} ) { this.validate(); } @@ -394,6 +397,7 @@ export class CsvBooking { fobTotal?: number; fobCurrency?: string; notes?: string; + options?: Record; }): void { if (this.status !== CsvBookingStatus.PENDING_PAYMENT) { throw new Error( @@ -430,6 +434,7 @@ export class CsvBooking { this.fobTotal = data.fobTotal; this.fobCurrency = data.fobCurrency; if (data.notes !== undefined) this.notes = data.notes; + if (data.options !== undefined) this.options = data.options; } /** @@ -600,7 +605,8 @@ export class CsvBooking { freightTotal?: number, freightCurrency?: string, fobTotal?: number, - fobCurrency?: string + fobCurrency?: string, + options?: Record ): CsvBooking { // Create instance without calling constructor validation const booking = Object.create(CsvBooking.prototype); @@ -636,6 +642,7 @@ export class CsvBooking { booking.freightCurrency = freightCurrency; booking.fobTotal = fobTotal; booking.fobCurrency = fobCurrency; + booking.options = options ?? {}; return booking; } diff --git a/apps/backend/src/infrastructure/persistence/typeorm/entities/csv-booking.orm-entity.ts b/apps/backend/src/infrastructure/persistence/typeorm/entities/csv-booking.orm-entity.ts index 083aaa8..ee8de24 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/entities/csv-booking.orm-entity.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/entities/csv-booking.orm-entity.ts @@ -183,6 +183,9 @@ export class CsvBookingOrmEntity { @Column({ name: 'fob_currency', type: 'varchar', length: 3, nullable: true }) fobCurrency: string | null; + @Column({ name: 'options', type: 'jsonb', default: {} }) + options: Record; + @CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' }) createdAt: Date; diff --git a/apps/backend/src/infrastructure/persistence/typeorm/mappers/csv-booking.mapper.ts b/apps/backend/src/infrastructure/persistence/typeorm/mappers/csv-booking.mapper.ts index 3b7f7b4..7f42516 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/mappers/csv-booking.mapper.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/mappers/csv-booking.mapper.ts @@ -49,7 +49,8 @@ export class CsvBookingMapper { ormEntity.freightTotal != null ? Number(ormEntity.freightTotal) : undefined, ormEntity.freightCurrency ?? undefined, ormEntity.fobTotal != null ? Number(ormEntity.fobTotal) : undefined, - ormEntity.fobCurrency ?? undefined + ormEntity.fobCurrency ?? undefined, + ormEntity.options ?? {} ); } @@ -87,6 +88,7 @@ export class CsvBookingMapper { freightCurrency: domain.freightCurrency ?? null, fobTotal: domain.fobTotal ?? null, fobCurrency: domain.fobCurrency ?? null, + options: domain.options ?? {}, }; } @@ -113,6 +115,7 @@ export class CsvBookingMapper { freightCurrency: domain.freightCurrency ?? null, fobTotal: domain.fobTotal ?? null, fobCurrency: domain.fobCurrency ?? null, + options: domain.options ?? {}, status: domain.status as CsvBookingOrmEntity['status'], respondedAt: domain.respondedAt, notes: domain.notes, diff --git a/apps/backend/src/infrastructure/persistence/typeorm/migrations/1749000000000-AddOptionsToCsvBookings.ts b/apps/backend/src/infrastructure/persistence/typeorm/migrations/1749000000000-AddOptionsToCsvBookings.ts new file mode 100644 index 0000000..8a50074 --- /dev/null +++ b/apps/backend/src/infrastructure/persistence/typeorm/migrations/1749000000000-AddOptionsToCsvBookings.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddOptionsToCsvBookings1749000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + 'csv_bookings', + new TableColumn({ + name: 'options', + type: 'jsonb', + isNullable: false, + default: "'{}'", + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('csv_bookings', 'options'); + } +} diff --git a/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx b/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx index 01ebbea..7228067 100644 --- a/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx +++ b/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx @@ -228,6 +228,10 @@ export default function PayCommissionPage() {