mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
fix(mile): only send bookings that bought EDR haulage to the mile queues
The rule that a customer's own truck and an EDR road leg are alternatives existed on the truck side only (CustomerTruckService.assertSelfHaulPaid). LastMileService had no counterpart: create checked payment and nothing else, so any paid booking could be accepted into the queue. A booking took a customer truck at 06:42 and an EDR last-mile leg with a real EDR truck at 06:47, neither side aware of the other, on a contract that had chosen no road legs at all. The road legs are chosen on the contract and copied onto the booking, and the pickup/delivery address is the only per-booking record of that choice. service_types cannot serve: every type ships with includes_first_mile and includes_last_mile set to true, so reading them would mean no booking could ever self-haul. That same always-true flag had already killed the first-mile guard, whose `address || serviceType.includesFirstMile` admitted every paid export booking. One shared rule now answers it for both sides, so the two halves cannot drift apart again: last-mile create rejects a booking that chose no road legs and one already carrying a customer truck; first-mile no longer honours the service-type flag; the customer-truck guard reads the same helper. Existing legs are untouched — the guards are on creation, so the one booking already carrying both needs a human to reconcile it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
52
apps/edr-freight-api/src/common/mile-haulage.util.spec.ts
Normal file
52
apps/edr-freight-api/src/common/mile-haulage.util.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { 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);
|
||||
});
|
||||
});
|
||||
49
apps/edr-freight-api/src/common/mile-haulage.util.ts
Normal file
49
apps/edr-freight-api/src/common/mile-haulage.util.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/** The booking fields that decide who hauls the road legs. */
|
||||
export interface MileHaulageRow {
|
||||
tradeDirection: string | null;
|
||||
/** `first_mile_pickup_address` — set when the customer asked EDR to collect. */
|
||||
firstMile: string | null;
|
||||
/** `last_mile_delivery_address` — set when the customer asked EDR to deliver. */
|
||||
lastMile: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the customer bought the EDR road leg that matters for their direction:
|
||||
* delivery at the end of an import, collection at the start of an export. A
|
||||
* DOMESTIC booking can use either, so either one counts.
|
||||
*
|
||||
* The address is the signal because it is the only per-booking record of the
|
||||
* choice. `service_types.includes_first_mile` / `includes_last_mile` cannot be
|
||||
* used — every service type ships with both set to true, so reading them would
|
||||
* mean every booking uses EDR haulage and none could ever self-haul.
|
||||
*/
|
||||
export function usesEdrMileService(booking: MileHaulageRow): boolean {
|
||||
const hasFirstMile = Boolean(booking.firstMile?.trim());
|
||||
const hasLastMile = Boolean(booking.lastMile?.trim());
|
||||
switch (booking.tradeDirection) {
|
||||
case 'IMPORT':
|
||||
return hasLastMile;
|
||||
case 'EXPORT':
|
||||
return hasFirstMile;
|
||||
default:
|
||||
return hasFirstMile || hasLastMile;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EDR haulage and a customer's own truck are alternatives, never both. Whichever
|
||||
* side is being set up, it has to reject the other — a guard on only one side
|
||||
* lets the two paths open on the same booking, each unaware of the other.
|
||||
*/
|
||||
export const SELF_HAUL_CONFLICT_MESSAGE =
|
||||
'This booking is delivered by the customer’s own truck — an EDR mile leg cannot also be assigned.';
|
||||
|
||||
export const EDR_HAULAGE_CONFLICT_MESSAGE =
|
||||
'Customer truck assignment is only allowed when first/last mile delivery is not selected';
|
||||
|
||||
/**
|
||||
* The road legs are chosen on the contract. A booking whose contract bought
|
||||
* neither has no business in the first/last-mile queues at all.
|
||||
*/
|
||||
export const NO_MILE_SERVICE_MESSAGE =
|
||||
'This booking did not select first/last mile delivery on its contract, so it cannot be assigned an EDR mile leg.';
|
||||
Reference in New Issue
Block a user