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:
Hagernesh
2026-07-20 08:03:16 +00:00
parent 2906c6bb45
commit c1c362ab9d
6 changed files with 262 additions and 15 deletions

View File

@@ -12,6 +12,10 @@ import { AddCustomerTruckDto } from './dto/add-customer-truck.dto';
import { DepartCustomerTruckDto } from './dto/depart-customer-truck.dto';
import { CustomerTruckAssignment } from './entities/customer-truck-assignment.entity';
import { CustomerTruckContainer } from './entities/customer-truck-container.entity';
import {
EDR_HAULAGE_CONFLICT_MESSAGE,
usesEdrMileService,
} from '../../common/mile-haulage.util';
import { CustomerTruckAssignmentsRepository } from './customer-truck-assignments.repository';
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
import { NotificationsService } from '../notifications/notifications.service';
@@ -534,18 +538,11 @@ export class CustomerTruckService {
}
private assertSelfHaulPaid(booking: BookingGuardRow): void {
const hasFirstMile = Boolean(booking.firstMile?.trim());
const hasLastMile = Boolean(booking.lastMile?.trim());
const usesMileService =
booking.tradeDirection === 'IMPORT'
? hasLastMile
: booking.tradeDirection === 'EXPORT'
? hasFirstMile
: hasFirstMile || hasLastMile;
if (usesMileService) {
throw new BadRequestException(
'Customer truck assignment is only allowed when first/last mile delivery is not selected',
);
// Shared with the EDR side (LastMileService.assertNoCustomerTruck) so the two
// halves of this rule cannot drift apart — they did, and a booking ended up
// with a customer truck and an EDR leg at once.
if (usesEdrMileService(booking)) {
throw new BadRequestException(EDR_HAULAGE_CONFLICT_MESSAGE);
}
if (booking.paymentStatus !== 'PAID') {
throw new BadRequestException(