From a2f234a1289023b383c9cd8fbe7ddf1218a3bf47 Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Tue, 14 Jul 2026 00:14:54 +0300 Subject: [PATCH 1/3] Removed skip for now button from passenger information --- .../portal/src/app/booking/passengers/page.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx index 4639bd196..607548f13 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx @@ -993,7 +993,8 @@ function PassengersForm() { const onInvalid = () => { // Sections still behind the Fayda verify screen stay collapsed here — they only expand - // when the user explicitly clicks "Skip for now" / "Enter details manually". + // when the user explicitly clicks "Enter details manually" (shown only when Fayda is + // unavailable). setSubmitError('Please fix the highlighted errors before continuing.'); }; @@ -1153,14 +1154,6 @@ function PassengersForm() { Finish verifying Passenger {(verifyingIndex ?? 0) + 1} first

)} - ) : showManualEntryLink ? (
From 957a185a4de6a5dabefc34ecd31d1a07bc8e63f1 Mon Sep 17 00:00:00 2001 From: Marshal Date: Mon, 13 Jul 2026 21:40:10 +0000 Subject: [PATCH 2/3] enhance contract clearance and train scheduling logic; add filters for clearance documents and improve booking validation --- .../contracts/contract-clearance.service.ts | 8 +++-- .../modules/contracts/contracts.repository.ts | 8 +++++ .../train-scheduling.service.ts | 29 ++++++++++++++++--- .../contracts/ExportClearanceStepper.tsx | 6 +++- .../bookings/DocumentClearanceDetailPage.tsx | 1 + 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts index 58f82856e..60eebf3da 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts @@ -848,8 +848,10 @@ export class ContractClearanceService { } /** - * GL ET clearance hub: every customs (Path B) contract in phased clearance, - * including after booking is created. + * GL ET clearance hub, Contracts tab: ONE_TIME customs (Path B) contracts in + * phased clearance that already carry at least one uploaded clearance + * document — a contract still waiting for its first document has nothing to + * review, and GENERAL contracts clear per booking, not at contract level. */ async queue(filter: FilterContractDto): Promise { return this.contractsRepository.findAllPaginated({ @@ -857,6 +859,8 @@ export class ContractClearanceService { pageSize: filter.pageSize ?? 100, statuses: [...PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES], customsClearingEnabled: true, + contractKind: 'ONE_TIME', + hasClearanceDocuments: true, sortBy: filter.sortBy, sortOrder: filter.sortOrder, }); diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts b/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts index be6316ade..3c9c7db14 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts @@ -26,6 +26,8 @@ export interface ContractListFilterOptions { tradeDirection?: string; paymentCurrency?: string; customsClearingEnabled?: boolean; + /** true → only contracts with at least one uploaded clearance document. */ + hasClearanceDocuments?: boolean; createdFrom?: string; createdTo?: string; } @@ -284,6 +286,12 @@ export class ContractsRepository extends BaseRepository { customsClearingEnabled: options.customsClearingEnabled, }); } + if (options.hasClearanceDocuments) { + qb.andWhere( + 'EXISTS (SELECT 1 FROM freight.contract_document_review cdr ' + + 'WHERE cdr.contract_id = contract.id AND cdr.deleted_at IS NULL)', + ); + } if (options.serviceTypeId) { qb.andWhere('contract.service_type_id = :serviceTypeId', { serviceTypeId: options.serviceTypeId, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts index c0aa14bd9..9e82bca33 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts @@ -1136,14 +1136,35 @@ export class TrainSchedulingService { throw new BadRequestException('Schedule has no train set'); } - // Batch parity: a schedule may only allocate bookings that targeted it. This mirrors - // the automatic fill, which only pulls bookings whose train_schedule_id is this schedule. + // Batch parity: a schedule may only allocate bookings from its route-day POOL. + // Under day-level pooling (see fillRouteDayInternal) an unreserved booking has + // a NULL train_schedule_id and is only pinned by reserve(); a reserved one is + // pinned to whichever train in the day's group first held it. Every train + // sharing this origin + destination + EAT departure day draws from ONE shared + // pool (one shared booking window), so a booking is allocatable here when it is + // either unpinned (NULL) or pinned to THIS train or a GROUP SIBLING. A booking + // pinned to a train on a DIFFERENT route/day is a real stray. Genuine route/ + // day/capacity fit is enforced downstream by validateBookingsForScheduling. + // EXPORT never groups, so its pool is this schedule alone (plus NULL pool). if (dto.bookingIds.length) { + const groupScheduleIds = new Set([scheduleId]); + if (schedule.direction !== 'EXPORT') { + const siblings = await this.findGroupSiblings( + this.dataSource.manager, + schedule.originStationId, + schedule.destinationStationId, + schedule.scheduledDepartureDate, + scheduleId, + ); + for (const sib of siblings) groupScheduleIds.add(sib.id); + } const targeted = await this.bookingsRepository.findByIdsForScheduling(dto.bookingIds); - const stray = targeted.filter((b) => b.trainScheduleId !== scheduleId); + const stray = targeted.filter( + (b) => b.trainScheduleId != null && !groupScheduleIds.has(b.trainScheduleId), + ); if (stray.length) { throw new BadRequestException( - `These bookings are not assigned to this schedule: ${stray + `These bookings are pinned to a train on a different route or day: ${stray .map((b) => b.reference ?? b.id) .join(', ')}`, ); diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx index c94e005ae..df8871b07 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx @@ -149,8 +149,12 @@ export function ExportClearanceStepper({ const entityId = contractId ?? bookingId ?? ""; // The booking that carries the post-booking steps (gate pass, T1, invoice). const actionBookingId = clearance.linkedBookingId ?? bookingId ?? null; + // Per-booking GENERAL clearance runs on a bare instance that only becomes a + // real booking once GL completes it — the caller's bookingCreated prop carries + // that signal, so a booking-keyed view must NOT count as "created" by itself + // (it would lock GL Ethiopia out of the declaration step right after the RO). const effectiveBookingCreated = - bookingCreated || Boolean(clearance.linkedBookingId) || isBooking; + bookingCreated || Boolean(clearance.linkedBookingId); const activeStep = useMemo( () => computeExportActiveStep(clearance, bookingMilestones, effectiveBookingCreated), diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx index bb271e205..79d35aa7d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx @@ -248,6 +248,7 @@ export default function DocumentClearanceDetailPage() { // stepper's "Create booking" step must read as NOT-yet-created // so it never claims the booking is done before GL completes it. bookingCreated={Number(booking?.totalAmount ?? 0) > 0} + bookingMilestones={bookingMilestones ?? []} onChanged={() => void refetch()} onViewFile={view} onDownloadFile={(f) => void downloadBookingFile(f.id, f.name)} From 0e18df6ad998e6a7bbfdcfcfb9c9dc62c0a8a6e4 Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Tue, 14 Jul 2026 09:03:10 +0300 Subject: [PATCH 3/3] Updated sms text --- .../notifications/notifications.service.ts | 43 +++++++++++++++---- .../src/modules/tickets/tickets.service.ts | 18 ++++++-- .../src/app/booking/auth-check/page.tsx | 30 ++++++------- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts index fc8bffa27..a1678d131 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts @@ -303,7 +303,7 @@ export class NotificationsService { const booking = await this.prisma.booking.findUnique({ where: { id: bookingId }, include: { - schedule: { include: { originStation: true, destinationStation: true, train: true } }, + schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } }, seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } }, }, }); @@ -353,6 +353,27 @@ export class NotificationsService { } } + /** + * Resolves the user's actual boarding/alighting stations from the booking's originStationId / + * destinationStationId via stopTimes, falling back to the schedule's full-route endpoints when + * the booking has no segment override (e.g. older records or packages). + */ + private resolveSegmentStations(booking: any): { originStation: any; destinationStation: any } { + const s = booking?.schedule ?? {}; + const stopTimes: any[] = s.stopTimes ?? []; + const findStation = (stationId: string | null | undefined, fallback: any) => { + if (stationId && stopTimes.length > 0) { + const stop = stopTimes.find((st: any) => st.stationId === stationId); + if (stop?.station) return stop.station; + } + return fallback ?? null; + }; + return { + originStation: findStation(booking?.originStationId, s.originStation), + destinationStation: findStation(booking?.destinationStationId, s.destinationStation), + }; + } + /** * Builds the interpolation context for the `booking.created` template. `trainSeatLines` is a * pre-joined block of one "Train/Seat: …" line per booked seat (multi-passenger bookings get @@ -379,12 +400,13 @@ export class NotificationsService { // Lead passenger (leg-1 seat). Booking has no contactName; the traveller name lives on the seat. const passengerName = seats[0]?.passengerName ?? 'Passenger'; const payLink = `${process.env.PORTAL_URL ?? 'http://localhost:5174'}/booking/detail?ref=${ref}`; + const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking); return { passengerName, bookingRef: ref, - origin: s.originStation?.name ?? '', - destination: s.destinationStation?.name ?? '', + origin: originSt?.name ?? '', + destination: destSt?.name ?? '', trainSeatLines, travelDate: fmtDate(s.departureAt), departureTime: fmtTime(s.departureAt), @@ -406,7 +428,7 @@ export class NotificationsService { const booking = await this.prisma.booking.findUnique({ where: { id: bookingId }, include: { - schedule: { include: { originStation: true, destinationStation: true, train: true } }, + schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } }, seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } }, }, }); @@ -489,9 +511,10 @@ export class NotificationsService { const s = booking.schedule ?? {}; const dep = s.departureAt ? new Date(s.departureAt).toLocaleString('en-GB') : 'TBD'; const passengers = (booking.seats ?? []).map((bs: any) => bs.passengerName).filter(Boolean).join(', '); + const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking); return [ `Booking ${booking.bookingRef} confirmed.`, - `${s.originStation?.name ?? ''} -> ${s.destinationStation?.name ?? ''}`, + `${originSt?.name ?? ''} -> ${destSt?.name ?? ''}`, `Train: ${s.train?.name ?? s.train?.number ?? ''}`, `Departs: ${dep}`, passengers ? `Passengers: ${passengers}` : '', @@ -504,6 +527,7 @@ export class NotificationsService { const s = booking.schedule ?? {}; const fmt = (d: any) => d ? new Date(d).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' }) : 'TBD'; + const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking); const seatRows = (booking.seats ?? []) .map((bs: any) => { const coach = bs.seat?.coach?.number ?? '-'; @@ -532,11 +556,11 @@ export class NotificationsService { - + - + @@ -612,8 +636,9 @@ export class NotificationsService { const fmt = (d: any) => d ? new Date(d).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' }) : 'TBD'; const legLabel = leg ? ` (${leg.replace(/_/g, ' ')})` : ''; - const origin = s.originStation?.name ?? ''; - const dest = s.destinationStation?.name ?? ''; + const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking); + const origin = originSt?.name ?? ''; + const dest = destSt?.name ?? ''; const train = s.train?.name ?? s.train?.number ?? ''; const dep = fmt(s.departureAt); const arr = fmt(s.arrivalAt); diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 1a9c88d4e..656e726ab 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -386,7 +386,7 @@ export class TicketsService { let booking = await this.prisma.booking.findUnique({ where: { bookingRef }, include: { - schedule: { include: { originStation: true, destinationStation: true, train: true } }, + schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } }, returnSchedule: { include: { originStation: true, destinationStation: true } }, tickets: true, seats: { include: { seat: { include: { coach: true } } } }, @@ -400,7 +400,7 @@ export class TicketsService { booking = await this.prisma.booking.findUnique({ where: { bookingRef: ticket.bookingRef }, include: { - schedule: { include: { originStation: true, destinationStation: true, train: true } }, + schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } }, returnSchedule: { include: { originStation: true, destinationStation: true } }, tickets: true, seats: { include: { seat: { include: { coach: true } } } }, @@ -453,6 +453,18 @@ export class TicketsService { // Send notifications after successful boarding await this.sendBoardingNotifications(booking, ticket, result.leg || 'OUTBOUND'); + // Resolve user-selected segment rather than the full schedule route + const _schedStops = (booking as any).schedule?.stopTimes ?? []; + const _resolveStation = (id: string | null | undefined, fallback: any) => { + if (id) { + const found = _schedStops.find((st: any) => st.stationId === id)?.station; + if (found) return found; + } + return fallback; + }; + const boardingOrigin = _resolveStation((booking as any).originStationId, (booking as any).schedule?.originStation); + const boardingDest = _resolveStation((booking as any).destinationStationId, (booking as any).schedule?.destinationStation); + return { success: true, message: `Passenger boarded successfully (${result.leg || 'OUTBOUND'} leg)`, @@ -461,7 +473,7 @@ export class TicketsService { ticketNumber: ticket.barcodePayload, bookingRef: booking.bookingRef, passengerName: seatInfo?.passengerName || ticket.passengerName || 'N/A', - route: `${(booking as any).schedule?.originStation?.name || 'N/A'} → ${(booking as any).schedule?.destinationStation?.name || 'N/A'}`, + route: `${boardingOrigin?.name || 'N/A'} → ${boardingDest?.name || 'N/A'}`, seat: seatNumber, coach: coachNumber, trainName: (booking as any).schedule?.train?.name || (booking as any).schedule?.train?.number || 'N/A', diff --git a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx index 43775b1c7..9e1cdc9c9 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx @@ -81,20 +81,6 @@ export default function AuthCheckPage() {

- - - - + + + +
From${s.originStation?.name ?? ''} (${s.originStation?.code ?? ''})${originSt?.name ?? ''} (${originSt?.code ?? ''})
To${s.destinationStation?.name ?? ''} (${s.destinationStation?.code ?? ''})${destSt?.name ?? ''} (${destSt?.code ?? ''})
Train