Le statut PENDING_PAYMENT ne designe plus un paiement en attente mais un devis : une reservation construite dont les frais de booking ne sont pas encore regles. L'enum backend devient QUOTE, converti en place par migration. Les deux onglets se partagent desormais la meme liste : - « Reservations » n'affiche que PENDING, ACCEPTED et REJECTED ; - « Historique des devis » regroupe QUOTE, PENDING_BANK_TRANSFER et CANCELLED, avec les actions propres a un devis (modifier, payer, supprimer). La table, l'export et les filtres sont extraits dans BookingListView pour eviter de dupliquer la page entre les deux vues. Tableau de bord : - l'alerte « Paiement en attente » disparait des points a traiter, les devis non regles ne bloquant aucun envoi en cours ; - « Sans reponse du transporteur » devient « En attente du transporteur ». Migration verifiee en transaction annulee sur la base de dev : les lignes PENDING_PAYMENT deviennent QUOTE, l'enum et le defaut de colonne suivent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJ5aowZJVAcyPZw5TiNEm
673 lines
20 KiB
TypeScript
673 lines
20 KiB
TypeScript
import { PortCode } from '../value-objects/port-code.vo';
|
|
|
|
/**
|
|
* CSV Booking Status Enum
|
|
*
|
|
* Represents the lifecycle of a CSV-based booking request
|
|
*/
|
|
export enum CsvBookingStatus {
|
|
QUOTE = 'QUOTE', // Devis : reservation creee, frais de booking non regles
|
|
PENDING_BANK_TRANSFER = 'PENDING_BANK_TRANSFER', // Bank transfer declared, awaiting admin validation
|
|
PENDING = 'PENDING', // Awaiting carrier response
|
|
ACCEPTED = 'ACCEPTED', // Carrier accepted the booking
|
|
REJECTED = 'REJECTED', // Carrier rejected the booking
|
|
CANCELLED = 'CANCELLED', // User cancelled the booking
|
|
}
|
|
|
|
/**
|
|
* Statuts dans lesquels aucun paiement n'a ete encaisse : la reservation peut
|
|
* alors etre supprimee. Voir `CsvBooking.isDeletable()`.
|
|
*/
|
|
export const DELETABLE_STATUSES: readonly CsvBookingStatus[] = [CsvBookingStatus.QUOTE];
|
|
|
|
/**
|
|
* Document Interface
|
|
*
|
|
* Represents a document attached to a booking
|
|
*/
|
|
export interface CsvBookingDocument {
|
|
id: string;
|
|
type: DocumentType;
|
|
fileName: string;
|
|
filePath: string;
|
|
mimeType: string;
|
|
size: number;
|
|
uploadedAt: Date;
|
|
}
|
|
|
|
/**
|
|
* Document Type Enum
|
|
*
|
|
* Types of documents that can be attached to a booking
|
|
*/
|
|
export enum DocumentType {
|
|
BILL_OF_LADING = 'BILL_OF_LADING',
|
|
PACKING_LIST = 'PACKING_LIST',
|
|
COMMERCIAL_INVOICE = 'COMMERCIAL_INVOICE',
|
|
CERTIFICATE_OF_ORIGIN = 'CERTIFICATE_OF_ORIGIN',
|
|
OTHER = 'OTHER',
|
|
}
|
|
|
|
/**
|
|
* CSV Booking Entity
|
|
*
|
|
* Domain entity representing a shipping booking request from CSV rate search.
|
|
* This is a simplified booking workflow for CSV-based rates where the user
|
|
* selects a rate and sends a booking request to the carrier with documents.
|
|
*
|
|
* Business Rules:
|
|
* - Booking can only be accepted/rejected when status is PENDING
|
|
* - Once accepted/rejected, status cannot be changed
|
|
* - Booking expires after 7 days if not responded to
|
|
* - At least one document is required for booking creation
|
|
* - Confirmation token is used for email accept/reject links
|
|
* - Only carrier can accept/reject via email link
|
|
* - User can cancel pending bookings
|
|
*/
|
|
export class CsvBooking {
|
|
constructor(
|
|
public readonly id: string,
|
|
public readonly userId: string,
|
|
public readonly organizationId: string,
|
|
// Carrier + route + transit + container — editable before payment when the
|
|
// user re-runs the search and picks another rate (see editFromRate).
|
|
public carrierName: string,
|
|
public carrierEmail: string,
|
|
public origin: PortCode,
|
|
public destination: PortCode,
|
|
// Cargo characteristics — editable before payment (see editDetails).
|
|
public volumeCBM: number,
|
|
public weightKG: number,
|
|
public palletCount: number,
|
|
// Pricing — recomputed when cargo details change before payment (see editDetails).
|
|
public priceUSD: number,
|
|
public priceEUR: number,
|
|
public primaryCurrency: string,
|
|
public transitDays: number,
|
|
public containerType: string,
|
|
public status: CsvBookingStatus,
|
|
public readonly documents: CsvBookingDocument[],
|
|
public readonly confirmationToken: string,
|
|
public readonly requestedAt: Date,
|
|
public respondedAt?: Date,
|
|
public notes?: string,
|
|
public rejectionReason?: string,
|
|
public readonly bookingNumber?: string,
|
|
public commissionRate?: number,
|
|
public commissionAmountEur?: number,
|
|
public stripePaymentIntentId?: string,
|
|
// Detailed transport cost breakdown (informational — paid to the carrier,
|
|
// not collected by Xpeditis). Freight and FOB may be in different currencies.
|
|
public freightTotal?: number,
|
|
public freightCurrency?: string,
|
|
public fobTotal?: number,
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Validate booking data
|
|
*/
|
|
private validate(): void {
|
|
if (!this.id || this.id.trim().length === 0) {
|
|
throw new Error('Booking ID is required');
|
|
}
|
|
|
|
if (!this.userId || this.userId.trim().length === 0) {
|
|
throw new Error('User ID is required');
|
|
}
|
|
|
|
if (!this.organizationId || this.organizationId.trim().length === 0) {
|
|
throw new Error('Organization ID is required');
|
|
}
|
|
|
|
if (!this.carrierName || this.carrierName.trim().length === 0) {
|
|
throw new Error('Carrier name is required');
|
|
}
|
|
|
|
if (!this.carrierEmail || this.carrierEmail.trim().length === 0) {
|
|
throw new Error('Carrier email is required');
|
|
}
|
|
|
|
// Validate email format
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(this.carrierEmail)) {
|
|
throw new Error('Invalid carrier email format');
|
|
}
|
|
|
|
if (this.volumeCBM <= 0) {
|
|
throw new Error('Volume must be positive');
|
|
}
|
|
|
|
if (this.weightKG <= 0) {
|
|
throw new Error('Weight must be positive');
|
|
}
|
|
|
|
if (this.palletCount < 0) {
|
|
throw new Error('Pallet count cannot be negative');
|
|
}
|
|
|
|
if (this.priceUSD < 0 || this.priceEUR < 0) {
|
|
throw new Error('Price cannot be negative');
|
|
}
|
|
|
|
if (this.transitDays <= 0) {
|
|
throw new Error('Transit days must be positive');
|
|
}
|
|
|
|
if (!this.confirmationToken || this.confirmationToken.trim().length === 0) {
|
|
throw new Error('Confirmation token is required');
|
|
}
|
|
|
|
// Note: at least one document is required *at creation* (enforced in the
|
|
// create flow). A booking may end up with zero documents after the owner
|
|
// deletes them, so reconstitution must not fail here.
|
|
}
|
|
|
|
/**
|
|
* Apply commission to the booking
|
|
*
|
|
* @deprecated Commission has been replaced by a flat per-booking fee.
|
|
* Use {@link applyBookingFee} instead.
|
|
*/
|
|
applyCommission(ratePercent: number, baseAmountEur: number): void {
|
|
this.commissionRate = ratePercent;
|
|
this.commissionAmountEur = Math.round(baseAmountEur * ratePercent) / 100;
|
|
}
|
|
|
|
/**
|
|
* Apply the flat per-booking service fee (forfait par booking).
|
|
*
|
|
* Replaces the percentage-based commission: the amount is the subscription
|
|
* plan's fixed booking fee, so the percentage rate is cleared. A fee <= 0
|
|
* (e.g. Platinium "sur mesure") means no automatic charge.
|
|
*/
|
|
applyBookingFee(feeEur: number): void {
|
|
this.commissionRate = undefined;
|
|
this.commissionAmountEur = feeEur > 0 ? feeEur : 0;
|
|
}
|
|
|
|
/**
|
|
* Mark commission payment as completed → transition to PENDING
|
|
*
|
|
* @throws Error if booking is not in QUOTE status
|
|
*/
|
|
markPaymentCompleted(): void {
|
|
if (this.status !== CsvBookingStatus.QUOTE) {
|
|
throw new Error(
|
|
`Cannot mark payment completed for booking with status ${this.status}. Only QUOTE bookings can transition.`
|
|
);
|
|
}
|
|
|
|
this.status = CsvBookingStatus.PENDING;
|
|
}
|
|
|
|
/**
|
|
* Declare bank transfer → transition to PENDING_BANK_TRANSFER
|
|
* Called when user confirms they have sent the bank transfer
|
|
*
|
|
* @throws Error if booking is not in QUOTE status
|
|
*/
|
|
markBankTransferDeclared(): void {
|
|
if (this.status !== CsvBookingStatus.QUOTE) {
|
|
throw new Error(
|
|
`Cannot declare bank transfer for booking with status ${this.status}. Only QUOTE bookings can transition.`
|
|
);
|
|
}
|
|
|
|
this.status = CsvBookingStatus.PENDING_BANK_TRANSFER;
|
|
}
|
|
|
|
/**
|
|
* Admin validates bank transfer → transition to PENDING
|
|
* Called by admin once bank transfer has been received and verified
|
|
*
|
|
* @throws Error if booking is not in PENDING_BANK_TRANSFER status
|
|
*/
|
|
markBankTransferValidated(): void {
|
|
if (this.status !== CsvBookingStatus.PENDING_BANK_TRANSFER) {
|
|
throw new Error(
|
|
`Cannot validate bank transfer for booking with status ${this.status}. Only PENDING_BANK_TRANSFER bookings can transition.`
|
|
);
|
|
}
|
|
|
|
this.status = CsvBookingStatus.PENDING;
|
|
}
|
|
|
|
/**
|
|
* Accept the booking
|
|
*
|
|
* @throws Error if booking is not in PENDING status
|
|
*/
|
|
accept(): void {
|
|
if (this.status !== CsvBookingStatus.PENDING) {
|
|
throw new Error(
|
|
`Cannot accept booking with status ${this.status}. Only PENDING bookings can be accepted.`
|
|
);
|
|
}
|
|
|
|
if (this.isExpired()) {
|
|
throw new Error('Cannot accept expired booking');
|
|
}
|
|
|
|
this.status = CsvBookingStatus.ACCEPTED;
|
|
this.respondedAt = new Date();
|
|
}
|
|
|
|
/**
|
|
* Reject the booking
|
|
*
|
|
* @param reason Optional reason for rejection
|
|
* @throws Error if booking is not in PENDING status
|
|
*/
|
|
reject(reason?: string): void {
|
|
if (this.status !== CsvBookingStatus.PENDING) {
|
|
throw new Error(
|
|
`Cannot reject booking with status ${this.status}. Only PENDING bookings can be rejected.`
|
|
);
|
|
}
|
|
|
|
if (this.isExpired()) {
|
|
throw new Error('Cannot reject expired booking (already expired)');
|
|
}
|
|
|
|
this.status = CsvBookingStatus.REJECTED;
|
|
this.respondedAt = new Date();
|
|
if (reason) {
|
|
this.rejectionReason = reason;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Can this booking be deleted outright?
|
|
*
|
|
* Une reservation impayee n'engage personne : elle n'est pas partie chez le
|
|
* transporteur et ne porte aucune trace comptable. La supprimer est donc sans
|
|
* consequence, la ou une reservation payee doit rester tracable et ne peut
|
|
* qu'etre annulee.
|
|
*
|
|
* `PENDING_BANK_TRANSFER` est volontairement exclu : le virement declare peut
|
|
* etre en cours d'acheminement, et supprimer la reservation priverait
|
|
* l'administration de ce qu'elle doit rapprocher a sa reception. Etendre la
|
|
* regle a ce statut est une decision comptable, pas technique : il suffirait
|
|
* de l'ajouter a `DELETABLE_STATUSES`.
|
|
*/
|
|
isDeletable(): boolean {
|
|
return DELETABLE_STATUSES.includes(this.status);
|
|
}
|
|
|
|
/**
|
|
* Cancel the booking (by user)
|
|
*
|
|
* @throws Error if booking is already accepted/rejected
|
|
*/
|
|
cancel(): void {
|
|
if (this.status === CsvBookingStatus.ACCEPTED) {
|
|
throw new Error('Cannot cancel accepted booking. Contact carrier to cancel.');
|
|
}
|
|
|
|
if (this.status === CsvBookingStatus.REJECTED) {
|
|
throw new Error('Cannot cancel rejected booking');
|
|
}
|
|
|
|
if (this.status === CsvBookingStatus.CANCELLED) {
|
|
throw new Error('Booking is already cancelled');
|
|
}
|
|
|
|
this.status = CsvBookingStatus.CANCELLED;
|
|
this.respondedAt = new Date();
|
|
}
|
|
|
|
/**
|
|
* Edit the cargo details of a booking before it is paid.
|
|
*
|
|
* Only allowed while the booking is awaiting payment (QUOTE), i.e.
|
|
* before it is sent to the carrier. Carrier, route and price derive from the
|
|
* selected rate and are not editable here.
|
|
*
|
|
* @throws Error if the booking is not in QUOTE status or values are invalid
|
|
*/
|
|
editDetails(details: {
|
|
volumeCBM?: number;
|
|
weightKG?: number;
|
|
palletCount?: number;
|
|
notes?: string;
|
|
pricing?: {
|
|
priceUSD?: number;
|
|
priceEUR?: number;
|
|
primaryCurrency?: string;
|
|
freightTotal?: number;
|
|
freightCurrency?: string;
|
|
fobTotal?: number;
|
|
fobCurrency?: string;
|
|
};
|
|
}): void {
|
|
if (this.status !== CsvBookingStatus.QUOTE) {
|
|
throw new Error(
|
|
`Cannot edit booking with status ${this.status}. Only QUOTE bookings can be edited.`
|
|
);
|
|
}
|
|
|
|
if (details.volumeCBM !== undefined) {
|
|
if (details.volumeCBM <= 0) {
|
|
throw new Error('Volume must be positive');
|
|
}
|
|
this.volumeCBM = details.volumeCBM;
|
|
}
|
|
|
|
if (details.weightKG !== undefined) {
|
|
if (details.weightKG <= 0) {
|
|
throw new Error('Weight must be positive');
|
|
}
|
|
this.weightKG = details.weightKG;
|
|
}
|
|
|
|
if (details.palletCount !== undefined) {
|
|
if (details.palletCount < 0) {
|
|
throw new Error('Pallet count cannot be negative');
|
|
}
|
|
this.palletCount = details.palletCount;
|
|
}
|
|
|
|
if (details.notes !== undefined) {
|
|
this.notes = details.notes;
|
|
}
|
|
|
|
// Pricing is recomputed from the current rate when cargo details change.
|
|
const p = details.pricing;
|
|
if (p) {
|
|
if (p.priceUSD !== undefined) {
|
|
if (p.priceUSD < 0) throw new Error('Price cannot be negative');
|
|
this.priceUSD = p.priceUSD;
|
|
}
|
|
if (p.priceEUR !== undefined) {
|
|
if (p.priceEUR < 0) throw new Error('Price cannot be negative');
|
|
this.priceEUR = p.priceEUR;
|
|
}
|
|
if (p.primaryCurrency !== undefined) this.primaryCurrency = p.primaryCurrency;
|
|
if (p.freightTotal !== undefined) this.freightTotal = p.freightTotal;
|
|
if (p.freightCurrency !== undefined) this.freightCurrency = p.freightCurrency;
|
|
if (p.fobTotal !== undefined) this.fobTotal = p.fobTotal;
|
|
if (p.fobCurrency !== undefined) this.fobCurrency = p.fobCurrency;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Re-apply a full rate selection before payment: the user re-ran the search
|
|
* and picked a rate, so carrier, route, container, transit, cargo and price
|
|
* are all replaced. Only allowed while the booking is QUOTE.
|
|
*
|
|
* @throws Error if the booking is not QUOTE or values are invalid
|
|
*/
|
|
editFromRate(data: {
|
|
carrierName: string;
|
|
carrierEmail: string;
|
|
origin: PortCode;
|
|
destination: PortCode;
|
|
containerType: string;
|
|
transitDays: number;
|
|
volumeCBM: number;
|
|
weightKG: number;
|
|
palletCount: number;
|
|
priceUSD: number;
|
|
priceEUR: number;
|
|
primaryCurrency: string;
|
|
freightTotal?: number;
|
|
freightCurrency?: string;
|
|
fobTotal?: number;
|
|
fobCurrency?: string;
|
|
notes?: string;
|
|
options?: Record<string, boolean>;
|
|
}): void {
|
|
if (this.status !== CsvBookingStatus.QUOTE) {
|
|
throw new Error(
|
|
`Cannot edit booking with status ${this.status}. Only QUOTE bookings can be edited.`
|
|
);
|
|
}
|
|
if (!data.carrierName || data.carrierName.trim().length === 0) {
|
|
throw new Error('Carrier name is required');
|
|
}
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!data.carrierEmail || !emailRegex.test(data.carrierEmail)) {
|
|
throw new Error('Invalid carrier email format');
|
|
}
|
|
if (data.volumeCBM <= 0) throw new Error('Volume must be positive');
|
|
if (data.weightKG <= 0) throw new Error('Weight must be positive');
|
|
if (data.palletCount < 0) throw new Error('Pallet count cannot be negative');
|
|
if (data.transitDays <= 0) throw new Error('Transit days must be positive');
|
|
if (data.priceUSD < 0 || data.priceEUR < 0) throw new Error('Price cannot be negative');
|
|
|
|
this.carrierName = data.carrierName;
|
|
this.carrierEmail = data.carrierEmail;
|
|
this.origin = data.origin;
|
|
this.destination = data.destination;
|
|
this.containerType = data.containerType;
|
|
this.transitDays = data.transitDays;
|
|
this.volumeCBM = data.volumeCBM;
|
|
this.weightKG = data.weightKG;
|
|
this.palletCount = data.palletCount;
|
|
this.priceUSD = data.priceUSD;
|
|
this.priceEUR = data.priceEUR;
|
|
this.primaryCurrency = data.primaryCurrency;
|
|
this.freightTotal = data.freightTotal;
|
|
this.freightCurrency = data.freightCurrency;
|
|
this.fobTotal = data.fobTotal;
|
|
this.fobCurrency = data.fobCurrency;
|
|
if (data.notes !== undefined) this.notes = data.notes;
|
|
if (data.options !== undefined) this.options = data.options;
|
|
}
|
|
|
|
/**
|
|
* Un devis : la reservation est construite mais les frais de booking ne sont
|
|
* pas regles, donc rien n'est encore parti chez le transporteur.
|
|
*/
|
|
isQuote(): boolean {
|
|
return this.status === CsvBookingStatus.QUOTE;
|
|
}
|
|
|
|
isExpired(): boolean {
|
|
if (this.status !== CsvBookingStatus.PENDING) {
|
|
return false;
|
|
}
|
|
|
|
const expirationDate = new Date(this.requestedAt);
|
|
expirationDate.setDate(expirationDate.getDate() + 7);
|
|
|
|
return new Date() > expirationDate;
|
|
}
|
|
|
|
/**
|
|
* Check if booking is still pending (awaiting response)
|
|
*/
|
|
isPending(): boolean {
|
|
return this.status === CsvBookingStatus.PENDING && !this.isExpired();
|
|
}
|
|
|
|
/**
|
|
* Check if booking was accepted
|
|
*/
|
|
isAccepted(): boolean {
|
|
return this.status === CsvBookingStatus.ACCEPTED;
|
|
}
|
|
|
|
/**
|
|
* Check if booking was rejected
|
|
*/
|
|
isRejected(): boolean {
|
|
return this.status === CsvBookingStatus.REJECTED;
|
|
}
|
|
|
|
/**
|
|
* Check if booking was cancelled
|
|
*/
|
|
isCancelled(): boolean {
|
|
return this.status === CsvBookingStatus.CANCELLED;
|
|
}
|
|
|
|
/**
|
|
* Get route description (origin → destination)
|
|
*/
|
|
getRouteDescription(): string {
|
|
return `${this.origin.getValue()} → ${this.destination.getValue()}`;
|
|
}
|
|
|
|
/**
|
|
* Get booking summary
|
|
*/
|
|
getSummary(): string {
|
|
return `CSV Booking ${this.id}: ${this.carrierName} - ${this.getRouteDescription()} (${this.status})`;
|
|
}
|
|
|
|
/**
|
|
* Get price in specified currency
|
|
*/
|
|
getPriceInCurrency(currency: 'USD' | 'EUR'): number {
|
|
return currency === 'USD' ? this.priceUSD : this.priceEUR;
|
|
}
|
|
|
|
/**
|
|
* Get days until expiration (negative if expired)
|
|
*/
|
|
getDaysUntilExpiration(): number {
|
|
if (this.status !== CsvBookingStatus.PENDING) {
|
|
return 0;
|
|
}
|
|
|
|
const expirationDate = new Date(this.requestedAt);
|
|
expirationDate.setDate(expirationDate.getDate() + 7);
|
|
|
|
const now = new Date();
|
|
const diffTime = expirationDate.getTime() - now.getTime();
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
return diffDays;
|
|
}
|
|
|
|
/**
|
|
* Check if booking has a specific document type
|
|
*/
|
|
hasDocumentType(type: DocumentType): boolean {
|
|
return this.documents.some(doc => doc.type === type);
|
|
}
|
|
|
|
/**
|
|
* Get documents by type
|
|
*/
|
|
getDocumentsByType(type: DocumentType): CsvBookingDocument[] {
|
|
return this.documents.filter(doc => doc.type === type);
|
|
}
|
|
|
|
/**
|
|
* Check if all required documents are present
|
|
*/
|
|
hasAllRequiredDocuments(): boolean {
|
|
const requiredTypes = [
|
|
DocumentType.BILL_OF_LADING,
|
|
DocumentType.PACKING_LIST,
|
|
DocumentType.COMMERCIAL_INVOICE,
|
|
];
|
|
|
|
return requiredTypes.every(type => this.hasDocumentType(type));
|
|
}
|
|
|
|
/**
|
|
* Get response time in hours (if responded)
|
|
*/
|
|
getResponseTimeHours(): number | null {
|
|
if (!this.respondedAt) {
|
|
return null;
|
|
}
|
|
|
|
const diffTime = this.respondedAt.getTime() - this.requestedAt.getTime();
|
|
const diffHours = diffTime / (1000 * 60 * 60);
|
|
|
|
return Math.round(diffHours * 100) / 100; // Round to 2 decimals
|
|
}
|
|
|
|
toString(): string {
|
|
return this.getSummary();
|
|
}
|
|
|
|
/**
|
|
* Create a CsvBooking from persisted data (skips document validation)
|
|
*
|
|
* Use this when loading from database where bookings might have been created
|
|
* before document requirement was enforced, or documents were lost.
|
|
*/
|
|
static fromPersistence(
|
|
id: string,
|
|
userId: string,
|
|
organizationId: string,
|
|
carrierName: string,
|
|
carrierEmail: string,
|
|
origin: PortCode,
|
|
destination: PortCode,
|
|
volumeCBM: number,
|
|
weightKG: number,
|
|
palletCount: number,
|
|
priceUSD: number,
|
|
priceEUR: number,
|
|
primaryCurrency: string,
|
|
transitDays: number,
|
|
containerType: string,
|
|
status: CsvBookingStatus,
|
|
documents: CsvBookingDocument[],
|
|
confirmationToken: string,
|
|
requestedAt: Date,
|
|
respondedAt?: Date,
|
|
notes?: string,
|
|
rejectionReason?: string,
|
|
bookingNumber?: string,
|
|
commissionRate?: number,
|
|
commissionAmountEur?: number,
|
|
stripePaymentIntentId?: string,
|
|
freightTotal?: number,
|
|
freightCurrency?: string,
|
|
fobTotal?: number,
|
|
fobCurrency?: string,
|
|
options?: Record<string, boolean>
|
|
): CsvBooking {
|
|
// Create instance without calling constructor validation
|
|
const booking = Object.create(CsvBooking.prototype);
|
|
|
|
// Assign all properties directly
|
|
booking.id = id;
|
|
booking.userId = userId;
|
|
booking.organizationId = organizationId;
|
|
booking.carrierName = carrierName;
|
|
booking.carrierEmail = carrierEmail;
|
|
booking.origin = origin;
|
|
booking.destination = destination;
|
|
booking.volumeCBM = volumeCBM;
|
|
booking.weightKG = weightKG;
|
|
booking.palletCount = palletCount;
|
|
booking.priceUSD = priceUSD;
|
|
booking.priceEUR = priceEUR;
|
|
booking.primaryCurrency = primaryCurrency;
|
|
booking.transitDays = transitDays;
|
|
booking.containerType = containerType;
|
|
booking.status = status;
|
|
booking.documents = documents || [];
|
|
booking.confirmationToken = confirmationToken;
|
|
booking.requestedAt = requestedAt;
|
|
booking.respondedAt = respondedAt;
|
|
booking.notes = notes;
|
|
booking.rejectionReason = rejectionReason;
|
|
booking.bookingNumber = bookingNumber;
|
|
booking.commissionRate = commissionRate;
|
|
booking.commissionAmountEur = commissionAmountEur;
|
|
booking.stripePaymentIntentId = stripePaymentIntentId;
|
|
booking.freightTotal = freightTotal;
|
|
booking.freightCurrency = freightCurrency;
|
|
booking.fobTotal = fobTotal;
|
|
booking.fobCurrency = fobCurrency;
|
|
booking.options = options ?? {};
|
|
|
|
return booking;
|
|
}
|
|
}
|