mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 11:21:18 +00:00
598 lines
23 KiB
TypeScript
598 lines
23 KiB
TypeScript
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
|
import { InjectDataSource } from '@nestjs/typeorm';
|
|
import { DataSource } from 'typeorm';
|
|
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;
|
|
gender?: string;
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}
|
|
|
|
type IamUserRow = {
|
|
id: string;
|
|
email: string;
|
|
name: { en: string; am: string } | null;
|
|
phone_number: string | null;
|
|
metadata: Record<string, any> | null;
|
|
verified_by?: string | null;
|
|
};
|
|
|
|
@Injectable()
|
|
export class PassengersService {
|
|
private readonly logger = new Logger(PassengersService.name);
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
@InjectDataSource() private readonly dataSource: DataSource,
|
|
private readonly verifaydaService: VerifaydaService,
|
|
private readonly auditService: AuditService,
|
|
) {}
|
|
|
|
async findAll(filters: PassengerFilters = {}) {
|
|
const { search, gender, dateFrom, dateTo, page = 1, pageSize = 20 } = filters;
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
const where: any = {};
|
|
|
|
if (search) {
|
|
// IAM user search: resolve matching iamUserIds first, then filter by passengerId
|
|
const iamRows = await this.dataSource.query<{ id: string }[]>(
|
|
`SELECT id FROM iam.users WHERE (name->>'en') ILIKE $1 OR (name->>'am') ILIKE $1 OR email ILIKE $1 OR phone_number ILIKE $1`,
|
|
[`%${search}%`],
|
|
);
|
|
const matchedPassengers = iamRows.length > 0
|
|
? await this.prisma.passenger.findMany({ where: { iamUserId: { in: iamRows.map(r => r.id) } }, select: { id: true } })
|
|
: [];
|
|
|
|
where.OR = [
|
|
{ fullName: { contains: search, mode: 'insensitive' } },
|
|
...(matchedPassengers.length > 0 ? [{ passengerId: { in: matchedPassengers.map(p => p.id) } }] : []),
|
|
];
|
|
}
|
|
|
|
if (gender) {
|
|
where.gender = gender;
|
|
}
|
|
|
|
if (dateFrom || dateTo) {
|
|
where.createdAt = {
|
|
...(dateFrom ? { gte: new Date(dateFrom) } : {}),
|
|
...(dateTo ? { lte: new Date(new Date(dateTo).setHours(23, 59, 59, 999)) } : {}),
|
|
};
|
|
}
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.travelerProfile.findMany({
|
|
where,
|
|
skip,
|
|
take: pageSize,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
passenger: {
|
|
include: {
|
|
loyalty: true,
|
|
wallet: true,
|
|
_count: { select: { bookings: true } },
|
|
bookings: {
|
|
take: 1,
|
|
orderBy: { createdAt: 'desc' },
|
|
select: {
|
|
contactPhone: true,
|
|
contactEmail: true,
|
|
seats: {
|
|
take: 1,
|
|
orderBy: { id: 'asc' },
|
|
select: {
|
|
passengerName: true,
|
|
passportNumber: true,
|
|
passportCountry: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
this.prisma.travelerProfile.count({ where }),
|
|
]);
|
|
|
|
const iamUserIds = items.map(p => p.passenger?.iamUserId).filter(Boolean) as string[];
|
|
const iamRows = iamUserIds.length > 0
|
|
? await this.dataSource.query<IamUserRow[]>(
|
|
`SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = ANY($1)`,
|
|
[iamUserIds],
|
|
)
|
|
: [];
|
|
const iamMap = new Map(iamRows.map(r => [r.id, r]));
|
|
|
|
return {
|
|
items: items.map(profile => {
|
|
const passenger = profile.passenger;
|
|
const iam = passenger?.iamUserId ? iamMap.get(passenger.iamUserId) : undefined;
|
|
const faydaVerified = iam?.metadata?.faydaVerified === true
|
|
|| iam?.metadata?.faydaVerified === 'true';
|
|
|
|
const guestBooking = (passenger as any)?.bookings?.[0] ?? null;
|
|
const guestSeat = guestBooking?.seats?.[0] ?? null;
|
|
|
|
let notesData: any = null;
|
|
if (profile.notes) {
|
|
try {
|
|
notesData = typeof profile.notes === 'string' ? JSON.parse(profile.notes) : profile.notes;
|
|
} catch {
|
|
notesData = null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: profile.id,
|
|
fullName: profile.fullName,
|
|
email: iam?.email ?? notesData?.email ?? guestBooking?.contactEmail ?? null,
|
|
phone: iam?.phone_number ?? notesData?.phone ?? guestBooking?.contactPhone ?? null,
|
|
gender: profile.gender ?? iam?.metadata?.gender ?? null,
|
|
dateOfBirth: profile.dateOfBirth
|
|
? new Date(profile.dateOfBirth).toISOString().split('T')[0]
|
|
: (iam?.metadata?.dateOfBirth ?? null),
|
|
nationality: iam?.metadata?.nationality ?? notesData?.nationality ?? (guestSeat?.passportCountry ? (guestSeat.passportCountry === 'Ethiopia' ? 'Ethiopian' : guestSeat.passportCountry) : null),
|
|
nationalityCode: iam?.metadata?.nationalityCode ?? null,
|
|
faydaVerified,
|
|
faydaVerifiedAt: iam?.metadata?.faydaVerifiedAt ?? null,
|
|
passportNumber: iam?.metadata?.passportNumber ?? notesData?.passportNumber ?? guestSeat?.passportNumber ?? null,
|
|
passportCountry: iam?.metadata?.passportCountry ?? notesData?.passportCountry ?? guestSeat?.passportCountry ?? null,
|
|
passportExpiryDate: iam?.metadata?.passportExpiryDate ?? null,
|
|
idDocumentType: profile.nationalId ? 'NATIONAL_ID' : (notesData?.idDocumentType ?? null),
|
|
verified: faydaVerified,
|
|
lastLoginAt: null,
|
|
role: null,
|
|
loyalty: passenger?.loyalty
|
|
? { tier: passenger.loyalty.tier, pointsBalance: passenger.loyalty.pointsBalance, lifetimePoints: (passenger.loyalty as any).lifetimePoints ?? 0 }
|
|
: null,
|
|
wallet: (passenger as any)?.wallet
|
|
? { balanceMinor: (passenger as any).wallet.balanceMinor, currency: (passenger as any).wallet.currency ?? 'ETB' }
|
|
: null,
|
|
loyaltyTier: passenger?.loyalty?.tier || 'BRONZE',
|
|
loyaltyPoints: passenger?.loyalty?.pointsBalance || 0,
|
|
totalBookings: passenger?._count?.bookings || 0,
|
|
createdAt: profile.createdAt,
|
|
};
|
|
}),
|
|
meta: {
|
|
page,
|
|
pageSize,
|
|
total,
|
|
totalPages: Math.ceil(total / pageSize),
|
|
},
|
|
};
|
|
}
|
|
|
|
async getProfile(passengerId: string) {
|
|
const passenger = await this.prisma.passenger.findUnique({
|
|
where: { id: passengerId },
|
|
include: {
|
|
bookings: {
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 10,
|
|
include: {
|
|
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
|
seats: { include: { seat: { include: { coach: true } } } },
|
|
},
|
|
},
|
|
loyalty: true,
|
|
wallet: true,
|
|
travelerProfiles: true,
|
|
savedRoutes: true,
|
|
},
|
|
});
|
|
if (!passenger) throw new NotFoundException('Passenger not found');
|
|
|
|
let iamUser: IamUserRow | null = null;
|
|
if (passenger.iamUserId) {
|
|
const rows = await this.dataSource.query<IamUserRow[]>(
|
|
`SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`,
|
|
[passenger.iamUserId],
|
|
);
|
|
iamUser = rows[0] ?? null;
|
|
}
|
|
|
|
return {
|
|
id: passenger.id,
|
|
fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null,
|
|
email: iamUser?.email ?? null,
|
|
phone: iamUser?.phone_number ?? null,
|
|
createdAt: passenger.createdAt,
|
|
bookings: passenger.bookings.map((b) => ({
|
|
id: b.id,
|
|
bookingRef: b.bookingRef,
|
|
status: b.status,
|
|
totalFare: b.totalMinor / 100,
|
|
createdAt: b.createdAt,
|
|
trip: {
|
|
number: b.schedule.train.number,
|
|
origin: {
|
|
id: b.schedule.originStation.id,
|
|
name: b.schedule.originStation.name,
|
|
code: b.schedule.originStation.code,
|
|
city: b.schedule.originStation.city
|
|
},
|
|
destination: {
|
|
id: b.schedule.destinationStation.id,
|
|
name: b.schedule.destinationStation.name,
|
|
code: b.schedule.destinationStation.code,
|
|
city: b.schedule.destinationStation.city
|
|
},
|
|
departureAt: b.schedule.departureAt,
|
|
},
|
|
passengers: b.seats.map((bs) => ({
|
|
fullName: bs.passengerName,
|
|
seat: { number: bs.seat.seatNumber, coach: bs.seat.coach.number, class: 'N/A' },
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Full passenger-form payload for the logged-in user, sourced from the IAM user record
|
|
* (iam.users) — where the Fayda-verified identity actually lives — rather than the sparse
|
|
* Passenger row. The booking passenger form (/booking/passengers) calls this via
|
|
* GET /passengers/me to prefill (and lock) an already-verified passenger's details.
|
|
*
|
|
* Identity fields don't depend on a Passenger row existing; only `id` (used later to tag the
|
|
* primary passenger on the booking) does, and it's null if no Passenger row is linked yet.
|
|
*/
|
|
async getMyProfile(iamUserId: string) {
|
|
const [passenger, iamRows] = await Promise.all([
|
|
this.prisma.passenger.findUnique({ where: { iamUserId } }),
|
|
this.dataSource.query<IamUserRow[]>(
|
|
`SELECT id, email, name, phone_number, metadata, verified_by FROM iam.users WHERE id = $1 LIMIT 1`,
|
|
[iamUserId],
|
|
),
|
|
]);
|
|
|
|
const iam = iamRows[0] ?? null;
|
|
if (!iam && !passenger) return null;
|
|
|
|
const meta = iam?.metadata ?? {};
|
|
const faydaVerified =
|
|
iam?.verified_by === 'fayda' ||
|
|
meta.faydaVerified === true ||
|
|
meta.faydaVerified === 'true';
|
|
|
|
// Fayda writes gender as { am, en }; older/manual records may store a plain string.
|
|
const gender =
|
|
meta.gender && typeof meta.gender === 'object'
|
|
? (meta.gender.en ?? meta.gender.am ?? null)
|
|
: (meta.gender ?? null);
|
|
|
|
const fullName = iam?.name?.en ?? iam?.name?.am ?? null;
|
|
|
|
// Stored as ISO by the Fayda upsert; tolerate a "/"-separated legacy value.
|
|
const rawDob = meta.dateOfBirth ?? meta.birthdate ?? null;
|
|
const dateOfBirth = rawDob ? String(rawDob).replace(/\//g, '-') : null;
|
|
|
|
// Nationality isn't always in metadata; a Fayda-verified holder is Ethiopian by definition.
|
|
const nationality = meta.nationality ?? (faydaVerified ? 'ETHIOPIAN' : null);
|
|
|
|
return {
|
|
id: passenger?.id ?? null,
|
|
fullName,
|
|
email: iam?.email ?? meta.email ?? null,
|
|
phone: iam?.phone_number ?? meta.phoneNumber ?? null,
|
|
gender,
|
|
dateOfBirth,
|
|
nationality,
|
|
faydaVerified,
|
|
faydaSub: meta.sub ?? null,
|
|
passportNumber: meta.passportNumber ?? null,
|
|
passportCountry: meta.passportCountry ?? null,
|
|
passportIssueDate: meta.passportIssueDate ?? null,
|
|
passportExpiryDate: meta.passportExpiryDate ?? null,
|
|
passportIssuingAuthority: meta.passportIssuingAuthority ?? null,
|
|
};
|
|
}
|
|
|
|
async getStats(passengerId: string) {
|
|
const [totalTrips, totalSpendResult, loyalty] = await Promise.all([
|
|
this.prisma.booking.count({ where: { passengerId, status: 'BOARDED' as any } }),
|
|
this.prisma.booking.aggregate({ where: { passengerId, status: 'BOARDED' as any }, _sum: { totalMinor: true } }),
|
|
this.prisma.loyaltyAccount.findUnique({ where: { passengerId } }),
|
|
]);
|
|
const totalSpend = ((totalSpendResult._sum?.totalMinor ?? 0) as number) / 100;
|
|
return { totalTrips, totalSpend, loyaltyPoints: loyalty?.pointsBalance ?? 0, co2Saved: totalTrips * 6 };
|
|
}
|
|
|
|
async savePassengers(passengers: any[], userId?: string, deviceId?: string) {
|
|
if (!passengers || !Array.isArray(passengers)) {
|
|
throw new BadRequestException('Passengers array is required');
|
|
}
|
|
|
|
if (passengers.length === 0) {
|
|
throw new BadRequestException('At least one passenger is required');
|
|
}
|
|
|
|
const savedProfiles = await Promise.all(
|
|
passengers.map((p) =>
|
|
this.prisma.savedPassengerProfile.create({
|
|
data: {
|
|
userId,
|
|
deviceId,
|
|
passengerName: p.name || p.passengerName,
|
|
dateOfBirth: new Date(p.dateOfBirth),
|
|
idDocumentType: p.nationalId ? 'NATIONAL_ID' : 'PASSPORT',
|
|
passportNumber: p.passportNumber,
|
|
passportCountry: p.passportCountry,
|
|
nationality: p.nationality,
|
|
phone: p.phone,
|
|
email: p.email,
|
|
},
|
|
})
|
|
)
|
|
);
|
|
return {
|
|
count: savedProfiles.length,
|
|
passengerIds: savedProfiles.map(p => p.id),
|
|
passengers: savedProfiles.map(p => ({
|
|
id: p.id,
|
|
passengerName: p.passengerName,
|
|
dateOfBirth: p.dateOfBirth,
|
|
nationality: p.nationality,
|
|
phone: p.phone ?? null,
|
|
email: p.email ?? null,
|
|
})),
|
|
message: 'Passenger details saved successfully',
|
|
};
|
|
}
|
|
|
|
createTravelerProfile(dto: CreateTravelerProfileDto) {
|
|
return this.prisma.travelerProfile.create({
|
|
data: {
|
|
passengerId: dto.passengerId,
|
|
fullName: dto.fullName,
|
|
relationship: dto.relationship,
|
|
dateOfBirth: dto.dateOfBirth ? new Date(dto.dateOfBirth) : null,
|
|
nationalId: dto.nationalId || null,
|
|
gender: dto.gender || null,
|
|
notes: dto.notes || null,
|
|
}
|
|
});
|
|
}
|
|
|
|
getTravelerProfiles(passengerId: string) {
|
|
return this.prisma.travelerProfile.findMany({ where: { passengerId } });
|
|
}
|
|
|
|
createSavedRoute(dto: CreateSavedRouteDto) {
|
|
return this.prisma.savedRoute.create({ data: dto });
|
|
}
|
|
|
|
getSavedRoutes(passengerId: string) {
|
|
return this.prisma.savedRoute.findMany({ where: { passengerId }, orderBy: { tripCount: 'desc' } });
|
|
}
|
|
|
|
async updatePassenger(id: string, dto: any) {
|
|
const passenger = await this.prisma.passenger.findUnique({ where: { id } });
|
|
if (!passenger) throw new NotFoundException('Passenger not found');
|
|
|
|
if (passenger.iamUserId && (dto.fullName || dto.email || dto.phone)) {
|
|
const updates: string[] = [];
|
|
const params: any[] = [];
|
|
let idx = 1;
|
|
|
|
if (dto.fullName) {
|
|
updates.push(`name = COALESCE(name, '{}') || jsonb_build_object('en', $${idx}::text, 'am', $${idx}::text)`);
|
|
params.push(dto.fullName);
|
|
idx++;
|
|
}
|
|
if (dto.email) {
|
|
updates.push(`email = $${idx}`);
|
|
params.push(dto.email);
|
|
idx++;
|
|
}
|
|
if (dto.phone) {
|
|
updates.push(`phone_number = $${idx}`);
|
|
params.push(dto.phone);
|
|
idx++;
|
|
}
|
|
|
|
params.push(passenger.iamUserId);
|
|
await this.dataSource.query(
|
|
`UPDATE iam.users SET ${updates.join(', ')} WHERE id = $${idx}`,
|
|
params,
|
|
);
|
|
}
|
|
|
|
const [updated, iamRows] = await Promise.all([
|
|
this.prisma.passenger.findUnique({ where: { id }, include: { loyalty: true } }),
|
|
passenger.iamUserId
|
|
? this.dataSource.query<IamUserRow[]>(
|
|
`SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`,
|
|
[passenger.iamUserId],
|
|
)
|
|
: Promise.resolve([] as IamUserRow[]),
|
|
]);
|
|
|
|
const iamUser = iamRows[0] ?? null;
|
|
return {
|
|
id: updated!.id,
|
|
fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null,
|
|
email: iamUser?.email ?? null,
|
|
phone: iamUser?.phone_number ?? null,
|
|
loyalty: updated!.loyalty,
|
|
};
|
|
}
|
|
|
|
async registerPassenger(dto: RegisterPassengerDto) {
|
|
const isEthiopian = !!dto.nationalId;
|
|
const isLoggedIn = !!dto.userId;
|
|
let verifiedData: any = null;
|
|
|
|
if (isEthiopian && dto.verifyWithFayda !== false) {
|
|
try {
|
|
const verification = await this.verifaydaService.verifyNationalId(dto.nationalId!);
|
|
if (verification.verified && verification.passengerData) {
|
|
verifiedData = verification.passengerData;
|
|
}
|
|
} catch (error) {
|
|
this.logger.warn('Fayda verification failed, using manual data:', error);
|
|
}
|
|
}
|
|
|
|
const finalData = {
|
|
passengerName: verifiedData?.fullName || dto.passengerName,
|
|
dateOfBirth: verifiedData?.dateOfBirth || new Date(dto.dateOfBirth),
|
|
nationality: verifiedData?.nationality || dto.nationality || (isEthiopian ? 'Ethiopian' : null),
|
|
gender: verifiedData?.gender || dto.gender,
|
|
phone: dto.phone,
|
|
email: dto.email,
|
|
};
|
|
|
|
if (isLoggedIn) {
|
|
const linkedPassenger = await this.prisma.passenger.findUnique({
|
|
where: { iamUserId: dto.userId },
|
|
});
|
|
|
|
if (!linkedPassenger) {
|
|
throw new BadRequestException('Passenger not found');
|
|
}
|
|
|
|
return {
|
|
id: linkedPassenger.id,
|
|
passengerName: finalData.passengerName,
|
|
dateOfBirth: finalData.dateOfBirth,
|
|
nationality: finalData.nationality,
|
|
verified: !!verifiedData,
|
|
linked: true,
|
|
message: 'Passenger details saved and linked to user account',
|
|
};
|
|
}
|
|
|
|
const profile = await this.prisma.savedPassengerProfile.create({
|
|
data: {
|
|
deviceId: dto.deviceId,
|
|
passengerName: finalData.passengerName,
|
|
dateOfBirth: finalData.dateOfBirth,
|
|
idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT',
|
|
passportNumber: dto.passportNumber,
|
|
passportCountry: dto.passportCountry,
|
|
nationality: finalData.nationality,
|
|
phone: dto.phone,
|
|
email: dto.email,
|
|
},
|
|
});
|
|
|
|
return {
|
|
id: profile.id,
|
|
passengerName: finalData.passengerName,
|
|
dateOfBirth: finalData.dateOfBirth,
|
|
nationality: finalData.nationality,
|
|
verified: !!verifiedData,
|
|
linked: false,
|
|
message: 'Passenger details saved for guest booking',
|
|
};
|
|
}
|
|
|
|
async deletePassenger(id: string, cascade = false) {
|
|
// id may be a TravelerProfile.id (from the list endpoint) or a Passenger.id
|
|
let passenger = await this.prisma.passenger.findUnique({ where: { id } });
|
|
|
|
if (!passenger) {
|
|
const profile = await this.prisma.travelerProfile.findUnique({ where: { id } });
|
|
if (!profile?.passengerId) throw new NotFoundException('Passenger not found');
|
|
passenger = await this.prisma.passenger.findUnique({ where: { id: profile.passengerId } });
|
|
if (!passenger) throw new NotFoundException('Passenger not found');
|
|
}
|
|
|
|
const passengerId = passenger.id;
|
|
|
|
if (!cascade) {
|
|
const usage = await this.checkPassengerUsage(passengerId);
|
|
if (usage.isInUse && usage.constraints) {
|
|
const passengerName = `Passenger ${passengerId.slice(-8)}`;
|
|
throw new DeleteOperationException('Passenger', passengerName, usage.constraints);
|
|
}
|
|
}
|
|
|
|
// Resolve booking IDs first (needed for multi-step child deletion)
|
|
const bookings = await this.prisma.booking.findMany({
|
|
where: { passengerId },
|
|
select: { id: true },
|
|
});
|
|
const bookingIds = bookings.map(b => b.id);
|
|
|
|
if (bookingIds.length > 0) {
|
|
const tickets = await this.prisma.ticket.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
|
if (tickets.length > 0) {
|
|
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: { in: tickets.map(t => t.id) } } });
|
|
}
|
|
await this.prisma.ticket.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
|
|
const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
|
if (foodOrders.length > 0) {
|
|
await this.prisma.foodOrderItem.deleteMany({ where: { orderId: { in: foodOrders.map(o => o.id) } } });
|
|
}
|
|
await this.prisma.foodOrder.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
|
|
const paymentIntents = await this.prisma.paymentIntent.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
|
if (paymentIntents.length > 0) {
|
|
await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: { in: paymentIntents.map(p => p.id) } } });
|
|
}
|
|
await this.prisma.paymentIntent.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
|
|
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
await this.prisma.agentBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
await this.prisma.bookingModification.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
await this.prisma.baggageBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
await this.prisma.journey.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
|
await this.prisma.booking.deleteMany({ where: { id: { in: bookingIds } } });
|
|
}
|
|
|
|
// Package bookings
|
|
const packageBookings = await this.prisma.packageBooking.findMany({ where: { passengerId }, select: { id: true } });
|
|
if (packageBookings.length > 0) {
|
|
const pbIds = packageBookings.map(pb => pb.id);
|
|
await this.prisma.packageBookingPassenger.deleteMany({ where: { bookingId: { in: pbIds } } });
|
|
await this.prisma.packagePaymentIntent.deleteMany({ where: { packageBookingId: { in: pbIds } } });
|
|
await this.prisma.packageBooking.deleteMany({ where: { id: { in: pbIds } } });
|
|
}
|
|
|
|
await this.prisma.loyaltyLedgerEntry.deleteMany({ where: { account: { passengerId } } });
|
|
await this.prisma.loyaltyAccount.deleteMany({ where: { passengerId } });
|
|
await this.prisma.walletLedgerEntry.deleteMany({ where: { wallet: { passengerId } } });
|
|
await this.prisma.walletAccount.deleteMany({ where: { passengerId } });
|
|
await this.prisma.notification.deleteMany({ where: { passengerId } });
|
|
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 };
|
|
}
|
|
|
|
async checkPassengerUsage(id: string) {
|
|
const [bookingCount, loyaltyAccount, walletAccount] = await Promise.all([
|
|
this.prisma.booking.count({ where: { passengerId: id } }),
|
|
this.prisma.loyaltyAccount.findUnique({ where: { passengerId: id } }),
|
|
this.prisma.walletAccount.findUnique({ where: { passengerId: id } }),
|
|
]);
|
|
|
|
const constraints = [];
|
|
if (bookingCount > 0) constraints.push({ entityName: 'booking', count: bookingCount, action: 'complete' as const });
|
|
if (loyaltyAccount) constraints.push({ entityName: 'loyalty account', count: 1, action: 'delete' as const });
|
|
if (walletAccount) constraints.push({ entityName: 'wallet account', count: 1, action: 'delete' as const });
|
|
|
|
return {
|
|
isInUse: constraints.length > 0,
|
|
affectedModules: constraints.map(c => `${c.count} ${c.entityName}${c.count > 1 ? 's' : ''}`),
|
|
constraints
|
|
};
|
|
}
|
|
}
|