Files
edr-platform/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts
2026-06-23 14:47:24 +03:00

409 lines
13 KiB
TypeScript

import { Injectable, 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';
interface PassengerFilters {
search?: string;
verified?: boolean;
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;
};
@Injectable()
export class PassengersService {
constructor(
private readonly prisma: PrismaService,
@InjectDataSource() private readonly dataSource: DataSource,
private readonly verifaydaService: VerifaydaService,
) {}
async findAll(filters: PassengerFilters = {}) {
const { search, verified, page = 1, pageSize = 20 } = filters;
const skip = (page - 1) * pageSize;
let iamUserIdFilter: string[] | null = null;
if (search || verified !== undefined) {
const conditions: string[] = [];
const params: any[] = [];
let idx = 1;
if (search) {
conditions.push(`(
u.email ILIKE $${idx} OR
u.phone_number ILIKE $${idx} OR
(u.name->>'en') ILIKE $${idx} OR
(u.name->>'am') ILIKE $${idx}
)`);
params.push(`%${search}%`);
idx++;
}
if (verified !== undefined) {
if (verified) {
conditions.push(`u.metadata->>'faydaVerified' = 'true'`);
} else {
conditions.push(`(u.metadata IS NULL OR u.metadata->>'faydaVerified' IS DISTINCT FROM 'true')`);
}
}
const rows = await this.dataSource.query<{ id: string }[]>(
`SELECT u.id FROM iam.users u WHERE ${conditions.join(' AND ')}`,
params,
);
iamUserIdFilter = rows.map(r => r.id);
if (iamUserIdFilter.length === 0) {
return { items: [], meta: { page, pageSize, total: 0, totalPages: 0 } };
}
}
const where: any = {};
if (iamUserIdFilter) {
where.iamUserId = { in: iamUserIdFilter };
}
const [items, total] = await Promise.all([
this.prisma.passenger.findMany({
where,
skip,
take: pageSize,
orderBy: { createdAt: 'desc' },
include: {
loyalty: true,
_count: { select: { bookings: true } },
},
}),
this.prisma.passenger.count({ where }),
]);
const iamUserIds = items.map(p => p.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(passenger => {
const iam = passenger.iamUserId ? iamMap.get(passenger.iamUserId) : undefined;
const faydaVerified = iam?.metadata?.faydaVerified === true || iam?.metadata?.faydaVerified === 'true';
return {
id: passenger.id,
fullName: iam?.name?.en ?? iam?.name?.am ?? null,
email: iam?.email ?? null,
phone: iam?.phone_number ?? null,
verified: faydaVerified,
loyaltyTier: passenger.loyalty?.tier || 'BRONZE',
loyaltyPoints: passenger.loyalty?.pointsBalance || 0,
totalBookings: passenger._count.bookings,
createdAt: passenger.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' },
})),
})),
};
}
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,
})),
message: 'Passenger details saved successfully',
};
}
createTravelerProfile(dto: CreateTravelerProfileDto) {
return this.prisma.travelerProfile.create({
data: {
...dto,
dateOfBirth: dto.dateOfBirth ? new Date(dto.dateOfBirth) : 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) {
console.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) {
const passenger = await this.prisma.passenger.findUnique({ where: { id } });
if (!passenger) throw new NotFoundException('Passenger not found');
await this.prisma.passenger.delete({ where: { id } });
return { deleted: true, passengerId: id };
}
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 usage = [];
if (bookingCount > 0) usage.push(`${bookingCount} booking(s)`);
if (loyaltyAccount) usage.push('Loyalty account');
if (walletAccount) usage.push('Wallet account');
return {
isInUse: usage.length > 0,
affectedModules: usage,
};
}
}