mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
Merge pull request #963 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
This commit is contained in:
@@ -200,7 +200,7 @@ export class BookingPricingService {
|
||||
// route's container freight, never a frozen OVERWEIGHT_PER_TON value.
|
||||
const frozen = isDerived
|
||||
? null
|
||||
: this.frozenRateByCode(frozenRates, mod.surchargeCode, paymentCurrency);
|
||||
: this.frozenRateByCode(frozenRates, mod.surchargeCode, paymentCurrency, usdToEtb);
|
||||
const unitAmount = frozen
|
||||
? Number(frozen.unitPrice)
|
||||
: isEtbBooking
|
||||
@@ -547,14 +547,15 @@ export class BookingPricingService {
|
||||
booking.originYardId,
|
||||
booking.destinationYardId,
|
||||
);
|
||||
// H15: frozen contract rate for this container size, when present — its
|
||||
// unitPrice is already in the booking currency (no USD→currency convert).
|
||||
// It also stands on its own: a contract line prices off the agreed rate
|
||||
// even when nobody configured a live rate for this leg + type yet.
|
||||
// H15: frozen contract rate for this container size, when present —
|
||||
// converted into the booking currency by frozenRateForContainer. It also
|
||||
// stands on its own: a contract line prices off the agreed rate even when
|
||||
// nobody configured a live rate for this leg + type yet.
|
||||
const frozen = await this.frozenRateForContainer(
|
||||
frozenRates,
|
||||
container.containerTypeId,
|
||||
paymentCurrency,
|
||||
usdToEtb,
|
||||
);
|
||||
const label = await this.containerTypeLabel(container.containerTypeId);
|
||||
if (!rate && !frozen) {
|
||||
@@ -632,7 +633,7 @@ export class BookingPricingService {
|
||||
const unitUsd = Number(fallback.rateValue);
|
||||
// H15: bulk freight uses the frozen BULK_FREIGHT snapshot when present.
|
||||
const frozen = isBulk
|
||||
? this.frozenRateByCode(frozenRates, 'BULK_FREIGHT', paymentCurrency)
|
||||
? this.frozenRateByCode(frozenRates, 'BULK_FREIGHT', paymentCurrency, usdToEtb)
|
||||
: null;
|
||||
let amount: number;
|
||||
let unitAmount: number;
|
||||
@@ -744,12 +745,13 @@ export class BookingPricingService {
|
||||
break;
|
||||
}
|
||||
|
||||
// H15: frozen mile rate (already in booking currency) when the contract
|
||||
// has one; else the live USD rate converted as before.
|
||||
// H15: frozen mile rate (converted into the booking currency) when the
|
||||
// contract has one; else the live USD rate converted as before.
|
||||
const frozen = this.frozenRateByCode(
|
||||
frozenRates,
|
||||
leg.rateType,
|
||||
paymentCurrency,
|
||||
usdToEtb,
|
||||
);
|
||||
let amount: number;
|
||||
let unitAmount: number;
|
||||
@@ -908,20 +910,45 @@ export class BookingPricingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* The frozen snapshot for a rate code, or null when there is none, its price
|
||||
* is negative, or it is in a different currency than the booking (in which
|
||||
* case the live-rate path is safer than a mis-converted frozen price).
|
||||
* The frozen snapshot for a rate code, expressed in the BOOKING's currency.
|
||||
*
|
||||
* A contract quotes in USD and freezes USD unit prices; the customer chooses
|
||||
* the billing currency per booking. So a currency mismatch is the normal case
|
||||
* now, not an error — the snapshot is converted rather than discarded. (It
|
||||
* previously returned null on mismatch, which silently dropped the agreed
|
||||
* contract price and re-priced the booking at whatever the live rate had
|
||||
* drifted to.) Grandfathered ETB contracts convert the other way for the same
|
||||
* reason.
|
||||
*
|
||||
* Returns null only when there is no snapshot or its price is unusable.
|
||||
*/
|
||||
private frozenRateByCode(
|
||||
frozenRates: Map<string, ContractRateSnapshot> | null,
|
||||
code: string,
|
||||
bookingCurrency: string,
|
||||
usdToEtb: number,
|
||||
): ContractRateSnapshot | null {
|
||||
const snap = frozenRates?.get(code);
|
||||
if (!snap) return null;
|
||||
if (snap.currency !== bookingCurrency) return null;
|
||||
if (!(Number(snap.unitPrice) >= 0)) return null;
|
||||
return snap;
|
||||
const unitPrice = Number(snap.unitPrice);
|
||||
if (!(unitPrice >= 0)) return null;
|
||||
if (snap.currency === bookingCurrency) return snap;
|
||||
|
||||
// Only USD <-> ETB exist; a rate of 0/NaN would silently zero the price.
|
||||
if (!(usdToEtb > 0)) return null;
|
||||
const converted =
|
||||
snap.currency === 'USD' && bookingCurrency === 'ETB'
|
||||
? Math.round(unitPrice * usdToEtb)
|
||||
: snap.currency === 'ETB' && bookingCurrency === 'USD'
|
||||
? unitPrice / usdToEtb
|
||||
: null;
|
||||
if (converted == null) return null;
|
||||
|
||||
// A copy — the snapshot rows are shared across the pricing pass.
|
||||
return Object.assign(Object.create(Object.getPrototypeOf(snap)), snap, {
|
||||
unitPrice: converted,
|
||||
currency: bookingCurrency,
|
||||
}) as ContractRateSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -933,6 +960,7 @@ export class BookingPricingService {
|
||||
frozenRates: Map<string, ContractRateSnapshot> | null,
|
||||
containerTypeId: string,
|
||||
bookingCurrency: string,
|
||||
usdToEtb: number,
|
||||
): Promise<ContractRateSnapshot | null> {
|
||||
if (!frozenRates) return null;
|
||||
let sizeFt: number | null = null;
|
||||
@@ -942,7 +970,7 @@ export class BookingPricingService {
|
||||
return null;
|
||||
}
|
||||
if (!sizeFt) return null;
|
||||
return this.frozenRateByCode(frozenRates, `CONTAINER_${sizeFt}FT`, bookingCurrency);
|
||||
return this.frozenRateByCode(frozenRates, `CONTAINER_${sizeFt}FT`, bookingCurrency, usdToEtb);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -985,7 +1013,7 @@ export class BookingPricingService {
|
||||
const hasPerSizeSnapshot =
|
||||
frozenRates?.has('CUSTOMS_CLEARANCE_20FT') ||
|
||||
frozenRates?.has('CUSTOMS_CLEARANCE_40FT');
|
||||
const legacyFlat = this.frozenRateByCode(frozenRates, 'CUSTOMS_CLEARANCE', currency);
|
||||
const legacyFlat = this.frozenRateByCode(frozenRates, 'CUSTOMS_CLEARANCE', currency, usdToEtb);
|
||||
if (legacyFlat && !hasPerSizeSnapshot) {
|
||||
const amount = Number(legacyFlat.unitPrice);
|
||||
if (amount > 0) {
|
||||
@@ -1014,7 +1042,7 @@ export class BookingPricingService {
|
||||
// unknown type — falls through to the live per-type lookup below
|
||||
}
|
||||
const frozen = sizeFt
|
||||
? this.frozenRateByCode(frozenRates, `CUSTOMS_CLEARANCE_${sizeFt}FT`, currency)
|
||||
? this.frozenRateByCode(frozenRates, `CUSTOMS_CLEARANCE_${sizeFt}FT`, currency, usdToEtb)
|
||||
: null;
|
||||
const live = onLeg.find((r) => r.containerTypeId === bc.containerTypeId);
|
||||
if (!frozen && !live) {
|
||||
@@ -1049,7 +1077,7 @@ export class BookingPricingService {
|
||||
// flat snapshot share the CUSTOMS_CLEARANCE code; both are the agreed fee.
|
||||
// Live lookup: the rate scoped to the booking's commodity wins; a
|
||||
// commodity-less rate (legacy) is the catch-all fallback.
|
||||
const frozen = this.frozenRateByCode(frozenRates, 'CUSTOMS_CLEARANCE', currency);
|
||||
const frozen = this.frozenRateByCode(frozenRates, 'CUSTOMS_CLEARANCE', currency, usdToEtb);
|
||||
const live =
|
||||
(booking.cargoTypeId
|
||||
? onLeg.find(
|
||||
|
||||
@@ -916,14 +916,15 @@ export class BookingsController {
|
||||
async uploadBookingDeliveryOrder(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body('vesselDepartureDate') vesselDepartureDate: string | undefined,
|
||||
@Body('vesselArrivalDate') vesselArrivalDate: string | undefined,
|
||||
@Body('doCollectedDate') doCollectedDate: string | undefined,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
const booking = await this.bookingClearanceService.uploadDeliveryOrder(
|
||||
id,
|
||||
file,
|
||||
resolveAuthUserId(user),
|
||||
vesselDepartureDate,
|
||||
{ vesselArrivalDate, doCollectedDate },
|
||||
);
|
||||
return this.transitionService.enrichBookingResponse(booking);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,9 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
'booking.files',
|
||||
FileRecord,
|
||||
'file',
|
||||
"file.resource_id = booking.id AND file.resource = 'bookings'",
|
||||
// Superseded versions are soft-deleted, not dropped — keep them out of
|
||||
// the live file list (a manual join condition is not filtered for us).
|
||||
"file.resource_id = booking.id AND file.resource = 'bookings' AND file.deleted_at IS NULL",
|
||||
)
|
||||
.getOne();
|
||||
|
||||
|
||||
@@ -540,6 +540,14 @@ export class Booking extends BaseEntity {
|
||||
@Column({ name: 'vessel_departure_date', type: 'date', nullable: true })
|
||||
vesselDepartureDate?: string | null;
|
||||
|
||||
/** Import DO: when the vessel arrived in Djibouti. Required on DO upload. */
|
||||
@Column({ name: 'vessel_arrival_date', type: 'date', nullable: true })
|
||||
vesselArrivalDate?: string | null;
|
||||
|
||||
/** Import DO: when GL Djibouti collected the DO. Required on DO upload. */
|
||||
@Column({ name: 'do_collected_date', type: 'date', nullable: true })
|
||||
doCollectedDate?: string | null;
|
||||
|
||||
@Column({ name: 'ro_amendment_requested_at', type: 'timestamptz', nullable: true })
|
||||
roAmendmentRequestedAt?: Date | null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user