From d43229528ee07b6e464e17d9338d40c3485d6369 Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Sun, 19 Jul 2026 22:46:36 +0300 Subject: [PATCH] Fix price inconsistency --- .../src/modules/bookings/bookings.service.ts | 118 ++++++++++-------- .../modules/bookings/guest-booking.service.ts | 75 ++++++----- .../src/modules/packages/packages.service.ts | 21 +++- .../src/modules/search/search.service.ts | 17 ++- .../portal/src/app/booking/review/page.tsx | 9 +- .../portal/src/app/packages/[id]/page.tsx | 21 +++- 6 files changed, 153 insertions(+), 108 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index ada6e2a78..64dcb2c6c 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -834,53 +834,63 @@ export class BookingsService { // Track per-seat fare. Use the client-supplied seatFareMinor when present (berth-specific // pricing for Upper/Middle/Lower beds). Fall back to the fare engine's baseFareMinor. - let freeChildUsed = false; - let pkgChildIdx = 0; const passengersWithFares = passengersData.map(p => { let fareMinor: number; if (p.category === PassengerCategory.ADULT) { fareMinor = p.seatFareMinor ?? fareCalculation.baseFareMinor; - } else if (dto.packageId) { - fareMinor = pkgChildIdx < adultCount ? 0 : (p.seatFareMinor ?? fareCalculation.baseFareMinor); - pkgChildIdx++; } else { - if (!freeChildUsed) { fareMinor = 0; freeChildUsed = true; } - else fareMinor = p.seatFareMinor ?? fareCalculation.baseFareMinor; + // Free children have no seat (frontend excludes them from the DTO). + // Guard by seatId: unseated = free (0), seated = paid child. + // Applies to both package and regular bookings. + fareMinor = p.seatId ? (p.seatFareMinor ?? fareCalculation.baseFareMinor) : 0; } return { ...p, fareMinor }; }); - // Use the sum of per-seat fares as the authoritative total when the client supplied - // seatFareMinor for every seat-holding passenger — this captures berth-specific pricing - // (Upper/Middle/Lower) that the fare engine cannot resolve from seatClassId alone. - // Free children have no seatId and no seatFareMinor — exclude them from the check. + // Determine the authoritative total. + // Priority (one-way, non-package): + // 1. Server-computed sum of per-seat fares when every seated passenger supplied + // seatFareMinor — this captures berth-specific pricing (Upper/Middle/Lower) + // exactly as shown to the user and cannot be corrupted by a frontend race + // condition that sends reviewedTotalMinor before all fares are resolved. + // 2. reviewedTotalMinor from the frontend — fallback when the server doesn't + // have complete per-seat data (e.g. auto-assign with no seat map loaded). + // 3. Fare engine total — last resort when neither is available. + // For package bookings reviewedTotalMinor always wins because the tier price + // may include berth-specific adjustments the server cannot derive alone. + // Free children have no seatId and no seatFareMinor — exclude from the check. const seatedPassengers = passengersData.filter(p => p.seatId); const allFaresProvided = seatedPassengers.length > 0 && seatedPassengers.every(p => p.seatFareMinor != null); - // seatFareMinor values from the client are in display-currency minor units (matching - // displayAmountMinor from search results). reviewedTotalMinor is also display-currency minor. - // In both cases: store as displayTotalMinor as-is, back-convert to ETB for totalMinor. let resolvedTotalMinor: number; let displayTotalMinor: number; - if (dto.reviewedTotalMinor != null) { + + if (allFaresProvided && !dto.packageId) { + // Server has every passenger's berth fare — sum is the authoritative display total. + displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0); + resolvedTotalMinor = displayCurrency !== Currency.ETB + ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) + : displayTotalMinor; + if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor !== displayTotalMinor) { + this.logger.warn(`createOneWayBooking: reviewedTotalMinor=${dto.reviewedTotalMinor} ignored — using server-computed sum=${displayTotalMinor}`); + } + } else if (dto.packageId && dto.reviewedTotalMinor != null) { + // Package booking: client-supplied tier-adjusted total. displayTotalMinor = dto.reviewedTotalMinor; resolvedTotalMinor = displayCurrency !== Currency.ETB ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB) : dto.reviewedTotalMinor; - // For package bookings where per-seat fares weren't supplied, back-derive the - // per-seat fare from reviewedTotalMinor so BookingSeat.fareMinor reflects the - // actual berth price (Upper/Middle/Lower) rather than the tier's minimum price. - if (dto.packageId && seatedPassengers.length > 0) { + if (seatedPassengers.length > 0) { const perSeatFare = Math.round(dto.reviewedTotalMinor / seatedPassengers.length); passengersWithFares.forEach(p => { if (p.fareMinor > 0) p.fareMinor = p.seatFareMinor ?? perSeatFare; }); } - } else if (allFaresProvided) { - // seatFareMinor is in display currency — sum is already the display total - displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0); + } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) { + // Partial data on server: use client's total as best available. + displayTotalMinor = dto.reviewedTotalMinor; resolvedTotalMinor = displayCurrency !== Currency.ETB - ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) - : displayTotalMinor; + ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB) + : dto.reviewedTotalMinor; } else { resolvedTotalMinor = fareCalculation.totalMinor; displayTotalMinor = displayCurrency !== Currency.ETB @@ -1028,8 +1038,6 @@ export class BookingsService { // Track per-seat fare. Use client-supplied seatFareMinor/returnSeatFareMinor when // present (berth-specific pricing). Fall back to fare engine values. - let outboundFreeChildUsed = false; - let returnFreeChildUsed = false; const passengersWithFares = passengersData.map(p => { let outboundFareMinor: number; let returnFareMinor: number; @@ -1037,14 +1045,12 @@ export class BookingsService { if (p.category === PassengerCategory.ADULT) { outboundFareMinor = p.seatFareMinor ?? outboundFare.baseFareMinor; returnFareMinor = p.returnSeatFareMinor ?? returnFare.baseFareMinor; - } else if (dto.packageId) { - outboundFareMinor = 0; - returnFareMinor = 0; } else { - if (!outboundFreeChildUsed) { outboundFareMinor = 0; outboundFreeChildUsed = true; } - else outboundFareMinor = p.seatFareMinor ?? outboundFare.baseFareMinor; - if (!returnFreeChildUsed) { returnFareMinor = 0; returnFreeChildUsed = true; } - else returnFareMinor = p.returnSeatFareMinor ?? returnFare.baseFareMinor; + // Free children have no outbound seat (frontend excludes them). + // Guard by outboundSeatId: unseated = free (0), seated = paid child. + // Applies to both package and regular bookings. + outboundFareMinor = p.outboundSeatId ? (p.seatFareMinor ?? outboundFare.baseFareMinor) : 0; + returnFareMinor = p.outboundSeatId ? (p.returnSeatFareMinor ?? returnFare.baseFareMinor) : 0; } return { ...p, outboundFareMinor, returnFareMinor }; @@ -1052,33 +1058,39 @@ export class BookingsService { // Override totalMinor with the sum of actual per-seat fares when all seated passengers // supplied their fares — free children (no seatId) are excluded from the check. + // Same priority logic as one-way: server-computed sum wins when all per-seat fares + // are present; reviewedTotalMinor is used only as fallback to avoid a frontend + // race condition from under-counting passengers. const rtSeatedPassengers = passengersData.filter(p => p.outboundSeatId); const allRTFaresProvided = rtSeatedPassengers.length > 0 && rtSeatedPassengers.every(p => p.seatFareMinor != null && p.returnSeatFareMinor != null); - if (dto.reviewedTotalMinor != null) { - displayTotalMinor = dto.reviewedTotalMinor; - totalMinor = displayCurrency !== Currency.ETB - ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB) - : dto.reviewedTotalMinor; - // For package bookings where per-seat fares weren't supplied, back-derive the - // per-leg per-seat fare from reviewedTotalMinor so BookingSeat.fareMinor reflects - // the actual berth price (Upper/Middle/Lower) rather than the tier's minimum price. - if (dto.packageId) { - const seatedCount = passengersData.filter(p => p.outboundSeatId).length; - if (seatedCount > 0) { - const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2)); - passengersWithFares.forEach(p => { - if (p.outboundFareMinor > 0) p.outboundFareMinor = p.seatFareMinor ?? perSeatPerLeg; - if (p.returnFareMinor > 0) p.returnFareMinor = p.returnSeatFareMinor ?? perSeatPerLeg; - }); - } - } - } else if (allRTFaresProvided && !dto.packageId) { - // seatFareMinor/returnSeatFareMinor are in display currency — sum is already the display total + + if (allRTFaresProvided && !dto.packageId) { displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0); totalMinor = displayCurrency !== Currency.ETB ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) : displayTotalMinor; + if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor !== displayTotalMinor) { + this.logger.warn(`createRoundTripBooking: reviewedTotalMinor=${dto.reviewedTotalMinor} ignored — using server-computed sum=${displayTotalMinor}`); + } + } else if (dto.packageId && dto.reviewedTotalMinor != null) { + displayTotalMinor = dto.reviewedTotalMinor; + totalMinor = displayCurrency !== Currency.ETB + ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB) + : dto.reviewedTotalMinor; + const seatedCount = rtSeatedPassengers.length; + if (seatedCount > 0) { + const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2)); + passengersWithFares.forEach(p => { + if (p.outboundFareMinor > 0) p.outboundFareMinor = p.seatFareMinor ?? perSeatPerLeg; + if (p.returnFareMinor > 0) p.returnFareMinor = p.returnSeatFareMinor ?? perSeatPerLeg; + }); + } + } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) { + displayTotalMinor = dto.reviewedTotalMinor; + totalMinor = displayCurrency !== Currency.ETB + ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB) + : dto.reviewedTotalMinor; } const booking = await this.prisma.booking.create({ 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 17e3a585e..af5321e2d 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 @@ -204,43 +204,41 @@ export class GuestBookingService { const taxesMinor = 0; // Per-seat fare: use client-supplied seatFareMinor when present (berth-specific pricing). - // Free children (first child, non-package) get fareMinor=0. - let freeChildUsed = false; - let pkgChildIdx = 0; const passengersWithFares = passengersData.map(p => { let fareMinor: number; if (p.category === PassengerCategory.ADULT) { fareMinor = p.seatFareMinor ?? baseFareMinor; - } else if (isPackageOneway) { - fareMinor = pkgChildIdx < adultCount ? 0 : (p.seatFareMinor ?? childUnitFare); - pkgChildIdx++; } else { - if (!freeChildUsed) { fareMinor = 0; freeChildUsed = true; } - else fareMinor = p.seatFareMinor ?? childUnitFare; + // Free children have no seat (frontend excludes them from the DTO). + // Guard by seatId: unseated = free (0), seated = paid child. + // Applies to both package and regular bookings. + fareMinor = p.seatId ? (p.seatFareMinor ?? childUnitFare) : 0; } return { ...p, fareMinor }; }); - // reviewedTotalMinor and seatFareMinor are both in display-currency minor units. - // Store as displayTotalMinor as-is; back-convert to ETB for totalMinor. + // Server-computed sum from per-seat fares is the authoritative total when all + // seated passengers supplied seatFareMinor. This prevents a frontend race + // condition (fareBreakdown not yet loaded → only partial fares summed → + // reviewedTotalMinor reflects one passenger's fare instead of all). const displayCurrency = dto.displayCurrency || Currency.ETB; const seatedPassengers = passengersData.filter(p => p.seatId); const allFaresProvided = seatedPassengers.length > 0 && seatedPassengers.every(p => p.seatFareMinor != null); let displayTotalMinor: number; let resolvedTotalMinor: number; - if (dto.reviewedTotalMinor != null) { + if (allFaresProvided && !isPackageOneway) { + displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0); + } else if (isPackageOneway && dto.reviewedTotalMinor != null) { displayTotalMinor = dto.reviewedTotalMinor; - // For package bookings, back-derive per-seat fareMinor from reviewedTotalMinor - // so BookingSeat records store the actual berth price, not the tier minimum. - if (isPackageOneway && seatedPassengers.length > 0) { + if (seatedPassengers.length > 0) { const perSeatFare = Math.round(dto.reviewedTotalMinor / seatedPassengers.length); passengersWithFares.forEach(p => { if (p.fareMinor > 0) p.fareMinor = p.seatFareMinor ?? perSeatFare; }); } - } else if (allFaresProvided) { - displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0); + } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) { + displayTotalMinor = dto.reviewedTotalMinor; } else { // fare engine returns ETB — convert forward to display currency const etbTotal = Math.max(0, totalBaseFareMinor - discountMinor); @@ -494,22 +492,18 @@ export class GuestBookingService { : totalMinor; // Per-seat fares: use client-supplied seatFareMinor/returnSeatFareMinor when present. - let outboundFreeChildUsed = false; - let returnFreeChildUsed = false; const passengersWithFares = passengersData.map(p => { let outboundFareMinor: number; let returnFareMinor: number; if (p.category === PassengerCategory.ADULT) { outboundFareMinor = p.seatFareMinor ?? outboundBaseFare; returnFareMinor = p.returnSeatFareMinor ?? returnBaseFare; - } else if (isPackageRoundTrip) { - outboundFareMinor = 0; - returnFareMinor = 0; } else { - if (!outboundFreeChildUsed) { outboundFareMinor = 0; outboundFreeChildUsed = true; } - else outboundFareMinor = p.seatFareMinor ?? outboundChildUnitFare; - if (!returnFreeChildUsed) { returnFareMinor = 0; returnFreeChildUsed = true; } - else returnFareMinor = p.returnSeatFareMinor ?? returnChildUnitFare; + // Free children have no seat (frontend excludes them from the DTO). + // Guard by seatId: unseated = free (0), seated = paid child. + // Applies to both package and regular bookings. + outboundFareMinor = p.seatId ? (p.seatFareMinor ?? outboundChildUnitFare) : 0; + returnFareMinor = p.seatId ? (p.returnSeatFareMinor ?? returnChildUnitFare) : 0; } return { ...p, outboundFareMinor, returnFareMinor }; }); @@ -519,25 +513,28 @@ export class GuestBookingService { const rtSeatedPassengers = passengersData.filter(p => p.seatId); const allRTFaresProvided = rtSeatedPassengers.length > 0 && rtSeatedPassengers.every(p => p.seatFareMinor != null && p.returnSeatFareMinor != null); - if (dto.reviewedTotalMinor != null) { + + if (allRTFaresProvided && !isPackageRoundTrip) { + // Server-computed sum is authoritative — prevents race-condition under-count. + displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0); + totalMinor = displayCurrency !== Currency.ETB + ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) + : displayTotalMinor; + } else if (isPackageRoundTrip && dto.reviewedTotalMinor != null) { displayTotalMinor = dto.reviewedTotalMinor; totalMinor = displayCurrency !== Currency.ETB ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) : displayTotalMinor; - // For package bookings, back-derive per-leg per-seat fareMinor from reviewedTotalMinor. - if (isPackageRoundTrip) { - const seatedCount = passengersData.filter(p => p.seatId).length; - if (seatedCount > 0) { - const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2)); - passengersWithFares.forEach(p => { - if (p.outboundFareMinor > 0) p.outboundFareMinor = p.seatFareMinor ?? perSeatPerLeg; - if (p.returnFareMinor > 0) p.returnFareMinor = p.returnSeatFareMinor ?? perSeatPerLeg; - }); - } + const seatedCount = passengersData.filter(p => p.seatId).length; + if (seatedCount > 0) { + const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2)); + passengersWithFares.forEach(p => { + if (p.outboundFareMinor > 0) p.outboundFareMinor = p.seatFareMinor ?? perSeatPerLeg; + if (p.returnFareMinor > 0) p.returnFareMinor = p.returnSeatFareMinor ?? perSeatPerLeg; + }); } - } else if (allRTFaresProvided && !isPackageRoundTrip) { - // seatFareMinor/returnSeatFareMinor are display-currency — sum is already display total - displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0); + } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) { + displayTotalMinor = dto.reviewedTotalMinor; totalMinor = displayCurrency !== Currency.ETB ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB) : displayTotalMinor; 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 c1a30f243..e16f8f025 100644 --- a/apps/edr-passenger-api/src/modules/packages/packages.service.ts +++ b/apps/edr-passenger-api/src/modules/packages/packages.service.ts @@ -236,18 +236,27 @@ export class PackagesService { }); if (!pkg) throw new NotFoundException('Package not found'); - // Fetch live route stops so the departure station dropdown always reflects - // the current route definition, not stale TripStopTime snapshots. + // Build departure station list from live route stops when a routeId exists, + // falling back to the schedule's own stopTimes (already included in the query). let routeStops: { sequence: number; station: any }[] = []; if (pkg.outboundSchedule.routeId) { const stops = await this.prisma.routeStop.findMany({ where: { routeId: pkg.outboundSchedule.routeId }, orderBy: { sequence: 'asc' }, }); - const stationIds = stops.map((s) => s.stationId); - const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } }); - const stationMap = Object.fromEntries(stations.map((s) => [s.id, s])); - routeStops = stops.map((s) => ({ sequence: s.sequence, station: stationMap[s.stationId] })); + if (stops.length > 0) { + const stationIds = stops.map((s) => s.stationId); + const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } }); + const stationMap = Object.fromEntries(stations.map((s) => [s.id, s])); + routeStops = stops.map((s) => ({ sequence: s.sequence, station: stationMap[s.stationId] })); + } + } + // Fall back to the schedule's own TripStopTimes when RouteStop table has no rows + // for this route (e.g. route exists but stops were never seeded). + if (routeStops.length === 0) { + routeStops = (pkg.outboundSchedule.stopTimes ?? []) + .filter((st: any) => st.station) + .map((st: any) => ({ sequence: st.sequence, station: st.station })); } return { diff --git a/apps/edr-passenger-api/src/modules/search/search.service.ts b/apps/edr-passenger-api/src/modules/search/search.service.ts index 89dfe0ca0..18bd9ce09 100644 --- a/apps/edr-passenger-api/src/modules/search/search.service.ts +++ b/apps/edr-passenger-api/src/modules/search/search.service.ts @@ -595,10 +595,20 @@ export class SearchService { }); const freeChildrenAllowed = groupFare.freeChildrenCount; - // Calculate per-passenger fare rate (engine called with 1 adult, 0 children — pure rate lookup) + // Pre-compute isFree per passenger index synchronously so the race-free + // counter assignment isn't corrupted by concurrent Promise.all resolution. let freeChildrenUsed = 0; + const isFreeByIndex = categorised.map(p => { + if (p.category === 'CHILD' && freeChildrenUsed < freeChildrenAllowed) { + freeChildrenUsed++; + return true; + } + return false; + }); + + // Calculate per-passenger fare rate (engine called with 1 adult, 0 children — pure rate lookup) const passengerLines = await Promise.all( - categorised.map(async (p) => { + categorised.map(async (p, idx) => { const fare = await this.fareEngine.calculate({ routeId: schedule.routeId!, originStationId: dto.originStationId, @@ -610,8 +620,7 @@ export class SearchService { childCount: 0, }); - const isFree = p.category === 'CHILD' && freeChildrenUsed < freeChildrenAllowed; - if (isFree) freeChildrenUsed++; + const isFree = isFreeByIndex[idx]; const fareMinor = isFree ? fare.premiumPerPassenger + fare.insurancePerPassenger 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 95d390182..0ec2599b7 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 @@ -190,13 +190,18 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p => } return null; } + // One-way: seat-specific fare (set at seat selection) is the authoritative price — + // it is exactly what was shown to the user. Use the fare-breakdown API only as fallback + // for cases where seatFareMinor was not captured (e.g. auto-assign without seat map). + if (p.seatFareMinor != null) { + return isPackageBooking ? p.seatFareMinor * 2 : p.seatFareMinor; + } if (!isPackageBooking && fareBreakdown?.passengers && index != null) { const line = fareBreakdown.passengers[index]; const displayFare = line?.displayFareMinor ?? line?.fareMinor; if (displayFare != null) return displayFare; } - if (p.seatFareMinor == null) return null; - return isPackageBooking ? p.seatFareMinor * 2 : p.seatFareMinor; + return null; }; const createBookingMutation = useMutation({ 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 2c5123def..361090b25 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 @@ -603,11 +603,20 @@ export default function PackageDetailPage() { })), }); - const outboundSched = toSchedule(ctx.outboundSchedule); + // booking-context returns flat originStationId/destinationStationId, not nested objects. + // Use the user's selected departure station as the true origin for both the + // search criteria and the schedule objects so hold/booking APIs get the right segment. + const outboundDestId = ctx.outboundSchedule.destinationStationId ?? ctx.outboundSchedule.destinationStation?.id ?? ""; + + const outboundSched = { + ...toSchedule(ctx.outboundSchedule), + originStationId: departureStationId, + origin: departureStationName, + }; setSearchCriteria({ - originStationId: ctx.outboundSchedule.originStation?.id ?? "", - destinationStationId: ctx.outboundSchedule.destinationStation?.id ?? "", + originStationId: departureStationId, + destinationStationId: outboundDestId, departureDate: ctx.outboundSchedule.departureAt?.slice(0, 10) ?? "", returnDate: ctx.returnSchedule?.departureAt?.slice(0, 10), tripType: isRoundTrip ? "ROUND_TRIP" : "ONE_WAY", @@ -618,7 +627,11 @@ export default function PackageDetailPage() { if (isRoundTrip) { setOutboundSchedule(outboundSched); - setInboundSchedule(toSchedule(ctx.returnSchedule)); + setInboundSchedule({ + ...toSchedule(ctx.returnSchedule), + destinationStationId: departureStationId, + destination: departureStationName, + }); } else { setSelectedSchedule(outboundSched); }