mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 09:42:53 +00:00
Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<CreatePackageDto>) {
|
||||
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,
|
||||
},
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
@@ -593,6 +595,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 };
|
||||
}
|
||||
|
||||
@@ -899,6 +902,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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<CreateFareRuleDto>) {
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user