From c5f823f8b75801a7166bbe87d18b7513ee4f4904 Mon Sep 17 00:00:00 2001
From: David
Date: Thu, 2 Jul 2026 19:12:18 +0200
Subject: [PATCH] feat(bookings): persist Options & Services
(customs/insurance/DG/handling)
Les options du formulaire de recherche etaient collectees mais jamais stockees.
Ajout d'une colonne jsonb options sur csv_bookings (+ migration), stockee a la
creation et a l'edition (editFromRate), renvoyee dans la reponse, et re-pre-
remplie a la reprise (search-advanced <- URL <- booking.options).
Co-Authored-By: Claude Opus 4.8
---
.../src/application/dto/csv-booking.dto.ts | 25 ++++++++++++++
.../services/csv-booking.service.ts | 15 ++++++++-
.../src/domain/entities/csv-booking.entity.ts | 11 +++++--
.../entities/csv-booking.orm-entity.ts | 3 ++
.../typeorm/mappers/csv-booking.mapper.ts | 5 ++-
.../1749000000000-AddOptionsToCsvBookings.ts | 19 +++++++++++
.../dashboard/booking/[id]/pay/page.tsx | 7 +++-
.../[locale]/dashboard/booking/new/page.tsx | 11 +++++++
.../app/[locale]/dashboard/bookings/page.tsx | 6 +++-
.../dashboard/search-advanced/page.tsx | 33 ++++++++++++++++++-
.../search-advanced/results/page.tsx | 12 ++++++-
apps/frontend/src/lib/api/bookings.ts | 2 ++
12 files changed, 141 insertions(+), 8 deletions(-)
create mode 100644 apps/backend/src/infrastructure/persistence/typeorm/migrations/1749000000000-AddOptionsToCsvBookings.ts
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() {