From 25fdf88a77c9d8e647fee0d9c30830ef3061c524 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Wed, 15 Jul 2026 00:39:53 +0300 Subject: [PATCH] Booking currency updates, audit logging updates --- .../modules/bookings/bookings.controller.ts | 15 ++++++---- .../src/modules/bookings/bookings.dto.ts | 4 +-- .../src/modules/bookings/bookings.service.ts | 30 ++++++++++++++----- .../modules/bookings/guest-booking.service.ts | 4 +-- .../modules/currencies/currencies.module.ts | 3 +- .../modules/currencies/currencies.service.ts | 6 +++- .../excess-baggage/excess-baggage.module.ts | 3 +- .../excess-baggage/excess-baggage.service.ts | 7 ++++- .../src/modules/fleet/fleet.module.ts | 3 +- .../src/modules/fleet/fleet.service.ts | 26 +++++++++++----- .../src/modules/packages/packages.module.ts | 3 +- .../src/modules/packages/packages.service.ts | 23 ++++++++++---- .../modules/passengers/passengers.module.ts | 9 +++--- .../modules/passengers/passengers.service.ts | 4 ++- .../src/modules/payments/payments.module.ts | 2 ++ .../src/modules/payments/payments.service.ts | 6 ++++ .../src/modules/schedules/routes.service.ts | 9 ++++-- .../src/modules/schedules/schedules.module.ts | 3 +- .../modules/schedules/schedules.service.ts | 26 ++++++++++++---- .../seat-classes/seat-classes.module.ts | 3 +- .../seat-classes/seat-classes.service.ts | 15 +++++++--- .../src/modules/seats/seats.module.ts | 3 +- .../src/modules/seats/seats.service.ts | 8 +++-- .../src/modules/tickets/tickets.module.ts | 3 +- .../src/modules/tickets/tickets.service.ts | 7 ++++- .../portal/src/app/booking/review/page.tsx | 8 +++-- 26 files changed, 169 insertions(+), 64 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts index 854621e0d..88bbe6ec4 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts @@ -525,8 +525,11 @@ export class BookingsController { "Missing required fields for bookingType, or Verifayda verification failed", }) @ApiResponse({ status: 404, description: "Schedule or seat hold not found" }) - create(@Body() dto: CreateBookingDto) { - return this.service.create(dto); + create(@Req() req: any, @Body() dto: CreateBookingDto) { + // Always resolve passengerId from the authenticated JWT — never trust the request body + const iamUserId = req.user?.id; + if (!iamUserId) throw new UnauthorizedException(); + return this.service.create({ ...dto, passengerId: iamUserId }); } @Get(":id/usage") @@ -612,8 +615,8 @@ Results are ordered most-recent first. Use the returned \`bookingRef\` to open b status: 400, description: "Cannot modify cancelled or past bookings", }) - modify(@Body() dto: ModifyBookingDto) { - return this.service.modify(dto); + modify(@Req() req: any, @Body() dto: ModifyBookingDto) { + return this.service.modify(dto, req.user?.id); } @Delete(":id") @@ -658,7 +661,7 @@ Results are ordered most-recent first. Use the returned \`bookingRef\` to open b description: "Booking cancelled with refund amount", }) @ApiResponse({ status: 400, description: "Booking already cancelled" }) - cancel(@Param("bookingRef") ref: string, @Body() dto: CancelBookingDto) { - return this.service.cancel(ref, dto.reason); + cancel(@Req() req: any, @Param("bookingRef") ref: string, @Body() dto: CancelBookingDto) { + return this.service.cancel(ref, dto.reason, req.user?.id); } } diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.dto.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.dto.ts index 4b355c1fe..73887454c 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.dto.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.dto.ts @@ -88,8 +88,8 @@ export class RoundTripPassengerDto { } export class CreateBookingDto { - @ApiProperty({ description: 'Passenger ID' }) - @IsString() passengerId: string; + @ApiPropertyOptional({ description: 'Passenger ID — resolved automatically from JWT token; only required for agent/back-office calls' }) + @IsOptional() @IsString() passengerId: string; @ApiProperty({ description: 'Outbound / leg-1 schedule ID' }) @IsString() scheduleId: string; 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 70fef6b08..dea2517c0 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -12,6 +12,7 @@ import { FareEngineService } from '../fare-engine/fare-engine.service'; import { Currency, PassengerCategory, IdDocumentType } from '@prisma/client'; import { resolveCurrencyFromNationality } from '../fare-engine/fare-engine.dto'; import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception'; +import { AuditService } from '../../common/audit.service'; function generateRef(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; @@ -104,6 +105,7 @@ export class BookingsService { private readonly verifaydaService: VerifaydaService, private readonly currencyService: CurrencyService, private readonly fareEngine: FareEngineService, + private readonly auditService: AuditService, ) {} async findByIamUserId(iamUserId: string, filters: BookingFilters = {}) { @@ -151,7 +153,7 @@ export class BookingsService { bookingRef: booking.bookingRef, status: booking.status, totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount), - currency: 'ETB', + currency: booking.displayCurrency, displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor, adultCount: booking.adultCount, @@ -295,7 +297,7 @@ export class BookingsService { bookingRef: booking.bookingRef, status: booking.status, totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount), - currency: 'ETB', + currency: booking.displayCurrency, displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor, adultCount: booking.adultCount, @@ -408,7 +410,7 @@ export class BookingsService { bookingRef: booking.bookingRef, status: booking.status, totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount), - currency: 'ETB', + currency: booking.displayCurrency, displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor, adultCount: booking.adultCount, @@ -681,7 +683,7 @@ export class BookingsService { bookingRef: booking.bookingRef, status: booking.status, totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount), - currency: 'ETB', + currency: booking.displayCurrency, displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor, contactEmail: resolvedEmail, @@ -756,7 +758,14 @@ export class BookingsService { }; } - async create(dto: CreateBookingDto) { + async create(dto: CreateBookingDto) { + // Resolve passengerId from iamUserId when the caller is authenticated + if (dto.passengerId && !dto.passengerId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) { + // passengerId is actually an iamUserId — resolve the passenger record + const passenger = await this.prisma.passenger.findUnique({ where: { iamUserId: dto.passengerId }, select: { id: true } }); + if (!passenger) throw new NotFoundException('Passenger profile not found for this account'); + dto = { ...dto, passengerId: passenger.id }; + } if (dto.bookingType === 'ROUND_TRIP') return this.createRoundTripBooking(dto); if (dto.bookingType === 'TRANSIT') return this.createTransitBooking(dto); if (dto.bookingType === 'ROUND_TRIP_TRANSIT') return this.createRoundTripTransitBooking(dto); @@ -906,6 +915,7 @@ export class BookingsService { }); } this.eventEmitter.emit('booking.created', { booking }); + await this.auditService.log({ userId: dto.passengerId, action: 'CREATE', entityType: 'Booking', entityId: booking.id, newData: { bookingRef: booking.bookingRef, bookingType: 'ONE_WAY', totalMinor: resolvedTotalMinor } }); return { ...booking, fareBreakdown: fareCalculation }; } @@ -1118,6 +1128,7 @@ export class BookingsService { } this.eventEmitter.emit('booking.created', { booking }); + await this.auditService.log({ userId: dto.passengerId, action: 'CREATE', entityType: 'Booking', entityId: booking.id, newData: { bookingRef: booking.bookingRef, bookingType: 'ROUND_TRIP', totalMinor } }); return { ...booking, @@ -1129,7 +1140,7 @@ export class BookingsService { loyaltyRedemptionMinor: loyaltyMinor, taxesFeesMinor: taxesMinor, totalMinor, - currency: 'ETB', + currency: booking.displayCurrency, displayCurrency, displayTotalMinor } @@ -1941,7 +1952,7 @@ export class BookingsService { }; } - async modify(dto: ModifyBookingDto) { + async modify(dto: ModifyBookingDto, iamUserId?: string) { const booking = await this.prisma.booking.findUnique({ where: { bookingRef: dto.bookingRef }, include: { seats: true, schedule: true } }); if (!booking) throw new NotFoundException('Booking not found'); if (booking.status !== 'CONFIRMED') throw new BadRequestException('Only confirmed bookings can be modified'); @@ -1953,10 +1964,11 @@ export class BookingsService { }); await this.seatsService.releaseSeats(booking.id); await this.seatsService.confirmSeats(dto.newSeatIds); + await this.auditService.log({ userId: iamUserId ?? booking.passengerId, action: 'UPDATE', entityType: 'Booking', entityId: booking.id, oldData: { seatIds: oldSeats }, newData: { seatIds: dto.newSeatIds, reason: dto.reason } }); return { modified: true, bookingRef: dto.bookingRef }; } - async cancel(bookingRef: string, reason?: string) { + async cancel(bookingRef: string, reason?: string, iamUserId?: string) { const booking = await this.prisma.booking.findUnique({ where: { bookingRef }, include: { seats: true, paymentIntent: true } }); if (!booking) throw new NotFoundException('Booking not found'); if (booking.status === 'CANCELLED') throw new BadRequestException('Booking already cancelled'); @@ -1965,6 +1977,7 @@ export class BookingsService { await this.seatsService.releaseSeats(booking.id); await this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } }); this.eventEmitter.emit('booking.cancelled', { booking, refundAmount }); + await this.auditService.log({ userId: iamUserId ?? booking.passengerId, action: 'DELETE', entityType: 'Booking', entityId: booking.id, oldData: { bookingRef, status: booking.status }, newData: { status: 'CANCELLED', reason, refundAmount } }); return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' }; } @@ -2031,6 +2044,7 @@ export class BookingsService { await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } }); await this.prisma.booking.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'Booking', entityId: id, oldData: { bookingRef: booking.bookingRef } }); return { deleted: true, bookingRef: booking.bookingRef }; } 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 080b4404f..321e2269b 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 @@ -324,7 +324,7 @@ export class GuestBookingService { discountMinor, taxesFeesMinor: taxesMinor, totalMinor: resolvedTotalMinor, - currency: 'ETB', + currency: booking.displayCurrency, displayCurrency, displayTotalMinor, }, @@ -805,7 +805,7 @@ export class GuestBookingService { paidChildrenCount, combinedBaseFareMinor: combinedBase, discountMinor, taxesFeesMinor: taxesMinor, totalMinor, - currency: 'ETB', displayCurrency, displayTotalMinor, + currency: displayCurrency, displayTotalMinor, }, }; } diff --git a/apps/edr-passenger-api/src/modules/currencies/currencies.module.ts b/apps/edr-passenger-api/src/modules/currencies/currencies.module.ts index 0adb94656..614359dff 100644 --- a/apps/edr-passenger-api/src/modules/currencies/currencies.module.ts +++ b/apps/edr-passenger-api/src/modules/currencies/currencies.module.ts @@ -4,9 +4,10 @@ import { CurrenciesController } from './currencies.controller'; import { CurrenciesService } from './currencies.service'; import { CurrencyModule } from '../currency/currency.module'; import { PrismaModule } from '../../common/prisma.module'; +import { AuditModule } from '../../common/audit.module'; @Module({ - imports: [HttpModule, PrismaModule, CurrencyModule], + imports: [HttpModule, PrismaModule, CurrencyModule, AuditModule], controllers: [CurrenciesController], providers: [CurrenciesService], exports: [CurrenciesService], diff --git a/apps/edr-passenger-api/src/modules/currencies/currencies.service.ts b/apps/edr-passenger-api/src/modules/currencies/currencies.service.ts index 8c875eb8b..648deb77c 100644 --- a/apps/edr-passenger-api/src/modules/currencies/currencies.service.ts +++ b/apps/edr-passenger-api/src/modules/currencies/currencies.service.ts @@ -2,12 +2,14 @@ import { Injectable, BadRequestException, NotFoundException } from '@nestjs/comm import { PrismaService } from '../../common/prisma.service'; import { CurrencyService } from '../currency/currency.service'; import { CreateCurrencyDto, UpdateCurrencyDto } from './currencies.dto'; +import { AuditService } from '../../common/audit.service'; @Injectable() export class CurrenciesService { constructor( private prisma: PrismaService, private currencyService: CurrencyService, + private auditService: AuditService, ) {} async getAllCurrencies() { @@ -57,6 +59,7 @@ export class CurrenciesService { }, }); + await this.auditService.log({ action: 'CREATE', entityType: 'Currency', entityId: rate.id, newData: { code, exchangeRate } }); return { id: rate.id, code: rate.toCurrency, @@ -92,6 +95,7 @@ export class CurrenciesService { 'MANUAL', ); + await this.auditService.log({ action: 'UPDATE', entityType: 'Currency', entityId: updated.id, newData: { exchangeRate: Number(updated.rate) } }); return { id: updated.id, code: updated.toCurrency, @@ -125,7 +129,7 @@ export class CurrenciesService { await this.prisma.currencyExchangeRate.deleteMany({ where: { fromCurrency: existing.fromCurrency, toCurrency: existing.toCurrency }, }); - + await this.auditService.log({ action: 'DELETE', entityType: 'Currency', entityId: id, oldData: { toCurrency: existing.toCurrency } }); return { message: 'Currency deleted successfully' }; } diff --git a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.module.ts b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.module.ts index e0545d44f..e734d4fb4 100644 --- a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.module.ts +++ b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.module.ts @@ -7,9 +7,10 @@ import { } from './excess-baggage.controller'; import { PaymentsModule } from '../payments/payments.module'; import { NotificationsModule } from '../notifications/notifications.module'; +import { AuditModule } from '../../common/audit.module'; @Module({ - imports: [HttpModule, PaymentsModule, NotificationsModule], + imports: [HttpModule, PaymentsModule, NotificationsModule, AuditModule], controllers: [ExcessBaggageAgentController, ExcessBaggagePublicController], providers: [ExcessBaggageService], exports: [ExcessBaggageService], diff --git a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts index 30ab0fd5e..ee968ba61 100644 --- a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts +++ b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts @@ -5,6 +5,7 @@ import { Logger, } from '@nestjs/common'; import { PrismaService } from '../../common/prisma.service'; +import { AuditService } from '../../common/audit.service'; import { PaymentClientService } from '../payments/payment-client.service'; import { NotificationsService } from '../notifications/notifications.service'; import { SmsClientService } from '../notifications/sms-client.service'; @@ -30,6 +31,7 @@ export class ExcessBaggageService { constructor( private prisma: PrismaService, + private auditService: AuditService, private paymentClient: PaymentClientService, private notifications: NotificationsService, private smsClient: SmsClientService, @@ -91,6 +93,7 @@ export class ExcessBaggageService { await this.sendPaymentLink(charge, booking, contactPhone, contactEmail); } + await this.auditService.log({ action: 'CREATE', entityType: 'ExcessBaggageCharge', entityId: charge.id, newData: { bookingId: dto.bookingId, excessWeightKg: dto.excessWeightKg, totalMinor, status } }); return charge; } @@ -208,10 +211,12 @@ export class ExcessBaggageService { if (['PAID', 'CASH_COLLECTED'].includes(charge.status)) { throw new BadRequestException('Cannot waive a charge that has already been paid'); } - return this.prisma.excessBaggageCharge.update({ + const waived = await this.prisma.excessBaggageCharge.update({ where: { id }, data: { status: 'WAIVED', waivedBy: dto.waivedBy, waivedReason: dto.waivedReason }, }); + await this.auditService.log({ action: 'UPDATE', entityType: 'ExcessBaggageCharge', entityId: id, newData: { status: 'WAIVED', waivedBy: dto.waivedBy, waivedReason: dto.waivedReason } }); + return waived; } async resendLink(id: string) { diff --git a/apps/edr-passenger-api/src/modules/fleet/fleet.module.ts b/apps/edr-passenger-api/src/modules/fleet/fleet.module.ts index e2f4a28c9..dd39a72ec 100644 --- a/apps/edr-passenger-api/src/modules/fleet/fleet.module.ts +++ b/apps/edr-passenger-api/src/modules/fleet/fleet.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { FleetController } from './fleet.controller'; import { FleetService } from './fleet.service'; +import { AuditModule } from '../../common/audit.module'; -@Module({ controllers: [FleetController], providers: [FleetService], exports: [FleetService] }) +@Module({ imports: [AuditModule], controllers: [FleetController], providers: [FleetService], exports: [FleetService] }) export class FleetModule {} diff --git a/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts b/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts index dc29f4ecc..a4c1d8ccf 100644 --- a/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts +++ b/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from '../../common/prisma.service'; import { CreateTrainDto, CreateCoachDto, UpdateCoachDto, AssignCoachDto, ListCoachesDto, CreateCoachTypeDto, UpdateCoachTypeDto, CreateClassDto, UpdateClassDto, GenerateSeatMapDto } from './fleet.dto'; import { SeatKind } from '@prisma/client'; import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception'; +import { AuditService } from '../../common/audit.service'; // Parses '2+2' → [2, 2], '2+2+2' → [2, 2, 2] function parseArrangement(arrangement: string): number[] { @@ -147,7 +148,7 @@ type SeatRow = { @Injectable() export class FleetService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService, private auditService: AuditService) {} async createCoachType(dto: CreateCoachTypeDto) { return this.prisma.coachType.create({ @@ -333,8 +334,8 @@ export class FleetService { }); } - createTrain(dto: CreateTrainDto) { - return this.prisma.train.create({ + async createTrain(dto: CreateTrainDto) { + const train = await this.prisma.train.create({ data: { number: dto.number, name: dto.name, @@ -344,12 +345,14 @@ export class FleetService { isActive: dto.isActive ?? true, }, }); + await this.auditService.log({ action: 'CREATE', entityType: 'Train', entityId: train.id, newData: { number: train.number, name: train.name } }); + return train; } async updateTrain(id: string, dto: CreateTrainDto) { const train = await this.prisma.train.findUnique({ where: { id } }); if (!train) throw new NotFoundException('Train not found'); - return this.prisma.train.update({ + const updated = await this.prisma.train.update({ where: { id }, data: { number: dto.number, @@ -360,6 +363,8 @@ export class FleetService { ...(dto.isActive !== undefined && { isActive: dto.isActive }), }, }); + await this.auditService.log({ action: 'UPDATE', entityType: 'Train', entityId: id, newData: { number: dto.number, name: dto.name } }); + return updated; } async deleteTrain(id: string, cascade = false) { @@ -436,7 +441,9 @@ export class FleetService { await this.prisma.trainSchedule.deleteMany({ where: { id: { in: scheduleIds } } }); } - return this.prisma.train.delete({ where: { id } }); + const deleted = await this.prisma.train.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'Train', entityId: id, oldData: { number: train.number, name: train.name } }); + return deleted; } async restoreTrain(id: string) { @@ -519,6 +526,7 @@ export class FleetService { await this.prisma.seat.createMany({ data: seats }); } + await this.auditService.log({ action: 'CREATE', entityType: 'Coach', entityId: coach.id, newData: { number: coach.number, capacity: coach.capacity } }); return coach; } @@ -526,7 +534,7 @@ export class FleetService { const coach = await this.prisma.coach.findUnique({ where: { id } }); if (!coach) throw new NotFoundException('Coach not found'); - return this.prisma.coach.update({ + const updated = await this.prisma.coach.update({ where: { id }, data: { number: dto.number, @@ -537,6 +545,8 @@ export class FleetService { }, include: { coachType: true }, }); + await this.auditService.log({ action: 'UPDATE', entityType: 'Coach', entityId: id, newData: { number: dto.number, status: dto.status } }); + return updated; } async deleteCoach(id: string, cascade = false) { @@ -611,7 +621,9 @@ export class FleetService { await this.prisma.seat.deleteMany({ where: { coachId: id } }); - return this.prisma.coach.delete({ where: { id } }); + const deleted = await this.prisma.coach.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'Coach', entityId: id, oldData: { number: coach.number } }); + return deleted; } async assignCoach(dto: AssignCoachDto) { diff --git a/apps/edr-passenger-api/src/modules/packages/packages.module.ts b/apps/edr-passenger-api/src/modules/packages/packages.module.ts index 32aec44fc..41e9da839 100644 --- a/apps/edr-passenger-api/src/modules/packages/packages.module.ts +++ b/apps/edr-passenger-api/src/modules/packages/packages.module.ts @@ -4,9 +4,10 @@ import { PackagesController } from './packages.controller'; import { PackagesService } from './packages.service'; import { CurrencyModule } from '../currency/currency.module'; import { BookingsModule } from '../bookings/bookings.module'; +import { AuditModule } from '../../common/audit.module'; @Module({ - imports: [PrismaModule, CurrencyModule, BookingsModule], + imports: [PrismaModule, CurrencyModule, BookingsModule, AuditModule], controllers: [PackagesController], providers: [PackagesService], exports: [PackagesService], 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 916a0425b..9ddc1fb0a 100644 --- a/apps/edr-passenger-api/src/modules/packages/packages.service.ts +++ b/apps/edr-passenger-api/src/modules/packages/packages.service.ts @@ -5,6 +5,7 @@ import { CreatePackageDto, BookPackageDto, UpdatePriceTierDto, CreatePriceTierDt import { Currency } from '@prisma/client'; import { BookingsService } from '../bookings/bookings.service'; import { GuestBookingService } from '../bookings/guest-booking.service'; +import { AuditService } from '../../common/audit.service'; /** Package-specific fare rules */ const PKG_MAX_ADULTS = 5; @@ -49,6 +50,7 @@ export class PackagesService { private readonly currencyService: CurrencyService, private readonly bookingsService: BookingsService, private readonly guestBookingService: GuestBookingService, + private readonly auditService: AuditService, ) {} async getBookingContext(packageId: string, tierId: string, adultCount: number, childCount = 0) { @@ -254,8 +256,8 @@ export class PackagesService { }; } - create(dto: CreatePackageDto) { - return this.prisma.travelPackage.create({ + async create(dto: CreatePackageDto) { + const pkg = await this.prisma.travelPackage.create({ data: { code: dto.code, name: dto.name, @@ -279,12 +281,14 @@ export class PackagesService { }, include: { priceTiers: true }, }); + await this.auditService.log({ action: 'CREATE', entityType: 'Package', entityId: pkg.id, newData: { code: pkg.code, name: pkg.name } }); + return pkg; } async update(id: string, dto: Partial) { const pkg = await this.prisma.travelPackage.findUnique({ where: { id } }); if (!pkg) throw new NotFoundException('Package not found'); - return this.prisma.travelPackage.update({ + const updated = await this.prisma.travelPackage.update({ where: { id }, data: { ...(dto.code && { code: dto.code }), @@ -307,6 +311,8 @@ export class PackagesService { }, include: { priceTiers: true }, }); + await this.auditService.log({ action: 'UPDATE', entityType: 'Package', entityId: id, newData: { code: dto.code, name: dto.name } }); + return updated; } async addTier(packageId: string, dto: CreatePriceTierDto) { @@ -353,19 +359,24 @@ export class PackagesService { await this.prisma.packageInquiry.deleteMany({ where: { packageId: id } }); await this.prisma.packagePriceTier.deleteMany({ where: { packageId: id } }); await this.prisma.travelPackage.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'Package', entityId: id }); return { deleted: true }; } async activate(id: string) { const pkg = await this.prisma.travelPackage.findUnique({ where: { id } }); if (!pkg) throw new NotFoundException('Package not found'); - return this.prisma.travelPackage.update({ where: { id }, data: { status: 'ACTIVE' } }); + const activated = await this.prisma.travelPackage.update({ where: { id }, data: { status: 'ACTIVE' } }); + await this.auditService.log({ action: 'UPDATE', entityType: 'Package', entityId: id, newData: { status: 'ACTIVE' } }); + return activated; } async deactivate(id: string) { const pkg = await this.prisma.travelPackage.findUnique({ where: { id } }); if (!pkg) throw new NotFoundException('Package not found'); - return this.prisma.travelPackage.update({ where: { id }, data: { status: 'DRAFT' } }); + const deactivated = await this.prisma.travelPackage.update({ where: { id }, data: { status: 'DRAFT' } }); + await this.auditService.log({ action: 'UPDATE', entityType: 'Package', entityId: id, newData: { status: 'DRAFT' } }); + return deactivated; } async book(dto: BookPackageDto, passengerId?: string) { @@ -478,7 +489,7 @@ export class PackagesService { paidChildFareMinor: adultFareMinor, childFareNote: `First child per adult travels free (no seat); additional children pay full adult fare`, totalMinor, - currency: 'ETB', + currency: booking.displayCurrency, displayCurrency, displayTotalMinor, }, diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.module.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.module.ts index cc7748457..9fdb95b6c 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.module.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.module.ts @@ -4,10 +4,11 @@ import { PassengersController } from './passengers.controller'; import { PassengersService } from './passengers.service'; import { VerifaydaModule } from '../verifayda/verifayda.module'; import { PrismaModule } from '../../common/prisma.module'; +import { AuditModule } from '../../common/audit.module'; -@Module({ - imports: [VerifaydaModule, HttpModule, PrismaModule], - controllers: [PassengersController], - providers: [PassengersService] +@Module({ + imports: [VerifaydaModule, HttpModule, PrismaModule, AuditModule], + controllers: [PassengersController], + providers: [PassengersService], }) export class PassengersModule {} diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 2c52db955..742b7ae89 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -5,6 +5,7 @@ import { PrismaService } from '../../common/prisma.service'; import { CreateTravelerProfileDto, CreateSavedRouteDto, RegisterPassengerDto } from './passengers.dto'; import { VerifaydaService } from '../verifayda/verifayda.service'; import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception'; +import { AuditService } from '../../common/audit.service'; interface PassengerFilters { search?: string; @@ -30,6 +31,7 @@ export class PassengersService { private readonly prisma: PrismaService, @InjectDataSource() private readonly dataSource: DataSource, private readonly verifaydaService: VerifaydaService, + private readonly auditService: AuditService, ) {} async findAll(filters: PassengerFilters = {}) { @@ -509,7 +511,7 @@ export class PassengersService { await this.prisma.travelerProfile.deleteMany({ where: { passengerId } }); await this.prisma.savedRoute.deleteMany({ where: { passengerId } }); await this.prisma.passenger.delete({ where: { id: passengerId } }); - + await this.auditService.log({ action: 'DELETE', entityType: 'Passenger', entityId: passengerId }); return { deleted: true, passengerId }; } diff --git a/apps/edr-passenger-api/src/modules/payments/payments.module.ts b/apps/edr-passenger-api/src/modules/payments/payments.module.ts index 1b9af89f3..3c08eb3cd 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.module.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.module.ts @@ -19,6 +19,7 @@ import { ServiceAuthGuard } from "../../common/guards/service-auth.guard"; import { SeatsModule } from "../seats/seats.module"; import { TicketsModule } from "../tickets/tickets.module"; import { CurrencyModule } from "../currency/currency.module"; +import { AuditModule } from "../../common/audit.module"; const PASSENGER_QUEUE = PAYMENT_QUEUES[PaymentService.PASSENGER]; @@ -53,6 +54,7 @@ function rabbitMQImport(): DynamicModule[] { SeatsModule, TicketsModule, CurrencyModule, + AuditModule, // The payment service proxies slow provider calls (e.g. CAC Bank initiate, which SMSes an // OTP and can take tens of seconds). Keep this hop generous; overridable via env. HttpModule.register({ diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index 20fac61a5..5ef160135 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -26,6 +26,7 @@ import { import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto"; import { PaymentClientService } from "./payment-client.service"; import { CurrencyService } from "../currency/currency.service"; +import { AuditService } from "../../common/audit.service"; import { rebaseUrlOrigin } from "../../common/utils/redirect-origin.util"; import { PaymentService as PaymentServiceEnum, @@ -64,6 +65,7 @@ export class PaymentsService { private eventEmitter: EventEmitter2, private paymentClient: PaymentClientService, private currencyService: CurrencyService, + private auditService: AuditService, ) {} async deletePayment(id: string) { @@ -592,6 +594,7 @@ export class PaymentsService { data: { status: "CANCELLED" }, }); } + await this.auditService.log({ action: 'UPDATE', entityType: 'Payment', entityId: intent.id, newData: { status: 'REFUNDED', bookingId: dto.bookingId } }); return { refunded: true, bookingRef: booking?.bookingRef }; } @@ -898,6 +901,9 @@ export class PaymentsService { return this.finalizePaymentSuccess({ intentId: intent.id, providerTxnId: dto.paymentReference ?? intent.providerTxnId ?? undefined, + }).then(async (result) => { + await this.auditService.log({ action: 'UPDATE', entityType: 'Payment', entityId: intent.id, newData: { status: 'FORCE_CONFIRMED', bookingId, paymentMethod: dto.paymentMethod, paymentReference: dto.paymentReference } }); + return result; }); } diff --git a/apps/edr-passenger-api/src/modules/schedules/routes.service.ts b/apps/edr-passenger-api/src/modules/schedules/routes.service.ts index e459e3663..9a9bdd30a 100644 --- a/apps/edr-passenger-api/src/modules/schedules/routes.service.ts +++ b/apps/edr-passenger-api/src/modules/schedules/routes.service.ts @@ -2,10 +2,11 @@ import { Injectable, NotFoundException, ConflictException, BadRequestException } import { PrismaService } from '../../common/prisma.service'; import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto, SetRouteCoachTemplateDto } from './routes.dto'; import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception'; +import { AuditService } from '../../common/audit.service'; @Injectable() export class RoutesService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService, private auditService: AuditService) {} // ── Route CRUD ───────────────────────────────────────────────────────────── @@ -22,7 +23,7 @@ export class RoutesService { const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } }); if (stations.length !== stationIds.length) throw new BadRequestException('One or more station IDs not found'); - return this.prisma.route.create({ + const route = await this.prisma.route.create({ data: { code: dto.code, name: dto.name, @@ -40,6 +41,8 @@ export class RoutesService { }, include: { stops: { include: { route: false }, orderBy: { sequence: 'asc' } } }, }); + await this.auditService.log({ action: 'CREATE', entityType: 'Route', entityId: route.id, newData: { code: route.code, name: route.name } }); + return route; } async listRoutes(activeOnly = false) { @@ -104,6 +107,7 @@ export class RoutesService { }); } + await this.auditService.log({ action: 'UPDATE', entityType: 'Route', entityId: id, newData: { name: dto.name, active: dto.active } }); return this.prisma.route.findUnique({ where: { id }, include: { stops: { orderBy: { sequence: 'asc' } } }, @@ -192,6 +196,7 @@ export class RoutesService { } await this.prisma.route.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'Route', entityId: id, oldData: { code: route.code, name: route.name } }); return { deleted: true, id }; } diff --git a/apps/edr-passenger-api/src/modules/schedules/schedules.module.ts b/apps/edr-passenger-api/src/modules/schedules/schedules.module.ts index 5eca9f1be..18d88d631 100644 --- a/apps/edr-passenger-api/src/modules/schedules/schedules.module.ts +++ b/apps/edr-passenger-api/src/modules/schedules/schedules.module.ts @@ -4,9 +4,10 @@ import { SchedulesService } from './schedules.service'; import { RoutesController } from './routes.controller'; import { RoutesService } from './routes.service'; import { FareEngineModule } from '../fare-engine/fare-engine.module'; +import { AuditModule } from '../../common/audit.module'; @Module({ - imports: [FareEngineModule], + imports: [FareEngineModule, AuditModule], controllers: [RoutesController, SchedulesController], providers: [RoutesService, SchedulesService], exports: [RoutesService, SchedulesService], 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 06bb4c2dc..7cca548aa 100644 --- a/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts +++ b/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts @@ -5,6 +5,7 @@ import { FareEngineService } from '../fare-engine/fare-engine.service'; import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto } from './schedules.dto'; import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception'; import { parseEthiopianTime, startOfDayEAT, startOfNextDayEAT } from '../../common/utils/timezone.utils'; +import { AuditService } from '../../common/audit.service'; @Injectable() export class SchedulesService { @@ -12,6 +13,7 @@ export class SchedulesService { private prisma: PrismaService, private routesService: RoutesService, private fareEngine: FareEngineService, + private auditService: AuditService, ) { } async bulkGenerateSchedules(dto: BulkCreateSchedulesDto) { @@ -191,7 +193,9 @@ export class SchedulesService { ); } - return this.getSchedule(schedule.id); + const result = await this.getSchedule(schedule.id); + await this.auditService.log({ action: 'CREATE', entityType: 'Schedule', entityId: schedule.id, newData: { trainId: dto.trainId, routeId: dto.routeId, departureAt: dep } }); + return result; } async getSchedule(id: string) { @@ -325,10 +329,12 @@ export class SchedulesService { const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t])); await this.routesService.applyRouteToSchedule(dto.routeId, id, plannedTimesMap); - return this.getSchedule(id); + const result = await this.getSchedule(id); + await this.auditService.log({ action: 'UPDATE', entityType: 'Schedule', entityId: id, newData: { trainId: dto.trainId, departureAt: dep } }); + return result; } - updateScheduleStatus(id: string, dto: UpdateScheduleStatusDto) { + async updateScheduleStatus(id: string, dto: UpdateScheduleStatusDto) { return this.prisma.trainSchedule.update({ where: { id }, data: { status: dto.status } }); } @@ -409,7 +415,9 @@ export class SchedulesService { await this.prisma.packagePriceTier.deleteMany({ where: { packageId: { in: packageIds } } }); await this.prisma.travelPackage.deleteMany({ where: { id: { in: packageIds } } }); } - return this.prisma.trainSchedule.delete({ where: { id } }); + await this.prisma.trainSchedule.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'Schedule', entityId: id }); + return { deleted: true, id }; } getStops(scheduleId: string) { @@ -467,7 +475,7 @@ export class SchedulesService { createFareRule(dto: CreateFareRuleDto) { const { validFrom, validUntil, scheduleId, nationality, passengerCategory, ...rest } = dto; - return this.prisma.fareRule.create({ + const result = this.prisma.fareRule.create({ data: { ...rest, tripId: scheduleId, @@ -477,6 +485,8 @@ export class SchedulesService { }, include: { seatClass: true }, }); + result.then(r => this.auditService.log({ action: 'CREATE', entityType: 'FareRule', entityId: r.id, newData: { seatClassId: r.seatClassId, baseFareMinor: r.baseFareMinor } })); + return result; } async updateFareRule(id: string, dto: Partial) { @@ -501,6 +511,7 @@ export class SchedulesService { const existing = await this.prisma.fareRule.findUnique({ where: { id } }); if (!existing) throw new NotFoundException('Fare rule not found'); await this.prisma.fareRule.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'FareRule', entityId: id }); return { deleted: true, id }; } @@ -701,7 +712,7 @@ export class SchedulesService { ]); if (!route) throw new NotFoundException('Route not found'); if (!seatClass) throw new NotFoundException('Seat class not found'); - return this.prisma.routeFareRule.create({ + const rule = await this.prisma.routeFareRule.create({ data: { routeId: dto.routeId, seatClassId: dto.seatClassId, @@ -712,6 +723,8 @@ export class SchedulesService { }, include: { seatClass: true, route: true }, }); + await this.auditService.log({ action: 'CREATE', entityType: 'RouteFareRule', entityId: rule.id, newData: { routeId: dto.routeId, seatClassId: dto.seatClassId, baseFareMinor: dto.baseFareMinor } }); + return rule; } async updateRouteFareRule(id: string, dto: { baseFareMinor?: number; surchargeMinor?: number; validFrom?: string; validUntil?: string }) { @@ -733,6 +746,7 @@ export class SchedulesService { const rule = await this.prisma.routeFareRule.findUnique({ where: { id } }); if (!rule) throw new NotFoundException('Route fare rule not found'); await this.prisma.routeFareRule.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'RouteFareRule', entityId: id }); return { deleted: true, id }; } } diff --git a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts index a7e8648e1..1882bd577 100644 --- a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts +++ b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { SeatClassesController } from './seat-classes.controller'; import { SeatClassesService } from './seat-classes.service'; +import { AuditModule } from '../../common/audit.module'; -@Module({ controllers: [SeatClassesController], providers: [SeatClassesService], exports: [SeatClassesService] }) +@Module({ imports: [AuditModule], controllers: [SeatClassesController], providers: [SeatClassesService], exports: [SeatClassesService] }) export class SeatClassesModule {} diff --git a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts index 99c67a665..f33ecac64 100644 --- a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts +++ b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts @@ -1,10 +1,11 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { PrismaService } from '../../common/prisma.service'; import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception'; +import { AuditService } from '../../common/audit.service'; @Injectable() export class SeatClassesService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService, private auditService: AuditService) {} listSeatClasses() { return this.prisma.seatClass.findMany({ @@ -28,7 +29,9 @@ export class SeatClassesService { ...rest, ...(basePrice !== undefined && { baseFareMinor: basePrice }), }; - return this.prisma.seatClass.update({ where: { id }, data }); + const updated = await this.prisma.seatClass.update({ where: { id }, data }); + await this.auditService.log({ action: 'UPDATE', entityType: 'SeatClass', entityId: id, newData: { name: updated.name } }); + return updated; } async createSeatClass(dto: any) { @@ -38,7 +41,9 @@ export class SeatClassesService { ...rest, ...(basePrice !== undefined && { baseFareMinor: basePrice }), }; - return await this.prisma.seatClass.create({ data }); + const sc = await this.prisma.seatClass.create({ data }); + await this.auditService.log({ action: 'CREATE', entityType: 'SeatClass', entityId: sc.id, newData: { name: sc.name } }); + return sc; } catch (e: any) { if (e.code === 'P2002') throw new ConflictException(`Seat class "${dto.name}" already exists`); throw e; @@ -70,6 +75,8 @@ export class SeatClassesService { await this.prisma.segmentFareRule.deleteMany({ where: { seatClassId: id } }); } - return this.prisma.seatClass.delete({ where: { id } }); + const deleted = await this.prisma.seatClass.delete({ where: { id } }); + await this.auditService.log({ action: 'DELETE', entityType: 'SeatClass', entityId: id, oldData: { name: sc.name } }); + return deleted; } } diff --git a/apps/edr-passenger-api/src/modules/seats/seats.module.ts b/apps/edr-passenger-api/src/modules/seats/seats.module.ts index 425f517c6..0725f4393 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.module.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.module.ts @@ -4,9 +4,10 @@ import { SeatsController } from './seats.controller'; import { SeatsService } from './seats.service'; import { SegmentsModule } from '../segments/segments.module'; import { SystemConfigModule } from '../system-config/system-config.module'; +import { AuditModule } from '../../common/audit.module'; @Module({ - imports: [SegmentsModule, HttpModule, SystemConfigModule], + imports: [SegmentsModule, HttpModule, SystemConfigModule, AuditModule], controllers: [SeatsController], providers: [SeatsService], exports: [SeatsService], diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index 3d485d22c..ad92660a4 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -4,6 +4,7 @@ import { HoldSeatsDto, JourneyDirection } from './seats.dto'; import { Cron, CronExpression } from '@nestjs/schedule'; import { SegmentsService } from '../segments/segments.service'; import { SystemConfigService, CONFIG_KEYS } from '../system-config/system-config.service'; +import { AuditService } from '../../common/audit.service'; @Injectable() export class SeatsService { @@ -11,6 +12,7 @@ export class SeatsService { private prisma: PrismaService, private segmentsService: SegmentsService, private systemConfig: SystemConfigService, + private auditService: AuditService, ) {} async getSeatMap(scheduleId: string, coachTypeId?: string, journeyDirection?: JourneyDirection, originStationId?: string, destinationStationId?: string) { @@ -803,7 +805,7 @@ export class SeatsService { await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'BLOCKED' } }); await this.prisma.seatBlock.create({ data: { seatId, reason, blockedBy: 'system' } }); - + await this.auditService.log({ action: 'UPDATE', entityType: 'Seat', entityId: seatId, newData: { status: 'BLOCKED', reason } }); return { blocked: true, seatId, reason }; } @@ -813,7 +815,7 @@ export class SeatsService { await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'AVAILABLE' } }); await this.prisma.seatBlock.deleteMany({ where: { seatId } }); - + await this.auditService.log({ action: 'UPDATE', entityType: 'Seat', entityId: seatId, newData: { status: 'AVAILABLE' } }); return { unblocked: true, seatId }; } @@ -846,7 +848,7 @@ export class SeatsService { }); await this.renumberCoachSeats(seat.coachId); - + await this.auditService.log({ action: 'DELETE', entityType: 'Seat', entityId: seatId, oldData: { seatNumber: seat.seatNumber, coachId: seat.coachId } }); return { removed: true, seatId, originalSeatNumber: seat.seatNumber }; } diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts index 1ccc2e392..2056d4eda 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts @@ -4,9 +4,10 @@ import { TicketsService } from './tickets.service'; import { JwtGuard } from '../../common/jwt.guard'; import { NotificationsModule } from '../notifications/notifications.module'; import { SystemConfigModule } from '../system-config/system-config.module'; +import { AuditModule } from '../../common/audit.module'; @Module({ - imports: [NotificationsModule, SystemConfigModule], + imports: [NotificationsModule, SystemConfigModule, AuditModule], controllers: [TicketsController], providers: [TicketsService, JwtGuard], exports: [TicketsService, JwtGuard], 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 656e726ab..648b43cad 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -4,6 +4,7 @@ import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { NotificationsService } from '../notifications/notifications.service'; import { SystemConfigService, CONFIG_KEYS } from '../system-config/system-config.service'; +import { AuditService } from '../../common/audit.service'; import * as QRCode from 'qrcode'; interface OfflineValidation { @@ -22,6 +23,7 @@ export class TicketsService { private readonly prisma: PrismaService, private readonly notifications: NotificationsService, private readonly systemConfig: SystemConfigService, + private readonly auditService: AuditService, @InjectDataSource() private readonly dataSource: DataSource, ) {} @@ -229,7 +231,6 @@ export class TicketsService { } } - // Delete existing tickets if any await this.prisma.ticket.deleteMany({ where: { bookingId } }); // Generate one ticket per unique passenger (grouped by passengerName) @@ -291,6 +292,7 @@ export class TicketsService { }).catch(() => null); } + await this.auditService.log({ action: 'CREATE', entityType: 'Ticket', entityId: booking.id, newData: { bookingRef: booking.bookingRef, totalTickets: tickets.length } }); return { tickets, totalTickets: tickets.length }; } @@ -557,6 +559,7 @@ export class TicketsService { await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, validatorId: resolvedValidatorId } }); await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, status: 'APPROVED' } }); this.fireBoardingPassNotification(booking, ticket, null); + await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: 'ONE_WAY' } }); return { validated: true, ticketId: ticket.id, validatedAt: now }; } @@ -575,6 +578,7 @@ export class TicketsService { if (!ticket.validatedAt) await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, validatorId: resolvedValidatorId } }); await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any }); this.fireBoardingPassNotification(booking, ticket, resolvedLeg); + await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: resolvedLeg } }); return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt: now }; } @@ -608,6 +612,7 @@ export class TicketsService { } await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any }); this.fireBoardingPassNotification(booking, ticket, resolvedLeg); + await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: resolvedLeg } }); return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt: now }; } 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 f36324bd1..10051f0d3 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 @@ -56,9 +56,13 @@ export default function ReviewPage() { const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP'; - // Derive display currency from nationality so fares show in the passenger's home currency. + // Use the display currency stored on the schedule (set at search/selection time). + // Fall back to nationality-based derivation only if the schedule has no displayCurrency. + const scheduleCurrency = isRoundTrip + ? outboundSchedule?.displayCurrency + : selectedSchedule?.displayCurrency; const nat = (searchCriteria?.nationality ?? '').toUpperCase(); - const displayCurrencyCode = nat === 'DJIBOUTIAN' ? 'DJF' : nat === 'ETHIOPIAN' ? 'ETB' : 'USD'; + const displayCurrencyCode = scheduleCurrency || (nat === 'DJIBOUTIAN' ? 'DJF' : nat === 'ETHIOPIAN' ? 'ETB' : 'USD'); useEffect(() => { if (!seatHold?.expiresAt) return;