mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
544 lines
22 KiB
TypeScript
544 lines
22 KiB
TypeScript
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
|
import { PrismaService } from '../../common/prisma.service';
|
|
import { SeatsService } from '../seats/seats.service';
|
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
import { CreateBookingDto, ModifyBookingDto } from './bookings.dto';
|
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
import { VerifaydaService } from '../verifayda/verifayda.service';
|
|
import { CurrencyService } from '../currency/currency.service';
|
|
import { Currency, PassengerCategory, IdDocumentType } from '@prisma/client';
|
|
|
|
function generateRef(): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
|
}
|
|
|
|
function calculateAge(dateOfBirth: Date): number {
|
|
const today = new Date();
|
|
let age = today.getFullYear() - dateOfBirth.getFullYear();
|
|
const monthDiff = today.getMonth() - dateOfBirth.getMonth();
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dateOfBirth.getDate())) age--;
|
|
return age;
|
|
}
|
|
|
|
interface BookingFilters {
|
|
search?: string;
|
|
status?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}
|
|
|
|
@Injectable()
|
|
export class BookingsService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private seatsService: SeatsService,
|
|
private eventEmitter: EventEmitter2,
|
|
private verifaydaService: VerifaydaService,
|
|
private currencyService: CurrencyService,
|
|
) {}
|
|
|
|
async findByPassengerId(passengerId: string, filters: BookingFilters = {}) {
|
|
const { search, status, page = 1, pageSize = 20 } = filters;
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
const where: any = { passengerId };
|
|
|
|
if (search) {
|
|
where.OR = [
|
|
{ bookingRef: { contains: search, mode: 'insensitive' } },
|
|
{ schedule: { originStation: { name: { contains: search, mode: 'insensitive' } } } },
|
|
{ schedule: { destinationStation: { name: { contains: search, mode: 'insensitive' } } } },
|
|
];
|
|
}
|
|
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.booking.findMany({
|
|
where,
|
|
skip,
|
|
take: pageSize,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
|
paymentIntent: true,
|
|
seats: { include: { seat: true } },
|
|
},
|
|
}),
|
|
this.prisma.booking.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
items: items.map(booking => ({
|
|
id: booking.id,
|
|
bookingRef: booking.bookingRef,
|
|
status: booking.status,
|
|
totalMinor: booking.totalMinor,
|
|
currency: 'ETB',
|
|
displayCurrency: booking.displayCurrency,
|
|
displayTotalMinor: booking.displayTotalMinor,
|
|
adultCount: booking.adultCount,
|
|
childCount: booking.childCount,
|
|
createdAt: booking.createdAt,
|
|
schedule: {
|
|
train: booking.schedule.train,
|
|
originStation: booking.schedule.originStation,
|
|
destinationStation: booking.schedule.destinationStation,
|
|
departureAt: booking.schedule.departureAt,
|
|
arrivalAt: booking.schedule.arrivalAt,
|
|
},
|
|
paymentIntent: booking.paymentIntent,
|
|
seatCount: booking.seats.length,
|
|
})),
|
|
meta: {
|
|
page,
|
|
pageSize,
|
|
total,
|
|
totalPages: Math.ceil(total / pageSize),
|
|
},
|
|
};
|
|
}
|
|
|
|
async findByDeviceId(deviceId: string, filters: BookingFilters = {}) {
|
|
const { search, status, page = 1, pageSize = 20 } = filters;
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
// Find passenger linked to this device via iamUserId
|
|
const device = await this.prisma.device.findUnique({ where: { id: deviceId } }).catch(() => null);
|
|
const passenger = device?.iamUserId
|
|
? await this.prisma.passenger.findUnique({ where: { iamUserId: device.iamUserId } }).catch(() => null)
|
|
: null;
|
|
|
|
const searchConditions = search ? [
|
|
{ bookingRef: { contains: search, mode: 'insensitive' } },
|
|
{ schedule: { originStation: { name: { contains: search, mode: 'insensitive' } } } },
|
|
{ schedule: { destinationStation: { name: { contains: search, mode: 'insensitive' } } } },
|
|
] : [];
|
|
|
|
const where: any = {
|
|
OR: [
|
|
{ userAgent: deviceId },
|
|
...(passenger ? [{ passengerId: passenger.id }] : []),
|
|
],
|
|
};
|
|
|
|
if (search) {
|
|
where.AND = [{ OR: searchConditions }];
|
|
}
|
|
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.booking.findMany({
|
|
where,
|
|
skip,
|
|
take: pageSize,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
|
paymentIntent: true,
|
|
seats: { include: { seat: true } },
|
|
},
|
|
}),
|
|
this.prisma.booking.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
items: items.map(booking => ({
|
|
id: booking.id,
|
|
bookingRef: booking.bookingRef,
|
|
status: booking.status,
|
|
totalMinor: booking.totalMinor,
|
|
currency: 'ETB',
|
|
displayCurrency: booking.displayCurrency,
|
|
displayTotalMinor: booking.displayTotalMinor,
|
|
adultCount: booking.adultCount,
|
|
childCount: booking.childCount,
|
|
createdAt: booking.createdAt,
|
|
schedule: {
|
|
train: booking.schedule.train,
|
|
originStation: booking.schedule.originStation,
|
|
destinationStation: booking.schedule.destinationStation,
|
|
departureAt: booking.schedule.departureAt,
|
|
arrivalAt: booking.schedule.arrivalAt,
|
|
},
|
|
paymentIntent: booking.paymentIntent,
|
|
seatCount: booking.seats.length,
|
|
})),
|
|
meta: {
|
|
page,
|
|
pageSize,
|
|
total,
|
|
totalPages: Math.ceil(total / pageSize),
|
|
},
|
|
};
|
|
}
|
|
|
|
async findAll(filters: BookingFilters = {}) {
|
|
const { search, status, page = 1, pageSize = 20 } = filters;
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
const where: any = {};
|
|
|
|
if (search) {
|
|
where.OR = [
|
|
{ bookingRef: { contains: search, mode: 'insensitive' } },
|
|
{ contactEmail: { contains: search, mode: 'insensitive' } },
|
|
{ contactPhone: { contains: search, mode: 'insensitive' } },
|
|
{ passenger: { user: { fullName: { contains: search, mode: 'insensitive' } } } },
|
|
];
|
|
}
|
|
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.booking.findMany({
|
|
where,
|
|
skip,
|
|
take: pageSize,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
passenger: { include: { user: true } },
|
|
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
|
paymentIntent: true,
|
|
seats: { include: { seat: true } },
|
|
},
|
|
}),
|
|
this.prisma.booking.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
items: items.map(booking => ({
|
|
id: booking.id,
|
|
bookingRef: booking.bookingRef,
|
|
status: booking.status,
|
|
totalMinor: booking.totalMinor,
|
|
currency: 'ETB',
|
|
displayCurrency: booking.displayCurrency,
|
|
displayTotalMinor: booking.displayTotalMinor,
|
|
contactEmail: booking.contactEmail,
|
|
contactPhone: booking.contactPhone,
|
|
createdAt: booking.createdAt,
|
|
passenger: booking.passenger?.user,
|
|
schedule: {
|
|
train: booking.schedule.train,
|
|
originStation: booking.schedule.originStation,
|
|
destinationStation: booking.schedule.destinationStation,
|
|
departureAt: booking.schedule.departureAt,
|
|
},
|
|
paymentIntent: booking.paymentIntent,
|
|
seatCount: booking.seats.length,
|
|
})),
|
|
meta: {
|
|
page,
|
|
pageSize,
|
|
total,
|
|
totalPages: Math.ceil(total / pageSize),
|
|
},
|
|
};
|
|
}
|
|
|
|
async create(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 schedule = await this.prisma.trainSchedule.findUnique({
|
|
where: { id: dto.scheduleId },
|
|
include: {
|
|
originStation: true,
|
|
destinationStation: true,
|
|
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
|
|
},
|
|
});
|
|
if (!schedule) throw new NotFoundException('Schedule not found');
|
|
|
|
const originStop = schedule.stopTimes.find(s => s.stationId === dto.originStationId);
|
|
const destStop = schedule.stopTimes.find(s => s.stationId === dto.destinationStationId);
|
|
if (!originStop || !destStop) throw new NotFoundException('Origin or destination not found');
|
|
|
|
const segmentRoute = `${originStop.station.code}-${destStop.station.code}`;
|
|
const fullRoute = `${schedule.originStation.code}-${schedule.destinationStation.code}`;
|
|
|
|
const seatIds = dto.passengers.map((p) => p.seatId);
|
|
const passengersData = [];
|
|
let adultCount = 0, childCount = 0;
|
|
|
|
for (const passenger of dto.passengers) {
|
|
const dateOfBirth = new Date(passenger.dateOfBirth);
|
|
const age = calculateAge(dateOfBirth);
|
|
const category: PassengerCategory = age < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;
|
|
if (category === PassengerCategory.ADULT) adultCount++; else childCount++;
|
|
|
|
let passengerName = passenger.passengerName;
|
|
let verifaydaVerified = false;
|
|
let verifaydaData: Record<string, any> | undefined;
|
|
let nationality = passenger.nationality;
|
|
|
|
if (passenger.idDocumentType === IdDocumentType.NATIONAL_ID && passenger.idDocumentNumber) {
|
|
const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber);
|
|
if (!verification.verified) throw new BadRequestException(`Verifayda verification failed for ${passenger.passengerName}: ${verification.failureReason}`);
|
|
passengerName = verification.passengerData?.fullName || passengerName;
|
|
verifaydaVerified = true;
|
|
verifaydaData = verification.passengerData?.profileData;
|
|
nationality = nationality || 'Ethiopian';
|
|
} else if (passenger.idDocumentType === IdDocumentType.PASSPORT) {
|
|
if (!passenger.passportNumber || !passenger.passportCountry) throw new BadRequestException(`Passport number and country required for ${passenger.passengerName}`);
|
|
nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
|
|
}
|
|
|
|
passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality });
|
|
}
|
|
|
|
const primaryNationality = passengersData[0]?.nationality;
|
|
const baseFareMinor = await this.getBaseFare(dto.scheduleId, dto.seatClassId, segmentRoute, fullRoute, primaryNationality);
|
|
const adultFareMinor = baseFareMinor * adultCount;
|
|
const paidChildrenCount = Math.max(0, childCount - 1);
|
|
const childFareMinor = baseFareMinor * paidChildrenCount;
|
|
const totalBaseFareMinor = adultFareMinor + childFareMinor;
|
|
|
|
let discountMinor = 0;
|
|
if (dto.promoCode) {
|
|
const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
|
|
if (promo?.active && promo.validUntil > new Date()) {
|
|
discountMinor = promo.percentOff ? Math.round(totalBaseFareMinor * promo.percentOff / 100) : (promo.amountOffMinor ?? 0);
|
|
}
|
|
}
|
|
|
|
const loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * 10;
|
|
const taxesMinor = Math.round(totalBaseFareMinor * 0.05);
|
|
const totalMinor = Math.max(0, totalBaseFareMinor - discountMinor - loyaltyMinor + taxesMinor);
|
|
|
|
const displayCurrency = dto.displayCurrency || Currency.ETB;
|
|
let displayTotalMinor = totalMinor;
|
|
if (displayCurrency !== Currency.ETB) {
|
|
displayTotalMinor = await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency);
|
|
}
|
|
|
|
const booking = await this.prisma.booking.create({
|
|
data: {
|
|
bookingRef: generateRef(),
|
|
passengerId: dto.passengerId,
|
|
scheduleId: dto.scheduleId,
|
|
status: 'PENDING_PAYMENT',
|
|
totalMinor, adultCount, childCount, displayCurrency, displayTotalMinor,
|
|
bookingType: dto.bookingType ?? 'ONE_WAY',
|
|
seats: {
|
|
create: passengersData.map((p) => ({
|
|
seat: { connect: { id: p.seatId } },
|
|
passengerName: p.passengerName,
|
|
dateOfBirth: p.dateOfBirth,
|
|
passengerCategory: p.category,
|
|
idDocumentType: p.idDocumentType,
|
|
idDocumentNumber: p.idDocumentType === IdDocumentType.NATIONAL_ID ? undefined : p.idDocumentNumber,
|
|
passportNumber: p.passportNumber,
|
|
passportCountry: p.passportCountry,
|
|
verifaydaVerified: p.verifaydaVerified,
|
|
verifaydaData: p.verifaydaData || undefined,
|
|
fareMinor: p.category === PassengerCategory.ADULT ? baseFareMinor : (paidChildrenCount > 0 ? baseFareMinor : 0),
|
|
displayCurrency,
|
|
})),
|
|
},
|
|
},
|
|
include: { seats: { include: { seat: true } }, schedule: { include: { originStation: true, destinationStation: true, train: true } } },
|
|
});
|
|
|
|
await this.seatsService.confirmSeats(seatIds);
|
|
this.eventEmitter.emit('booking.created', { booking });
|
|
|
|
return {
|
|
...booking,
|
|
fareBreakdown: { baseFareMinor, adultCount, adultFareMinor, childCount, freeChildrenCount: Math.min(childCount, 1), paidChildrenCount, childFareMinor, totalBaseFareMinor, discountMinor, loyaltyRedemptionMinor: loyaltyMinor, taxesFeesMinor: taxesMinor, totalMinor, currency: 'ETB', displayCurrency, displayTotalMinor },
|
|
};
|
|
}
|
|
|
|
private async getBaseFare(
|
|
scheduleId: string,
|
|
seatClassId: string,
|
|
segmentRoute?: string,
|
|
fullRoute?: string,
|
|
nationality?: string,
|
|
): Promise<number> {
|
|
const now = new Date();
|
|
const candidates = await this.prisma.fareRule.findMany({
|
|
where: {
|
|
seatClassId,
|
|
validFrom: { lte: now },
|
|
OR: [
|
|
{ validUntil: null },
|
|
{ validUntil: { gte: now } },
|
|
],
|
|
},
|
|
});
|
|
|
|
const bestMatch = this.selectBestFareRule(
|
|
candidates,
|
|
scheduleId,
|
|
segmentRoute,
|
|
fullRoute,
|
|
nationality,
|
|
);
|
|
|
|
return bestMatch?.baseFareMinor ?? 35000;
|
|
}
|
|
|
|
async getByRef(bookingRef: string) {
|
|
const booking = await this.prisma.booking.findUnique({
|
|
where: { bookingRef },
|
|
include: {
|
|
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
|
seats: { include: { seat: { include: { coach: { include: { seatClass: true } } } } } },
|
|
paymentIntent: true, ticket: true,
|
|
},
|
|
});
|
|
if (!booking) throw new NotFoundException('Booking not found');
|
|
return {
|
|
id: booking.id, bookingRef: booking.bookingRef, status: booking.status,
|
|
totalFare: booking.totalMinor / 100, adultCount: booking.adultCount, childCount: booking.childCount,
|
|
displayCurrency: booking.displayCurrency, displayTotalFare: booking.displayTotalMinor ? booking.displayTotalMinor / 100 : undefined,
|
|
bookingType: booking.bookingType, createdAt: booking.createdAt,
|
|
schedule: {
|
|
number: booking.schedule.train.number,
|
|
origin: { id: booking.schedule.originStation.id, name: booking.schedule.originStation.name, code: booking.schedule.originStation.code, city: booking.schedule.originStation.city },
|
|
destination: { id: booking.schedule.destinationStation.id, name: booking.schedule.destinationStation.name, code: booking.schedule.destinationStation.code, city: booking.schedule.destinationStation.city },
|
|
departureAt: booking.schedule.departureAt, arrivalAt: booking.schedule.arrivalAt,
|
|
},
|
|
passengers: booking.seats.map((bs) => ({
|
|
fullName: bs.passengerName, category: bs.passengerCategory, verifaydaVerified: bs.verifaydaVerified,
|
|
seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClass.name },
|
|
})),
|
|
payment: booking.paymentIntent ? { method: booking.paymentIntent.method, status: booking.paymentIntent.status } : undefined,
|
|
};
|
|
}
|
|
|
|
async modify(dto: ModifyBookingDto) {
|
|
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');
|
|
if (booking.schedule.departureAt < new Date()) throw new BadRequestException('Cannot modify past bookings');
|
|
|
|
const oldSeats = booking.seats.map(s => s.seatId);
|
|
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.confirmSeats(dto.newSeatIds);
|
|
return { modified: true, bookingRef: dto.bookingRef };
|
|
}
|
|
|
|
async cancel(bookingRef: string, reason?: 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');
|
|
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.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } });
|
|
return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' };
|
|
}
|
|
|
|
async update(id: string, dto: any) {
|
|
const booking = await this.prisma.booking.findUnique({ where: { id } });
|
|
if (!booking) throw new NotFoundException('Booking not found');
|
|
return this.prisma.booking.update({
|
|
where: { id },
|
|
data: {
|
|
status: dto.status || booking.status,
|
|
totalMinor: dto.totalMinor !== undefined ? dto.totalMinor : booking.totalMinor,
|
|
displayCurrency: dto.displayCurrency || booking.displayCurrency,
|
|
displayTotalMinor: dto.displayTotalMinor !== undefined ? dto.displayTotalMinor : booking.displayTotalMinor,
|
|
},
|
|
include: {
|
|
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
|
paymentIntent: true,
|
|
seats: { include: { seat: true } },
|
|
},
|
|
});
|
|
}
|
|
|
|
async delete(id: string) {
|
|
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.prisma.bookingSeat.deleteMany({ where: { bookingId: id } });
|
|
await this.prisma.booking.delete({ where: { id } });
|
|
|
|
return { deleted: true, bookingRef: booking.bookingRef };
|
|
}
|
|
|
|
async checkBookingUsage(id: string) {
|
|
const booking = await this.prisma.booking.findUnique({ where: { id } });
|
|
if (!booking) throw new NotFoundException('Booking not found');
|
|
|
|
const [ticketCount, paymentIntentCount, modificationsCount, cancellationCount] = await Promise.all([
|
|
this.prisma.ticket.count({ where: { bookingId: id } }),
|
|
this.prisma.paymentIntent.count({ where: { bookingId: id } }),
|
|
this.prisma.bookingModification.count({ where: { bookingId: id } }),
|
|
this.prisma.bookingCancellation.count({ where: { bookingId: id } }),
|
|
]);
|
|
|
|
const usage = [];
|
|
if (ticketCount > 0) usage.push('Ticket(s)');
|
|
if (paymentIntentCount > 0) usage.push('Payment record(s)');
|
|
if (modificationsCount > 0) usage.push('Modification history');
|
|
if (cancellationCount > 0) usage.push('Cancellation record(s)');
|
|
|
|
return {
|
|
isInUse: usage.length > 0,
|
|
affectedModules: usage,
|
|
};
|
|
}
|
|
|
|
@Cron(CronExpression.EVERY_MINUTE)
|
|
async expirePendingBookings() {
|
|
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.prisma.booking.update({ where: { id: b.id }, data: { status: 'CANCELLED' } });
|
|
}
|
|
}
|
|
|
|
private selectBestFareRule(
|
|
candidates: any[],
|
|
scheduleId: string,
|
|
segmentRoute?: string,
|
|
fullRoute?: string,
|
|
nationality?: string,
|
|
): any | null {
|
|
const priorities = [
|
|
{ tripId: scheduleId, route: segmentRoute, nationality },
|
|
{ tripId: scheduleId, route: segmentRoute, nationality: null },
|
|
{ tripId: scheduleId, route: fullRoute, nationality },
|
|
{ tripId: scheduleId, route: fullRoute, nationality: null },
|
|
{ tripId: scheduleId, route: null, nationality },
|
|
{ tripId: scheduleId, route: null, nationality: null },
|
|
{ tripId: null, route: segmentRoute, nationality },
|
|
{ tripId: null, route: segmentRoute, nationality: null },
|
|
{ tripId: null, route: fullRoute, nationality },
|
|
{ tripId: null, route: fullRoute, nationality: null },
|
|
{ tripId: null, route: null, nationality },
|
|
{ tripId: null, route: null, nationality: null },
|
|
];
|
|
|
|
for (const priority of priorities) {
|
|
const match = candidates.find(
|
|
(c) =>
|
|
c.tripId === priority.tripId &&
|
|
c.route === priority.route &&
|
|
c.nationality === priority.nationality,
|
|
);
|
|
if (match) return match;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|