refactor( iam ): replace prisma.user joins with batch IAM fetch in bookings, tickets, and payments e2e

This commit is contained in:
Abubeker Yasin
2026-06-09 13:54:38 +03:00
parent 5ae6d9600a
commit 431b1c4f3c
3 changed files with 100 additions and 65 deletions

View File

@@ -1,4 +1,6 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { PrismaService } from '../../common/prisma.service';
import { SeatsService } from '../seats/seats.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
@@ -31,11 +33,12 @@ interface BookingFilters {
@Injectable()
export class BookingsService {
constructor(
private prisma: PrismaService,
private seatsService: SeatsService,
private eventEmitter: EventEmitter2,
private verifaydaService: VerifaydaService,
private currencyService: CurrencyService,
private readonly prisma: PrismaService,
@InjectDataSource() private readonly dataSource: DataSource,
private readonly seatsService: SeatsService,
private readonly eventEmitter: EventEmitter2,
private readonly verifaydaService: VerifaydaService,
private readonly currencyService: CurrencyService,
) {}
async findByPassengerId(passengerId: string, filters: BookingFilters = {}) {
@@ -205,7 +208,7 @@ export class BookingsService {
take: pageSize,
orderBy: { createdAt: 'desc' },
include: {
passenger: { include: { user: true } },
passenger: { select: { id: true, iamUserId: true } },
schedule: { include: { originStation: true, destinationStation: true, train: true } },
paymentIntent: true,
seats: { include: { seat: true } },
@@ -213,29 +216,43 @@ export class BookingsService {
}),
this.prisma.booking.count({ where }),
]);
const iamUserIds = items.map(b => b.passenger?.iamUserId).filter(Boolean) as string[];
const iamRows = iamUserIds.length > 0
? await this.dataSource.query<{ id: string; email: string; name: any; phone_number: string | null }[]>(
`SELECT id, email, name, phone_number FROM iam.users WHERE id = ANY($1)`,
[iamUserIds],
)
: [];
const iamMap = new Map(iamRows.map(r => [r.id, r]));
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,
})),
items: items.map(booking => {
const iam = booking.passenger?.iamUserId ? iamMap.get(booking.passenger.iamUserId) : undefined;
return {
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: iam
? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number }
: null,
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,

View File

@@ -21,11 +21,7 @@ describe('Payments E2E', () => {
prisma = app.get<PrismaService>(PrismaService);
const testUser = await prisma.user.create({
data: { email: 'payment-test@example.com', phone: '+251911111112', fullName: 'Payment Test User', passwordHash: '$2b$10$abcdefghijklmnopqrstuvwxyz', role: 'PASSENGER' },
});
const passenger = await prisma.passenger.create({ data: { userId: testUser.id } });
const passenger = await prisma.passenger.create({ data: { iamUserId: 'test-iam-payments-user' } });
await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 100000, currency: 'ETB' } });
@@ -77,7 +73,6 @@ describe('Payments E2E', () => {
prisma.walletLedgerEntry.deleteMany(),
prisma.walletAccount.deleteMany(),
prisma.passenger.deleteMany(),
prisma.user.deleteMany({ where: { email: 'payment-test@example.com' } }),
]);
await app.close();
});

View File

@@ -1,4 +1,6 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { PrismaService } from '../../common/prisma.service';
import * as QRCode from 'qrcode';
@@ -11,7 +13,10 @@ interface OfflineValidation {
@Injectable()
export class TicketsService {
constructor(private prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
@InjectDataSource() private readonly dataSource: DataSource,
) {}
async listTickets(filters: { search?: string; status?: string; skip: number; take: number }) {
const where: any = {};
@@ -25,39 +30,57 @@ export class TicketsService {
if (filters.status) {
where.booking = { status: filters.status };
}
const tickets = await this.prisma.ticket.findMany({
where,
include: {
booking: {
include: {
schedule: { include: { originStation: true, destinationStation: true, train: true } },
seats: { include: { seat: { include: { coach: true } } } },
passenger: { include: { user: true } },
const [tickets, total] = await Promise.all([
this.prisma.ticket.findMany({
where,
include: {
booking: {
include: {
schedule: { include: { originStation: true, destinationStation: true, train: true } },
seats: { include: { seat: { include: { coach: true } } } },
passenger: { select: { id: true, iamUserId: true } },
},
},
},
},
skip: filters.skip,
take: filters.take,
orderBy: { issuedAt: 'desc' },
});
const total = await this.prisma.ticket.count({ where });
skip: filters.skip,
take: filters.take,
orderBy: { issuedAt: 'desc' },
}),
this.prisma.ticket.count({ where }),
]);
const iamUserIds = tickets.map(t => t.booking.passenger?.iamUserId).filter(Boolean) as string[];
const iamRows = iamUserIds.length > 0
? await this.dataSource.query<{ id: string; email: string; name: any }[]>(
`SELECT id, email, name FROM iam.users WHERE id = ANY($1)`,
[iamUserIds],
)
: [];
const iamMap = new Map(iamRows.map(r => [r.id, r]));
return {
items: tickets.map((t) => ({
id: t.id,
ticketNumber: t.barcodePayload,
bookingRef: t.bookingRef,
booking: {
bookingRef: t.booking.bookingRef,
items: tickets.map((t) => {
const iam = t.booking.passenger?.iamUserId ? iamMap.get(t.booking.passenger.iamUserId) : undefined;
const passengerInfo = iam
? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email }
: { fullName: 'Guest', email: t.booking.contactEmail };
return {
id: t.id,
ticketNumber: t.barcodePayload,
bookingRef: t.bookingRef,
booking: {
bookingRef: t.booking.bookingRef,
status: t.booking.status,
passenger: passengerInfo,
contactEmail: t.booking.contactEmail,
},
schedule: t.booking.schedule,
seat: t.booking.seats[0]?.seat,
status: t.booking.status,
passenger: t.booking.passenger?.user || { fullName: 'Guest', email: t.booking.contactEmail },
contactEmail: t.booking.contactEmail,
},
schedule: t.booking.schedule,
seat: t.booking.seats[0]?.seat,
status: t.booking.status,
validatedAt: t.validatedAt,
createdAt: t.issuedAt,
})),
validatedAt: t.validatedAt,
createdAt: t.issuedAt,
};
}),
total,
skip: filters.skip,
take: filters.take,
@@ -199,7 +222,7 @@ export class TicketsService {
include: {
ticket: true,
seats: { include: { seat: { include: { coach: true } } } },
passenger: { include: { user: true } },
passenger: { select: { id: true, iamUserId: true } },
},
});