mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 07:10:57 +00:00
Fix look up by phone number
This commit is contained in:
@@ -53,15 +53,17 @@ function normalizePhoneVariants(raw: string): string[] {
|
||||
const variants = new Set<string>([stripped]);
|
||||
|
||||
if (stripped.startsWith('+251') && digits.length === 12) {
|
||||
// +251 9XXXXXXXX → 09XXXXXXXX
|
||||
variants.add('0' + digits.slice(3));
|
||||
// +251 9XXXXXXXX → 251 9XXXXXXXX (no +) → 09XXXXXXXX
|
||||
variants.add(digits); // 251XXXXXXXXX
|
||||
variants.add('0' + digits.slice(3)); // 09XXXXXXXXX
|
||||
} else if (stripped.startsWith('251') && digits.length === 12) {
|
||||
// 251 9XXXXXXXX → +251 9XXXXXXXX → 09XXXXXXXX
|
||||
variants.add('+' + stripped);
|
||||
variants.add('0' + digits.slice(3));
|
||||
variants.add('+' + stripped); // +251XXXXXXXXX
|
||||
variants.add('0' + digits.slice(3)); // 09XXXXXXXXX
|
||||
} else if (stripped.startsWith('0') && digits.length === 10) {
|
||||
// 09XXXXXXXX → +251 9XXXXXXXX
|
||||
variants.add('+251' + digits.slice(1));
|
||||
// 09XXXXXXXX → +251 9XXXXXXXX → 251 9XXXXXXXX (no +)
|
||||
variants.add('+251' + digits.slice(1)); // +251XXXXXXXXX
|
||||
variants.add('251' + digits.slice(1)); // 251XXXXXXXXX
|
||||
} else if (!stripped.startsWith('+') && digits.length >= 9) {
|
||||
// bare international digits without +
|
||||
variants.add('+' + digits);
|
||||
@@ -183,15 +185,81 @@ export class BookingsService {
|
||||
const { status, page = 1, pageSize = 20 } = filters;
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
// Authenticated-user bookings don't store contactPhone — their phone lives in
|
||||
// iam.users.phone_number linked via passenger.iamUserId. Mirror the same lookup
|
||||
// that findAll uses for the search field.
|
||||
const iamRows = await this.dataSource
|
||||
.query<{ id: string }[]>(
|
||||
`SELECT u.id FROM iam.users u WHERE u.phone_number = ANY($1::text[])`,
|
||||
[variants],
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
this.logger.warn(`IAM phone lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return [] as { id: string }[];
|
||||
});
|
||||
|
||||
const iamPassengerIds = iamRows.length > 0
|
||||
? (await this.prisma.passenger.findMany({
|
||||
where: { iamUserId: { in: iamRows.map(r => r.id) } },
|
||||
select: { id: true },
|
||||
})).map(p => p.id)
|
||||
: [];
|
||||
|
||||
// Guest bookings store phone in TravelerProfile.notes JSON (created for every guest booking).
|
||||
// This catches cases where contactPhone was null but the phone was still recorded in the profile.
|
||||
const travelerRows = await this.dataSource
|
||||
.query<{ passengerId: string }[]>(
|
||||
`SELECT DISTINCT passenger_id AS "passengerId"
|
||||
FROM passenger.traveler_profiles
|
||||
WHERE notes IS NOT NULL
|
||||
AND (notes::jsonb->>'phone') = ANY($1::text[])`,
|
||||
[variants],
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
this.logger.warn(`TravelerProfile phone lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return [] as { passengerId: string }[];
|
||||
});
|
||||
const travelerPassengerIds = travelerRows.map(r => r.passengerId);
|
||||
|
||||
// Guests who saved their profile (savePassengerDetails:true) have a SavedPassengerProfile
|
||||
// row with phone + deviceId. Guest bookings store the deviceId in Booking.userAgent.
|
||||
const savedProfileRows = await this.dataSource
|
||||
.query<{ deviceId: string }[]>(
|
||||
`SELECT DISTINCT device_id AS "deviceId"
|
||||
FROM passenger.saved_passenger_profiles
|
||||
WHERE phone = ANY($1::text[]) AND device_id IS NOT NULL`,
|
||||
[variants],
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
this.logger.warn(`SavedPassengerProfile phone lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return [] as { deviceId: string }[];
|
||||
});
|
||||
const guestDeviceIds = savedProfileRows.map(r => r.deviceId);
|
||||
|
||||
// Merge all passenger IDs from every source
|
||||
const allPassengerIds = [...new Set([...iamPassengerIds, ...travelerPassengerIds])];
|
||||
|
||||
const where: any = {
|
||||
OR: [
|
||||
{ contactPhone: { in: variants } },
|
||||
{ passenger: { user: { phone: { in: variants } } } },
|
||||
...(allPassengerIds.length > 0 ? [{ passengerId: { in: allPassengerIds } }] : []),
|
||||
...(guestDeviceIds.length > 0 ? [{ userAgent: { in: guestDeviceIds } }] : []),
|
||||
],
|
||||
};
|
||||
if (status) where.status = status;
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
// PackageBooking is a separate table with its own contactPhone field —
|
||||
// must be queried independently or guest package bookings are invisible.
|
||||
const pkgWhere: any = {
|
||||
OR: [
|
||||
{ contactPhone: { in: variants } },
|
||||
...(allPassengerIds.length > 0 ? [{ passengerId: { in: allPassengerIds } }] : []),
|
||||
],
|
||||
};
|
||||
if (status) pkgWhere.status = status;
|
||||
|
||||
const [items, total, pkgItems, pkgTotal] = await Promise.all([
|
||||
this.prisma.booking.findMany({
|
||||
where,
|
||||
skip,
|
||||
@@ -205,37 +273,84 @@ export class BookingsService {
|
||||
},
|
||||
}),
|
||||
this.prisma.booking.count({ where }),
|
||||
this.prisma.packageBooking.findMany({
|
||||
where: pkgWhere,
|
||||
skip,
|
||||
take: pageSize,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
package: {
|
||||
include: {
|
||||
outboundSchedule: { include: { originStation: true, destinationStation: true } },
|
||||
},
|
||||
},
|
||||
paymentIntent: { select: { method: true, status: true, amountMinor: true, currency: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.packageBooking.count({ where: pkgWhere }),
|
||||
]);
|
||||
|
||||
const mappedBookings = items.map(booking => ({
|
||||
id: booking.id,
|
||||
bookingRef: booking.bookingRef,
|
||||
status: booking.status,
|
||||
totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
|
||||
currency: 'ETB',
|
||||
displayCurrency: booking.displayCurrency,
|
||||
displayTotalMinor: booking.displayTotalMinor,
|
||||
adultCount: booking.adultCount,
|
||||
childCount: booking.childCount,
|
||||
bookingType: booking.bookingType,
|
||||
returnLegStatus: (booking as any).returnLegStatus ?? null,
|
||||
createdAt: booking.createdAt,
|
||||
schedule: {
|
||||
train: booking.schedule.train,
|
||||
originStation: booking.schedule.originStation,
|
||||
destinationStation: booking.schedule.destinationStation,
|
||||
departureAt: booking.schedule.departureAt,
|
||||
arrivalAt: booking.schedule.arrivalAt,
|
||||
},
|
||||
payment: booking.paymentIntent ?? undefined,
|
||||
seatCount: booking.seats.length,
|
||||
}));
|
||||
|
||||
const mappedPkg = pkgItems.map((b: any) => ({
|
||||
id: b.id,
|
||||
bookingRef: b.bookingRef,
|
||||
status: b.status,
|
||||
totalMinor: b.totalMinor,
|
||||
currency: b.currency || 'ETB',
|
||||
displayCurrency: b.displayCurrency ?? null,
|
||||
displayTotalMinor: b.displayTotalMinor ?? null,
|
||||
adultCount: b.adultCount,
|
||||
childCount: b.childCount,
|
||||
bookingType: 'PACKAGE',
|
||||
returnLegStatus: null,
|
||||
createdAt: b.createdAt,
|
||||
schedule: b.package?.outboundSchedule
|
||||
? {
|
||||
train: null,
|
||||
originStation: b.package.outboundSchedule.originStation,
|
||||
destinationStation: b.package.outboundSchedule.destinationStation,
|
||||
departureAt: b.package.outboundSchedule.departureAt,
|
||||
arrivalAt: b.package.outboundSchedule.arrivalAt,
|
||||
}
|
||||
: null,
|
||||
payment: b.paymentIntent ?? undefined,
|
||||
seatCount: b.passengerCount,
|
||||
}));
|
||||
|
||||
const allItems = [...mappedBookings, ...mappedPkg]
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.slice(0, pageSize);
|
||||
|
||||
return {
|
||||
items: items.map(booking => ({
|
||||
id: booking.id,
|
||||
bookingRef: booking.bookingRef,
|
||||
status: booking.status,
|
||||
totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
|
||||
currency: 'ETB',
|
||||
displayCurrency: booking.displayCurrency,
|
||||
displayTotalMinor: booking.displayTotalMinor,
|
||||
adultCount: booking.adultCount,
|
||||
childCount: booking.childCount,
|
||||
bookingType: booking.bookingType,
|
||||
returnLegStatus: (booking as any).returnLegStatus ?? null,
|
||||
createdAt: booking.createdAt,
|
||||
schedule: {
|
||||
train: booking.schedule.train,
|
||||
originStation: booking.schedule.originStation,
|
||||
destinationStation: booking.schedule.destinationStation,
|
||||
departureAt: booking.schedule.departureAt,
|
||||
arrivalAt: booking.schedule.arrivalAt,
|
||||
},
|
||||
payment: booking.paymentIntent ?? undefined,
|
||||
seatCount: booking.seats.length,
|
||||
})),
|
||||
items: allItems,
|
||||
meta: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
total: total + pkgTotal,
|
||||
totalPages: Math.ceil((total + pkgTotal) / pageSize),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user