From 102905d7e321843fee24efd77e4164ef9316a006 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sun, 5 Jul 2026 23:05:14 +0300 Subject: [PATCH] Additional UAT issue resolutions --- .../modules/bookings/guest-booking.service.ts | 76 +++++++++++++------ .../src/modules/packages/packages.service.ts | 13 +++- .../src/modules/schedules/schedules.dto.ts | 4 +- .../modules/schedules/schedules.service.ts | 2 +- .../src/app/booking/confirmation/page.tsx | 10 +-- .../src/app/booking/passengers/page.tsx | 2 +- .../portal/src/app/booking/payment/page.tsx | 45 +++++++---- .../portal/src/app/booking/review/page.tsx | 10 +-- .../portal/src/app/packages/[id]/page.tsx | 31 +++----- .../portal/src/app/profile/page.tsx | 2 +- .../portal/src/app/verify-account/page.tsx | 5 +- 11 files changed, 119 insertions(+), 81 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts index 3aaa85fc2..f4139d842 100644 --- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts @@ -144,22 +144,33 @@ export class GuestBookingService { }); } - // Calculate fare - const primaryNationality = passengersData[0]?.nationality; - const baseFareMinor = await this.getBaseFare( - dto.scheduleId, - dto.seatClassId, - segmentRoute, - fullRoute, - primaryNationality, - dto.originStationId, - dto.destinationStationId, - ); + // Calculate fare — package bookings use the fixed tier price, bypassing the fare engine + const isPackageOneway = !!dto.packageId && !!dto.priceTierId; + let baseFareMinor: number; + let paidChildrenCount: number; + let childUnitFare: number; + + if (isPackageOneway) { + const tier = await this.prisma.packagePriceTier.findUniqueOrThrow({ where: { id: dto.priceTierId! } }); + baseFareMinor = tier.priceMinor; + paidChildrenCount = childCount; + childUnitFare = Math.round(baseFareMinor * 0.1); + } else { + const primaryNationality = passengersData[0]?.nationality; + baseFareMinor = await this.getBaseFare( + dto.scheduleId, + dto.seatClassId, + segmentRoute, + fullRoute, + primaryNationality, + dto.originStationId, + dto.destinationStationId, + ); + paidChildrenCount = Math.max(0, childCount - 1); + childUnitFare = baseFareMinor; + } const adultFareMinor = baseFareMinor * adultCount; - const isPackageOneway = !!dto.packageId; - const paidChildrenCount = isPackageOneway ? childCount : Math.max(0, childCount - 1); - const childUnitFare = isPackageOneway ? Math.round(baseFareMinor * 0.1) : baseFareMinor; const childFareMinor = childUnitFare * paidChildrenCount; const totalBaseFareMinor = adultFareMinor + childFareMinor; @@ -369,19 +380,34 @@ export class GuestBookingService { passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality }); } - // Calculate fares for both legs + // Calculate fares for both legs — package bookings use the fixed tier price split across legs const returnSeatClassId = dto.returnSeatClassId || dto.seatClassId; - const primaryNationality = passengersData[0]?.nationality; + const isPackageRoundTrip = !!dto.packageId && !!dto.priceTierId; + let outboundBaseFare: number; + let returnBaseFare: number; + let paidChildrenCount: number; + let outboundChildUnitFare: number; + let returnChildUnitFare: number; - const [outboundBaseFare, returnBaseFare] = await Promise.all([ - this.getBaseFare(dto.scheduleId, dto.seatClassId, outboundSegmentRoute, outboundFullRoute, primaryNationality, dto.originStationId, dto.destinationStationId), - this.getBaseFare(dto.returnScheduleId, returnSeatClassId, returnSegmentRoute, returnFullRoute, primaryNationality, dto.returnOriginStationId, dto.returnDestinationStationId), - ]); - - const isPackageRoundTrip = !!dto.packageId; - const paidChildrenCount = isPackageRoundTrip ? childCount : Math.max(0, childCount - 1); - const outboundChildUnitFare = isPackageRoundTrip ? Math.round(outboundBaseFare * 0.1) : outboundBaseFare; - const returnChildUnitFare = isPackageRoundTrip ? Math.round(returnBaseFare * 0.1) : returnBaseFare; + if (isPackageRoundTrip) { + const tier = await this.prisma.packagePriceTier.findUniqueOrThrow({ where: { id: dto.priceTierId! } }); + // tier.priceMinor is the full round-trip price per adult; split evenly across legs + const halfMinor = Math.round(tier.priceMinor / 2); + outboundBaseFare = halfMinor; + returnBaseFare = tier.priceMinor - halfMinor; + paidChildrenCount = childCount; + outboundChildUnitFare = Math.round(outboundBaseFare * 0.1); + returnChildUnitFare = Math.round(returnBaseFare * 0.1); + } else { + const primaryNationality = passengersData[0]?.nationality; + [outboundBaseFare, returnBaseFare] = await Promise.all([ + this.getBaseFare(dto.scheduleId, dto.seatClassId, outboundSegmentRoute, outboundFullRoute, primaryNationality, dto.originStationId, dto.destinationStationId), + this.getBaseFare(dto.returnScheduleId, returnSeatClassId, returnSegmentRoute, returnFullRoute, primaryNationality, dto.returnOriginStationId, dto.returnDestinationStationId), + ]); + paidChildrenCount = Math.max(0, childCount - 1); + outboundChildUnitFare = outboundBaseFare; + returnChildUnitFare = returnBaseFare; + } const outboundTotalBase = outboundBaseFare * adultCount + outboundChildUnitFare * paidChildrenCount; const returnTotalBase = returnBaseFare * adultCount + returnChildUnitFare * paidChildrenCount; const combinedBaseFareMinor = outboundTotalBase + returnTotalBase; diff --git a/apps/edr-passenger-api/src/modules/packages/packages.service.ts b/apps/edr-passenger-api/src/modules/packages/packages.service.ts index 05b886ea8..3b14af6dd 100644 --- a/apps/edr-passenger-api/src/modules/packages/packages.service.ts +++ b/apps/edr-passenger-api/src/modules/packages/packages.service.ts @@ -117,8 +117,8 @@ export class PackagesService { remainingSeats: remaining, outboundSchedule: { scheduleId: pkg.outboundScheduleId, - originStationId: pkg.originStationId, - destinationStationId: pkg.destinationStationId, + originStationId: pkg.outboundSchedule.originStationId, + destinationStationId: pkg.outboundSchedule.destinationStationId, departureAt: pkg.outboundSchedule.departureAt, arrivalAt: pkg.outboundSchedule.arrivalAt, originStation: pkg.outboundSchedule.originStation, @@ -204,7 +204,14 @@ export class PackagesService { where: { id }, include: { priceTiers: true, - outboundSchedule: { include: { originStation: true, destinationStation: true, train: true } }, + outboundSchedule: { + include: { + originStation: true, + destinationStation: true, + train: true, + stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } }, + }, + }, returnSchedule: { include: { originStation: true, destinationStation: true, train: true } }, }, }); diff --git a/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts b/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts index 044768e90..80f3ad13b 100644 --- a/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts +++ b/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts @@ -50,8 +50,8 @@ export class CreateScheduleDto { { sequence: 6, plannedArrivalAt: '2026-06-15T20:00:00Z' }, ], }) - @IsArray() @ValidateNested({ each: true }) @Type(() => PlannedStopTimeDto) - plannedTimes: PlannedStopTimeDto[]; + @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => PlannedStopTimeDto) + plannedTimes?: PlannedStopTimeDto[]; } export class UpdateScheduleDto { diff --git a/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts b/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts index 2d194fd7b..19aa5ed0e 100644 --- a/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts +++ b/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts @@ -153,7 +153,7 @@ export class SchedulesService { }); } - const providedSeqs = new Set(plannedTimes.map(t => t.sequence)); + const providedSeqs = new Set((plannedTimes ?? []).map(t => t.sequence)); const missingSeqs = route.stops.map(s => s.sequence).filter(seq => !providedSeqs.has(seq)); if (missingSeqs.length > 0) { throw new BadRequestException(`Missing planned times for stop sequences: ${missingSeqs.join(', ')}`); diff --git a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx index 6b5bcb715..e84837af1 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx @@ -158,7 +158,7 @@ export default function ConfirmationPage() { useEffect(() => { if (!bookingId || !pnr) { - router.push('/booking/search'); + window.location.href = '/'; } }, [bookingId, pnr, router]); @@ -370,19 +370,19 @@ export default function ConfirmationPage() {

