From 92df6c427c3ae4eb29467470b117420710337988 Mon Sep 17 00:00:00 2001 From: natib21 Date: Sat, 27 Jun 2026 09:07:28 +0000 Subject: [PATCH] fix --- .../modules/first-mile/first-mile.service.ts | 60 ++++++++++++------- .../src/pages/operations/FirstMilePage.tsx | 10 +++- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index 1ac78355a..73d98b8fe 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; @@ -44,30 +44,19 @@ export class FirstMileService { * paid before any first-mile work proceeds. Throws if the reference is * unknown or the booking has not reached PAID status. */ - async acceptBooking(bookingId: string): Promise { + async acceptBooking(bookingId: string): Promise { const booking = await this.bookingsRepository.findById(bookingId, { relations: { serviceType: true }, }); if (!booking) { - return null; + throw new NotFoundException(`Booking ${bookingId} not found`); } - if (booking.paymentStatus !== 'PAID') { - return null; - } - - if (!this.bookingRequestsFirstMile(booking)) { - return null; - } - - return this.create({ - bookingId: booking.id, - advancedPayment: 0, - }); + return this.acceptEligibleBooking(booking); } - async acceptBookingByReference(bookingReference: string): Promise { + async acceptBookingByReference(bookingReference: string): Promise { const [booking] = await this.bookingsRepository.findAll({ where: { reference: bookingReference }, relations: { serviceType: true }, @@ -75,15 +64,39 @@ export class FirstMileService { }); if (!booking) { - return null; + throw new NotFoundException(`Booking ${bookingReference} not found`); } + return this.acceptEligibleBooking(booking); + } + + /** + * Shared accept path: validates payment + first-mile eligibility, rejects an + * already-assigned booking, then creates the first-mile record. Throws a + * meaningful HTTP error instead of returning null so the client can surface + * why an accept was refused. + */ + private async acceptEligibleBooking(booking: { + id: string; + reference?: string; + paymentStatus?: string | null; + tradeDirection?: string | null; + firstMilePickupAddress?: string | null; + serviceType?: { includesFirstMile?: boolean | null } | null; + }): Promise { + const label = booking.reference ?? booking.id; + if (booking.paymentStatus !== 'PAID') { - return null; + throw new BadRequestException(`Booking ${label} is not paid`); } if (!this.bookingRequestsFirstMile(booking)) { - return null; + throw new BadRequestException(`Booking ${label} does not require a first mile`); + } + + const existing = await this.findByBookingId(booking.id); + if (existing) { + throw new ConflictException(`Booking ${label} already has a first-mile assignment`); } return this.create({ @@ -174,10 +187,17 @@ export class FirstMileService { } private bookingRequestsFirstMile(booking: { + tradeDirection?: string | null; firstMilePickupAddress?: string | null; serviceType?: { includesFirstMile?: boolean | null } | null; }): boolean { - return Boolean(booking.firstMilePickupAddress?.trim() || booking.serviceType?.includesFirstMile); + // Export bookings always need a first mile (pickup → origin yard); the + // pickup address is captured at assignment time, not required upfront. + return Boolean( + booking.tradeDirection === 'EXPORT' || + booking.firstMilePickupAddress?.trim() || + booking.serviceType?.includesFirstMile, + ); } async update(id: string, dto: UpdateFirstMileDto): Promise { diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 4cbe12b53..208db85f8 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -379,6 +379,9 @@ const FirstMilePage = () => { mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => { const res = await firstMileService.accept(reference); const created = res.data; + if (!created?.id) { + throw new Error("First-mile leg was not created for this booking."); + } if (vehicleId) await firstMileService.update(created.id, { vehicleId }); return created; }, @@ -387,8 +390,11 @@ const FirstMilePage = () => { toast({ title: "Booking accepted", description: "First-mile leg created successfully." }); closeAccept(); }, - onError: () => { - toast({ title: "Accept failed", variant: "destructive" }); + onError: (err: unknown) => { + const description = + (err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? + (err instanceof Error ? err.message : undefined); + toast({ title: "Accept failed", description, variant: "destructive" }); }, });