xpeditis2.0/apps/backend/src/domain/entities/csv-rate.entity.ts
2026-05-12 01:11:04 +02:00

132 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}