|
|
|
|
@@ -3,10 +3,10 @@ import { PrismaService } from '../../common/prisma.service';
|
|
|
|
|
import { SeatsService } from '../seats/seats.service';
|
|
|
|
|
import { VerifaydaService } from '../verifayda/verifayda.service';
|
|
|
|
|
import { CurrencyService } from '../currency/currency.service';
|
|
|
|
|
import { PassengerAuthService } from '../auth/passenger-auth.service';
|
|
|
|
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
|
|
|
import { CreateGuestBookingDto, SavedPassengerProfileDto } from './guest-booking.dto';
|
|
|
|
|
import { Currency, PassengerCategory, IdDocumentType } from '@prisma/client';
|
|
|
|
|
import * as bcrypt from 'bcrypt';
|
|
|
|
|
|
|
|
|
|
function generateRef(): string {
|
|
|
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
|
|
|
@@ -28,10 +28,11 @@ export class GuestBookingService {
|
|
|
|
|
private seatsService: SeatsService,
|
|
|
|
|
private verifaydaService: VerifaydaService,
|
|
|
|
|
private currencyService: CurrencyService,
|
|
|
|
|
private passengerAuthService: PassengerAuthService,
|
|
|
|
|
private eventEmitter: EventEmitter2,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async createGuestBooking(dto: CreateGuestBookingDto) {
|
|
|
|
|
async createGuestBooking(dto: CreateGuestBookingDto, req: any) {
|
|
|
|
|
// Validate hold
|
|
|
|
|
const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } });
|
|
|
|
|
if (!hold || hold.expiresAt < new Date()) {
|
|
|
|
|
@@ -71,15 +72,12 @@ export class GuestBookingService {
|
|
|
|
|
let verifaydaData: Record<string, any> | undefined;
|
|
|
|
|
let nationality = passenger.nationality;
|
|
|
|
|
|
|
|
|
|
// Determine if passenger is Ethiopian
|
|
|
|
|
const isEthiopian = passenger.nationality === 'Ethiopian' ||
|
|
|
|
|
const isEthiopian = passenger.nationality === 'Ethiopian' ||
|
|
|
|
|
passenger.nationality === 'ETHIOPIAN' ||
|
|
|
|
|
passenger.idDocumentType === IdDocumentType.NATIONAL_ID;
|
|
|
|
|
|
|
|
|
|
// Ethiopian with National ID
|
|
|
|
|
|
|
|
|
|
if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) {
|
|
|
|
|
if (passenger.idDocumentNumber) {
|
|
|
|
|
// Attempt Fayda verification
|
|
|
|
|
const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber);
|
|
|
|
|
if (!verification.verified) {
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
@@ -91,22 +89,14 @@ export class GuestBookingService {
|
|
|
|
|
verifaydaData = verification.passengerData?.profileData;
|
|
|
|
|
}
|
|
|
|
|
nationality = 'Ethiopian';
|
|
|
|
|
}
|
|
|
|
|
// International passenger with Passport (non-Ethiopian)
|
|
|
|
|
else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
|
|
|
|
|
// Passport details are required for international passengers
|
|
|
|
|
} else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
|
|
|
|
|
if (!passenger.passportNumber || !passenger.passportCountry) {
|
|
|
|
|
throw new BadRequestException(`Passport number and country required for ${passenger.passengerName}`);
|
|
|
|
|
}
|
|
|
|
|
nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
|
|
|
|
|
}
|
|
|
|
|
// Ethiopian with Passport (manual entry without Fayda)
|
|
|
|
|
else if (isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
|
|
|
|
|
// Ethiopians can use passport instead of national ID
|
|
|
|
|
} else if (isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
|
|
|
|
|
nationality = 'Ethiopian';
|
|
|
|
|
}
|
|
|
|
|
// International with National ID (e.g., Djiboutian national ID)
|
|
|
|
|
else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) {
|
|
|
|
|
} else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) {
|
|
|
|
|
nationality = nationality || 'Other';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -155,85 +145,57 @@ export class GuestBookingService {
|
|
|
|
|
displayTotalMinor = await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create or get guest passenger
|
|
|
|
|
// Resolve or create the guest Passenger record
|
|
|
|
|
const firstPassenger = passengersData[0];
|
|
|
|
|
let guestPassenger = null;
|
|
|
|
|
let userId = null;
|
|
|
|
|
let guestPassengerId: string;
|
|
|
|
|
let iamUserId: string | null = null;
|
|
|
|
|
let createdAccount = false;
|
|
|
|
|
|
|
|
|
|
// Optional account creation
|
|
|
|
|
if (dto.createAccount && firstPassenger.email && dto.password) {
|
|
|
|
|
const existingUser = await this.prisma.user.findUnique({ where: { email: firstPassenger.email } });
|
|
|
|
|
if (existingUser) {
|
|
|
|
|
throw new BadRequestException('Email already registered. Please login instead.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let accountPhone = firstPassenger.phone || null;
|
|
|
|
|
if (accountPhone) {
|
|
|
|
|
const existingPhone = await this.prisma.user.findUnique({ where: { phone: accountPhone } });
|
|
|
|
|
if (existingPhone) throw new BadRequestException('Phone number already registered. Please login instead.');
|
|
|
|
|
}
|
|
|
|
|
if (!accountPhone) accountPhone = `+guest-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
|
|
|
|
|
|
|
|
const passwordHash = await bcrypt.hash(dto.password, 10);
|
|
|
|
|
const user = await this.prisma.user.create({
|
|
|
|
|
data: {
|
|
|
|
|
// Delegate full IAM account creation to PassengerAuthService
|
|
|
|
|
const result = await this.passengerAuthService.register(
|
|
|
|
|
{
|
|
|
|
|
fullName: firstPassenger.passengerName,
|
|
|
|
|
email: firstPassenger.email,
|
|
|
|
|
phone: accountPhone,
|
|
|
|
|
passwordHash,
|
|
|
|
|
phone: firstPassenger.phone || `+guest-${Date.now()}`,
|
|
|
|
|
password: dto.password,
|
|
|
|
|
nationality: firstPassenger.nationality,
|
|
|
|
|
nationalId: firstPassenger.idDocumentType === IdDocumentType.NATIONAL_ID ? firstPassenger.idDocumentNumber : undefined,
|
|
|
|
|
passportNumber: firstPassenger.passportNumber,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
guestPassenger = await this.prisma.passenger.create({ data: { userId: user.id } });
|
|
|
|
|
await this.prisma.loyaltyAccount.create({ data: { passengerId: guestPassenger.id, pointsBalance: 0, tier: 'BRONZE' } });
|
|
|
|
|
await this.prisma.walletAccount.create({ data: { passengerId: guestPassenger.id, balanceMinor: 0 } });
|
|
|
|
|
|
|
|
|
|
userId = user.id;
|
|
|
|
|
req,
|
|
|
|
|
);
|
|
|
|
|
guestPassengerId = result.user.passengerId;
|
|
|
|
|
iamUserId = result.user.iamUserId;
|
|
|
|
|
createdAccount = true;
|
|
|
|
|
} else {
|
|
|
|
|
// Create anonymous guest passenger with minimal data
|
|
|
|
|
const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
|
|
|
|
|
|
|
|
// Check if email exists and use a unique guest email if it does
|
|
|
|
|
let guestEmail = firstPassenger.email || `guest-${uniqueId}@edr-platform.com`;
|
|
|
|
|
if (firstPassenger.email) {
|
|
|
|
|
const existingUser = await this.prisma.user.findUnique({ where: { email: firstPassenger.email } });
|
|
|
|
|
if (existingUser) {
|
|
|
|
|
// Email exists, use guest email instead for anonymous booking
|
|
|
|
|
guestEmail = `guest-${uniqueId}@edr-platform.com`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use a guaranteed-unique guest phone to avoid constraint collisions
|
|
|
|
|
let guestPhone = firstPassenger.phone || null;
|
|
|
|
|
if (guestPhone) {
|
|
|
|
|
const existingPhone = await this.prisma.user.findUnique({ where: { phone: guestPhone } });
|
|
|
|
|
if (existingPhone) guestPhone = null;
|
|
|
|
|
}
|
|
|
|
|
if (!guestPhone) guestPhone = `+guest-${uniqueId}`;
|
|
|
|
|
|
|
|
|
|
const tempUser = await this.prisma.user.create({
|
|
|
|
|
// Anonymous guest — Passenger with no User, no IAM account
|
|
|
|
|
const guestPassenger = await this.prisma.passenger.create({
|
|
|
|
|
data: {
|
|
|
|
|
fullName: firstPassenger.passengerName,
|
|
|
|
|
email: guestEmail,
|
|
|
|
|
phone: guestPhone,
|
|
|
|
|
passwordHash: await bcrypt.hash(Math.random().toString(36), 10),
|
|
|
|
|
role: 'PASSENGER',
|
|
|
|
|
// userId intentionally omitted — guest has no local User or IAM account
|
|
|
|
|
...(dto.deviceId ? {} : {}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
guestPassenger = await this.prisma.passenger.create({ data: { userId: tempUser.id } });
|
|
|
|
|
await this.prisma.loyaltyAccount.create({ data: { passengerId: guestPassenger.id, pointsBalance: 0, tier: 'BRONZE' } });
|
|
|
|
|
await this.prisma.walletAccount.create({ data: { passengerId: guestPassenger.id, balanceMinor: 0 } });
|
|
|
|
|
guestPassengerId = guestPassenger.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save passenger details for future use (if requested)
|
|
|
|
|
if (dto.savePassengerDetails && (dto.createAccount || dto.deviceId)) {
|
|
|
|
|
for (const passenger of passengersData) {
|
|
|
|
|
// Note: SavedPassengerProfile will be available after migration
|
|
|
|
|
// Temporarily disabled until prisma generate completes
|
|
|
|
|
// await this.prisma.savedPassengerProfile.create({ ... });
|
|
|
|
|
await this.prisma.savedPassengerProfile.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: iamUserId ?? undefined,
|
|
|
|
|
deviceId: dto.deviceId,
|
|
|
|
|
passengerName: passenger.passengerName,
|
|
|
|
|
dateOfBirth: passenger.dateOfBirth,
|
|
|
|
|
idDocumentType: passenger.idDocumentType,
|
|
|
|
|
passportNumber: passenger.passportNumber,
|
|
|
|
|
passportCountry: passenger.passportCountry,
|
|
|
|
|
nationality: passenger.nationality,
|
|
|
|
|
phone: passenger.phone,
|
|
|
|
|
email: passenger.email,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -241,7 +203,7 @@ export class GuestBookingService {
|
|
|
|
|
const booking = await this.prisma.booking.create({
|
|
|
|
|
data: {
|
|
|
|
|
bookingRef: generateRef(),
|
|
|
|
|
passengerId: guestPassenger.id,
|
|
|
|
|
passengerId: guestPassengerId,
|
|
|
|
|
scheduleId: dto.scheduleId,
|
|
|
|
|
status: 'PENDING_PAYMENT',
|
|
|
|
|
totalMinor,
|
|
|
|
|
@@ -251,8 +213,6 @@ export class GuestBookingService {
|
|
|
|
|
displayTotalMinor,
|
|
|
|
|
bookingType: 'ONE_WAY',
|
|
|
|
|
userAgent: dto.deviceId,
|
|
|
|
|
// contactEmail: firstPassenger.email, // Temporarily disabled until migration
|
|
|
|
|
// contactPhone: firstPassenger.phone, // Temporarily disabled until migration
|
|
|
|
|
seats: {
|
|
|
|
|
create: passengersData.map((p) => ({
|
|
|
|
|
seat: { connect: { id: p.seatId } },
|
|
|
|
|
@@ -282,7 +242,7 @@ export class GuestBookingService {
|
|
|
|
|
return {
|
|
|
|
|
...booking,
|
|
|
|
|
createdAccount,
|
|
|
|
|
userId,
|
|
|
|
|
iamUserId,
|
|
|
|
|
fareBreakdown: {
|
|
|
|
|
baseFareMinor,
|
|
|
|
|
adultCount,
|
|
|
|
|
@@ -307,10 +267,6 @@ export class GuestBookingService {
|
|
|
|
|
throw new BadRequestException('Either userId or deviceId is required');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Temporarily return empty array until Prisma client is regenerated
|
|
|
|
|
return [];
|
|
|
|
|
|
|
|
|
|
/* Uncomment after running migration and prisma generate
|
|
|
|
|
const profiles = await this.prisma.savedPassengerProfile.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
OR: [
|
|
|
|
|
@@ -325,14 +281,13 @@ export class GuestBookingService {
|
|
|
|
|
passengerName: p.passengerName,
|
|
|
|
|
dateOfBirth: p.dateOfBirth.toISOString().split('T')[0],
|
|
|
|
|
idDocumentType: p.idDocumentType,
|
|
|
|
|
idDocumentNumber: undefined, // Never return sensitive data
|
|
|
|
|
idDocumentNumber: undefined,
|
|
|
|
|
passportNumber: p.passportNumber || undefined,
|
|
|
|
|
passportCountry: p.passportCountry || undefined,
|
|
|
|
|
nationality: p.nationality || undefined,
|
|
|
|
|
phone: p.phone || undefined,
|
|
|
|
|
email: p.email || undefined,
|
|
|
|
|
}));
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getBaseFare(
|
|
|
|
|
@@ -376,6 +331,6 @@ export class GuestBookingService {
|
|
|
|
|
if (match) return match.baseFareMinor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 35000; // Default fallback
|
|
|
|
|
return 35000;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|