{passenger.nationality}

-

Seat

+

Seat(s)

{isRoundTrip ? (

- Outbound: {(passenger as any).outboundSeatNumber || 'Auto-assigned at boarding'}{(passenger as any).outboundCoachNumber && (Coach {(passenger as any).outboundCoachNumber})} + Outbound: {(passenger as any).outboundCoachNumber && {(passenger as any).outboundCoachNumber}} — {(passenger as any).outboundSeatNumber || 'Auto-assigned at boarding'}

- Return: {(passenger as any).inboundSeatNumber || 'Auto-assigned at boarding'}{(passenger as any).inboundCoachNumber && (Coach {(passenger as any).inboundCoachNumber})} + Return: {(passenger as any).inboundCoachNumber && {(passenger as any).inboundCoachNumber}} — {(passenger as any).inboundSeatNumber || 'Auto-assigned at boarding'}

) : (

- {passenger.seatNumber || 'Auto-assigned at boarding'}{passenger.coachNumber && (Coach {passenger.coachNumber})} + {passenger.coachNumber && (Coach {passenger.coachNumber})} — {passenger.seatNumber || 'Auto-assigned at boarding'}

)}
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 4263108d1..ea2dbc88d 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 @@ -962,7 +962,7 @@ export default function PassengersPage() { useEffect(() => { if (!searchCriteria) { - router.push('/booking/search'); + window.location.href = '/'; } }, [searchCriteria, router]); diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx index dc20d76d8..c2c32a8cf 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx @@ -69,19 +69,24 @@ export default function PaymentPage() { enabled: !!selectedMethod && !!bookingId, }); - // Fallback: estimate from local store while API hasn't responded yet. - // Uses the same first-child-free calculation as the review page so the - // breakdown shown here matches what the passenger already saw there. - const outboundBaseFare = !isPackage && isRoundTrip && outboundSchedule ? passengers.reduce((sum, _, i) => { - return sum + calculatePassengerFare(passengers, i, outboundSchedule.baseFareAdult || 0); - }, 0) : 0; + // Per-leg totals across all passengers. + // Package: one leg = pkgAdultFare/pkgChildFare (already ×1 per leg; pkgAdultFare already has ×2 for round-trip baked in via pkgRoundTripMultiplier — so per-leg is packageTierPriceMinor). + const adultCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length; + const childCount = searchCriteria?.childCount ?? passengers.filter(p => isChild(p)).length; + const pkgPerLegAdultFare = isPackage ? packageTierPriceMinor! : 0; + const pkgPerLegChildFare = isPackage ? Math.round(pkgPerLegAdultFare * 0.1) : 0; + const pkgPerLegTotal = isPackage ? adultCount * pkgPerLegAdultFare + childCount * pkgPerLegChildFare : 0; - const inboundBaseFare = !isPackage && isRoundTrip && inboundSchedule ? passengers.reduce((sum, _, i) => { - return sum + calculatePassengerFare(passengers, i, inboundSchedule.baseFareAdult || 0); - }, 0) : 0; + const outboundBaseFare = isPackage + ? pkgPerLegTotal + : (isRoundTrip && outboundSchedule ? passengers.reduce((sum, _, i) => sum + calculatePassengerFare(passengers, i, outboundSchedule.baseFareAdult || 0), 0) : 0); + + const inboundBaseFare = isPackage + ? pkgPerLegTotal + : (isRoundTrip && inboundSchedule ? passengers.reduce((sum, _, i) => sum + calculatePassengerFare(passengers, i, inboundSchedule.baseFareAdult || 0), 0) : 0); const baseFare = isPackage - ? (searchCriteria?.adultCount ?? 0) * pkgAdultFare + (searchCriteria?.childCount ?? 0) * pkgChildFare + ? adultCount * pkgAdultFare + childCount * pkgChildFare : isRoundTrip ? (outboundBaseFare + inboundBaseFare) : passengers.reduce((sum, _, i) => { const farePerPassenger = selectedSchedule?.baseFareAdult || (selectedSchedule as any)?.fareAdult || (selectedSchedule as any)?.price || 0; return sum + calculatePassengerFare(passengers, i, farePerPassenger); @@ -299,15 +304,25 @@ export default function PaymentPage() { {formatFare(passengerTotal, displayCurrency)} - {!isPackage && isRoundTrip && ( + {isRoundTrip && (
- Outbound {isFreeChild ? '(Free)' : ''} - {formatFare(calculatePassengerFare(passengers, i, outboundSchedule?.baseFareAdult || 0), displayCurrency)} + Outbound {!isPackage && isFreeChild ? '(Free)' : ''} + {formatFare( + isPackage + ? (isChildPassenger ? pkgPerLegChildFare : pkgPerLegAdultFare) + : calculatePassengerFare(passengers, i, outboundSchedule?.baseFareAdult || 0), + displayCurrency + )}
- Return {isFreeChild ? '(Free)' : ''} - {formatFare(calculatePassengerFare(passengers, i, inboundSchedule?.baseFareAdult || 0), displayCurrency)} + Return {!isPackage && isFreeChild ? '(Free)' : ''} + {formatFare( + isPackage + ? (isChildPassenger ? pkgPerLegChildFare : pkgPerLegAdultFare) + : calculatePassengerFare(passengers, i, inboundSchedule?.baseFareAdult || 0), + displayCurrency + )}
)} diff --git a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx index 7b449dc8b..862a0ba8f 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx @@ -191,7 +191,7 @@ export default function ReviewPage() { if (!searchCriteria?.originStationId || !searchCriteria?.destinationStationId) { alert('Missing search criteria. Please start over.'); - router.push('/booking/search'); + window.location.href = '/'; return; } @@ -372,13 +372,13 @@ export default function ReviewPage() { if (isRoundTrip) { if (!outboundSchedule || !inboundSchedule || !passengers.length) { if (!createBookingMutation.isPending && !createBookingMutation.isSuccess) { - router.push('/booking/search'); + window.location.href = '/'; } } } else { if (!selectedSchedule || !passengers.length) { if (!createBookingMutation.isPending && !createBookingMutation.isSuccess) { - router.push('/booking/search'); + window.location.href = '/'; } } } @@ -441,8 +441,8 @@ export default function ReviewPage() { const pkgRoundTripMultiplier = isPackageBooking && isRoundTrip ? 2 : 1; const pkgAdultFare = isPackageBooking ? packageTierPriceMinor! * pkgRoundTripMultiplier : 0; const pkgChildFare = isPackageBooking ? Math.round(pkgAdultFare * 0.1) : 0; - const adultPassengerCount = searchCriteria?.adultCount ?? passengers.length; - const childPassengerCount = searchCriteria?.childCount ?? 0; + const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length; + const childPassengerCount = searchCriteria?.childCount ?? passengers.filter(p => isChild(p)).length; const total = isPackageBooking ? adultPassengerCount * pkgAdultFare + childPassengerCount * pkgChildFare diff --git a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx index 69558e49e..16f9197e7 100644 --- a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx @@ -40,6 +40,11 @@ interface TrainInfo { operatorName: string; } +interface StopTime { + sequence: number; + station: Station; +} + interface Schedule { id: string; departureAt: string; @@ -50,6 +55,7 @@ interface Schedule { originStation: Station; destinationStation: Station; train: TrainInfo; + stopTimes?: StopTime[]; } interface PriceTier { @@ -432,25 +438,10 @@ export default function PackageDetailPage() { enabled: !!id, }); - const { data: stationsData } = useQuery({ - queryKey: ["stations-simple"], - queryFn: async () => (await apiClient.get(`/stations?pageSize=100`)) as any, - }); - - const allStations: Station[] = Array.isArray(stationsData) ? stationsData : stationsData?.items || stationsData?.data?.items || []; - - // Fetch stops for the outbound schedule to filter stations to only those on the route - const outboundScheduleId = pkg?.outboundSchedule?.id; - const { data: scheduleStops } = useQuery({ - queryKey: ["schedule-stops", outboundScheduleId], - queryFn: async () => (await apiClient.get(`/schedules/${outboundScheduleId}/stops`)) as any[], - enabled: !!outboundScheduleId, - }); - - // Only show stations that are actual stops on the route - const stations: Station[] = scheduleStops?.length - ? allStations.filter((s) => scheduleStops.some((stop: any) => stop.stationId === s.id || stop.station?.id === s.id)) - : allStations; + // Build departure station list from the outbound schedule's route stops (ordered by sequence) + const routeStopStations: Station[] = pkg?.outboundSchedule?.stopTimes?.length + ? pkg.outboundSchedule.stopTimes.map((st) => st.station).filter(Boolean) + : []; const selectedTier = pkg?.priceTiers?.find((t) => t.id === selectedTierId); @@ -574,7 +565,7 @@ export default function PackageDetailPage() { loading={bookingContextLoading} error={bookingContextError} priceMultiplier={isRoundTripPkg ? 2 : 1} - stations={stations} + stations={routeStopStations} /> )} diff --git a/apps/edr-passenger-web/portal/src/app/profile/page.tsx b/apps/edr-passenger-web/portal/src/app/profile/page.tsx index dbc64d4a9..d44474976 100644 --- a/apps/edr-passenger-web/portal/src/app/profile/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/profile/page.tsx @@ -161,7 +161,7 @@ export default function ProfilePage() { mutationFn: () => apiClient.delete('/auth/account'), onSuccess: () => { logout(); - router.push('/booking/search'); + window.location.href = '/'; }, }); diff --git a/apps/edr-passenger-web/portal/src/app/verify-account/page.tsx b/apps/edr-passenger-web/portal/src/app/verify-account/page.tsx index f9f0b61c9..b2fd491b3 100644 --- a/apps/edr-passenger-web/portal/src/app/verify-account/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/verify-account/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { Suspense, useState } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { Train, ArrowLeft, ShieldCheck } from 'lucide-react'; import { iamAuthApi } from '@/lib/api/auth'; @@ -20,7 +20,6 @@ function isStrongPassword(pw: string): boolean { } function VerifyAccountContent() { - const router = useRouter(); const searchParams = useSearchParams(); const login = useAuthStore((s) => s.login); @@ -65,7 +64,7 @@ function VerifyAccountContent() { }); // Auto-login with the freshly-set password; login lazy-provisions the passenger record. await login(email, newPassword); - router.push('/booking/search'); + window.location.href = '/'; } catch (err: any) { const msg = err.response?.data?.message || err.message || ''; setError(msg || 'Could not verify your account. Check the code and try again, or resend it.');