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 { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { PrismaService } from '../../common/prisma.service'; import { PrismaService } from '../../common/prisma.service';
import { SeatsService } from '../seats/seats.service'; import { SeatsService } from '../seats/seats.service';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
@@ -31,11 +33,12 @@ interface BookingFilters {
@Injectable() @Injectable()
export class BookingsService { export class BookingsService {
constructor( constructor(
private prisma: PrismaService, private readonly prisma: PrismaService,
private seatsService: SeatsService, @InjectDataSource() private readonly dataSource: DataSource,
private eventEmitter: EventEmitter2, private readonly seatsService: SeatsService,
private verifaydaService: VerifaydaService, private readonly eventEmitter: EventEmitter2,
private currencyService: CurrencyService, private readonly verifaydaService: VerifaydaService,
private readonly currencyService: CurrencyService,
) {} ) {}
async findByPassengerId(passengerId: string, filters: BookingFilters = {}) { async findByPassengerId(passengerId: string, filters: BookingFilters = {}) {
@@ -205,7 +208,7 @@ export class BookingsService {
take: pageSize, take: pageSize,
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
include: { include: {
passenger: { include: { user: true } }, passenger: { select: { id: true, iamUserId: true } },
schedule: { include: { originStation: true, destinationStation: true, train: true } }, schedule: { include: { originStation: true, destinationStation: true, train: true } },
paymentIntent: true, paymentIntent: true,
seats: { include: { seat: true } }, seats: { include: { seat: true } },
@@ -213,29 +216,43 @@ export class BookingsService {
}), }),
this.prisma.booking.count({ where }), 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 { return {
items: items.map(booking => ({ items: items.map(booking => {
id: booking.id, const iam = booking.passenger?.iamUserId ? iamMap.get(booking.passenger.iamUserId) : undefined;
bookingRef: booking.bookingRef, return {
status: booking.status, id: booking.id,
totalMinor: booking.totalMinor, bookingRef: booking.bookingRef,
currency: 'ETB', status: booking.status,
displayCurrency: booking.displayCurrency, totalMinor: booking.totalMinor,
displayTotalMinor: booking.displayTotalMinor, currency: 'ETB',
contactEmail: booking.contactEmail, displayCurrency: booking.displayCurrency,
contactPhone: booking.contactPhone, displayTotalMinor: booking.displayTotalMinor,
createdAt: booking.createdAt, contactEmail: booking.contactEmail,
passenger: booking.passenger?.user, contactPhone: booking.contactPhone,
schedule: { createdAt: booking.createdAt,
train: booking.schedule.train, passenger: iam
originStation: booking.schedule.originStation, ? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number }
destinationStation: booking.schedule.destinationStation, : null,
departureAt: booking.schedule.departureAt, schedule: {
}, train: booking.schedule.train,
paymentIntent: booking.paymentIntent, originStation: booking.schedule.originStation,
seatCount: booking.seats.length, destinationStation: booking.schedule.destinationStation,
})), departureAt: booking.schedule.departureAt,
},
paymentIntent: booking.paymentIntent,
seatCount: booking.seats.length,
};
}),
meta: { meta: {
page, page,
pageSize, pageSize,

View File

@@ -21,11 +21,7 @@ describe('Payments E2E', () => {
prisma = app.get<PrismaService>(PrismaService); prisma = app.get<PrismaService>(PrismaService);
const testUser = await prisma.user.create({ const passenger = await prisma.passenger.create({ data: { iamUserId: 'test-iam-payments-user' } });
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 } });
await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 100000, currency: 'ETB' } }); await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 100000, currency: 'ETB' } });
@@ -77,7 +73,6 @@ describe('Payments E2E', () => {
prisma.walletLedgerEntry.deleteMany(), prisma.walletLedgerEntry.deleteMany(),
prisma.walletAccount.deleteMany(), prisma.walletAccount.deleteMany(),
prisma.passenger.deleteMany(), prisma.passenger.deleteMany(),
prisma.user.deleteMany({ where: { email: 'payment-test@example.com' } }),
]); ]);
await app.close(); await app.close();
}); });

View File

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