mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
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:
@@ -527,3 +527,145 @@ describe('RuleEngineService — fuel surcharge (per lane + cargo type)', () => {
|
||||
expect(fuelMods(result)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RuleEngineService — shipping-line rates override the standard ones', () => {
|
||||
const LINE = 'slc-msc';
|
||||
|
||||
/** Standard customer container-import rate on the lane. */
|
||||
const standardBase: Rate = {
|
||||
id: 'rate-standard-20',
|
||||
rateType: 'CONTAINER_IMPORT',
|
||||
trigger: 'ALWAYS',
|
||||
rateValue: 1000,
|
||||
rateUnit: 'PER_CONTAINER',
|
||||
currency: 'USD',
|
||||
status: 'LIVE',
|
||||
containerTypeId: 'ct-20',
|
||||
cargoTypeId: null,
|
||||
shippingLineCompanyId: null,
|
||||
originYardId: 'yard-dj',
|
||||
destinationYardId: 'yard-adama',
|
||||
} as Rate;
|
||||
|
||||
/** The same lane, priced for one shipping line. */
|
||||
const lineBase: Rate = {
|
||||
...standardBase,
|
||||
id: 'rate-line-20',
|
||||
rateValue: 1200,
|
||||
shippingLineCompanyId: LINE,
|
||||
} as Rate;
|
||||
|
||||
const standardHazard: Rate = {
|
||||
id: 'rate-hazard-standard',
|
||||
rateType: 'HAZARD_SURCHARGE',
|
||||
trigger: 'HAZARDOUS',
|
||||
rateValue: 50,
|
||||
rateUnit: 'PER_CONTAINER',
|
||||
currency: 'USD',
|
||||
status: 'LIVE',
|
||||
containerTypeId: null,
|
||||
cargoTypeId: null,
|
||||
shippingLineCompanyId: null,
|
||||
} as Rate;
|
||||
|
||||
const lineHazard: Rate = {
|
||||
...standardHazard,
|
||||
id: 'rate-hazard-line',
|
||||
rateValue: 80,
|
||||
shippingLineCompanyId: LINE,
|
||||
} as Rate;
|
||||
|
||||
const buildService = (rates: Rate[]) =>
|
||||
new RuleEngineService(
|
||||
{ findById: jest.fn().mockResolvedValue(null) } as never,
|
||||
{ findById: jest.fn().mockResolvedValue(null) } as never,
|
||||
{
|
||||
findActiveByContainerTypeId: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: 'wlr-20', maxVgmTons: 20, maxCapacityTons: null }]),
|
||||
} as never,
|
||||
{ findAllActive: jest.fn().mockResolvedValue([]) } as never,
|
||||
{ findLiveRates: jest.fn().mockResolvedValue(rates) } as never,
|
||||
{ findById: jest.fn().mockResolvedValue(null) } as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
// One 20ft at 25 t against a 20 t limit → 5 t excess.
|
||||
const bookingInput = (
|
||||
overrides: Partial<BookingEvaluationInput> = {},
|
||||
): BookingEvaluationInput => ({
|
||||
serviceTypeId: 'svc-1',
|
||||
paymentCurrency: 'USD',
|
||||
tradeDirection: 'IMPORT',
|
||||
isHazardous: false,
|
||||
totalWagons: 1,
|
||||
originYardId: 'yard-dj',
|
||||
destinationYardId: 'yard-adama',
|
||||
containers: [
|
||||
{ containerTypeId: 'ct-20', quantity: 1, vgmPerUnitTons: 25, totalVgmTons: 25 },
|
||||
],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const overweightOf = (result: { appliedModifiers: Array<{ surchargeCode: string }> }) =>
|
||||
result.appliedModifiers.filter((m) => m.surchargeCode === 'OVERWEIGHT_PER_TON');
|
||||
|
||||
it('derives a line booking\'s overweight from the LINE\'s base rate, not the standard one', async () => {
|
||||
const result = await buildService([standardBase, lineBase]).evaluate(
|
||||
bookingInput({ shippingLineCompanyId: LINE }),
|
||||
);
|
||||
const ow = overweightOf(result);
|
||||
expect(ow).toHaveLength(1);
|
||||
// The line's 1200 / (2 × 20) = 30 USD/t, not the standard 1000 → 25 USD/t.
|
||||
expect(ow[0]).toMatchObject({
|
||||
rateId: lineBase.id,
|
||||
unitPriceUsd: 30,
|
||||
calculatedAmount: 150,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a customer booking on the standard rate even when a line rate exists', async () => {
|
||||
const result = await buildService([standardBase, lineBase]).evaluate(bookingInput());
|
||||
const ow = overweightOf(result);
|
||||
expect(ow).toHaveLength(1);
|
||||
expect(ow[0]).toMatchObject({
|
||||
rateId: standardBase.id,
|
||||
unitPriceUsd: 25,
|
||||
calculatedAmount: 125,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fall back to the standard rate when the line has none for the lane', async () => {
|
||||
const result = await buildService([standardBase]).evaluate(
|
||||
bookingInput({ shippingLineCompanyId: LINE }),
|
||||
);
|
||||
// No line rate on the lane → nothing to derive from. Base freight is what
|
||||
// hard-blocks the booking; the standard 1000 must never be borrowed here.
|
||||
expect(overweightOf(result)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('bills the line\'s own surcharge and never the standard one alongside it', async () => {
|
||||
const result = await buildService([
|
||||
standardBase,
|
||||
lineBase,
|
||||
standardHazard,
|
||||
lineHazard,
|
||||
]).evaluate(bookingInput({ shippingLineCompanyId: LINE, isHazardous: true }));
|
||||
|
||||
const hazard = result.appliedModifiers.filter(
|
||||
(m) => m.surchargeCode === 'HAZARD_SURCHARGE',
|
||||
);
|
||||
expect(hazard).toHaveLength(1);
|
||||
expect(hazard[0]).toMatchObject({ rateId: lineHazard.id, calculatedAmount: 80 });
|
||||
});
|
||||
|
||||
it('hard-blocks a requested service the line has no surcharge rate for', async () => {
|
||||
const result = await buildService([standardBase, lineBase, standardHazard]).evaluate(
|
||||
bookingInput({ shippingLineCompanyId: LINE, isHazardous: true }),
|
||||
);
|
||||
// The standard hazard rate exists but belongs to customers, so the line's
|
||||
// hazardous booking must block rather than borrow it.
|
||||
expect(result.hardBlocked).toHaveLength(1);
|
||||
expect(result.hardBlocked[0]).toContain('hazardous');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user