mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-04 02:03:39 +00:00
The customer truck card on the booking's Logistics tab is now usable for every cargo type, with an Excel template and bulk upload that match what the API enforces. Excel template and bulk upload - The template is built per booking. Container bookings get the booking's own containers with their sizes on a reference sheet and sample rows paired 20ft+20ft; bulk (PER_TON) bookings get a Planned Tons column; counted cargo (PER_ITEM: machinery, RoRo vehicles) gets Planned Quantity. - Parsing validates the whole file before anything is posted: plate, driver, truck type, ISO container numbers, containers on the booking, duplicates across rows, containers already on a truck, and the capacity rule (one 40ft alone, or two 20ft). Errors quote the Excel row number. - The bulk DTO reuses AddCustomerTruckDto instead of a drifted copy that lacked plannedTons/plannedQuantity, so bulk cargo can be uploaded at all. - A partial failure is reported per row in the modal instead of closing it as if every truck had been created. Capacity rule in the form - The container picker shows sizes and stops offering a second container once a 40ft is picked, or a 40ft once a 20ft is picked. - PER_ITEM cargo commits by item count; tonnage becomes optional. Shared vocabulary - CUSTOMER_TRUCK_TYPES and ISO_CONTAINER_NUMBER move to @edr/types so the API validators, the dropdown and the template read one list. When assignment is open - The card always renders and states why assignment is closed (EDR haulage, unpaid, train not arrived, cargo already loaded) rather than vanishing. - Self-haul is blocked only once EDR has committed to the road leg: an approved last-mile request or an existing last-mile leg. A delivery address whose request is still awaiting confirmation, submitted or rejected no longer blocks the customer from bringing their own truck. Collection on exports has no approval step and still blocks as before. Both the multi-truck service and the legacy single-truck path read the same SQL fragment (edrHaulsThisBooking), and the portal applies the same rule with a notice that assigning a truck makes the pending request unapprovable. The last-mile side already refuses to approve a booking carrying a customer truck, so the two paths stay mutually exclusive. Verified: EXPLAIN on the new SQL against edr_dev, type-check clean for freight-api and portal, 12 util specs pass (5 new). Backoffice type-check fails only in pre-existing user-management files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import { edrHaulsThisBooking, usesEdrMileService } from './mile-haulage.util';
|
|
|
|
/**
|
|
* The road legs are chosen on the contract and copied onto the booking. EDR
|
|
* haulage and a customer's own truck are alternatives, so this one answer gates
|
|
* both sides — the customer-truck guard and the mile-queue guard.
|
|
*/
|
|
describe('usesEdrMileService', () => {
|
|
const booking = (over: Partial<Parameters<typeof usesEdrMileService>[0]> = {}) => ({
|
|
tradeDirection: 'IMPORT',
|
|
firstMile: null,
|
|
lastMile: null,
|
|
...over,
|
|
});
|
|
|
|
it('an import that chose delivery uses EDR haulage', () => {
|
|
expect(usesEdrMileService(booking({ lastMile: 'Bole, Addis Ababa' }))).toBe(true);
|
|
});
|
|
|
|
it('an import that chose nothing does not', () => {
|
|
expect(usesEdrMileService(booking())).toBe(false);
|
|
});
|
|
|
|
it('ignores the pickup address on an import — collection is the export leg', () => {
|
|
expect(usesEdrMileService(booking({ firstMile: 'Modjo' }))).toBe(false);
|
|
});
|
|
|
|
it('an export that chose collection uses EDR haulage', () => {
|
|
expect(usesEdrMileService(booking({ tradeDirection: 'EXPORT', firstMile: 'Modjo' }))).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('ignores the delivery address on an export — delivery is the import leg', () => {
|
|
expect(usesEdrMileService(booking({ tradeDirection: 'EXPORT', lastMile: 'Djibouti' }))).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('a domestic booking counts either leg', () => {
|
|
expect(usesEdrMileService(booking({ tradeDirection: 'DOMESTIC', firstMile: 'Adama' }))).toBe(
|
|
true,
|
|
);
|
|
expect(usesEdrMileService(booking({ tradeDirection: 'DOMESTIC', lastMile: 'Dire Dawa' }))).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('treats a whitespace-only address as no choice', () => {
|
|
expect(usesEdrMileService(booking({ lastMile: ' ' }))).toBe(false);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Self-haul is closed only once EDR has committed to the leg. Delivery chosen
|
|
* on the contract is a request the chief still has to approve; collection has
|
|
* no approval step.
|
|
*/
|
|
describe('edrHaulsThisBooking', () => {
|
|
const booking = (over: Partial<Parameters<typeof edrHaulsThisBooking>[0]> = {}) => ({
|
|
tradeDirection: 'IMPORT',
|
|
firstMile: null,
|
|
lastMile: null,
|
|
lastMileCommitted: false,
|
|
...over,
|
|
});
|
|
|
|
it('an import whose last-mile request is not yet approved may still self-haul', () => {
|
|
expect(edrHaulsThisBooking(booking({ lastMile: 'Bole, Addis Ababa' }))).toBe(false);
|
|
});
|
|
|
|
it('an import whose last-mile request was approved is hauled by EDR', () => {
|
|
expect(
|
|
edrHaulsThisBooking(booking({ lastMile: 'Bole, Addis Ababa', lastMileCommitted: true })),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('an import that chose no delivery self-hauls, whatever the leg tables say', () => {
|
|
expect(edrHaulsThisBooking(booking({ lastMileCommitted: true }))).toBe(false);
|
|
});
|
|
|
|
it('an export that chose collection is hauled by EDR — no approval step on that leg', () => {
|
|
expect(edrHaulsThisBooking(booking({ tradeDirection: 'EXPORT', firstMile: 'Modjo' }))).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('a domestic booking is blocked by collection, or by an approved delivery', () => {
|
|
expect(edrHaulsThisBooking(booking({ tradeDirection: 'DOMESTIC', firstMile: 'Adama' }))).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
edrHaulsThisBooking(booking({ tradeDirection: 'DOMESTIC', lastMile: 'Dire Dawa' })),
|
|
).toBe(false);
|
|
expect(
|
|
edrHaulsThisBooking(
|
|
booking({ tradeDirection: 'DOMESTIC', lastMile: 'Dire Dawa', lastMileCommitted: true }),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|