132 lines
3.9 KiB
TypeScript
132 lines
3.9 KiB
TypeScript
import { PortCode } from '../value-objects/port-code.vo';
|
||
import { ContainerType } from '../value-objects/container-type.vo';
|
||
import { DateRange } from '../value-objects/date-range.vo';
|
||
|
||
export type DgSurchargeValue = number | 'ON REQUEST' | 'NOT ACCEPTED';
|
||
export type HandlingUnit = 'W' | 'UP'; // W = tonne revenue (max CBM/T), UP = per CBM
|
||
export type FrequencyType = 'Weekly' | 'Bi-Weekly' | 'Bi-Monthly' | 'Monthly';
|
||
|
||
export interface FreightPricing {
|
||
freightCurrency: string;
|
||
freightRatePerCBM: number; // 0.0 = included/to negotiate
|
||
freightMinimum: number;
|
||
}
|
||
|
||
export interface FobCharges {
|
||
fobCurrency: string;
|
||
fobDocumentation: number;
|
||
fobISPS: number;
|
||
fobHandling: number;
|
||
fobHandlingUnit: HandlingUnit;
|
||
fobHandlingMinimum: number;
|
||
fobSolas: number;
|
||
fobCustoms: number;
|
||
fobAMS_ACI: number;
|
||
fobISF5: number;
|
||
fobDGAdmin: number; // Only if DG shipment
|
||
}
|
||
|
||
export interface DgSurchargeInfo {
|
||
dgSurchargeCurrency: string;
|
||
dgSurchargeRate: DgSurchargeValue;
|
||
dgSurchargeUnit: 'UP' | 'LS' | '%'; // per CBM, lump sum, or percentage
|
||
dgSurchargeMin: DgSurchargeValue;
|
||
}
|
||
|
||
/**
|
||
* CsvRate — Shipping rate from a consolidator CSV file.
|
||
*
|
||
* Business Rules:
|
||
* - Route matching uses originCode + destinationCode (UN/LOCODE)
|
||
* - Price = max(freightRatePerCBM×V, freightMinimum) + FOB fixed + handling
|
||
* - FOB and freight may be in different currencies
|
||
* - DG surcharge applies only when hasDangerousGoods = true
|
||
*/
|
||
export class CsvRate {
|
||
constructor(
|
||
// Supplier identity
|
||
public readonly companyName: string,
|
||
public readonly companyEmail: string,
|
||
// Route geography
|
||
public readonly originCFS: string,
|
||
public readonly originCode: PortCode,
|
||
public readonly portOfLoading: string,
|
||
public readonly routing: string,
|
||
public readonly destinationCFS: string,
|
||
public readonly destinationCode: PortCode,
|
||
public readonly destinationCountry: string,
|
||
// Container
|
||
public readonly containerType: ContainerType,
|
||
// Pricing
|
||
public readonly freight: FreightPricing,
|
||
public readonly fob: FobCharges,
|
||
public readonly dgSurcharge: DgSurchargeInfo,
|
||
// Metadata
|
||
public readonly remarks: string,
|
||
public readonly frequency: FrequencyType,
|
||
public readonly transitDays: number,
|
||
public readonly validity: DateRange
|
||
) {
|
||
this.validate();
|
||
}
|
||
|
||
private validate(): void {
|
||
if (!this.companyName?.trim()) throw new Error('Company name is required');
|
||
if (!this.companyEmail?.trim()) throw new Error('Company email is required');
|
||
if (this.transitDays <= 0) throw new Error('Transit days must be positive');
|
||
if (this.freight.freightMinimum < 0) throw new Error('Freight minimum cannot be negative');
|
||
if (this.fob.fobHandling < 0) throw new Error('FOB handling cannot be negative');
|
||
}
|
||
|
||
isValidForDate(date: Date): boolean {
|
||
return this.validity.contains(date);
|
||
}
|
||
|
||
isCurrentlyValid(): boolean {
|
||
return this.validity.isCurrentRange();
|
||
}
|
||
|
||
matchesRoute(origin: PortCode, destination: PortCode): boolean {
|
||
return this.originCode.equals(origin) && this.destinationCode.equals(destination);
|
||
}
|
||
|
||
isDgAccepted(): boolean {
|
||
return this.dgSurcharge.dgSurchargeRate !== 'NOT ACCEPTED';
|
||
}
|
||
|
||
isDgOnRequest(): boolean {
|
||
return this.dgSurcharge.dgSurchargeRate === 'ON REQUEST';
|
||
}
|
||
|
||
isDirectRoute(): boolean {
|
||
return this.routing.trim().toLowerCase() === 'direct';
|
||
}
|
||
|
||
getFrequencyScore(): number {
|
||
switch (this.frequency) {
|
||
case 'Weekly':
|
||
return 4;
|
||
case 'Bi-Weekly':
|
||
return 3;
|
||
case 'Bi-Monthly':
|
||
return 2;
|
||
case 'Monthly':
|
||
return 1;
|
||
default:
|
||
return 2;
|
||
}
|
||
}
|
||
|
||
getRouteDescription(): string {
|
||
return `${this.originCode.getValue()} → ${this.destinationCode.getValue()}`;
|
||
}
|
||
|
||
getSummary(): string {
|
||
return `${this.companyName}: ${this.getRouteDescription()} (${this.containerType.getValue()})`;
|
||
}
|
||
|
||
toString(): string {
|
||
return this.getSummary();
|
||
}
|
||
}
|