From 8bda06b2267c1ae3ff585d430dc44831ca9b7a44 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Wed, 15 Jul 2026 19:50:32 +0300 Subject: [PATCH] Currency issue resolution --- .../src/modules/bookings/bookings.service.ts | 5 +- .../modules/bookings/guest-booking.service.ts | 385 +++++++++--------- 2 files changed, 198 insertions(+), 192 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 563a095aa..2a9de63d2 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -881,6 +881,7 @@ export class BookingsService { status: 'PENDING_PAYMENT', bookingType: 'ONE_WAY', totalMinor: resolvedTotalMinor, + currency: displayCurrency, adultCount, childCount, displayCurrency, @@ -1058,6 +1059,7 @@ export class BookingsService { status: 'PENDING_PAYMENT', bookingType: 'ROUND_TRIP', totalMinor, + currency: displayCurrency, adultCount, childCount, displayCurrency, @@ -1250,6 +1252,7 @@ export class BookingsService { status: 'PENDING_PAYMENT', bookingType: 'TRANSIT', totalMinor, + currency: displayCurrency, adultCount, childCount, displayCurrency, @@ -1459,7 +1462,7 @@ export class BookingsService { destinationStationId: dto.leg2DestinationStationId, status: 'PENDING_PAYMENT', bookingType: 'ROUND_TRIP_TRANSIT', - totalMinor, adultCount, childCount, displayCurrency, displayTotalMinor, + totalMinor, currency: displayCurrency, adultCount, childCount, displayCurrency, displayTotalMinor, // Outbound transit leg-2 leg2ScheduleId: dto.leg2ScheduleId, leg2OriginStationId: dto.transitStationId, 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 259beb404..821db5177 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 @@ -18,7 +18,7 @@ function generateRef(): string { } // Ethiopian mobile prefixes: Ethio Telecom (09xx) and Safaricom ET (07xx) -const ETH_MOBILE_PREFIXES = ['911','912','913','914','915','916','917','921','922','923','924','930','931','932','933','934','935','936','937','938','939','961','962','963','964']; +const ETH_MOBILE_PREFIXES = ['911', '912', '913', '914', '915', '916', '917', '921', '922', '923', '924', '930', '931', '932', '933', '934', '935', '936', '937', '938', '939', '961', '962', '963', '964']; function generateEthiopianPhone(): string { const prefix = ETH_MOBILE_PREFIXES[Math.floor(Math.random() * ETH_MOBILE_PREFIXES.length)]; @@ -50,7 +50,7 @@ export class GuestBookingService { private passengerAuthService: PassengerAuthService, private fareEngine: FareEngineService, private eventEmitter: EventEmitter2, - ) {} + ) { } async createGuestBooking(dto: CreateGuestBookingDto, req?: any) { // Enrich passengers with phone/email from SavedPassengerProfile when not supplied inline. @@ -70,8 +70,8 @@ export class GuestBookingService { }); } } - if (dto.bookingType === 'ROUND_TRIP') return this.createGuestRoundTripBooking(dto, req); - if (dto.bookingType === 'TRANSIT') return this.createGuestTransitBooking(dto, req); + if (dto.bookingType === 'ROUND_TRIP') return this.createGuestRoundTripBooking(dto, req); + if (dto.bookingType === 'TRANSIT') return this.createGuestTransitBooking(dto, req); if (dto.bookingType === 'ROUND_TRIP_TRANSIT') return this.createGuestRoundTripTransitBooking(dto, req); return this.createGuestOneWayBooking(dto, req); } @@ -123,8 +123,8 @@ export class GuestBookingService { let nationality = passenger.nationality; const isEthiopian = passenger.nationality === 'Ethiopian' || - passenger.nationality === 'ETHIOPIAN' || - passenger.idDocumentType === IdDocumentType.NATIONAL_ID; + passenger.nationality === 'ETHIOPIAN' || + passenger.idDocumentType === IdDocumentType.NATIONAL_ID; if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) { if (passenger.idDocumentNumber) { @@ -278,13 +278,14 @@ export class GuestBookingService { destinationStationId: dto.destinationStationId, status: 'PENDING_PAYMENT', totalMinor: resolvedTotalMinor, + currency: displayCurrency, adultCount, childCount, displayCurrency, displayTotalMinor, bookingType: 'ONE_WAY', ...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}), - userAgent: dto.deviceId, + userAgent: dto.deviceId, contactEmail: firstPassenger.email || null, contactPhone: firstPassenger.phone || null, seats: { @@ -350,7 +351,7 @@ export class GuestBookingService { this.prisma.seatHold.findUnique({ where: { id: dto.returnHoldId } }), ]); if (!outboundHold || outboundHold.expiresAt < new Date()) throw new BadRequestException('Outbound seat hold expired or not found'); - if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired or not found'); + if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired or not found'); // Validate passengers have returnSeatId for (const p of dto.passengers) { @@ -369,7 +370,7 @@ export class GuestBookingService { }), ]); if (!outboundSchedule) throw new NotFoundException('Outbound schedule not found'); - if (!returnSchedule) throw new NotFoundException('Return schedule not found'); + if (!returnSchedule) throw new NotFoundException('Return schedule not found'); if (Date.now() >= outboundSchedule.departureAt.getTime() - BOOKING_CUTOFF_MS) { throw new BadRequestException('Bookings are not accepted within 30 minutes of departure'); @@ -379,19 +380,19 @@ export class GuestBookingService { const station = sched.originStationId === stationId ? sched.originStation : sched.destinationStation; return { stationId, sequence: seq, station }; }; - const obStops = outboundSchedule.stopTimes.length > 0 ? outboundSchedule.stopTimes : [synth(outboundSchedule, outboundSchedule.originStationId, 0), synth(outboundSchedule, outboundSchedule.destinationStationId, 1)]; - const retStops = returnSchedule.stopTimes.length > 0 ? returnSchedule.stopTimes : [synth(returnSchedule, returnSchedule.originStationId, 0), synth(returnSchedule, returnSchedule.destinationStationId, 1)]; - const outboundOriginStop = obStops.find((s: any) => s.stationId === dto.originStationId) ?? obStops[0]; - const outboundDestStop = obStops.find((s: any) => s.stationId === dto.destinationStationId) ?? obStops[obStops.length - 1]; - const returnOriginStop = retStops.find((s: any) => s.stationId === dto.returnOriginStationId) ?? retStops[0]; - const returnDestStop = retStops.find((s: any) => s.stationId === dto.returnDestinationStationId) ?? retStops[retStops.length - 1]; + const obStops = outboundSchedule.stopTimes.length > 0 ? outboundSchedule.stopTimes : [synth(outboundSchedule, outboundSchedule.originStationId, 0), synth(outboundSchedule, outboundSchedule.destinationStationId, 1)]; + const retStops = returnSchedule.stopTimes.length > 0 ? returnSchedule.stopTimes : [synth(returnSchedule, returnSchedule.originStationId, 0), synth(returnSchedule, returnSchedule.destinationStationId, 1)]; + const outboundOriginStop = obStops.find((s: any) => s.stationId === dto.originStationId) ?? obStops[0]; + const outboundDestStop = obStops.find((s: any) => s.stationId === dto.destinationStationId) ?? obStops[obStops.length - 1]; + const returnOriginStop = retStops.find((s: any) => s.stationId === dto.returnOriginStationId) ?? retStops[0]; + const returnDestStop = retStops.find((s: any) => s.stationId === dto.returnDestinationStationId) ?? retStops[retStops.length - 1]; if (!outboundOriginStop || !outboundDestStop) throw new NotFoundException('Outbound origin or destination not found on schedule'); - if (!returnOriginStop || !returnDestStop) throw new NotFoundException('Return origin or destination not found on schedule'); + if (!returnOriginStop || !returnDestStop) throw new NotFoundException('Return origin or destination not found on schedule'); const outboundSegmentRoute = `${outboundOriginStop.station.code}-${outboundDestStop.station.code}`; - const outboundFullRoute = `${outboundSchedule.originStation.code}-${outboundSchedule.destinationStation.code}`; - const returnSegmentRoute = `${returnOriginStop.station.code}-${returnDestStop.station.code}`; - const returnFullRoute = `${returnSchedule.originStation.code}-${returnSchedule.destinationStation.code}`; + const outboundFullRoute = `${outboundSchedule.originStation.code}-${outboundSchedule.destinationStation.code}`; + const returnSegmentRoute = `${returnOriginStop.station.code}-${returnDestStop.station.code}`; + const returnFullRoute = `${returnSchedule.originStation.code}-${returnSchedule.destinationStation.code}`; // Process passengers (verify identity once — same person travels both legs) const passengersData: any[] = []; @@ -409,16 +410,16 @@ export class GuestBookingService { let nationality = passenger.nationality; const isEthiopian = passenger.nationality === 'Ethiopian' || - passenger.nationality === 'ETHIOPIAN' || - passenger.idDocumentType === IdDocumentType.NATIONAL_ID; + passenger.nationality === 'ETHIOPIAN' || + passenger.idDocumentType === IdDocumentType.NATIONAL_ID; if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) { if (passenger.idDocumentNumber) { const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber); if (!verification.verified) throw new BadRequestException(`Verifayda verification failed for ${passenger.passengerName}: ${verification.failureReason}`); - passengerName = verification.passengerData?.fullName || passengerName; + passengerName = verification.passengerData?.fullName || passengerName; verifaydaVerified = true; - verifaydaData = verification.passengerData?.profileData; + verifaydaData = verification.passengerData?.profileData; } nationality = 'Ethiopian'; } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) { @@ -450,7 +451,7 @@ export class GuestBookingService { returnBaseFare = tier.priceMinor - halfMinor; paidChildrenCount = childCount; outboundChildUnitFare = Math.round(outboundBaseFare * 0.1); - returnChildUnitFare = Math.round(returnBaseFare * 0.1); + returnChildUnitFare = Math.round(returnBaseFare * 0.1); } else { const primaryNationality = passengersData[0]?.nationality; [outboundBaseFare, returnBaseFare] = await Promise.all([ @@ -459,11 +460,11 @@ export class GuestBookingService { ]); paidChildrenCount = Math.max(0, childCount - 1); outboundChildUnitFare = outboundBaseFare; - returnChildUnitFare = returnBaseFare; + returnChildUnitFare = returnBaseFare; } - const outboundTotalBase = outboundBaseFare * adultCount + outboundChildUnitFare * paidChildrenCount; - const returnTotalBase = returnBaseFare * adultCount + returnChildUnitFare * paidChildrenCount; - const combinedBaseFareMinor = outboundTotalBase + returnTotalBase; + const outboundTotalBase = outboundBaseFare * adultCount + outboundChildUnitFare * paidChildrenCount; + const returnTotalBase = returnBaseFare * adultCount + returnChildUnitFare * paidChildrenCount; + const combinedBaseFareMinor = outboundTotalBase + returnTotalBase; let discountMinor = 0; if (dto.promoCode) { @@ -490,16 +491,16 @@ export class GuestBookingService { let outboundFareMinor: number; let returnFareMinor: number; if (p.category === PassengerCategory.ADULT) { - outboundFareMinor = p.seatFareMinor ?? outboundBaseFare; - returnFareMinor = p.returnSeatFareMinor ?? returnBaseFare; + outboundFareMinor = p.seatFareMinor ?? outboundBaseFare; + returnFareMinor = p.returnSeatFareMinor ?? returnBaseFare; } else if (isPackageRoundTrip) { outboundFareMinor = 0; - returnFareMinor = 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; + if (!returnFreeChildUsed) { returnFareMinor = 0; returnFreeChildUsed = true; } + else returnFareMinor = p.returnSeatFareMinor ?? returnChildUnitFare; } return { ...p, outboundFareMinor, returnFareMinor }; }); @@ -527,69 +528,70 @@ export class GuestBookingService { // Create booking with outbound seats; return seats confirmed separately const outboundSeatIds = dto.passengers.map(p => p.seatId).filter((id): id is string => !!id); - const returnSeatIds = dto.passengers.map(p => p.returnSeatId!); + const returnSeatIds = dto.passengers.map(p => p.returnSeatId!); const booking = await this.prisma.booking.create({ data: { - bookingRef: generateRef(), - passengerId: guestPassengerId, - scheduleId: dto.scheduleId, - originStationId: dto.originStationId, - destinationStationId: dto.destinationStationId, - status: 'PENDING_PAYMENT', - bookingType: 'ROUND_TRIP', + bookingRef: generateRef(), + passengerId: guestPassengerId, + scheduleId: dto.scheduleId, + originStationId: dto.originStationId, + destinationStationId: dto.destinationStationId, + status: 'PENDING_PAYMENT', + bookingType: 'ROUND_TRIP', totalMinor, + currency: displayCurrency, adultCount, childCount, displayCurrency, displayTotalMinor, - returnScheduleId: dto.returnScheduleId, - returnOriginStationId: dto.returnOriginStationId, - returnDestinationStationId: dto.returnDestinationStationId, - returnHoldId: dto.returnHoldId, + returnScheduleId: dto.returnScheduleId, + returnOriginStationId: dto.returnOriginStationId, + returnDestinationStationId: dto.returnDestinationStationId, + returnHoldId: dto.returnHoldId, returnSeatClassId, - returnLegStatus: 'NEITHER_USED', + returnLegStatus: 'NEITHER_USED', ...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}), - userAgent: dto.deviceId, - contactEmail: passengersData[0]?.email || null, - contactPhone: passengersData[0]?.phone || null, + userAgent: dto.deviceId, + contactEmail: passengersData[0]?.email || null, + contactPhone: passengersData[0]?.phone || null, seats: { create: [ ...passengersWithFares.map((p) => ({ - seat: { connect: { id: p.seatId } }, - leg: 1, - scheduleId: dto.scheduleId, - passengerName: p.passengerName, - dateOfBirth: p.dateOfBirth, + seat: { connect: { id: p.seatId } }, + leg: 1, + scheduleId: dto.scheduleId, + passengerName: p.passengerName, + dateOfBirth: p.dateOfBirth, passengerCategory: p.category, - idDocumentType: p.idDocumentType, - passportNumber: p.passportNumber, - passportCountry: p.passportCountry, + idDocumentType: p.idDocumentType, + passportNumber: p.passportNumber, + passportCountry: p.passportCountry, verifaydaVerified: p.verifaydaVerified, - verifaydaData: p.verifaydaData || undefined, - fareMinor: p.outboundFareMinor, + verifaydaData: p.verifaydaData || undefined, + fareMinor: p.outboundFareMinor, displayCurrency, })), ...passengersWithFares.map((p) => ({ - seat: { connect: { id: p.returnSeatId } }, - leg: 2, - scheduleId: dto.returnScheduleId, - passengerName: p.passengerName, - dateOfBirth: p.dateOfBirth, + seat: { connect: { id: p.returnSeatId } }, + leg: 2, + scheduleId: dto.returnScheduleId, + passengerName: p.passengerName, + dateOfBirth: p.dateOfBirth, passengerCategory: p.category, - idDocumentType: p.idDocumentType, - passportNumber: p.passportNumber, - passportCountry: p.passportCountry, + idDocumentType: p.idDocumentType, + passportNumber: p.passportNumber, + passportCountry: p.passportCountry, verifaydaVerified: p.verifaydaVerified, - verifaydaData: p.verifaydaData || undefined, - fareMinor: p.returnFareMinor, + verifaydaData: p.verifaydaData || undefined, + fareMinor: p.returnFareMinor, displayCurrency, })), ], }, } as any, include: { - seats: { include: { seat: { include: { coach: true } } } }, + seats: { include: { seat: { include: { coach: true } } } }, schedule: { include: { originStation: true, destinationStation: true, train: true } }, }, }); @@ -608,16 +610,16 @@ export class GuestBookingService { iamUserId, fareBreakdown: { outboundBaseFareMinor: outboundBaseFare, - returnBaseFareMinor: returnBaseFare, + returnBaseFareMinor: returnBaseFare, adultCount, childCount, - freeChildrenCount: isPackageRoundTrip ? 0 : Math.min(childCount, 1), + freeChildrenCount: isPackageRoundTrip ? 0 : Math.min(childCount, 1), paidChildrenCount, combinedBaseFareMinor, discountMinor, - taxesFeesMinor: taxesMinor, + taxesFeesMinor: taxesMinor, totalMinor, - currency: 'ETB', + currency: displayCurrency, displayCurrency, displayTotalMinor, }, @@ -658,9 +660,9 @@ export class GuestBookingService { } const leg1OriginStop = leg1Schedule.stopTimes.find(s => s.stationId === dto.originStationId); - const leg1DestStop = leg1Schedule.stopTimes.find(s => s.stationId === dto.transitStationId); + const leg1DestStop = leg1Schedule.stopTimes.find(s => s.stationId === dto.transitStationId); const leg2OriginStop = leg2Schedule.stopTimes.find(s => s.stationId === dto.transitStationId); - const leg2DestStop = leg2Schedule.stopTimes.find(s => s.stationId === dto.leg2DestinationStationId); + const leg2DestStop = leg2Schedule.stopTimes.find(s => s.stationId === dto.leg2DestinationStationId); if (!leg1OriginStop || !leg1DestStop) throw new NotFoundException('Leg-1 origin or transit station not found on schedule'); if (!leg2OriginStop || !leg2DestStop) throw new NotFoundException('Transit or leg-2 destination not found on leg-2 schedule'); @@ -695,9 +697,9 @@ export class GuestBookingService { passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality }); } - const leg2SeatClassId = dto.leg2SeatClassId || dto.seatClassId; + const leg2SeatClassId = dto.leg2SeatClassId || dto.seatClassId; const primaryNationality = passengersData[0]?.nationality; - const paidChildrenCount = Math.max(0, childCount - 1); + const paidChildrenCount = Math.max(0, childCount - 1); const [leg1BaseFare, leg2BaseFare] = await Promise.all([ this.getBaseFare(dto.scheduleId, dto.seatClassId, @@ -710,9 +712,9 @@ export class GuestBookingService { primaryNationality, dto.transitStationId, dto.leg2DestinationStationId), ]); - const leg1Total = leg1BaseFare * adultCount + leg1BaseFare * paidChildrenCount; - const leg2Total = leg2BaseFare * adultCount + leg2BaseFare * paidChildrenCount; - const combinedBase = leg1Total + leg2Total; + const leg1Total = leg1BaseFare * adultCount + leg1BaseFare * paidChildrenCount; + const leg2Total = leg2BaseFare * adultCount + leg2BaseFare * paidChildrenCount; + const combinedBase = leg1Total + leg2Total; let discountMinor = 0; if (dto.promoCode) { @@ -734,62 +736,63 @@ export class GuestBookingService { // Single booking — leg-1 seats at leg=1, leg-2 seats at leg=2 const booking = await this.prisma.booking.create({ data: { - bookingRef: generateRef(), - passengerId: guestPassengerId, - scheduleId: dto.scheduleId, - originStationId: dto.originStationId, - destinationStationId: dto.leg2DestinationStationId, - status: 'PENDING_PAYMENT', - bookingType: 'TRANSIT', + bookingRef: generateRef(), + passengerId: guestPassengerId, + scheduleId: dto.scheduleId, + originStationId: dto.originStationId, + destinationStationId: dto.leg2DestinationStationId, + status: 'PENDING_PAYMENT', + bookingType: 'TRANSIT', totalMinor, + currency: displayCurrency, adultCount, childCount, displayCurrency, displayTotalMinor, - leg2ScheduleId: dto.leg2ScheduleId, - leg2OriginStationId: dto.transitStationId, + leg2ScheduleId: dto.leg2ScheduleId, + leg2OriginStationId: dto.transitStationId, leg2DestinationStationId: dto.leg2DestinationStationId, - leg2SeatClassId: leg2SeatClassId, - userAgent: dto.deviceId, - contactEmail: passengersData[0]?.email || null, - contactPhone: passengersData[0]?.phone || null, + leg2SeatClassId: leg2SeatClassId, + userAgent: dto.deviceId, + contactEmail: passengersData[0]?.email || null, + contactPhone: passengersData[0]?.phone || null, seats: { create: [ ...passengersData.map(p => ({ - seat: { connect: { id: p.seatId } }, - leg: 1, - scheduleId: dto.scheduleId, - passengerName: p.passengerName, - dateOfBirth: p.dateOfBirth, + seat: { connect: { id: p.seatId } }, + leg: 1, + scheduleId: dto.scheduleId, + passengerName: p.passengerName, + dateOfBirth: p.dateOfBirth, passengerCategory: p.category, - idDocumentType: p.idDocumentType, - passportNumber: p.passportNumber, - passportCountry: p.passportCountry, + idDocumentType: p.idDocumentType, + passportNumber: p.passportNumber, + passportCountry: p.passportCountry, verifaydaVerified: p.verifaydaVerified, - verifaydaData: p.verifaydaData || undefined, - fareMinor: p.category === PassengerCategory.ADULT ? leg1BaseFare : (paidChildrenCount > 0 ? leg1BaseFare : 0), + verifaydaData: p.verifaydaData || undefined, + fareMinor: p.category === PassengerCategory.ADULT ? leg1BaseFare : (paidChildrenCount > 0 ? leg1BaseFare : 0), displayCurrency, })), ...passengersData.map(p => ({ - seat: { connect: { id: p.leg2SeatId! } }, - leg: 2, - scheduleId: dto.leg2ScheduleId, - passengerName: p.passengerName, - dateOfBirth: p.dateOfBirth, + seat: { connect: { id: p.leg2SeatId! } }, + leg: 2, + scheduleId: dto.leg2ScheduleId, + passengerName: p.passengerName, + dateOfBirth: p.dateOfBirth, passengerCategory: p.category, - idDocumentType: p.idDocumentType, - passportNumber: p.passportNumber, - passportCountry: p.passportCountry, + idDocumentType: p.idDocumentType, + passportNumber: p.passportNumber, + passportCountry: p.passportCountry, verifaydaVerified: p.verifaydaVerified, - verifaydaData: p.verifaydaData || undefined, - fareMinor: p.category === PassengerCategory.ADULT ? leg2BaseFare : (paidChildrenCount > 0 ? leg2BaseFare : 0), + verifaydaData: p.verifaydaData || undefined, + fareMinor: p.category === PassengerCategory.ADULT ? leg2BaseFare : (paidChildrenCount > 0 ? leg2BaseFare : 0), displayCurrency, })), ], }, } as any, include: { - seats: { include: { seat: { include: { coach: true } } } }, + seats: { include: { seat: { include: { coach: true } } } }, schedule: { include: { originStation: true, destinationStation: true, train: true } }, }, }); @@ -821,15 +824,15 @@ export class GuestBookingService { private async createGuestRoundTripTransitBooking(dto: CreateGuestBookingDto, req?: any) { if (!dto.leg2ScheduleId || !dto.leg2HoldId || !dto.transitStationId || !dto.leg2DestinationStationId || - !dto.returnScheduleId || !dto.returnHoldId || !dto.returnOriginStationId || !dto.returnDestinationStationId || - !dto.returnLeg2ScheduleId || !dto.returnLeg2HoldId || !dto.returnTransitStationId || !dto.returnLeg2DestinationStationId) { + !dto.returnScheduleId || !dto.returnHoldId || !dto.returnOriginStationId || !dto.returnDestinationStationId || + !dto.returnLeg2ScheduleId || !dto.returnLeg2HoldId || !dto.returnTransitStationId || !dto.returnLeg2DestinationStationId) { throw new BadRequestException( 'ROUND_TRIP_TRANSIT requires all 4 holds and all transit/return station fields', ); } for (const p of dto.passengers) { - if (!p.leg2SeatId) throw new BadRequestException(`leg2SeatId required for ${p.passengerName}`); - if (!p.returnSeatId) throw new BadRequestException(`returnSeatId required for ${p.passengerName}`); + if (!p.leg2SeatId) throw new BadRequestException(`leg2SeatId required for ${p.passengerName}`); + if (!p.returnSeatId) throw new BadRequestException(`returnSeatId required for ${p.passengerName}`); if (!p.returnLeg2SeatId) throw new BadRequestException(`returnLeg2SeatId required for ${p.passengerName}`); } @@ -840,19 +843,19 @@ export class GuestBookingService { this.prisma.seatHold.findUnique({ where: { id: dto.returnHoldId } }), this.prisma.seatHold.findUnique({ where: { id: dto.returnLeg2HoldId } }), ]); - if (!obL1Hold || obL1Hold.expiresAt < now) throw new BadRequestException('Outbound leg-1 hold expired'); - if (!obL2Hold || obL2Hold.expiresAt < now) throw new BadRequestException('Outbound leg-2 hold expired'); + if (!obL1Hold || obL1Hold.expiresAt < now) throw new BadRequestException('Outbound leg-1 hold expired'); + if (!obL2Hold || obL2Hold.expiresAt < now) throw new BadRequestException('Outbound leg-2 hold expired'); if (!retL1Hold || retL1Hold.expiresAt < now) throw new BadRequestException('Return leg-1 hold expired'); if (!retL2Hold || retL2Hold.expiresAt < now) throw new BadRequestException('Return leg-2 hold expired'); const [obL1Sched, obL2Sched, retL1Sched, retL2Sched] = await Promise.all([ - this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), - this.prisma.trainSchedule.findUnique({ where: { id: dto.leg2ScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), - this.prisma.trainSchedule.findUnique({ where: { id: dto.returnScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), - this.prisma.trainSchedule.findUnique({ where: { id: dto.returnLeg2ScheduleId },include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), + this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), + this.prisma.trainSchedule.findUnique({ where: { id: dto.leg2ScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), + this.prisma.trainSchedule.findUnique({ where: { id: dto.returnScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), + this.prisma.trainSchedule.findUnique({ where: { id: dto.returnLeg2ScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), ]); - if (!obL1Sched) throw new NotFoundException('Outbound leg-1 schedule not found'); - if (!obL2Sched) throw new NotFoundException('Outbound leg-2 schedule not found'); + if (!obL1Sched) throw new NotFoundException('Outbound leg-1 schedule not found'); + if (!obL2Sched) throw new NotFoundException('Outbound leg-2 schedule not found'); if (!retL1Sched) throw new NotFoundException('Return leg-1 schedule not found'); if (!retL2Sched) throw new NotFoundException('Return leg-2 schedule not found'); @@ -860,16 +863,16 @@ export class GuestBookingService { throw new BadRequestException('Bookings are not accepted within 30 minutes of departure'); } - const obL1Origin = obL1Sched.stopTimes.find(s => s.stationId === dto.originStationId); - const obL1Dest = obL1Sched.stopTimes.find(s => s.stationId === dto.transitStationId); - const obL2Origin = obL2Sched.stopTimes.find(s => s.stationId === dto.transitStationId); - const obL2Dest = obL2Sched.stopTimes.find(s => s.stationId === dto.leg2DestinationStationId); + const obL1Origin = obL1Sched.stopTimes.find(s => s.stationId === dto.originStationId); + const obL1Dest = obL1Sched.stopTimes.find(s => s.stationId === dto.transitStationId); + const obL2Origin = obL2Sched.stopTimes.find(s => s.stationId === dto.transitStationId); + const obL2Dest = obL2Sched.stopTimes.find(s => s.stationId === dto.leg2DestinationStationId); const retL1Origin = retL1Sched.stopTimes.find(s => s.stationId === dto.returnOriginStationId); - const retL1Dest = retL1Sched.stopTimes.find(s => s.stationId === dto.returnTransitStationId); + const retL1Dest = retL1Sched.stopTimes.find(s => s.stationId === dto.returnTransitStationId); const retL2Origin = retL2Sched.stopTimes.find(s => s.stationId === dto.returnTransitStationId); - const retL2Dest = retL2Sched.stopTimes.find(s => s.stationId === dto.returnLeg2DestinationStationId); - if (!obL1Origin || !obL1Dest) throw new NotFoundException('Outbound leg-1: origin or transit stop not found'); - if (!obL2Origin || !obL2Dest) throw new NotFoundException('Outbound leg-2: transit or destination stop not found'); + const retL2Dest = retL2Sched.stopTimes.find(s => s.stationId === dto.returnLeg2DestinationStationId); + if (!obL1Origin || !obL1Dest) throw new NotFoundException('Outbound leg-1: origin or transit stop not found'); + if (!obL2Origin || !obL2Dest) throw new NotFoundException('Outbound leg-2: transit or destination stop not found'); if (!retL1Origin || !retL1Dest) throw new NotFoundException('Return leg-1: origin or transit stop not found'); if (!retL2Origin || !retL2Dest) throw new NotFoundException('Return leg-2: transit or destination stop not found'); @@ -901,21 +904,21 @@ export class GuestBookingService { passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality }); } - const nat = passengersData[0]?.nationality; - const paidChildren = Math.max(0, childCount - 1); - const obL2ClassId = dto.leg2SeatClassId ?? dto.seatClassId; - const retL1ClassId = dto.returnSeatClassId ?? dto.seatClassId; - const retL2ClassId = dto.returnLeg2SeatClassId ?? dto.seatClassId; + const nat = passengersData[0]?.nationality; + const paidChildren = Math.max(0, childCount - 1); + const obL2ClassId = dto.leg2SeatClassId ?? dto.seatClassId; + const retL1ClassId = dto.returnSeatClassId ?? dto.seatClassId; + const retL2ClassId = dto.returnLeg2SeatClassId ?? dto.seatClassId; const [obL1Fare, obL2Fare, retL1Fare, retL2Fare] = await Promise.all([ - this.getBaseFare(dto.scheduleId, dto.seatClassId, `${obL1Origin.station.code}-${obL1Dest.station.code}`, `${obL1Sched.originStation.code}-${obL1Sched.destinationStation.code}`, nat, dto.originStationId, dto.transitStationId), - this.getBaseFare(dto.leg2ScheduleId!, obL2ClassId, `${obL2Origin.station.code}-${obL2Dest.station.code}`, `${obL2Sched.originStation.code}-${obL2Sched.destinationStation.code}`, nat, dto.transitStationId, dto.leg2DestinationStationId), - this.getBaseFare(dto.returnScheduleId!, retL1ClassId, `${retL1Origin.station.code}-${retL1Dest.station.code}`, `${retL1Sched.originStation.code}-${retL1Sched.destinationStation.code}`, nat, dto.returnOriginStationId, dto.returnTransitStationId), - this.getBaseFare(dto.returnLeg2ScheduleId!,retL2ClassId, `${retL2Origin.station.code}-${retL2Dest.station.code}`, `${retL2Sched.originStation.code}-${retL2Sched.destinationStation.code}`, nat, dto.returnTransitStationId, dto.returnLeg2DestinationStationId), + this.getBaseFare(dto.scheduleId, dto.seatClassId, `${obL1Origin.station.code}-${obL1Dest.station.code}`, `${obL1Sched.originStation.code}-${obL1Sched.destinationStation.code}`, nat, dto.originStationId, dto.transitStationId), + this.getBaseFare(dto.leg2ScheduleId!, obL2ClassId, `${obL2Origin.station.code}-${obL2Dest.station.code}`, `${obL2Sched.originStation.code}-${obL2Sched.destinationStation.code}`, nat, dto.transitStationId, dto.leg2DestinationStationId), + this.getBaseFare(dto.returnScheduleId!, retL1ClassId, `${retL1Origin.station.code}-${retL1Dest.station.code}`, `${retL1Sched.originStation.code}-${retL1Sched.destinationStation.code}`, nat, dto.returnOriginStationId, dto.returnTransitStationId), + this.getBaseFare(dto.returnLeg2ScheduleId!, retL2ClassId, `${retL2Origin.station.code}-${retL2Dest.station.code}`, `${retL2Sched.originStation.code}-${retL2Sched.destinationStation.code}`, nat, dto.returnTransitStationId, dto.returnLeg2DestinationStationId), ]); const combinedBase = (obL1Fare + obL2Fare + retL1Fare + retL2Fare) * adultCount + - (obL1Fare + obL2Fare + retL1Fare + retL2Fare) * paidChildren; + (obL1Fare + obL2Fare + retL1Fare + retL2Fare) * paidChildren; let discountMinor = 0; if (dto.promoCode) { const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } }); @@ -923,8 +926,8 @@ export class GuestBookingService { discountMinor = promo.percentOff ? Math.round(combinedBase * promo.percentOff / 100) : (promo.amountOffMinor ?? 0); } } - const taxesMinor = Math.round(combinedBase * 0.05); - const totalMinor = Math.max(0, combinedBase - discountMinor + taxesMinor); + const taxesMinor = Math.round(combinedBase * 0.05); + const totalMinor = Math.max(0, combinedBase - discountMinor + taxesMinor); const displayCurrency = dto.displayCurrency || Currency.ETB; const displayTotalMinor = displayCurrency !== Currency.ETB ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency) @@ -933,58 +936,58 @@ export class GuestBookingService { const { guestPassengerId, iamUserId, createdAccount } = await this.resolveGuestPassenger(dto, passengersData[0], req); const makeSeat = (p: any, seatId: string, leg: number, scheduleId: string, fare: number) => ({ - seat: { connect: { id: seatId } }, + seat: { connect: { id: seatId } }, leg, scheduleId, - passengerName: p.passengerName, - dateOfBirth: p.dateOfBirth, + passengerName: p.passengerName, + dateOfBirth: p.dateOfBirth, passengerCategory: p.category, - idDocumentType: p.idDocumentType, - passportNumber: p.passportNumber, - passportCountry: p.passportCountry, + idDocumentType: p.idDocumentType, + passportNumber: p.passportNumber, + passportCountry: p.passportCountry, verifaydaVerified: p.verifaydaVerified, - verifaydaData: p.verifaydaData || undefined, - fareMinor: p.category === PassengerCategory.ADULT ? fare : (paidChildren > 0 ? fare : 0), + verifaydaData: p.verifaydaData || undefined, + fareMinor: p.category === PassengerCategory.ADULT ? fare : (paidChildren > 0 ? fare : 0), displayCurrency, }); const booking = await this.prisma.booking.create({ data: { - bookingRef: generateRef(), - passengerId: guestPassengerId, - scheduleId: dto.scheduleId, - originStationId: dto.originStationId, - destinationStationId: dto.returnLeg2DestinationStationId, - status: 'PENDING_PAYMENT', - bookingType: 'ROUND_TRIP_TRANSIT', - totalMinor, adultCount, childCount, displayCurrency, displayTotalMinor, - leg2ScheduleId: dto.leg2ScheduleId, - leg2OriginStationId: dto.transitStationId, - leg2DestinationStationId: dto.leg2DestinationStationId, - leg2SeatClassId: obL2ClassId, - returnScheduleId: dto.returnScheduleId, - returnOriginStationId: dto.returnOriginStationId, - returnDestinationStationId: dto.returnDestinationStationId, - returnSeatClassId: retL1ClassId, - returnLeg2ScheduleId: dto.returnLeg2ScheduleId, - returnLeg2OriginStationId: dto.returnTransitStationId, - returnLeg2DestStationId: dto.returnLeg2DestinationStationId, - returnLeg2SeatClassId: retL2ClassId, - returnLegStatus: 'NEITHER_USED', - userAgent: dto.deviceId, - contactEmail: passengersData[0]?.email || null, - contactPhone: passengersData[0]?.phone || null, + bookingRef: generateRef(), + passengerId: guestPassengerId, + scheduleId: dto.scheduleId, + originStationId: dto.originStationId, + destinationStationId: dto.returnLeg2DestinationStationId, + status: 'PENDING_PAYMENT', + bookingType: 'ROUND_TRIP_TRANSIT', + totalMinor, currency: displayCurrency, adultCount, childCount, displayCurrency, displayTotalMinor, + leg2ScheduleId: dto.leg2ScheduleId, + leg2OriginStationId: dto.transitStationId, + leg2DestinationStationId: dto.leg2DestinationStationId, + leg2SeatClassId: obL2ClassId, + returnScheduleId: dto.returnScheduleId, + returnOriginStationId: dto.returnOriginStationId, + returnDestinationStationId: dto.returnDestinationStationId, + returnSeatClassId: retL1ClassId, + returnLeg2ScheduleId: dto.returnLeg2ScheduleId, + returnLeg2OriginStationId: dto.returnTransitStationId, + returnLeg2DestStationId: dto.returnLeg2DestinationStationId, + returnLeg2SeatClassId: retL2ClassId, + returnLegStatus: 'NEITHER_USED', + userAgent: dto.deviceId, + contactEmail: passengersData[0]?.email || null, + contactPhone: passengersData[0]?.phone || null, seats: { create: [ - ...passengersData.map(p => makeSeat(p, p.seatId, 1, dto.scheduleId, obL1Fare)), - ...passengersData.map(p => makeSeat(p, p.leg2SeatId!, 2, dto.leg2ScheduleId!, obL2Fare)), - ...passengersData.map(p => makeSeat(p, p.returnSeatId!, 3, dto.returnScheduleId!, retL1Fare)), - ...passengersData.map(p => makeSeat(p, p.returnLeg2SeatId!,4, dto.returnLeg2ScheduleId!,retL2Fare)), + ...passengersData.map(p => makeSeat(p, p.seatId, 1, dto.scheduleId, obL1Fare)), + ...passengersData.map(p => makeSeat(p, p.leg2SeatId!, 2, dto.leg2ScheduleId!, obL2Fare)), + ...passengersData.map(p => makeSeat(p, p.returnSeatId!, 3, dto.returnScheduleId!, retL1Fare)), + ...passengersData.map(p => makeSeat(p, p.returnLeg2SeatId!, 4, dto.returnLeg2ScheduleId!, retL2Fare)), ], }, } as any, include: { - seats: { include: { seat: { include: { coach: true } } } }, + seats: { include: { seat: { include: { coach: true } } } }, schedule: { include: { originStation: true, destinationStation: true, train: true } }, }, }); @@ -1006,14 +1009,14 @@ export class GuestBookingService { fareBreakdown: { outboundLeg1FareMinor: obL1Fare, outboundLeg2FareMinor: obL2Fare, - returnLeg1FareMinor: retL1Fare, - returnLeg2FareMinor: retL2Fare, + returnLeg1FareMinor: retL1Fare, + returnLeg2FareMinor: retL2Fare, adultCount, childCount, freeChildrenCount: Math.min(childCount, 1), paidChildrenCount: paidChildren, combinedBaseFareMinor: combinedBase, discountMinor, taxesFeesMinor: taxesMinor, totalMinor, - currency: 'ETB', displayCurrency, displayTotalMinor, + currency: displayCurrency, displayCurrency, displayTotalMinor, }, }; } @@ -1042,7 +1045,7 @@ export class GuestBookingService { const guestPassenger = await this.prisma.passenger.create({ data: {} }); await this.prisma.loyaltyAccount.create({ data: { passengerId: guestPassenger.id, pointsBalance: 0, tier: 'BRONZE' } }); await this.prisma.walletAccount.create({ data: { passengerId: guestPassenger.id, balanceMinor: 0 } }); - + return { guestPassengerId: guestPassenger.id, iamUserId: null, createdAccount: false }; } @@ -1052,7 +1055,7 @@ export class GuestBookingService { if (passenger.verifaydaData && typeof passenger.verifaydaData === 'object') { gender = passenger.verifaydaData.gender || passenger.verifaydaData.Gender || null; } - + await this.prisma.travelerProfile.create({ data: { passengerId, @@ -1130,8 +1133,8 @@ export class GuestBookingService { }), ]); - const premiumMinor = seatClass?.premiumMinor ?? 0; - const insuranceMinor = seatClass?.insuranceFeeMinor ?? 0; + const premiumMinor = seatClass?.premiumMinor ?? 0; + const insuranceMinor = seatClass?.insuranceFeeMinor ?? 0; const priorities = [ { tripId: scheduleId, route: segmentRoute, nationality },