mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
Merge branch 'alpha' into passenger/feat/iam
This commit is contained in:
@@ -216,6 +216,7 @@ export class BookingsService {
|
||||
...(matchedPassengers.length > 0
|
||||
? [{ passengerId: { in: matchedPassengers.map(p => p.id) } }]
|
||||
: []),
|
||||
{ seats: { some: { passengerName: { contains: search, mode: 'insensitive' } } } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -268,6 +269,7 @@ export class BookingsService {
|
||||
passenger: iam
|
||||
? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number }
|
||||
: null,
|
||||
passengerNames: [...new Set(booking.seats.map((s: any) => s.passengerName))],
|
||||
schedule: {
|
||||
train: booking.schedule.train,
|
||||
originStation: booking.schedule.originStation,
|
||||
@@ -294,10 +296,23 @@ export class BookingsService {
|
||||
return this.createOneWayBooking(dto);
|
||||
}
|
||||
|
||||
private validateSeatIdsAgainstHold(holdId: string, holdSeatIds: string[], requestedSeatIds: string[]) {
|
||||
for (const seatId of requestedSeatIds) {
|
||||
if (!holdSeatIds.includes(seatId)) {
|
||||
throw new BadRequestException(
|
||||
`Seat ${seatId} is not part of hold ${holdId}. Use seat IDs returned from POST /seats/hold.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createOneWayBooking(dto: CreateBookingDto) {
|
||||
const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } });
|
||||
if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired');
|
||||
|
||||
|
||||
const requestedSeatIds = (dto.passengers as any[]).map(p => p.seatId);
|
||||
this.validateSeatIdsAgainstHold(dto.holdId, hold.seatIds, requestedSeatIds);
|
||||
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: dto.scheduleId },
|
||||
include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } }
|
||||
@@ -367,6 +382,11 @@ export class BookingsService {
|
||||
if (!outboundHold || outboundHold.expiresAt < new Date()) throw new BadRequestException('Outbound seat hold expired');
|
||||
if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired');
|
||||
|
||||
const holdObSeatIds = (dto.passengers as any[]).map((p: any) => p.seatId ?? p.outboundSeatId).filter(Boolean);
|
||||
const holdRetSeatIds = (dto.passengers as any[]).map((p: any) => p.returnSeatId).filter(Boolean);
|
||||
if (holdObSeatIds.length) this.validateSeatIdsAgainstHold(dto.holdId, outboundHold.seatIds, holdObSeatIds);
|
||||
if (holdRetSeatIds.length) this.validateSeatIdsAgainstHold(dto.returnHoldId!, returnHold.seatIds, holdRetSeatIds);
|
||||
|
||||
const [outboundSchedule, returnSchedule] = await Promise.all([
|
||||
this.prisma.trainSchedule.findUnique({
|
||||
where: { id: dto.scheduleId },
|
||||
@@ -510,6 +530,11 @@ export class BookingsService {
|
||||
if (!leg1Hold || leg1Hold.expiresAt < new Date()) throw new BadRequestException('Leg-1 seat hold expired');
|
||||
if (!leg2Hold || leg2Hold.expiresAt < new Date()) throw new BadRequestException('Leg-2 seat hold expired');
|
||||
|
||||
const leg1SeatIds = (dto.passengers as any[]).map(p => p.seatId);
|
||||
const leg2SeatIds = (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId);
|
||||
this.validateSeatIdsAgainstHold(dto.holdId, leg1Hold.seatIds, leg1SeatIds);
|
||||
this.validateSeatIdsAgainstHold(dto.leg2HoldId!, leg2Hold.seatIds, leg2SeatIds);
|
||||
|
||||
const [leg1Schedule, leg2Schedule] = await Promise.all([
|
||||
this.prisma.trainSchedule.findUnique({
|
||||
where: { id: dto.scheduleId },
|
||||
@@ -656,6 +681,11 @@ export class BookingsService {
|
||||
if (!retL1Hold || retL1Hold.expiresAt < now) throw new BadRequestException('Return leg-1 seat hold expired');
|
||||
if (!retL2Hold || retL2Hold.expiresAt < now) throw new BadRequestException('Return leg-2 seat hold expired');
|
||||
|
||||
this.validateSeatIdsAgainstHold(dto.holdId, obL1Hold.seatIds, (dto.passengers as any[]).map(p => p.seatId));
|
||||
this.validateSeatIdsAgainstHold(dto.leg2HoldId!, obL2Hold.seatIds, (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId));
|
||||
this.validateSeatIdsAgainstHold(dto.returnHoldId!, retL1Hold.seatIds, (dto.passengers as any[]).map(p => p.returnSeatId));
|
||||
this.validateSeatIdsAgainstHold(dto.returnLeg2HoldId!, retL2Hold.seatIds, (dto.passengers as any[]).map(p => p.returnLeg2SeatId ?? p.returnSeatId));
|
||||
|
||||
// Load all 4 schedules
|
||||
const [obL1Sched, obL2Sched, retL1Sched, retL2Sched] = await Promise.all([
|
||||
this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }),
|
||||
@@ -847,7 +877,21 @@ export class BookingsService {
|
||||
nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
|
||||
}
|
||||
|
||||
processedPassengers.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality });
|
||||
processedPassengers.push({
|
||||
...passenger,
|
||||
passengerName,
|
||||
dateOfBirth,
|
||||
category,
|
||||
verifaydaVerified,
|
||||
verifaydaData,
|
||||
nationality,
|
||||
// Normalise: PassengerInputDto uses seatId/returnSeatId; RoundTripPassengerDto uses
|
||||
// outboundSeatId/returnSeatId. Accept either form so both DTOs work.
|
||||
outboundSeatId: passenger.outboundSeatId ?? passenger.seatId,
|
||||
outboundLeg2SeatId: passenger.outboundLeg2SeatId ?? passenger.leg2SeatId,
|
||||
returnSeatId: passenger.returnSeatId,
|
||||
returnLeg2SeatId: passenger.returnLeg2SeatId,
|
||||
});
|
||||
}
|
||||
return processedPassengers;
|
||||
}
|
||||
@@ -1042,7 +1086,7 @@ export class BookingsService {
|
||||
await this.prisma.bookingModification.create({
|
||||
data: { bookingId: booking.id, modifiedBy: booking.passengerId, modificationType: 'SEAT_CHANGE', oldData: { scheduleId: booking.scheduleId, seatIds: oldSeats }, newData: { scheduleId: dto.newScheduleId, seatIds: dto.newSeatIds }, fareAdjustment: 0, reason: dto.reason },
|
||||
});
|
||||
await this.seatsService.releaseSeats(oldSeats);
|
||||
await this.seatsService.releaseSeats(booking.id);
|
||||
await this.seatsService.confirmSeats(dto.newSeatIds);
|
||||
return { modified: true, bookingRef: dto.bookingRef };
|
||||
}
|
||||
@@ -1053,7 +1097,7 @@ export class BookingsService {
|
||||
if (booking.status === 'CANCELLED') throw new BadRequestException('Booking already cancelled');
|
||||
const refundAmount = booking.status === 'CONFIRMED' ? Math.floor(booking.totalMinor * 0.8) : 0;
|
||||
await this.prisma.bookingCancellation.create({ data: { bookingId: booking.id, cancelledBy: booking.passengerId, reason, refundAmount, refundMethod: booking.paymentIntent?.method ?? 'ORIGINAL', refundStatus: 'PENDING' } });
|
||||
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
|
||||
await this.seatsService.releaseSeats(booking.id);
|
||||
await this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } });
|
||||
this.eventEmitter.emit('booking.cancelled', { booking, refundAmount });
|
||||
return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' };
|
||||
@@ -1082,7 +1126,7 @@ export class BookingsService {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { id }, include: { seats: true } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
|
||||
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
|
||||
await this.seatsService.releaseSeats(booking.id);
|
||||
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.booking.delete({ where: { id } });
|
||||
@@ -1118,7 +1162,7 @@ export class BookingsService {
|
||||
const cutoff = new Date(Date.now() - 20 * 60 * 1000);
|
||||
const expired = await this.prisma.booking.findMany({ where: { status: 'PENDING_PAYMENT', createdAt: { lt: cutoff } }, include: { seats: true } });
|
||||
for (const b of expired) {
|
||||
await this.seatsService.releaseSeats(b.seats.map((s) => s.seatId));
|
||||
await this.seatsService.releaseSeats(b.id);
|
||||
await this.prisma.booking.update({ where: { id: b.id }, data: { status: 'CANCELLED' } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,21 @@ function generateRef(): string {
|
||||
return 'EDR-' + Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
||||
}
|
||||
|
||||
// Ethiopian mobile prefixes: Ethio Telecom (09xx) and Safaricom ET (07xx)
|
||||
const ETH_MOBILE_PREFIXES = ['911','912','913','914','915','916','917','921','922','923','924','930','931','932','933','934','935','936','937','938','939','961','962','963','964'];
|
||||
|
||||
function generateEthiopianPhone(): string {
|
||||
const prefix = ETH_MOBILE_PREFIXES[Math.floor(Math.random() * ETH_MOBILE_PREFIXES.length)];
|
||||
const suffix = String(Math.floor(Math.random() * 1_000_000)).padStart(6, '0');
|
||||
return `+251${prefix}${suffix}`;
|
||||
}
|
||||
|
||||
function generateGuestEmail(uniqueId: string): string {
|
||||
const domains = ['gmail.com', 'yahoo.com', 'ethionet.et', 'telecom.et'];
|
||||
const domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
return `guest.edr.${uniqueId}@${domain}`;
|
||||
}
|
||||
|
||||
function calculateAge(dateOfBirth: Date): number {
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - dateOfBirth.getFullYear();
|
||||
|
||||
Reference in New Issue
Block a user