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 <noreply@anthropic.com>
This commit is contained in:
parent
795e635df3
commit
c5f823f8b7
@ -6,6 +6,7 @@ import {
|
|||||||
Min,
|
Min,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
|
IsObject,
|
||||||
MinLength,
|
MinLength,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@ -169,6 +170,16 @@ export class CreateCsvBookingDto {
|
|||||||
@MaxLength(1000)
|
@MaxLength(1000)
|
||||||
notes?: string;
|
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
|
// Documents will be handled via file upload interceptor
|
||||||
// Not included in DTO validation but processed separately
|
// Not included in DTO validation but processed separately
|
||||||
}
|
}
|
||||||
@ -346,6 +357,14 @@ export class UpdateCsvBookingRateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(2000)
|
@MaxLength(2000)
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Selected options & services (customs, insurance, DG, handling…)',
|
||||||
|
example: { insurance: true, customsStop: true },
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
options?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -602,6 +621,12 @@ export class CsvBookingResponseDto {
|
|||||||
example: 'EUR',
|
example: 'EUR',
|
||||||
})
|
})
|
||||||
fobCurrency?: string;
|
fobCurrency?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Selected options & services (customs, insurance, DG, handling…)',
|
||||||
|
example: { insurance: true },
|
||||||
|
})
|
||||||
|
options?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -172,6 +172,16 @@ export class CsvBookingService {
|
|||||||
: CsvBookingStatus.PENDING;
|
: CsvBookingStatus.PENDING;
|
||||||
|
|
||||||
// Create domain entity (no email sent yet when a payment is required)
|
// Create domain entity (no email sent yet when a payment is required)
|
||||||
|
let parsedOptions: Record<string, boolean> = {};
|
||||||
|
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(
|
const booking = new CsvBooking(
|
||||||
bookingId,
|
bookingId,
|
||||||
userId,
|
userId,
|
||||||
@ -202,7 +212,8 @@ export class CsvBookingService {
|
|||||||
dto.freightTotal,
|
dto.freightTotal,
|
||||||
dto.freightCurrency,
|
dto.freightCurrency,
|
||||||
dto.fobTotal,
|
dto.fobTotal,
|
||||||
dto.fobCurrency
|
dto.fobCurrency,
|
||||||
|
parsedOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
@ -1118,6 +1129,7 @@ export class CsvBookingService {
|
|||||||
fobTotal: dto.fobTotal,
|
fobTotal: dto.fobTotal,
|
||||||
fobCurrency: dto.fobCurrency,
|
fobCurrency: dto.fobCurrency,
|
||||||
notes: dto.notes,
|
notes: dto.notes,
|
||||||
|
options: dto.options,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new BadRequestException(err instanceof Error ? err.message : 'Invalid booking rate');
|
throw new BadRequestException(err instanceof Error ? err.message : 'Invalid booking rate');
|
||||||
@ -1571,6 +1583,7 @@ export class CsvBookingService {
|
|||||||
freightCurrency: booking.freightCurrency,
|
freightCurrency: booking.freightCurrency,
|
||||||
fobTotal: booking.fobTotal,
|
fobTotal: booking.fobTotal,
|
||||||
fobCurrency: booking.fobCurrency,
|
fobCurrency: booking.fobCurrency,
|
||||||
|
options: booking.options ?? {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,10 @@ export class CsvBooking {
|
|||||||
public freightTotal?: number,
|
public freightTotal?: number,
|
||||||
public freightCurrency?: string,
|
public freightCurrency?: string,
|
||||||
public fobTotal?: number,
|
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<string, boolean> = {}
|
||||||
) {
|
) {
|
||||||
this.validate();
|
this.validate();
|
||||||
}
|
}
|
||||||
@ -394,6 +397,7 @@ export class CsvBooking {
|
|||||||
fobTotal?: number;
|
fobTotal?: number;
|
||||||
fobCurrency?: string;
|
fobCurrency?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
options?: Record<string, boolean>;
|
||||||
}): void {
|
}): void {
|
||||||
if (this.status !== CsvBookingStatus.PENDING_PAYMENT) {
|
if (this.status !== CsvBookingStatus.PENDING_PAYMENT) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -430,6 +434,7 @@ export class CsvBooking {
|
|||||||
this.fobTotal = data.fobTotal;
|
this.fobTotal = data.fobTotal;
|
||||||
this.fobCurrency = data.fobCurrency;
|
this.fobCurrency = data.fobCurrency;
|
||||||
if (data.notes !== undefined) this.notes = data.notes;
|
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,
|
freightTotal?: number,
|
||||||
freightCurrency?: string,
|
freightCurrency?: string,
|
||||||
fobTotal?: number,
|
fobTotal?: number,
|
||||||
fobCurrency?: string
|
fobCurrency?: string,
|
||||||
|
options?: Record<string, boolean>
|
||||||
): CsvBooking {
|
): CsvBooking {
|
||||||
// Create instance without calling constructor validation
|
// Create instance without calling constructor validation
|
||||||
const booking = Object.create(CsvBooking.prototype);
|
const booking = Object.create(CsvBooking.prototype);
|
||||||
@ -636,6 +642,7 @@ export class CsvBooking {
|
|||||||
booking.freightCurrency = freightCurrency;
|
booking.freightCurrency = freightCurrency;
|
||||||
booking.fobTotal = fobTotal;
|
booking.fobTotal = fobTotal;
|
||||||
booking.fobCurrency = fobCurrency;
|
booking.fobCurrency = fobCurrency;
|
||||||
|
booking.options = options ?? {};
|
||||||
|
|
||||||
return booking;
|
return booking;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -183,6 +183,9 @@ export class CsvBookingOrmEntity {
|
|||||||
@Column({ name: 'fob_currency', type: 'varchar', length: 3, nullable: true })
|
@Column({ name: 'fob_currency', type: 'varchar', length: 3, nullable: true })
|
||||||
fobCurrency: string | null;
|
fobCurrency: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'options', type: 'jsonb', default: {} })
|
||||||
|
options: Record<string, boolean>;
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,8 @@ export class CsvBookingMapper {
|
|||||||
ormEntity.freightTotal != null ? Number(ormEntity.freightTotal) : undefined,
|
ormEntity.freightTotal != null ? Number(ormEntity.freightTotal) : undefined,
|
||||||
ormEntity.freightCurrency ?? undefined,
|
ormEntity.freightCurrency ?? undefined,
|
||||||
ormEntity.fobTotal != null ? Number(ormEntity.fobTotal) : 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,
|
freightCurrency: domain.freightCurrency ?? null,
|
||||||
fobTotal: domain.fobTotal ?? null,
|
fobTotal: domain.fobTotal ?? null,
|
||||||
fobCurrency: domain.fobCurrency ?? null,
|
fobCurrency: domain.fobCurrency ?? null,
|
||||||
|
options: domain.options ?? {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +115,7 @@ export class CsvBookingMapper {
|
|||||||
freightCurrency: domain.freightCurrency ?? null,
|
freightCurrency: domain.freightCurrency ?? null,
|
||||||
fobTotal: domain.fobTotal ?? null,
|
fobTotal: domain.fobTotal ?? null,
|
||||||
fobCurrency: domain.fobCurrency ?? null,
|
fobCurrency: domain.fobCurrency ?? null,
|
||||||
|
options: domain.options ?? {},
|
||||||
status: domain.status as CsvBookingOrmEntity['status'],
|
status: domain.status as CsvBookingOrmEntity['status'],
|
||||||
respondedAt: domain.respondedAt,
|
respondedAt: domain.respondedAt,
|
||||||
notes: domain.notes,
|
notes: domain.notes,
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddOptionsToCsvBookings1749000000000 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'csv_bookings',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'options',
|
||||||
|
type: 'jsonb',
|
||||||
|
isNullable: false,
|
||||||
|
default: "'{}'",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('csv_bookings', 'options');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -228,6 +228,10 @@ export default function PayCommissionPage() {
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const opts = (booking as any).options || {};
|
||||||
|
const enabledOptions = Object.keys(opts)
|
||||||
|
.filter(k => opts[k])
|
||||||
|
.join(',');
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
editBookingId: bookingId,
|
editBookingId: bookingId,
|
||||||
origin: booking.origin || '',
|
origin: booking.origin || '',
|
||||||
@ -235,8 +239,9 @@ export default function PayCommissionPage() {
|
|||||||
volumeCBM: String(booking.volumeCBM ?? ''),
|
volumeCBM: String(booking.volumeCBM ?? ''),
|
||||||
weightKG: String(booking.weightKG ?? ''),
|
weightKG: String(booking.weightKG ?? ''),
|
||||||
palletCount: String(booking.palletCount ?? ''),
|
palletCount: String(booking.palletCount ?? ''),
|
||||||
hasDangerousGoods: 'false',
|
hasDangerousGoods: String(!!opts.dangerousGoods),
|
||||||
});
|
});
|
||||||
|
if (enabledOptions) params.set('options', enabledOptions);
|
||||||
router.push(`/dashboard/search-advanced?${params.toString()}`);
|
router.push(`/dashboard/search-advanced?${params.toString()}`);
|
||||||
}}
|
}}
|
||||||
className="mb-8 inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
className="mb-8 inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
||||||
|
|||||||
@ -193,6 +193,17 @@ function NewBookingPageContent() {
|
|||||||
formDataToSend.append('notes', formData.notes);
|
formDataToSend.append('notes', formData.notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options & services selected in the search form (persisted with the booking)
|
||||||
|
const optionsParam = searchParams.get('options') || '';
|
||||||
|
if (optionsParam) {
|
||||||
|
const optionsObj = optionsParam
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((acc, k) => ({ ...acc, [k]: true }), {} as Record<string, boolean>);
|
||||||
|
formDataToSend.append('options', JSON.stringify(optionsObj));
|
||||||
|
}
|
||||||
|
|
||||||
// Append documents
|
// Append documents
|
||||||
formData.documents.forEach(file => {
|
formData.documents.forEach(file => {
|
||||||
formDataToSend.append('documents', file);
|
formDataToSend.append('documents', file);
|
||||||
|
|||||||
@ -33,6 +33,9 @@ export default function BookingsListPage() {
|
|||||||
// "Modifier" reopens the existing booking flow (search → carrier choice →
|
// "Modifier" reopens the existing booking flow (search → carrier choice →
|
||||||
// payment) pre-filled with the booking's data, so a change re-quotes the price.
|
// payment) pre-filled with the booking's data, so a change re-quotes the price.
|
||||||
const buildEditUrl = (b: any) => {
|
const buildEditUrl = (b: any) => {
|
||||||
|
const enabledOptions = Object.keys(b.options || {})
|
||||||
|
.filter(k => b.options[k])
|
||||||
|
.join(',');
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
editBookingId: b.id,
|
editBookingId: b.id,
|
||||||
origin: b.origin || b.originCity || '',
|
origin: b.origin || b.originCity || '',
|
||||||
@ -40,8 +43,9 @@ export default function BookingsListPage() {
|
|||||||
volumeCBM: String(b.volumeCBM ?? ''),
|
volumeCBM: String(b.volumeCBM ?? ''),
|
||||||
weightKG: String(b.weightKG ?? ''),
|
weightKG: String(b.weightKG ?? ''),
|
||||||
palletCount: String(b.palletCount ?? ''),
|
palletCount: String(b.palletCount ?? ''),
|
||||||
hasDangerousGoods: 'false',
|
hasDangerousGoods: String(!!(b.options && b.options.dangerousGoods)),
|
||||||
});
|
});
|
||||||
|
if (enabledOptions) params.set('options', enabledOptions);
|
||||||
return `/dashboard/search-advanced?${params.toString()}`;
|
return `/dashboard/search-advanced?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,22 @@ interface SearchForm {
|
|||||||
t1Document: boolean;
|
t1Document: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options & services persisted with the booking (order irrelevant).
|
||||||
|
const OPTION_KEYS: (keyof SearchForm)[] = [
|
||||||
|
'eurDocument',
|
||||||
|
't1Document',
|
||||||
|
'customsStop',
|
||||||
|
'exportAssistance',
|
||||||
|
'dangerousGoods',
|
||||||
|
'regulatedProducts',
|
||||||
|
'specialHandling',
|
||||||
|
'tailgate',
|
||||||
|
'straps',
|
||||||
|
'thermalCover',
|
||||||
|
'appointment',
|
||||||
|
'insurance',
|
||||||
|
];
|
||||||
|
|
||||||
export default function AdvancedSearchPage() {
|
export default function AdvancedSearchPage() {
|
||||||
const t = useTranslations('dashboard.rateSearch');
|
const t = useTranslations('dashboard.rateSearch');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -155,6 +171,16 @@ export default function AdvancedSearchPage() {
|
|||||||
const pallets = parseInt(searchParams.get('palletCount') || '0', 10);
|
const pallets = parseInt(searchParams.get('palletCount') || '0', 10);
|
||||||
const dg = searchParams.get('hasDangerousGoods') === 'true';
|
const dg = searchParams.get('hasDangerousGoods') === 'true';
|
||||||
|
|
||||||
|
// Prefill the options & services from the "options" CSV param.
|
||||||
|
const enabledOptions = (searchParams.get('options') || '')
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const optionFlags = OPTION_KEYS.reduce(
|
||||||
|
(acc, k) => ({ ...acc, [k]: enabledOptions.includes(k) }),
|
||||||
|
{} as Record<string, boolean>
|
||||||
|
);
|
||||||
|
|
||||||
// Reconstruct a single representative package that reproduces the aggregate
|
// Reconstruct a single representative package that reproduces the aggregate
|
||||||
// totals (the booking only stores totals, not individual packages).
|
// totals (the booking only stores totals, not individual packages).
|
||||||
const q = pallets > 0 ? pallets : 1;
|
const q = pallets > 0 ? pallets : 1;
|
||||||
@ -172,9 +198,10 @@ export default function AdvancedSearchPage() {
|
|||||||
|
|
||||||
setSearchForm(prev => ({
|
setSearchForm(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
...optionFlags,
|
||||||
origin: originParam || prev.origin,
|
origin: originParam || prev.origin,
|
||||||
destination: destinationParam || prev.destination,
|
destination: destinationParam || prev.destination,
|
||||||
dangerousGoods: dg,
|
dangerousGoods: dg || !!optionFlags.dangerousGoods,
|
||||||
packages: vol > 0 || wgt > 0 || pallets > 0 ? [pkg] : prev.packages,
|
packages: vol > 0 || wgt > 0 || pallets > 0 ? [pkg] : prev.packages,
|
||||||
}));
|
}));
|
||||||
if (originParam) setOriginSearch(originParam);
|
if (originParam) setOriginSearch(originParam);
|
||||||
@ -249,6 +276,10 @@ export default function AdvancedSearchPage() {
|
|||||||
params.set('editBookingId', editBookingId);
|
params.set('editBookingId', editBookingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carry the selected options & services so they can be persisted.
|
||||||
|
const enabledOptions = OPTION_KEYS.filter(k => searchForm[k]).join(',');
|
||||||
|
if (enabledOptions) params.set('options', enabledOptions);
|
||||||
|
|
||||||
router.push(`/dashboard/search-advanced/results?${params.toString()}`);
|
router.push(`/dashboard/search-advanced/results?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,14 @@ export default function SearchResultsPage() {
|
|||||||
// When present, we are re-selecting a rate for an existing unpaid booking:
|
// When present, we are re-selecting a rate for an existing unpaid booking:
|
||||||
// update it in place and go straight to payment (skip the new-booking form).
|
// update it in place and go straight to payment (skip the new-booking form).
|
||||||
const editBookingId = searchParams.get('editBookingId');
|
const editBookingId = searchParams.get('editBookingId');
|
||||||
|
const optionsParam = searchParams.get('options') || '';
|
||||||
|
const optionsObj: Record<string, boolean> = optionsParam
|
||||||
|
? optionsParam
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((acc, k) => ({ ...acc, [k]: true }), {})
|
||||||
|
: {};
|
||||||
const [submittingId, setSubmittingId] = useState<string | null>(null);
|
const [submittingId, setSubmittingId] = useState<string | null>(null);
|
||||||
const [selectError, setSelectError] = useState<string | null>(null);
|
const [selectError, setSelectError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -76,6 +84,7 @@ export default function SearchResultsPage() {
|
|||||||
freightCurrency,
|
freightCurrency,
|
||||||
fobTotal,
|
fobTotal,
|
||||||
fobCurrency,
|
fobCurrency,
|
||||||
|
options: optionsObj,
|
||||||
});
|
});
|
||||||
router.push(`/dashboard/booking/${editBookingId}/pay`);
|
router.push(`/dashboard/booking/${editBookingId}/pay`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -88,8 +97,9 @@ export default function SearchResultsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rateData = encodeURIComponent(JSON.stringify(option));
|
const rateData = encodeURIComponent(JSON.stringify(option));
|
||||||
|
const optionsQuery = optionsParam ? `&options=${encodeURIComponent(optionsParam)}` : '';
|
||||||
router.push(
|
router.push(
|
||||||
`/dashboard/booking/new?rateData=${rateData}&volumeCBM=${volumeCBM}&weightKG=${weightKG}&palletCount=${palletCount}`
|
`/dashboard/booking/new?rateData=${rateData}&volumeCBM=${volumeCBM}&weightKG=${weightKG}&palletCount=${palletCount}${optionsQuery}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export interface CsvBookingResponse {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
commissionRate?: number;
|
commissionRate?: number;
|
||||||
commissionAmountEur?: number;
|
commissionAmountEur?: number;
|
||||||
|
options?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommissionPaymentResponse {
|
export interface CommissionPaymentResponse {
|
||||||
@ -311,6 +312,7 @@ export interface UpdateCsvBookingRateRequest {
|
|||||||
fobTotal?: number;
|
fobTotal?: number;
|
||||||
fobCurrency?: string;
|
fobCurrency?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
options?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCsvBookingRate(
|
export async function updateCsvBookingRate(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user