mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
refactor( iam ): replace prisma.user joins with batch IAM fetch in bookings, tickets, and payments e2e
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user