feat: implement shipping line bookings management

- Add ShippingLineBookingsPage for listing and managing shipping line bookings.
- Create ShippingLineDocumentsModal for document uploads related to bookings.
- Introduce ShippingLineInitiateModal for initiating new shipping line bookings.
- Implement booking document state management with booking-doc-state utility.
- Add shipping line bookings service for API interactions.
- Update index to export new components and services.
- Enhance types for freight to include shipping line credits.
This commit is contained in:
marshalyordanos
2026-08-13 15:54:40 +03:00
parent 9aae132dd4
commit 9fff469ffa
50 changed files with 4485 additions and 77 deletions

View File

@@ -74,6 +74,16 @@ export interface BookingEvaluationInput {
isGovernment?: boolean;
allowConsolidation?: boolean;
shippingLineId?: string | null;
/**
* The shipping line that OWNS this booking (`bookings.shipping_line_company_id`),
* when it is a shipping-line booking rather than a customer one. Such a booking
* prices exclusively off that line's own rates — see {@link ratesForOwner}.
*
* Not to be confused with `shippingLineId` above, which is cargo metadata
* naming the carrier that physically moves the goods and only feeds the
* SHIPPING_LINE double-handling trigger.
*/
shippingLineCompanyId?: string | null;
/**
* The booking's rail leg. Import overweight derives its per-ton price from
* this route's own container freight rate, so the engine needs the yards.
@@ -282,7 +292,10 @@ export class RuleEngineService {
// scope) must contribute exactly ONE line. Duplicate LIVE rate rows — e.g.
// from a non-idempotent seeder — would otherwise repeat the same surcharge
// many times and inflate the total, so we collapse them to one row each.
const liveRates = await this.ratesRepo.findLiveRates();
const liveRates = this.ratesForOwner(
await this.ratesRepo.findLiveRates(),
input.shippingLineCompanyId,
);
const surchargeRates = this.dedupeRatesBySignature(
liveRates.filter((r) => r.trigger && r.trigger !== 'ALWAYS'),
);
@@ -812,6 +825,28 @@ export class RuleEngineService {
return rate.rateType ?? rate.trigger;
}
/**
* Narrow the LIVE rate pool to the ones this booking's owner may price off.
*
* A customer booking sees only standard rates (no owner) — a shipping line's
* negotiated price must never leak into a customer quote. A shipping-line
* booking sees only that line's own rates: line rates OVERRIDE the standard
* ones rather than stacking on them, and the standard rate is deliberately
* NOT a fallback, so a lane the line has no rate for hard-blocks downstream
* (base freight already blocks on "no rate for this route") instead of
* quietly billing the line at the customer price.
*
* Filtering once, here, is what makes the override apply uniformly: every
* downstream lookup (base freight, derived overweight, empty return, lashing,
* fuel, and the additive surcharges) reads from this same pool, so none of
* them needs its own owner check.
*/
private ratesForOwner(rates: Rate[], shippingLineCompanyId?: string | null): Rate[] {
return shippingLineCompanyId
? rates.filter((r) => r.shippingLineCompanyId === shippingLineCompanyId)
: rates.filter((r) => !r.shippingLineCompanyId);
}
/**
* Collapse rates that describe the same charge to a single representative.
*