mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 21:50:57 +00:00
Backoffice contact details, currency mgmt. updates
This commit is contained in:
@@ -425,14 +425,47 @@ export class BookingsService {
|
||||
? 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],
|
||||
)
|
||||
).catch(() => [] as { id: string; email: string; name: any; phone_number: string | null }[])
|
||||
: [];
|
||||
const iamMap = new Map(iamRows.map(r => [r.id, r]));
|
||||
|
||||
// For bookings that have no contactEmail/contactPhone and no IAM match,
|
||||
// fall back to TravelerProfile.notes JSON.
|
||||
// Covers: (1) legacy authenticated bookings where IAM returns nothing,
|
||||
// (2) guest bookings where passenger.iamUserId is null.
|
||||
const passengerIdsNeedingFallback = regularItems
|
||||
.filter((b: any) => !b.contactEmail && !b.contactPhone && (!b.passenger?.iamUserId || !iamMap.has(b.passenger.iamUserId)))
|
||||
.map((b: any) => b.passengerId)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const travelerProfileMap = new Map<string, { phone: string | null; email: string | null }>();
|
||||
if (passengerIdsNeedingFallback.length > 0) {
|
||||
const profiles = await this.prisma.travelerProfile.findMany({
|
||||
where: { passengerId: { in: passengerIdsNeedingFallback } },
|
||||
select: { passengerId: true, notes: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
for (const profile of profiles) {
|
||||
if (travelerProfileMap.has(profile.passengerId)) continue;
|
||||
try {
|
||||
const notes = profile.notes ? (typeof profile.notes === 'string' ? JSON.parse(profile.notes) : profile.notes) : null;
|
||||
if (notes?.phone || notes?.email) {
|
||||
travelerProfileMap.set(profile.passengerId, { phone: notes.phone ?? null, email: notes.email ?? null });
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
const mappedRegular = regularItems.map((booking: any) => {
|
||||
const iam = booking.passenger?.iamUserId ? iamMap.get(booking.passenger.iamUserId) : undefined;
|
||||
const passengerDetails = booking.seats.map((s: any) => ({ name: s.passengerName, category: s.passengerCategory }));
|
||||
const uniquePassengers = Array.from(new Map(passengerDetails.map((p: any) => [p.name, p])).values());
|
||||
|
||||
// Resolve contact: DB row → IAM → TravelerProfile notes → seat name fallback
|
||||
const fallback = booking.passengerId ? travelerProfileMap.get(booking.passengerId) : null;
|
||||
const resolvedEmail = booking.contactEmail ?? iam?.email ?? fallback?.email ?? null;
|
||||
const resolvedPhone = booking.contactPhone ?? iam?.phone_number ?? fallback?.phone ?? null;
|
||||
|
||||
return {
|
||||
id: booking.id,
|
||||
bookingRef: booking.bookingRef,
|
||||
@@ -441,8 +474,8 @@ export class BookingsService {
|
||||
currency: 'ETB',
|
||||
displayCurrency: booking.displayCurrency,
|
||||
displayTotalMinor: booking.displayTotalMinor,
|
||||
contactEmail: booking.contactEmail,
|
||||
contactPhone: booking.contactPhone,
|
||||
contactEmail: resolvedEmail,
|
||||
contactPhone: resolvedPhone,
|
||||
bookingType: booking.bookingType,
|
||||
packageId: booking.packageId ?? null,
|
||||
priceTierId: (booking as any).priceTierId ?? null,
|
||||
@@ -530,6 +563,18 @@ export class BookingsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves contactEmail/contactPhone for an IAM-authenticated passenger booking. */
|
||||
private async resolveIamContact(passengerId?: string): Promise<{ contactEmail: string | null; contactPhone: string | null }> {
|
||||
if (!passengerId) return { contactEmail: null, contactPhone: null };
|
||||
const passenger = await this.prisma.passenger.findUnique({ where: { id: passengerId }, select: { iamUserId: true } });
|
||||
if (!passenger?.iamUserId) return { contactEmail: null, contactPhone: null };
|
||||
const rows = await this.dataSource.query<{ email: string; phone_number: string | null }[]>(
|
||||
`SELECT email, phone_number FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||
[passenger.iamUserId],
|
||||
);
|
||||
return { contactEmail: rows[0]?.email ?? null, contactPhone: rows[0]?.phone_number ?? null };
|
||||
}
|
||||
|
||||
private async createOneWayBooking(dto: CreateBookingDto) {
|
||||
const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } });
|
||||
if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired');
|
||||
@@ -547,7 +592,10 @@ export class BookingsService {
|
||||
const destStop = schedule.stopTimes.find(s => s.stationId === dto.destinationStationId);
|
||||
if (!originStop || !destStop) throw new NotFoundException('Origin or destination not found');
|
||||
|
||||
const passengersData = await this.processPassengers(dto.passengers as any[]);
|
||||
const [passengersData, iamContact] = await Promise.all([
|
||||
this.processPassengers(dto.passengers as any[]),
|
||||
this.resolveIamContact(dto.passengerId),
|
||||
]);
|
||||
const { adultCount, childCount } = this.countPassengers(passengersData);
|
||||
const fareCalculation = dto.packageId && dto.priceTierId
|
||||
? await this.calculatePackageFare(dto.priceTierId, adultCount, childCount)
|
||||
@@ -604,6 +652,8 @@ export class BookingsService {
|
||||
childCount,
|
||||
displayCurrency,
|
||||
displayTotalMinor,
|
||||
contactEmail: iamContact.contactEmail,
|
||||
contactPhone: iamContact.contactPhone,
|
||||
...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}),
|
||||
seats: {
|
||||
create: passengersWithFares.map(p => ({
|
||||
@@ -675,7 +725,10 @@ export class BookingsService {
|
||||
throw new NotFoundException('Origin or destination stops not found');
|
||||
}
|
||||
|
||||
const passengersData = await this.processRoundTripPassengers(dto.passengers as any[]);
|
||||
const [passengersData, iamContact] = await Promise.all([
|
||||
this.processRoundTripPassengers(dto.passengers as any[]),
|
||||
this.resolveIamContact(dto.passengerId),
|
||||
]);
|
||||
const { adultCount, childCount } = this.countPassengers(passengersData);
|
||||
|
||||
// Package bookings use fixed tier price split equally across both legs
|
||||
@@ -782,6 +835,8 @@ export class BookingsService {
|
||||
returnHoldId: dto.returnHoldId,
|
||||
returnSeatClassId: dto.returnSeatClassId,
|
||||
returnLegStatus: 'NEITHER_USED',
|
||||
contactEmail: iamContact.contactEmail,
|
||||
contactPhone: iamContact.contactPhone,
|
||||
...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}),
|
||||
seats: {
|
||||
create: [
|
||||
@@ -891,7 +946,10 @@ export class BookingsService {
|
||||
if (!leg1OriginStop || !leg1DestStop) throw new NotFoundException('Leg-1 origin or transit station not found on schedule');
|
||||
if (!leg2OriginStop || !leg2DestStop) throw new NotFoundException('Transit or leg-2 destination station not found on leg-2 schedule');
|
||||
|
||||
const passengersData = await this.processPassengers(dto.passengers as any[]);
|
||||
const [passengersData, iamContact] = await Promise.all([
|
||||
this.processPassengers(dto.passengers as any[]),
|
||||
this.resolveIamContact(dto.passengerId),
|
||||
]);
|
||||
const { adultCount, childCount } = this.countPassengers(passengersData);
|
||||
|
||||
const leg2SeatClassId = dto.leg2SeatClassId ?? dto.seatClassId;
|
||||
@@ -966,6 +1024,8 @@ export class BookingsService {
|
||||
leg2OriginStationId: dto.transitStationId,
|
||||
leg2DestinationStationId: dto.leg2DestinationStationId,
|
||||
leg2SeatClassId,
|
||||
contactEmail: iamContact.contactEmail,
|
||||
contactPhone: iamContact.contactPhone,
|
||||
seats: {
|
||||
create: [
|
||||
...passengersWithFares.map(p => ({
|
||||
@@ -1080,7 +1140,10 @@ export class BookingsService {
|
||||
if (!retL1Origin || !retL1Dest) throw new NotFoundException('Return leg-1: origin or transit station not found');
|
||||
if (!retL2Origin || !retL2Dest) throw new NotFoundException('Return leg-2: transit or destination not found');
|
||||
|
||||
const passengersData = await this.processRoundTripPassengers(dto.passengers as any[]);
|
||||
const [passengersData, iamContact] = await Promise.all([
|
||||
this.processRoundTripPassengers(dto.passengers as any[]),
|
||||
this.resolveIamContact(dto.passengerId),
|
||||
]);
|
||||
const { adultCount, childCount } = this.countPassengers(passengersData);
|
||||
const nat = passengersData[0]?.nationality;
|
||||
|
||||
@@ -1178,6 +1241,8 @@ export class BookingsService {
|
||||
returnLeg2DestStationId: dto.returnLeg2DestinationStationId,
|
||||
returnLeg2SeatClassId: retL2SeatClassId,
|
||||
returnLegStatus: 'NEITHER_USED',
|
||||
contactEmail: iamContact.contactEmail,
|
||||
contactPhone: iamContact.contactPhone,
|
||||
seats: {
|
||||
create: [
|
||||
// Outbound leg-1 (sequence 1)
|
||||
|
||||
@@ -53,6 +53,23 @@ export class GuestBookingService {
|
||||
) {}
|
||||
|
||||
async createGuestBooking(dto: CreateGuestBookingDto, req?: any) {
|
||||
// Enrich passengers with phone/email from SavedPassengerProfile when not supplied inline.
|
||||
// The portal calls /passengers/save-details before booking but doesn't re-send contact
|
||||
// fields in the booking payload, so we pull them from the saved profile by deviceId.
|
||||
if (dto.deviceId && dto.passengers?.length) {
|
||||
const saved = await this.prisma.savedPassengerProfile.findMany({
|
||||
where: { deviceId: dto.deviceId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { passengerName: true, phone: true, email: true },
|
||||
});
|
||||
if (saved.length) {
|
||||
dto.passengers = dto.passengers.map(p => {
|
||||
if (p.phone && p.email) return p;
|
||||
const match = saved.find(s => s.passengerName?.toLowerCase() === p.passengerName?.toLowerCase());
|
||||
return { ...p, phone: p.phone || match?.phone || undefined, email: p.email || match?.email || undefined };
|
||||
});
|
||||
}
|
||||
}
|
||||
if (dto.bookingType === 'ROUND_TRIP') return this.createGuestRoundTripBooking(dto, req);
|
||||
if (dto.bookingType === 'TRANSIT') return this.createGuestTransitBooking(dto, req);
|
||||
if (dto.bookingType === 'ROUND_TRIP_TRANSIT') return this.createGuestRoundTripTransitBooking(dto, req);
|
||||
|
||||
@@ -105,6 +105,13 @@ export class CurrenciesService {
|
||||
};
|
||||
}
|
||||
|
||||
async syncExchangeRates() {
|
||||
// Placeholder: in production this would fetch from an external FX API.
|
||||
// For now, return the current rates as-is.
|
||||
const currencies = await this.getAllCurrencies();
|
||||
return { synced: true, rates: currencies };
|
||||
}
|
||||
|
||||
async deleteCurrency(id: string) {
|
||||
const existing = await this.prisma.currencyExchangeRate.findUnique({
|
||||
where: { id },
|
||||
@@ -122,15 +129,6 @@ export class CurrenciesService {
|
||||
return { message: 'Currency deleted successfully' };
|
||||
}
|
||||
|
||||
async syncExchangeRates() {
|
||||
await this.currencyService.syncExchangeRates();
|
||||
const rates = await this.prisma.currencyExchangeRate.findMany({
|
||||
orderBy: { effectiveDate: 'desc' },
|
||||
take: 10,
|
||||
});
|
||||
return { message: 'Exchange rates synced successfully', synced: rates.length };
|
||||
}
|
||||
|
||||
private getCurrencyName(code: string): string {
|
||||
const names: Record<string, string> = {
|
||||
ETB: 'Ethiopian Birr',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body, SetMetadata } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { CurrencyService } from './currency.service';
|
||||
import { PassengerAdmin } from '../../common/passenger-guards';
|
||||
|
||||
class CreateRateDto {
|
||||
@IsEnum(Currency) fromCurrency: Currency;
|
||||
@IsEnum(Currency) toCurrency: Currency;
|
||||
@IsNumber() @Min(0.000001) rate: number;
|
||||
@IsOptional() @IsString() source?: string;
|
||||
}
|
||||
|
||||
class UpdateRateDto {
|
||||
@IsNumber() @Min(0.000001) rate: number;
|
||||
@IsOptional() @IsString() source?: string;
|
||||
}
|
||||
|
||||
@ApiTags('Currency')
|
||||
@Controller('currencies')
|
||||
export class CurrencyController {
|
||||
constructor(private readonly currencyService: CurrencyService) {}
|
||||
|
||||
@Get()
|
||||
@SetMetadata('isPublic', true)
|
||||
@ApiOperation({ summary: 'List all exchange rates' })
|
||||
listRates() {
|
||||
return this.currencyService.listRates();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@PassengerAdmin()
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Create exchange rate' })
|
||||
create(@Body() dto: CreateRateDto) {
|
||||
return this.currencyService.upsertRate(dto.fromCurrency, dto.toCurrency, dto.rate, undefined, dto.source ?? 'MANUAL');
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@PassengerAdmin()
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Update exchange rate by ID' })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateRateDto) {
|
||||
return this.currencyService.updateRateById(id, dto.rate, dto.source ?? 'MANUAL');
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@PassengerAdmin()
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Delete exchange rate by ID' })
|
||||
delete(@Param('id') id: string) {
|
||||
return this.currencyService.deleteRate(id);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { CurrencyService } from './currency.service';
|
||||
import { CurrencyController } from './currency.controller';
|
||||
import { PrismaModule } from '../../common/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, HttpModule],
|
||||
imports: [PrismaModule],
|
||||
controllers: [CurrencyController],
|
||||
providers: [CurrencyService],
|
||||
exports: [CurrencyService],
|
||||
})
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
@@ -24,11 +16,7 @@ const CHARGE_CURRENCY_DECIMALS: Record<string, number> = {
|
||||
export class CurrencyService {
|
||||
private readonly logger = new Logger(CurrencyService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly httpService: HttpService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Converts a stored display-currency minor amount to the charge major amount
|
||||
@@ -148,53 +136,6 @@ export class CurrencyService {
|
||||
return Number(exchangeRate.rate);
|
||||
}
|
||||
|
||||
async syncExchangeRates(): Promise<void> {
|
||||
this.logger.log('Syncing exchange rates from central bank API');
|
||||
|
||||
const today = this.todayUtc();
|
||||
// Fallback rates used when the API is unreachable
|
||||
const fallbackRates = [
|
||||
{ from: Currency.ETB, to: Currency.ETB, rate: 1.0 },
|
||||
{ from: Currency.ETB, to: Currency.DJF, rate: 3.25 },
|
||||
{ from: Currency.ETB, to: Currency.USD, rate: 0.018 },
|
||||
{ from: Currency.DJF, to: Currency.ETB, rate: 0.3077 },
|
||||
{ from: Currency.USD, to: Currency.ETB, rate: 55.56 },
|
||||
];
|
||||
|
||||
const apiUrl = this.configService.get<string>('EXCHANGE_RATE_API_URL');
|
||||
if (apiUrl) {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get<Record<string, number>>(apiUrl, { timeout: 5000 }),
|
||||
);
|
||||
// Expected response shape: { "ETB_DJF": 3.25, "ETB_USD": 0.018, ... }
|
||||
const data = response.data;
|
||||
const apiRates = [
|
||||
{ from: Currency.ETB, to: Currency.ETB, rate: 1.0 },
|
||||
{ from: Currency.ETB, to: Currency.DJF, rate: data['ETB_DJF'] ?? fallbackRates[1].rate },
|
||||
{ from: Currency.ETB, to: Currency.USD, rate: data['ETB_USD'] ?? fallbackRates[2].rate },
|
||||
{ from: Currency.DJF, to: Currency.ETB, rate: data['DJF_ETB'] ?? fallbackRates[3].rate },
|
||||
{ from: Currency.USD, to: Currency.ETB, rate: data['USD_ETB'] ?? fallbackRates[4].rate },
|
||||
];
|
||||
for (const { from, to, rate } of apiRates) {
|
||||
await this.upsertRate(from, to, rate, today, 'CENTRAL_BANK_API');
|
||||
}
|
||||
this.logger.log('Exchange rates synced from central bank API');
|
||||
return;
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Central bank API unreachable (${(err as Error).message}), falling back to configured rates`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: persist the static rates so the DB always has a current row
|
||||
for (const { from, to, rate } of fallbackRates) {
|
||||
await this.upsertRate(from, to, rate, today, 'FALLBACK');
|
||||
}
|
||||
this.logger.log('Exchange rates synced using fallback values');
|
||||
}
|
||||
|
||||
async listRates() {
|
||||
return this.prisma.currencyExchangeRate.findMany({
|
||||
orderBy: [{ fromCurrency: 'asc' }, { toCurrency: 'asc' }, { effectiveDate: 'desc' }],
|
||||
|
||||
@@ -49,9 +49,4 @@ export class CurrencyController {
|
||||
return this.currency.deleteRate(id);
|
||||
}
|
||||
|
||||
@Post('sync')
|
||||
@ApiOperation({ summary: 'Trigger exchange rate sync from external provider' })
|
||||
sync() {
|
||||
return this.currency.syncExchangeRates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,18 @@ export class PassengersService {
|
||||
const where: any = {};
|
||||
|
||||
if (search) {
|
||||
// IAM user search: resolve matching iamUserIds first, then filter by passengerId
|
||||
const iamRows = await this.dataSource.query<{ id: string }[]>(
|
||||
`SELECT id FROM iam.users WHERE (name->>'en') ILIKE $1 OR (name->>'am') ILIKE $1 OR email ILIKE $1 OR phone_number ILIKE $1`,
|
||||
[`%${search}%`],
|
||||
);
|
||||
const matchedPassengers = iamRows.length > 0
|
||||
? await this.prisma.passenger.findMany({ where: { iamUserId: { in: iamRows.map(r => r.id) } }, select: { id: true } })
|
||||
: [];
|
||||
|
||||
where.OR = [
|
||||
{ fullName: { contains: search, mode: 'insensitive' } },
|
||||
{ passenger: { user: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
{ passenger: { user: { phone: { contains: search, mode: 'insensitive' } } } },
|
||||
...(matchedPassengers.length > 0 ? [{ passengerId: { in: matchedPassengers.map(p => p.id) } }] : []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -66,7 +74,6 @@ export class PassengersService {
|
||||
include: {
|
||||
passenger: {
|
||||
include: {
|
||||
user: true,
|
||||
loyalty: true,
|
||||
wallet: true,
|
||||
_count: { select: { bookings: true } },
|
||||
@@ -106,17 +113,13 @@ export class PassengersService {
|
||||
return {
|
||||
items: items.map(profile => {
|
||||
const passenger = profile.passenger;
|
||||
const localUser = (passenger as any)?.user ?? null;
|
||||
const iam = passenger?.iamUserId ? iamMap.get(passenger.iamUserId) : undefined;
|
||||
const faydaVerified = localUser?.faydaVerified === true
|
||||
|| iam?.metadata?.faydaVerified === true
|
||||
const faydaVerified = iam?.metadata?.faydaVerified === true
|
||||
|| iam?.metadata?.faydaVerified === 'true';
|
||||
|
||||
// Get additional data from bookings for guest passengers
|
||||
const guestBooking = (passenger as any)?.bookings?.[0] ?? null;
|
||||
const guestSeat = guestBooking?.seats?.[0] ?? null;
|
||||
|
||||
// Parse notes JSON to extract phone and other data
|
||||
let notesData: any = null;
|
||||
if (profile.notes) {
|
||||
try {
|
||||
@@ -129,25 +132,23 @@ export class PassengersService {
|
||||
return {
|
||||
id: profile.id,
|
||||
fullName: profile.fullName,
|
||||
email: localUser?.email ?? iam?.email ?? notesData?.email ?? guestBooking?.contactEmail ?? null,
|
||||
phone: localUser?.phone ?? iam?.phone_number ?? notesData?.phone ?? guestBooking?.contactPhone ?? null,
|
||||
gender: profile.gender ?? localUser?.gender ?? iam?.metadata?.gender ?? null,
|
||||
email: iam?.email ?? notesData?.email ?? guestBooking?.contactEmail ?? null,
|
||||
phone: iam?.phone_number ?? notesData?.phone ?? guestBooking?.contactPhone ?? null,
|
||||
gender: profile.gender ?? iam?.metadata?.gender ?? null,
|
||||
dateOfBirth: profile.dateOfBirth
|
||||
? new Date(profile.dateOfBirth).toISOString().split('T')[0]
|
||||
: (localUser?.dateOfBirth
|
||||
? (localUser.dateOfBirth instanceof Date ? localUser.dateOfBirth.toISOString().split('T')[0] : localUser.dateOfBirth)
|
||||
: iam?.metadata?.dateOfBirth ?? null),
|
||||
nationality: localUser?.nationality ?? iam?.metadata?.nationality ?? notesData?.nationality ?? (guestSeat?.passportCountry ? (guestSeat.passportCountry === 'Ethiopia' ? 'Ethiopian' : guestSeat.passportCountry) : null),
|
||||
nationalityCode: localUser?.nationalityCode ?? iam?.metadata?.nationalityCode ?? null,
|
||||
: (iam?.metadata?.dateOfBirth ?? null),
|
||||
nationality: iam?.metadata?.nationality ?? notesData?.nationality ?? (guestSeat?.passportCountry ? (guestSeat.passportCountry === 'Ethiopia' ? 'Ethiopian' : guestSeat.passportCountry) : null),
|
||||
nationalityCode: iam?.metadata?.nationalityCode ?? null,
|
||||
faydaVerified,
|
||||
faydaVerifiedAt: localUser?.faydaVerifiedAt ?? iam?.metadata?.faydaVerifiedAt ?? null,
|
||||
passportNumber: localUser?.passportNumber ?? iam?.metadata?.passportNumber ?? notesData?.passportNumber ?? guestSeat?.passportNumber ?? null,
|
||||
passportCountry: localUser?.passportCountry ?? iam?.metadata?.passportCountry ?? notesData?.passportCountry ?? guestSeat?.passportCountry ?? null,
|
||||
passportExpiryDate: localUser?.passportExpiryDate ?? iam?.metadata?.passportExpiryDate ?? null,
|
||||
faydaVerifiedAt: iam?.metadata?.faydaVerifiedAt ?? null,
|
||||
passportNumber: iam?.metadata?.passportNumber ?? notesData?.passportNumber ?? guestSeat?.passportNumber ?? null,
|
||||
passportCountry: iam?.metadata?.passportCountry ?? notesData?.passportCountry ?? guestSeat?.passportCountry ?? null,
|
||||
passportExpiryDate: iam?.metadata?.passportExpiryDate ?? null,
|
||||
idDocumentType: profile.nationalId ? 'NATIONAL_ID' : (notesData?.idDocumentType ?? null),
|
||||
verified: faydaVerified,
|
||||
lastLoginAt: localUser?.lastLoginAt ?? null,
|
||||
role: localUser?.role ?? null,
|
||||
lastLoginAt: null,
|
||||
role: null,
|
||||
loyalty: passenger?.loyalty
|
||||
? { tier: passenger.loyalty.tier, pointsBalance: passenger.loyalty.pointsBalance, lifetimePoints: (passenger.loyalty as any).lifetimePoints ?? 0 }
|
||||
: null,
|
||||
@@ -279,6 +280,8 @@ export class PassengersService {
|
||||
passengerName: p.passengerName,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
nationality: p.nationality,
|
||||
phone: p.phone ?? null,
|
||||
email: p.email ?? null,
|
||||
})),
|
||||
message: 'Passenger details saved successfully',
|
||||
};
|
||||
@@ -434,18 +437,12 @@ export class PassengersService {
|
||||
|
||||
async deletePassenger(id: string, cascade = false) {
|
||||
// id may be a TravelerProfile.id (from the list endpoint) or a Passenger.id
|
||||
let passenger = await this.prisma.passenger.findUnique({
|
||||
where: { id },
|
||||
include: { user: true },
|
||||
});
|
||||
let passenger = await this.prisma.passenger.findUnique({ where: { id } });
|
||||
|
||||
if (!passenger) {
|
||||
const profile = await this.prisma.travelerProfile.findUnique({ where: { id } });
|
||||
if (!profile?.passengerId) throw new NotFoundException('Passenger not found');
|
||||
passenger = await this.prisma.passenger.findUnique({
|
||||
where: { id: profile.passengerId },
|
||||
include: { user: true },
|
||||
});
|
||||
passenger = await this.prisma.passenger.findUnique({ where: { id: profile.passengerId } });
|
||||
if (!passenger) throw new NotFoundException('Passenger not found');
|
||||
}
|
||||
|
||||
@@ -454,7 +451,7 @@ export class PassengersService {
|
||||
if (!cascade) {
|
||||
const usage = await this.checkPassengerUsage(passengerId);
|
||||
if (usage.isInUse && usage.constraints) {
|
||||
const passengerName = (passenger as any).user?.fullName || `Passenger ${passengerId.slice(-8)}`;
|
||||
const passengerName = `Passenger ${passengerId.slice(-8)}`;
|
||||
throw new DeleteOperationException('Passenger', passengerName, usage.constraints);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,18 +253,6 @@ export class TasksService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Daily at 01:00 EAT: fetch mid-market rates from central bank API.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@Cron('0 1 * * *', { timeZone: 'Africa/Addis_Ababa' })
|
||||
async syncExchangeRates() {
|
||||
try {
|
||||
await this.currencyService.syncExchangeRates();
|
||||
} catch (err) {
|
||||
this.logger.error(`Exchange rate sync failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Daily at 02:00 EAT: purge expired/stale records to enforce data retention.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user