From 86c7b2e4eb733b66647046ec6d6e4a8c8d5bbc9b Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Fri, 26 Jun 2026 13:57:26 +0300 Subject: [PATCH 1/3] Package inquiry, UAT related update --- apps/edr-passenger-api/prisma/schema.prisma | 23 +++ .../modules/bookings/bookings.controller.ts | 22 +- .../src/modules/bookings/bookings.service.ts | 23 ++- .../excess-baggage.controller.ts | 4 + .../excess-baggage/excess-baggage.service.ts | 38 +++- .../notifications/email-client.service.ts | 57 ++++-- .../notifications/sms-client.service.ts | 4 +- .../modules/packages/packages.controller.ts | 38 +++- .../src/modules/packages/packages.dto.ts | 14 ++ .../src/modules/packages/packages.service.ts | 49 ++++- .../passengers/passengers.controller.ts | 22 +- .../modules/passengers/passengers.service.ts | 21 +- .../src/modules/tickets/tickets.controller.ts | 6 + .../src/modules/tickets/tickets.service.ts | 10 +- .../backoffice/src/app/bookings/page.tsx | 12 +- .../src/app/excess-baggage/page.tsx | 61 ++++-- .../src/app/package-inquiries/layout.tsx | 7 + .../src/app/package-inquiries/page.tsx | 193 ++++++++++++++++++ .../backoffice/src/app/packages/page.tsx | 62 +++++- .../backoffice/src/app/passengers/page.tsx | 10 +- .../backoffice/src/app/tickets/page.tsx | 48 +++-- .../src/components/layout/Sidebar.tsx | 1 + .../backoffice/src/lib/api/index.ts | 16 ++ 23 files changed, 648 insertions(+), 93 deletions(-) create mode 100644 apps/edr-passenger-web/backoffice/src/app/package-inquiries/layout.tsx create mode 100644 apps/edr-passenger-web/backoffice/src/app/package-inquiries/page.tsx diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index 74965df23..a5b46105d 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -1417,6 +1417,7 @@ model TravelPackage { returnSchedule TrainSchedule @relation("PackageReturn", fields: [returnScheduleId], references: [id]) priceTiers PackagePriceTier[] bookings PackageBooking[] + inquiries PackageInquiry[] @@index([status, validFrom]) @@schema("passenger") @@ -1434,6 +1435,7 @@ model PackagePriceTier { package TravelPackage @relation(fields: [packageId], references: [id]) bookings PackageBooking[] + inquiries PackageInquiry[] @@unique([packageId, seatType]) @@schema("passenger") @@ -1501,3 +1503,24 @@ model PackagePaymentIntent { @@schema("passenger") } + +model PackageInquiry { + id String @id @default(uuid()) + packageId String + priceTierId String? + travelerCount Int + contactName String + contactEmail String? + contactPhone String? + notes String? + status String @default("NEW") + enquiredAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + package TravelPackage @relation(fields: [packageId], references: [id]) + priceTier PackagePriceTier? @relation(fields: [priceTierId], references: [id]) + + @@index([packageId]) + @@schema("passenger") +} diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts index b04be6bb2..96bd4b387 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts @@ -80,15 +80,23 @@ export class BookingsController { @ApiOperation({ description: 'Returns paginated list of bookings. Use `returnLegStatus=OUTBOUND_ONLY` to find round-trip no-shows on the return leg, `INBOUND_ONLY` for passengers who only used the return leg, `BOTH_USED` for fully completed round-trips, and `NEITHER_USED` for confirmed but not yet boarded.' }) - @ApiQuery({ name: 'search', required: false, description: 'Search by booking reference, email, or phone' }) - @ApiQuery({ name: 'status', required: false, description: 'Filter by booking status' }) - @ApiQuery({ name: 'returnLegStatus', required: false, description: 'Filter round-trip leg usage: NEITHER_USED | OUTBOUND_ONLY | INBOUND_ONLY | BOTH_USED | NOT_APPLICABLE' }) - @ApiQuery({ name: 'page', required: false, description: 'Page number' }) - @ApiQuery({ name: 'pageSize', required: false, description: 'Items per page' }) + @ApiQuery({ name: 'search', required: false }) + @ApiQuery({ name: 'status', required: false }) + @ApiQuery({ name: 'returnLegStatus', required: false }) + @ApiQuery({ name: 'bookingType', required: false }) + @ApiQuery({ name: 'paymentStatus', required: false }) + @ApiQuery({ name: 'dateFrom', required: false }) + @ApiQuery({ name: 'dateTo', required: false }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'pageSize', required: false }) findAll( @Query('search') search?: string, @Query('status') status?: string, @Query('returnLegStatus') returnLegStatus?: string, + @Query('bookingType') bookingType?: string, + @Query('paymentStatus') paymentStatus?: string, + @Query('dateFrom') dateFrom?: string, + @Query('dateTo') dateTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { @@ -96,6 +104,10 @@ export class BookingsController { search, status, returnLegStatus, + bookingType, + paymentStatus, + dateFrom, + dateTo, page: page ? parseInt(page) : 1, pageSize: pageSize ? parseInt(pageSize) : 20 }); diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index 7567489bd..193b02ef8 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -28,6 +28,10 @@ interface BookingFilters { search?: string; status?: string; returnLegStatus?: string; + bookingType?: string; + paymentStatus?: string; + dateFrom?: string; + dateTo?: string; page?: number; pageSize?: number; } @@ -195,7 +199,7 @@ export class BookingsService { } async findAll(filters: BookingFilters = {}) { - const { search, status, returnLegStatus, page = 1, pageSize = 20 } = filters; + const { search, status, returnLegStatus, bookingType, paymentStatus, dateFrom, dateTo, page = 1, pageSize = 20 } = filters; const skip = (page - 1) * pageSize; const where: any = {}; @@ -227,6 +231,23 @@ export class BookingsService { if (status) where.status = status; if (returnLegStatus) (where as any).returnLegStatus = returnLegStatus; + if (bookingType) where.bookingType = bookingType; + if (dateFrom || dateTo) { + where.createdAt = { + ...(dateFrom ? { gte: new Date(dateFrom) } : {}), + ...(dateTo ? { lte: new Date(new Date(dateTo).setHours(23, 59, 59, 999)) } : {}), + }; + } + if (paymentStatus) { + const statusMap: Record = { + PAID: 'SUCCEEDED', + PENDING: 'REQUIRES_ACTION', + FAILED: 'FAILED', + REFUNDED: 'REFUNDED', + }; + const mapped = statusMap[paymentStatus] ?? paymentStatus; + where.paymentIntent = { is: { status: mapped } }; + } const [items, total] = await Promise.all([ this.prisma.booking.findMany({ diff --git a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts index 55d55b9e7..12d6dbd7b 100644 --- a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts +++ b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts @@ -35,12 +35,16 @@ export class ExcessBaggageAgentController { getAll( @Query('status') status?: string, @Query('bookingRef') bookingRef?: string, + @Query('dateFrom') dateFrom?: string, + @Query('dateTo') dateTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.getAll({ status, bookingRef, + dateFrom, + dateTo, page: page ? parseInt(page) : undefined, pageSize: pageSize ? parseInt(pageSize) : undefined, }); diff --git a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts index 91396f191..4a110df77 100644 --- a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts +++ b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.service.ts @@ -7,6 +7,8 @@ import { import { PrismaService } from '../../common/prisma.service'; import { PaymentClientService } from '../payments/payment-client.service'; import { NotificationsService } from '../notifications/notifications.service'; +import { SmsClientService } from '../notifications/sms-client.service'; +import { EmailClientService } from '../notifications/email-client.service'; import { LogExcessBaggageDto, WaiveChargeDto, @@ -30,6 +32,8 @@ export class ExcessBaggageService { private prisma: PrismaService, private paymentClient: PaymentClientService, private notifications: NotificationsService, + private smsClient: SmsClientService, + private emailClient: EmailClientService, ) {} async logCharge(dto: LogExcessBaggageDto) { @@ -101,23 +105,27 @@ export class ExcessBaggageService { const amountStr = (charge.totalMinor / 100).toFixed(2); const msg = `EDR: Excess baggage charge of ${amountStr} ETB for booking ${booking.bookingRef}. Pay here: ${payUrl} (valid 30 min)`; - const recipient = phone ?? email ?? booking.passengerId; - try { - await this.notifications['deliverSms'](recipient, msg); - } catch (err) { - this.logger.warn(`SMS send failed for excess baggage charge ${charge.id}: ${err}`); + if (phone) { + try { + await this.smsClient.sendSms({ to: phone, message: msg }); + } catch (err) { + this.logger.warn(`SMS send failed for excess baggage charge ${charge.id}: ${err}`); + } } if (email) { try { - await this.notifications['deliverEmail']( - recipient, - `EDR — Excess baggage payment required (${booking.bookingRef})`, - msg, - ); + await this.emailClient.sendEmail({ + to: email, + subject: `EDR — Excess baggage payment required (${booking.bookingRef})`, + text: msg, + }); } catch (err) { this.logger.warn(`Email send failed for excess baggage charge ${charge.id}: ${err}`); } } + if (!phone && !email) { + this.logger.warn(`No contact info to send excess baggage payment link for charge ${charge.id}`); + } } async getCharge(id: string) { @@ -227,14 +235,22 @@ export class ExcessBaggageService { async getAll(filters: { status?: string; bookingRef?: string; + dateFrom?: string; + dateTo?: string; page?: number; pageSize?: number; }) { - const { status, bookingRef, page = 1, pageSize = 20 } = filters; + const { status, bookingRef, dateFrom, dateTo, page = 1, pageSize = 20 } = filters; const skip = (page - 1) * pageSize; const where: any = {}; if (status) where.status = status; if (bookingRef) where.booking = { bookingRef: { contains: bookingRef, mode: 'insensitive' } }; + if (dateFrom || dateTo) { + where.createdAt = { + ...(dateFrom ? { gte: new Date(dateFrom) } : {}), + ...(dateTo ? { lte: new Date(new Date(dateTo).setHours(23, 59, 59, 999)) } : {}), + }; + } const [items, total] = await Promise.all([ this.prisma.excessBaggageCharge.findMany({ diff --git a/apps/edr-passenger-api/src/modules/notifications/email-client.service.ts b/apps/edr-passenger-api/src/modules/notifications/email-client.service.ts index e50ac8037..5659ba231 100644 --- a/apps/edr-passenger-api/src/modules/notifications/email-client.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/email-client.service.ts @@ -5,6 +5,7 @@ import { OnApplicationBootstrap, } from "@nestjs/common"; import { ClientProxy } from "@nestjs/microservices"; +import * as sgMail from "@sendgrid/mail"; import { SendEmail } from "./dtos/email.dto"; @Injectable() @@ -14,9 +15,15 @@ export class EmailClientService implements OnApplicationBootstrap { constructor( @Inject("EMAIL_SERVICE") private readonly emailServiceClient: ClientProxy, - ) {} + ) { + const apiKey = process.env.SENDGRID_API_KEY; + if (apiKey) sgMail.setApiKey(apiKey); + } private readonly enabled = process.env.RABBITMQ_ENABLED !== "false"; + private get sendgridEnabled() { + return !!process.env.SENDGRID_API_KEY; + } async onApplicationBootstrap() { if (!this.enabled) return; @@ -29,22 +36,38 @@ export class EmailClientService implements OnApplicationBootstrap { } async sendEmail(dto: SendEmail): Promise<{ queued: boolean }> { - if (!this.enabled) { - this.logger.warn(`RABBITMQ disabled — skipped EMAIL`); - return { queued: false }; + if (this.enabled) { + this.emailServiceClient.emit("send-email", { + ...dto, + appKey: "IFHCRS-LICENSE-MANAGEMENT", + }); + this.logger.log( + `EMAIL queued to RabbitMQ [${process.env.EMAIL_QUEUE ?? "email_queue"}] pattern='send-email'`, + ); + this.logger.debug( + `EMAIL payload to=${dto.to} subject="${dto.subject ?? ""}"`, + ); + return { queued: true }; } - this.emailServiceClient.emit("send-email", { - ...dto, - appKey: "IFHCRS-LICENSE-MANAGEMENT", - }); - // Fire-and-forget enqueue: this confirms the message was handed to RabbitMQ, NOT delivered. - this.logger.log( - `EMAIL queued to RabbitMQ [${process.env.EMAIL_QUEUE ?? "email_queue"}] pattern='send-email'`, - ); - // Recipient + content are PII — keep them at debug level only. - this.logger.debug( - `EMAIL payload to=${dto.to} subject="${dto.subject ?? ""}" body="${dto.text ?? dto.body ?? dto.html ?? ""}"`, - ); - return { queued: true }; + + if (this.sendgridEnabled) { + try { + await sgMail.send({ + to: dto.to, + from: process.env.SENDGRID_FROM_EMAIL ?? "noreply@edr-platform.com", + subject: dto.subject ?? "EDR Notification", + text: dto.text ?? dto.body ?? "", + ...(dto.html ? { html: dto.html } : {}), + }); + this.logger.log(`EMAIL sent via SendGrid to=${dto.to}`); + return { queued: true }; + } catch (err: any) { + this.logger.error(`SendGrid send failed to=${dto.to}: ${err?.message}`); + return { queued: false }; + } + } + + this.logger.warn(`EMAIL not sent (no transport) — to=${dto.to} subject="${dto.subject ?? ""}"`); + return { queued: false }; } } diff --git a/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts b/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts index f94c0c20e..04188842d 100644 --- a/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts @@ -32,7 +32,7 @@ export class SmsClientService implements OnApplicationBootstrap { async sendSms(dto: SingleMessageDto): Promise<{ queued: boolean }> { if (!this.enabled) { - this.logger.warn(`RABBITMQ disabled — skipped SMS`); + this.logger.warn(`SMS not sent (RabbitMQ disabled) — to=${dto.to} message="${dto.message}"`); return { queued: false }; } this.smsClient.emit("send-sms", { @@ -51,7 +51,7 @@ export class SmsClientService implements OnApplicationBootstrap { async sendBulkMessages(dto: BulkMessagesDto): Promise<{ queued: boolean }> { if (!this.enabled) { - this.logger.warn(`RABBITMQ disabled — skipped BULK SMS (${dto.messages?.length ?? 0} messages)`); + this.logger.warn(`BULK SMS not sent (RabbitMQ disabled) — ${dto.messages?.length ?? 0} messages skipped`); return { queued: false }; } const messages = (dto.messages ?? []).map((m) => ({ to: m.to, text: m.message, from: m.from })); diff --git a/apps/edr-passenger-api/src/modules/packages/packages.controller.ts b/apps/edr-passenger-api/src/modules/packages/packages.controller.ts index d7eb79ff0..c88bdc15d 100644 --- a/apps/edr-passenger-api/src/modules/packages/packages.controller.ts +++ b/apps/edr-passenger-api/src/modules/packages/packages.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Get, Param, Post, Patch, Delete, UseGuards, Request, import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator'; import { PackagesService } from './packages.service'; -import { CreatePackageDto, BookPackageDto, CreatePriceTierDto, UpdatePriceTierDto } from './packages.dto'; +import { CreatePackageDto, BookPackageDto, CreatePriceTierDto, UpdatePriceTierDto, CreateInquiryDto, UpdateInquiryStatusDto } from './packages.dto'; import { IamGuard } from '../../common/iam-adapter'; import { JwtGuard } from '../../common/jwt.guard'; import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard'; @@ -12,6 +12,42 @@ import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard'; export class PackagesController { constructor(private readonly service: PackagesService) {} + @Post('inquiries') + @IsPublic() + @ApiOperation({ summary: 'Submit a package inquiry (public)' }) + createInquiry(@Body() dto: CreateInquiryDto) { + return this.service.createInquiry(dto); + } + + @Get('inquiries') + @UseGuards(IamGuard) + @ApiBearerAuth('IAM-auth') + @ApiOperation({ summary: 'List all inquiries (backoffice)' }) + listInquiries( + @Query('packageId') packageId?: string, + @Query('status') status?: string, + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + return this.service.listInquiries({ packageId, status, page: page ? +page : 1, pageSize: pageSize ? +pageSize : 20 }); + } + + @Patch('inquiries/:id/status') + @UseGuards(IamGuard) + @ApiBearerAuth('IAM-auth') + @ApiOperation({ summary: 'Update inquiry status (backoffice)' }) + updateInquiryStatus(@Param('id') id: string, @Body() dto: UpdateInquiryStatusDto) { + return this.service.updateInquiryStatus(id, dto.status); + } + + @Delete('inquiries/:id') + @UseGuards(IamGuard) + @ApiBearerAuth('IAM-auth') + @ApiOperation({ summary: 'Delete inquiry (backoffice)' }) + deleteInquiry(@Param('id') id: string) { + return this.service.deleteInquiry(id); + } + @Get() @IsPublic() @ApiOperation({ summary: 'List active packages' }) diff --git a/apps/edr-passenger-api/src/modules/packages/packages.dto.ts b/apps/edr-passenger-api/src/modules/packages/packages.dto.ts index ec378f325..b4dac126d 100644 --- a/apps/edr-passenger-api/src/modules/packages/packages.dto.ts +++ b/apps/edr-passenger-api/src/modules/packages/packages.dto.ts @@ -16,6 +16,20 @@ export class CreatePriceTierDto { @IsInt() @Min(0) availableSeats: number; } +export class CreateInquiryDto { + @ApiProperty() @IsUUID() packageId: string; + @ApiPropertyOptional() @IsOptional() @IsUUID() priceTierId?: string; + @ApiProperty({ example: 2 }) @IsInt() @Min(1) travelerCount: number; + @ApiProperty() @IsString() contactName: string; + @ApiPropertyOptional() @IsOptional() @IsString() contactEmail?: string; + @ApiPropertyOptional() @IsOptional() @IsString() contactPhone?: string; + @ApiPropertyOptional() @IsOptional() @IsString() notes?: string; +} + +export class UpdateInquiryStatusDto { + @ApiProperty({ example: 'CONTACTED' }) @IsString() status: string; +} + export class UpdatePriceTierDto { @ApiPropertyOptional() @IsOptional() @IsString() seatType?: string; @ApiPropertyOptional() @IsOptional() @IsString() label?: string; diff --git a/apps/edr-passenger-api/src/modules/packages/packages.service.ts b/apps/edr-passenger-api/src/modules/packages/packages.service.ts index 943ffd778..f6c25611c 100644 --- a/apps/edr-passenger-api/src/modules/packages/packages.service.ts +++ b/apps/edr-passenger-api/src/modules/packages/packages.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../../common/prisma.service'; import { CurrencyService } from '../currency/currency.service'; -import { CreatePackageDto, BookPackageDto, UpdatePriceTierDto, CreatePriceTierDto } from './packages.dto'; +import { CreatePackageDto, BookPackageDto, UpdatePriceTierDto, CreatePriceTierDto, CreateInquiryDto } from './packages.dto'; import { Currency } from '@prisma/client'; function generateRef(): string { @@ -17,6 +17,53 @@ export class PackagesService { private readonly currencyService: CurrencyService, ) {} + async createInquiry(dto: CreateInquiryDto) { + return this.prisma.packageInquiry.create({ + data: { + packageId: dto.packageId, + priceTierId: dto.priceTierId ?? null, + travelerCount: dto.travelerCount, + contactName: dto.contactName, + contactEmail: dto.contactEmail ?? null, + contactPhone: dto.contactPhone ?? null, + notes: dto.notes ?? null, + enquiredAt: new Date(), + }, + include: { package: { select: { id: true, name: true, code: true } }, priceTier: { select: { id: true, label: true } } }, + }); + } + + async listInquiries({ packageId, status, page = 1, pageSize = 20 }: { packageId?: string; status?: string; page?: number; pageSize?: number }) { + const where: any = {}; + if (packageId) where.packageId = packageId; + if (status) where.status = status; + const skip = (page - 1) * pageSize; + const [items, total] = await Promise.all([ + this.prisma.packageInquiry.findMany({ + where, + include: { package: { select: { id: true, name: true, code: true } }, priceTier: { select: { id: true, label: true, priceMinor: true } } }, + orderBy: { enquiredAt: 'desc' }, + skip, + take: pageSize, + }), + this.prisma.packageInquiry.count({ where }), + ]); + return { items, total, page, pageSize }; + } + + async updateInquiryStatus(id: string, status: string) { + const inquiry = await this.prisma.packageInquiry.findUnique({ where: { id } }); + if (!inquiry) throw new NotFoundException('Inquiry not found'); + return this.prisma.packageInquiry.update({ where: { id }, data: { status } }); + } + + async deleteInquiry(id: string) { + const inquiry = await this.prisma.packageInquiry.findUnique({ where: { id } }); + if (!inquiry) throw new NotFoundException('Inquiry not found'); + await this.prisma.packageInquiry.delete({ where: { id } }); + return { deleted: true }; + } + listActive() { const now = new Date(); return this.prisma.travelPackage.findMany({ diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts index be3f9ba05..b0a7c8afe 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts @@ -24,19 +24,31 @@ export class PassengersController { summary: 'List all passengers with filters (Admin/Agent)', description: 'Returns paginated list of passengers with search filters' }) - @ApiQuery({ name: 'search', required: false, description: 'Search by name, email, or phone' }) - @ApiQuery({ name: 'verified', required: false, description: 'Filter by verification status' }) - @ApiQuery({ name: 'page', required: false, description: 'Page number' }) - @ApiQuery({ name: 'pageSize', required: false, description: 'Items per page' }) + @ApiQuery({ name: 'search', required: false }) + @ApiQuery({ name: 'verified', required: false }) + @ApiQuery({ name: 'gender', required: false }) + @ApiQuery({ name: 'nationality', required: false }) + @ApiQuery({ name: 'dateFrom', required: false }) + @ApiQuery({ name: 'dateTo', required: false }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'pageSize', required: false }) findAll( @Query('search') search?: string, @Query('verified') verified?: string, + @Query('gender') gender?: string, + @Query('nationality') nationality?: string, + @Query('dateFrom') dateFrom?: string, + @Query('dateTo') dateTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.findAll({ search, - verified: verified ? verified === 'true' : undefined, + verified: verified ? verified === 'true' : undefined, + gender, + nationality, + dateFrom, + dateTo, page: page ? parseInt(page) : 1, pageSize: pageSize ? parseInt(pageSize) : 20 }); diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 34b4a82f5..bee4d4d77 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -8,6 +8,10 @@ import { VerifaydaService } from '../verifayda/verifayda.service'; interface PassengerFilters { search?: string; verified?: boolean; + gender?: string; + nationality?: string; + dateFrom?: string; + dateTo?: string; page?: number; pageSize?: number; } @@ -29,7 +33,7 @@ export class PassengersService { ) {} async findAll(filters: PassengerFilters = {}) { - const { search, verified, page = 1, pageSize = 20 } = filters; + const { search, verified, gender, nationality, dateFrom, dateTo, page = 1, pageSize = 20 } = filters; const skip = (page - 1) * pageSize; const where: any = {}; @@ -48,6 +52,21 @@ export class PassengersService { where.user = { ...(where.user ?? {}), faydaVerified: verified }; } + if (gender) { + where.user = { ...(where.user ?? {}), gender }; + } + + if (nationality) { + where.user = { ...(where.user ?? {}), nationality: { contains: nationality, mode: 'insensitive' } }; + } + + if (dateFrom || dateTo) { + where.createdAt = { + ...(dateFrom ? { gte: new Date(dateFrom) } : {}), + ...(dateTo ? { lte: new Date(new Date(dateTo).setHours(23, 59, 59, 999)) } : {}), + }; + } + const [items, total] = await Promise.all([ this.prisma.passenger.findMany({ where, diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts index 2db2f9748..8afde9ac1 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts @@ -37,6 +37,8 @@ export class TicketsController { @ApiQuery({ name: 'originStationId', required: false }) @ApiQuery({ name: 'destinationStationId', required: false }) @ApiQuery({ name: 'arrivalDate', required: false }) + @ApiQuery({ name: 'dateFrom', required: false }) + @ApiQuery({ name: 'dateTo', required: false }) @ApiQuery({ name: 'skip', required: false }) @ApiQuery({ name: 'take', required: false }) listTickets( @@ -45,6 +47,8 @@ export class TicketsController { @Query('originStationId') originStationId?: string, @Query('destinationStationId') destinationStationId?: string, @Query('arrivalDate') arrivalDate?: string, + @Query('dateFrom') dateFrom?: string, + @Query('dateTo') dateTo?: string, @Query('skip') skip?: string, @Query('take') take?: string, ) { @@ -54,6 +58,8 @@ export class TicketsController { originStationId, destinationStationId, arrivalDate, + dateFrom, + dateTo, skip: skip ? parseInt(skip) : 0, take: take ? parseInt(take) : 50, }); diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 7c51237af..ecc17dc53 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -21,7 +21,7 @@ export class TicketsService { @InjectDataSource() private readonly dataSource: DataSource, ) {} - async listTickets(filters: { search?: string; status?: string; originStationId?: string; destinationStationId?: string; arrivalDate?: string; skip: number; take: number }) { + async listTickets(filters: { search?: string; status?: string; originStationId?: string; destinationStationId?: string; arrivalDate?: string; dateFrom?: string; dateTo?: string; skip: number; take: number }) { const where: any = {}; if (filters.search) { where.OR = [ @@ -31,7 +31,7 @@ export class TicketsService { ]; } if (filters.status) { - where.booking = { ...where.booking, status: filters.status }; + where.status = filters.status; } if (filters.originStationId) { where.booking = { ...where.booking, schedule: { ...where.booking?.schedule, originStationId: filters.originStationId } }; @@ -45,6 +45,12 @@ export class TicketsService { end.setDate(end.getDate() + 1); where.booking = { ...where.booking, schedule: { ...where.booking?.schedule, arrivalAt: { gte: start, lt: end } } }; } + if (filters.dateFrom || filters.dateTo) { + where.issuedAt = { + ...(filters.dateFrom ? { gte: new Date(filters.dateFrom) } : {}), + ...(filters.dateTo ? { lte: new Date(new Date(filters.dateTo).setHours(23, 59, 59, 999)) } : {}), + }; + } const [tickets, total] = await Promise.all([ this.prisma.ticket.findMany({ where, diff --git a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx index 9100d05cd..d1b7cdf87 100644 --- a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx @@ -47,8 +47,14 @@ export default function BookingsPage() { const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ - queryKey: ['bookings', filters], - queryFn: () => bookingsApi.getAll(filters), + queryKey: ['bookings', filters, extraFilters], + queryFn: () => bookingsApi.getAll({ + ...filters, + ...(extraFilters.bookingType && { bookingType: extraFilters.bookingType }), + ...(extraFilters.paymentStatus && { paymentStatus: extraFilters.paymentStatus }), + ...(extraFilters.dateFrom && { dateFrom: extraFilters.dateFrom }), + ...(extraFilters.dateTo && { dateTo: extraFilters.dateTo }), + }), }); const cancelMutation = useMutation({ @@ -257,8 +263,8 @@ export default function BookingsPage() { diff --git a/apps/edr-passenger-web/backoffice/src/app/excess-baggage/page.tsx b/apps/edr-passenger-web/backoffice/src/app/excess-baggage/page.tsx index 75ba7d6c4..1eb6b2946 100644 --- a/apps/edr-passenger-web/backoffice/src/app/excess-baggage/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/excess-baggage/page.tsx @@ -20,14 +20,21 @@ const STATUS_VARIANT: Record = { export default function ExcessBaggagePage() { const queryClient = useQueryClient(); - const [filters, setFilters] = useState({ status: '', bookingRef: '', page: '1' }); + const [filters, setFilters] = useState({ status: '', bookingRef: '', dateFrom: '', dateTo: '', page: '1' }); + const [showExtraFilters, setShowExtraFilters] = useState(false); const [waiveModal, setWaiveModal] = useState(null); const [waiveReason, setWaiveReason] = useState(''); const [waiveError, setWaiveError] = useState(null); const { data, isLoading } = useQuery({ queryKey: ['excess-baggage', filters], - queryFn: () => excessBaggageApi.getAll({ status: filters.status || undefined, bookingRef: filters.bookingRef || undefined, page: filters.page }), + queryFn: () => excessBaggageApi.getAll({ + status: filters.status || undefined, + bookingRef: filters.bookingRef || undefined, + dateFrom: filters.dateFrom || undefined, + dateTo: filters.dateTo || undefined, + page: filters.page, + }), }); const waiveMutation = useMutation({ @@ -104,14 +111,14 @@ export default function ExcessBaggagePage() { icon: Send, variant: 'secondary' as const, onClick: (c: any) => resendMutation.mutate(c.id), - hidden: (c: any) => c.status !== 'PENDING', + show: (c: any) => c.status === 'PENDING', }, { label: 'Waive', icon: RefreshCw, variant: 'secondary' as const, onClick: (c: any) => { setWaiveModal(c); setWaiveReason(''); setWaiveError(null); }, - hidden: (c: any) => ['PAID', 'CASH_COLLECTED', 'WAIVED'].includes(c.status), + show: (c: any) => !['PAID', 'CASH_COLLECTED', 'WAIVED'].includes(c.status), }, ]; @@ -125,31 +132,41 @@ export default function ExcessBaggagePage() {
-
-
- - setFilters({ ...filters, bookingRef: e.target.value, page: '1' })} - /> -
-
- - setFilters({ ...filters, bookingRef: e.target.value, page: '1' })} /> +
+ +
+ {showExtraFilters && ( +
+
+ + setFilters({ ...filters, dateFrom: e.target.value, page: '1' })} /> +
+
+ + setFilters({ ...filters, dateTo: e.target.value, page: '1' })} /> +
+
+ )}
diff --git a/apps/edr-passenger-web/backoffice/src/app/package-inquiries/layout.tsx b/apps/edr-passenger-web/backoffice/src/app/package-inquiries/layout.tsx new file mode 100644 index 000000000..289c6aa12 --- /dev/null +++ b/apps/edr-passenger-web/backoffice/src/app/package-inquiries/layout.tsx @@ -0,0 +1,7 @@ +'use client'; + +import DashboardLayout from '../dashboard/layout'; + +export default function PackageInquiriesLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/apps/edr-passenger-web/backoffice/src/app/package-inquiries/page.tsx b/apps/edr-passenger-web/backoffice/src/app/package-inquiries/page.tsx new file mode 100644 index 000000000..73f0ad658 --- /dev/null +++ b/apps/edr-passenger-web/backoffice/src/app/package-inquiries/page.tsx @@ -0,0 +1,193 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Trash2 } from 'lucide-react'; +import DataTable from '@/components/ui/DataTable'; +import Badge from '@/components/ui/Badge'; +import ActionButton from '@/components/ui/ActionButton'; +import ConfirmDialog from '@/components/ui/ConfirmDialog'; +import { packageInquiriesApi, packagesApi } from '@/lib/api'; +import { formatDateTime, formatCurrency } from '@/lib/utils'; + +const STATUSES = ['NEW', 'CONTACTED', 'CONVERTED', 'CLOSED']; + +const statusVariant: Record = { + NEW: 'info', + CONTACTED: 'warning', + CONVERTED: 'success', + CLOSED: 'default', +}; + +export default function PackageInquiriesPage() { + const [filters, setFilters] = useState({ packageId: '', status: '' }); + const [deleteConfirm, setDeleteConfirm] = useState(null); + const [deleteError, setDeleteError] = useState(null); + const queryClient = useQueryClient(); + + const { data, isLoading } = useQuery({ + queryKey: ['package-inquiries', filters], + queryFn: () => packageInquiriesApi.getAll({ ...filters, pageSize: 50 }), + }); + + const { data: packagesData } = useQuery({ + queryKey: ['packages-all-simple'], + queryFn: () => packagesApi.getAll({ pageSize: 100 }), + }); + + const packages: any[] = packagesData?.items || []; + + const statusMutation = useMutation({ + mutationFn: ({ id, status }: { id: string; status: string }) => + packageInquiriesApi.updateStatus(id, status), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['package-inquiries'] }), + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => packageInquiriesApi.remove(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['package-inquiries'] }); + setDeleteConfirm(null); + setDeleteError(null); + }, + onError: (e: any) => setDeleteError(e?.response?.data?.message || e?.message || 'Failed to delete'), + }); + + const columns = [ + { + key: 'contact', + label: 'Contact', + render: (row: any) => ( +
+
{row.contactName}
+
{row.contactEmail || row.contactPhone || '—'}
+
+ ), + }, + { + key: 'package', + label: 'Package', + render: (row: any) => ( +
+
{row.package?.name || '—'}
+
{row.package?.code}
+
+ ), + }, + { + key: 'priceTier', + label: 'Price Tier', + render: (row: any) => row.priceTier ? ( +
+
{row.priceTier.label}
+
{formatCurrency(row.priceTier.priceMinor, 'ETB')} / person
+
+ ) : , + }, + { + key: 'travelerCount', + label: 'Travelers', + render: (row: any) => ( + {row.travelerCount} + ), + }, + { + key: 'enquiredAt', + label: 'Enquired At', + render: (row: any) => ( + {formatDateTime(row.enquiredAt)} + ), + }, + { + key: 'notes', + label: 'Notes', + render: (row: any) => ( + {row.notes || '—'} + ), + }, + { + key: 'status', + label: 'Status', + render: (row: any) => ( + + ), + }, + ]; + + const actions = [ + { + label: 'Delete', + onClick: (row: any) => { setDeleteConfirm(row); setDeleteError(null); }, + variant: 'danger' as const, + icon: Trash2, + }, + ]; + + return ( +
+
+

Package Inquiries

+

Manage incoming package inquiries

+
+ +
+
+
+ + +
+
+ + +
+
+
+ + + + { setDeleteConfirm(null); setDeleteError(null); }} + onConfirm={() => deleteMutation.mutate(deleteConfirm.id)} + title="Delete Inquiry" + message={`Delete inquiry from ${deleteConfirm?.contactName}? This cannot be undone.`} + confirmText="Delete" + isDanger + isLoading={deleteMutation.isPending} + error={deleteError ?? undefined} + /> +
+ ); +} diff --git a/apps/edr-passenger-web/backoffice/src/app/packages/page.tsx b/apps/edr-passenger-web/backoffice/src/app/packages/page.tsx index 3ff0f9814..601cb17c6 100644 --- a/apps/edr-passenger-web/backoffice/src/app/packages/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/packages/page.tsx @@ -29,6 +29,11 @@ const emptyForm = { export default function PackagesPage() { const [page] = useState(1); + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + const [showExtraFilters, setShowExtraFilters] = useState(false); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); const [form, setForm] = useState(emptyForm); const [modalMode, setModalMode] = useState<'create' | 'edit' | null>(null); const [editingId, setEditingId] = useState(null); @@ -282,6 +287,15 @@ export default function PackagesPage() { const isPending = createMutation.isPending || updateMutation.isPending; + const allItems: any[] = data?.items || []; + const filteredItems = allItems.filter((p) => { + if (search && !p.name.toLowerCase().includes(search.toLowerCase()) && !p.code.toLowerCase().includes(search.toLowerCase())) return false; + if (statusFilter && p.status !== statusFilter) return false; + if (dateFrom && new Date(p.validFrom).toISOString().split('T')[0] < dateFrom) return false; + if (dateTo && new Date(p.validUntil).toISOString().split('T')[0] > dateTo) return false; + return true; + }); + return (
@@ -292,13 +306,47 @@ export default function PackagesPage() { New Package
- +
+
+
+
+ setSearch(e.target.value)} /> +
+ + +
+ {showExtraFilters && ( +
+
+ + setDateFrom(e.target.value)} /> +
+
+ + setDateTo(e.target.value)} /> +
+
+ )} +
+ +
{/* View Modal */} setViewPackage(null)} title="Package Details" size="lg"> diff --git a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx index 96101b16c..9765195e2 100644 --- a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx @@ -63,8 +63,14 @@ export default function PassengersPage() { }); const { data, isLoading, error } = useQuery({ - queryKey: ['passengers', filters], - queryFn: () => passengersApi.getAll(filters), + queryKey: ['passengers', filters, extraFilters], + queryFn: () => passengersApi.getAll({ + ...filters, + ...(extraFilters.gender && { gender: extraFilters.gender }), + ...(extraFilters.nationality && { nationality: extraFilters.nationality }), + ...(extraFilters.dateFrom && { dateFrom: extraFilters.dateFrom }), + ...(extraFilters.dateTo && { dateTo: extraFilters.dateTo }), + }), }); const PASSENGER_COLS = [ diff --git a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx index f8f717c0b..b0e4e0858 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx @@ -78,6 +78,8 @@ export default function TicketsPage() { originStationId: filters.originStationId || undefined, destinationStationId: filters.destinationStationId || undefined, arrivalDate: filters.arrivalDate || undefined, + dateFrom: filters.dateFrom || undefined, + dateTo: filters.dateTo || undefined, skip: 0, take: 50, }), @@ -483,7 +485,7 @@ export default function TicketsPage() { Error loading tickets: {error instanceof Error ? error.message : 'Unknown error'}
)} -
+
setFilters({ ...filters, arrivalDate: e.target.value })} />
-
- - +
+
+ {showExtraFilters && ( +
+
+ + +
+
+ + setFilters({ ...filters, dateFrom: e.target.value })} /> +
+
+ + setFilters({ ...filters, dateTo: e.target.value })} /> +
+
+ )}
{/* Tickets Table */} diff --git a/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx b/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx index 3722429d4..96798936b 100644 --- a/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx +++ b/apps/edr-passenger-web/backoffice/src/components/layout/Sidebar.tsx @@ -60,6 +60,7 @@ const navigationSections = [ title: 'Tourism', items: [ { name: 'Packages', href: '/packages', icon: Package }, + { name: 'Inquiries', href: '/package-inquiries', icon: MessageSquare }, ] }, { diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts index aff2c78e1..caf451291 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts @@ -397,6 +397,22 @@ export const packagesApi = { deleteTier: (tierId: string) => apiClient.delete(`/packages/tiers/${tierId}`), }; +// Package Inquiries API +export const packageInquiriesApi = { + getAll: async (params?: any) => { + const cleanParams = Object.fromEntries( + Object.entries(params || {}).filter(([_, v]) => v !== '' && v !== undefined && v !== null) + ) as Record; + const query = new URLSearchParams(cleanParams).toString(); + const response = await apiClient.get(`/packages/inquiries${query ? `?${query}` : ''}`); + if (response?.data) return Array.isArray(response.data) ? { items: response.data } : response; + return Array.isArray(response) ? { items: response } : response; + }, + create: (data: any) => apiClient.post('/packages/inquiries', data), + updateStatus: (id: string, status: string) => apiClient.patch(`/packages/inquiries/${id}/status`, { status }), + remove: (id: string) => apiClient.delete(`/packages/inquiries/${id}`), +}; + // Excess Baggage API export const excessBaggageApi = { logCharge: (data: any) => apiClient.post('/agents/excess-baggage', data), From 9c664ccce61c5b06eb2699a93af5db07608d0794 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Fri, 26 Jun 2026 14:20:32 +0300 Subject: [PATCH 2/3] feat(auth): add post-Fayda password setup flow --- .../src/config/fayda.config.ts | 4 +- apps/edr-passenger-api/src/main.ts | 2 +- .../src/modules/auth/auth.controller.ts | 24 +++- .../src/modules/auth/auth.dto.ts | 18 ++- .../modules/auth/passenger-auth.service.ts | 124 +++++++++++++++++- .../modules/verifayda/verifayda.controller.ts | 1 + .../src/modules/verifayda/verifayda.dto.ts | 29 +++- .../modules/verifayda/verifayda.service.ts | 99 +++++++++++++- 8 files changed, 286 insertions(+), 15 deletions(-) diff --git a/apps/edr-passenger-api/src/config/fayda.config.ts b/apps/edr-passenger-api/src/config/fayda.config.ts index e0bce45c6..d8c4b1868 100644 --- a/apps/edr-passenger-api/src/config/fayda.config.ts +++ b/apps/edr-passenger-api/src/config/fayda.config.ts @@ -66,7 +66,9 @@ function decodePrivateJwk(base64: string): FaydaJwk { export default registerAs('fayda', (): FaydaConfig => { const enabled = (process.env.FAYDA_ENABLED ?? 'false').toLowerCase() === 'true'; - const scope = process.env.FAYDA_SCOPE ?? 'openid profile email'; + // `profile` covers name/birthdate/gender/picture; `email`, `phone`, `address` + // are needed so the matching essential claims aren't rejected as out-of-scope. + const scope = process.env.FAYDA_SCOPE ?? 'openid profile email phone address'; const acrValues = process.env.FAYDA_ACR_VALUES ?? 'mosip:idp:acr:generated-code'; const claimsLocales = process.env.FAYDA_CLAIMS_LOCALES ?? 'en am'; const sessionTtl = Number.parseInt(process.env.FAYDA_SESSION_TTL_MINUTES ?? '10', 10); diff --git a/apps/edr-passenger-api/src/main.ts b/apps/edr-passenger-api/src/main.ts index 86d33debb..680e67866 100644 --- a/apps/edr-passenger-api/src/main.ts +++ b/apps/edr-passenger-api/src/main.ts @@ -314,7 +314,7 @@ Payment providers send notifications to: .addTag("Excess Baggage", "IAM-protected agent/supervisor endpoints to log excess baggage charges, waive fees, resend payment links, and manage allowance rules per seat class. Public token-based endpoints let passengers self-pay outstanding charges.") .addTag("Packages", "Bundled travel packages with tiered pricing. Public endpoints for browsing and booking; JWT-authenticated endpoints for purchase history; IAM-protected endpoints for admin CRUD and tier management.") .addTag("Audit", "User activity logging, system changes, compliance tracking, and audit trails") - .addTag("Auth", "Passenger registration, login, OTP, password reset, and profile management") + .addTag("Passenger Auth", "Passenger registration, login, OTP, password reset, Fayda password setup, and profile management") .addTag("Booking", "Complete booking lifecycle: create, modify, cancel, guest checkout. Supports ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT booking types. returnLegStatus filter for round-trip no-show management") .addTag("Config", "System settings, feature flags, and configuration management") .addTag("Currencies", "Multi-currency support, exchange rates, and currency conversion") diff --git a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts index 80e67d3de..d53ef40d7 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts @@ -3,10 +3,10 @@ import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nes import { Throttle, SkipThrottle } from '@nestjs/throttler'; import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator'; import { PassengerAuthService } from './passenger-auth.service'; -import { RegisterDto, LoginDto } from './auth.dto'; +import { RegisterDto, LoginDto, FaydaRequestPasswordSetupDto, FaydaVerifyAndLoginDto } from './auth.dto'; import { JwtGuard } from '../../common/jwt.guard'; -@ApiTags('Auth') +@ApiTags('Passenger Auth') @Controller('auth') @Throttle({ auth: { limit: 5, ttl: 60_000 } }) export class AuthController { @@ -118,4 +118,24 @@ export class AuthController { resetPassword(@Param('id') id: string, @Body() body: { tempPassword: string }) { return this.passengerAuthService.resetUserPassword(id, body.tempPassword); } + + @Post('fayda/request-password-setup') + @IsPublic() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Send OTP to phone for Fayda-verified account password setup' }) + @ApiResponse({ status: 200, description: 'OTP sent to registered phone number' }) + @ApiBody({ type: FaydaRequestPasswordSetupDto }) + requestFaydaPasswordSetup(@Body() dto: FaydaRequestPasswordSetupDto, @Request() req: any) { + return this.passengerAuthService.requestFaydaPasswordSetup(dto.phoneNumber, req); + } + + @Post('fayda/verify-and-login') + @IsPublic() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Verify OTP and receive session token for Fayda-verified account' }) + @ApiResponse({ status: 200, description: 'Returns token + requiresPassword flag. Use token with POST /v1/auth/set-fayda-password.' }) + @ApiBody({ type: FaydaVerifyAndLoginDto }) + verifyFaydaAndLogin(@Body() dto: FaydaVerifyAndLoginDto) { + return this.passengerAuthService.verifyFaydaAndLogin(dto.phoneNumber, dto.otp); + } } diff --git a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts index e12c8cd06..6f43e7212 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts @@ -1,6 +1,6 @@ import { IsEmail, IsString, MinLength, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class NameDto { @ApiProperty({ example: 'ቀለሙ ቀጸላ' }) @@ -49,3 +49,19 @@ export class LoginDto { @IsString() password: string; } + +export class FaydaRequestPasswordSetupDto { + @ApiProperty({ example: '+251911234567', description: 'Phone number of the Fayda-verified account' }) + @IsString() + phoneNumber: string; +} + +export class FaydaVerifyAndLoginDto { + @ApiProperty({ example: '+251911234567' }) + @IsString() + phoneNumber: string; + + @ApiProperty({ example: '123456', description: '6-digit OTP received via SMS' }) + @IsString() + otp: string; +} diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index 4192091dd..cd583517c 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -2,6 +2,7 @@ import { Injectable, ConflictException, InternalServerErrorException, + Logger, UnauthorizedException, } from '@nestjs/common'; import { ModuleRef, ContextIdFactory } from '@nestjs/core'; @@ -10,6 +11,7 @@ import { DataSource } from 'typeorm'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { AuthService as IamAuthService } from '@tria-plc/iamapi-common/module/auth/services/auth.service'; import { EUserType } from '@tria-plc/api-common/utils/enums/user.enum'; +import { EOtpType } from '@tria-plc/iamapi-common/enums/otp.enum'; import { PrismaService } from '../../common/prisma.service'; import { RegisterDto, LoginDto } from './auth.dto'; @@ -19,10 +21,13 @@ type IamUserRow = { name: { en: string; am: string } | null; phone_number: string | null; metadata: Record | null; + verified_by: string | null; }; @Injectable() export class PassengerAuthService { + private readonly logger = new Logger(PassengerAuthService.name); + constructor( private readonly prisma: PrismaService, @InjectDataSource() private readonly dataSource: DataSource, @@ -165,7 +170,7 @@ export class PassengerAuthService { include: { loyalty: true, wallet: true }, }), this.dataSource.query( - `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`, + `SELECT id, email, name, phone_number, metadata, verified_by FROM iam.users WHERE id = $1 LIMIT 1`, [iamUserId], ), ]); @@ -178,7 +183,7 @@ export class PassengerAuthService { email: iam?.email ?? null, phone: iam?.phone_number ?? null, fullName: iam?.name?.en ?? iam?.name?.am ?? null, - faydaVerified: iam?.metadata?.faydaVerified ?? false, + faydaVerified: iam?.verified_by === 'fayda', createdAt: passenger.createdAt, passenger: { id: passenger.id, @@ -396,6 +401,121 @@ export class PassengerAuthService { return { success: true, message: 'Password reset successfully' }; } + async requestFaydaPasswordSetup(phoneNumber: string, req: any): Promise<{ sent: boolean }> { + const phone = this.standardizePhone(phoneNumber); + + const users = await this.dataSource.query<{ id: string; email: string }[]>( + `SELECT id, email FROM iam.users WHERE phone_number = $1 AND verified_by = 'fayda' LIMIT 1`, + [phone], + ); + this.logger.log(`requestFaydaPasswordSetup: phone=${phone} found=${users.length > 0}`); + // Return success regardless to avoid phone enumeration + if (!users.length) return { sent: true }; + const u = users[0]; + + const iamAuthService = await this.resolveIamAuthService(req); + await iamAuthService.generateVerificationCode({ + email: u.email, + phoneNumber: phone, + type: EOtpType.SET_PASSWORD, + }); + + return { sent: true }; + } + + async verifyFaydaAndLogin( + phoneNumber: string, + otp: string, + ): Promise<{ token: string; refreshToken: string; requiresPassword: boolean; iamUserId: string }> { + const phone = this.standardizePhone(phoneNumber); + + const users = await this.dataSource.query<{ + id: string; + email: string; + name: { en: string; am: string } | null; + username: string; + phone_number: string | null; + has_set_password: boolean; + }[]>( + `SELECT id, email, name, username, phone_number, has_set_password + FROM iam.users WHERE phone_number = $1 AND verified_by = 'fayda' LIMIT 1`, + [phone], + ); + if (!users.length) throw new UnauthorizedException('Invalid phone number or OTP'); + const u = users[0]; + + const verifications = await this.dataSource.query<{ + id: string; verification_code: string; attempt_count: number; + }[]>( + `SELECT id, verification_code, attempt_count FROM iam.user_verifications + WHERE user_id = $1 AND otp_type = 'set-password' AND "isUsed" = false AND expires_at > NOW() + ORDER BY created_at DESC LIMIT 1`, + [u.id], + ); + if (!verifications.length) throw new UnauthorizedException('Invalid phone number or OTP'); + const v = verifications[0]; + + if (v.attempt_count >= 5) { + await this.dataSource.query( + `UPDATE iam.user_verifications SET "isUsed" = true WHERE id = $1`, [v.id], + ); + throw new UnauthorizedException('Too many attempts. Request a new code.'); + } + + await this.dataSource.query( + `UPDATE iam.user_verifications SET attempt_count = attempt_count + 1 WHERE id = $1`, [v.id], + ); + + const { verifyPassword } = await import('@tria-plc/api-common/utils/argon'); + const valid = await verifyPassword(otp, v.verification_code); + if (!valid) throw new UnauthorizedException('Invalid phone number or OTP'); + + await this.dataSource.query( + `UPDATE iam.user_verifications SET "isUsed" = true WHERE id = $1`, [v.id], + ); + + const userInfo = { + id: u.id, + email: u.email ?? '', + name: u.name ?? { en: '', am: '' }, + userType: 'individual', + status: 'accepted', + hasSetPassword: u.has_set_password, + isPhoneNumberVerified: false, + hasFinishedRegistration: false, + hasFinishedDMSOnboarding: false, + username: u.username, + phoneNumber: u.phone_number ?? '', + roles: [], + permissions: [], + employee: [], + }; + + const sessions = await this.dataSource.query<{ id: string }[]>( + `INSERT INTO iam.sessions + (id, email, device, "userInfo", expiry_time, refresh_count, status, user_id) + VALUES (gen_random_uuid(), $1, 'fayda-otp-setup', $2::jsonb, NOW() + INTERVAL '1 day', 0, 'ACTIVE', $3) + ON CONFLICT (user_id, device) DO UPDATE + SET status = 'ACTIVE', "userInfo" = EXCLUDED."userInfo", + expiry_time = NOW() + INTERVAL '1 day', updated_at = NOW() + RETURNING id`, + [u.email ?? '', JSON.stringify(userInfo), u.id], + ); + + const { generateToken, generateRefreshToken } = await import('@tria-plc/api-common/utils/token'); + const token = generateToken({ id: sessions[0].id }); + const refreshToken = generateRefreshToken({ id: sessions[0].id }); + + return { token, refreshToken, requiresPassword: !u.has_set_password, iamUserId: u.id }; + } + + private standardizePhone(phone: string): string { + const digits = phone.replace(/\D/g, ''); + if (digits.startsWith('251')) return `+${digits}`; + if (digits.startsWith('0')) return `+251${digits.slice(1)}`; + return `+${digits}`; + } + private async compensateIamSignup(email: string): Promise { try { const rows = await this.dataSource.query<{ id: string }[]>( diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts index b306d2cdc..9ba4cd515 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts @@ -74,6 +74,7 @@ export class VerifaydaController { purpose: dto.purpose ?? 'VERIFY', platform: dto.platform ?? 'WEB', userId: req.user?.id, + wantsPasswordSetup: dto.wantsPasswordSetup ?? false, }); return { authorizationUrl }; } diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts index 4842b479c..8e5e575e0 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts @@ -21,6 +21,17 @@ export class StartVerificationDto { @IsOptional() @IsIn(['WEB', 'MOBILE']) platform?: 'WEB' | 'MOBILE'; + + @ApiPropertyOptional({ + type: Boolean, + default: false, + description: + 'Set to true when the user opts in to full account registration (checkbox). ' + + 'When true, the /complete response includes a short-lived token and promptPasswordSetup=true ' + + 'so the frontend can immediately prompt for a password via POST /v1/auth/set-fayda-password.', + }) + @IsOptional() + wantsPasswordSetup?: boolean; } export class CompleteVerificationResultDto { @@ -29,9 +40,12 @@ export class CompleteVerificationResultDto { @ApiProperty() verified: boolean; - @ApiPropertyOptional({ description: 'JWT (LOGIN flow only).' }) + @ApiPropertyOptional({ description: 'JWT. LOGIN: session token for the authenticated user. VERIFY: short-lived token for calling /v1/auth/set-fayda-password.' }) token?: string; + @ApiPropertyOptional() + refreshToken?: string; + @ApiPropertyOptional({ description: 'Authenticated user summary (LOGIN flow only; same shape as /auth/login).', }) @@ -62,6 +76,19 @@ export class CompleteVerificationResultDto { @ApiPropertyOptional({ description: 'Whether the verified identity was saved to IAM. False if the IAM write failed.' }) userDataSaved?: boolean; + + @ApiPropertyOptional({ description: 'IAM user ID of the verified identity (VERIFY flow).' }) + iamUserId?: string; + + @ApiPropertyOptional({ description: 'True when the IAM account has not yet set a password (VERIFY flow).' }) + requiresPassword?: boolean; + + @ApiPropertyOptional({ + description: + 'True when the user opted in to immediate password setup (wantsPasswordSetup=true at start) ' + + 'AND they have not yet set a password. Frontend should navigate to the set-password screen.', + }) + promptPasswordSetup?: boolean; } export class VerifaydaCallbackDto { diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index 407046cb4..e51bf1846 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -8,6 +8,7 @@ import { import { ConfigService } from '@nestjs/config'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; +import { generateToken, generateRefreshToken } from '@tria-plc/api-common/utils/token'; import axios, { AxiosInstance } from 'axios'; import { PrismaService } from '../../common/prisma.service'; import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config'; @@ -47,6 +48,7 @@ export interface StartVerificationInput { purpose: VerifaydaPurpose; platform?: FaydaPlatform; userId?: string; // iamUserId of the authenticated user, if any + wantsPasswordSetup?: boolean; } export interface FaydaUserSummary { @@ -66,6 +68,10 @@ export interface CompleteVerificationResult { purpose: VerifaydaPurpose; verified: boolean; token?: string; + refreshToken?: string; + requiresPassword?: boolean; + promptPasswordSetup?: boolean; + iamUserId?: string; user?: FaydaUserSummary; fullName?: string; email?: string; @@ -145,6 +151,7 @@ export class VerifaydaService { codeVerifier, purpose: input.purpose, platform: input.platform ?? 'WEB', + saveToAccount: input.wantsPasswordSetup ?? false, iamUserId: input.userId ?? null, expiresAt, }, @@ -220,8 +227,18 @@ export class VerifaydaService { const login = await this.issueLoginToken(userId); result = { purpose: 'LOGIN', verified: true, ...login }; } else { - // VERIFY — prove identity, save to IAM, return verified attributes. - const { userDataSaved } = await this.upsertIamUser(normalized); + // VERIFY — prove identity, save to IAM, return verified attributes + short-lived token. + const { iamUserId, userDataSaved } = await this.upsertIamUser(normalized); + + let sessionToken: { token: string; refreshToken: string; requiresPassword: boolean } | undefined; + if (iamUserId) { + try { + sessionToken = await this.createFaydaSession(iamUserId); + } catch (err) { + this.logger.warn(`Fayda session creation failed: ${(err as Error).message}`); + } + } + result = { purpose: 'VERIFY', verified: true, @@ -231,6 +248,11 @@ export class VerifaydaService { birthdate: normalized.birthdate, gender: normalized.gender, userDataSaved, + iamUserId: iamUserId ?? undefined, + token: sessionToken?.token, + refreshToken: sessionToken?.refreshToken, + requiresPassword: sessionToken?.requiresPassword, + promptPasswordSetup: session.saveToAccount && (sessionToken?.requiresPassword ?? false), }; } @@ -298,14 +320,19 @@ export class VerifaydaService { claims_locales: this.faydaConfig.claimsLocales, }); + // Every claim is marked essential so eSignet shows them locked/pre-checked + // on the consent screen — the user cannot toggle any off; they either + // consent to all of them or the whole flow is cancelled (?error=...). const claims = { userinfo: { name: { essential: true }, phone_number: { essential: true }, - email: { essential: false }, + email: { essential: true }, birthdate: { essential: true }, - gender: { essential: false }, - picture: { essential: false }, + gender: { essential: true }, + address: { essential: true }, + nationality: { essential: true }, + picture: { essential: true }, }, id_token: {}, }; @@ -447,12 +474,16 @@ export class VerifaydaService { phoneNumber: normalized.rawPhoneNumber ?? '', }; - // Step 1 — already verified with same Fayda sub + // Step 1 — already linked to this Fayda sub; ensure verified_by is set const bySub = await this.dataSource.query<{ id: string }[]>( `SELECT id FROM iam.users WHERE metadata->>'sub' = $1 LIMIT 1`, [normalized.sub], ); if (bySub.length > 0) { + await this.dataSource.query( + `UPDATE iam.users SET verified_by = 'fayda', updated_at = NOW() WHERE id = $1`, + [bySub[0].id], + ); return { iamUserId: bySub[0].id, userDataSaved: true }; } @@ -497,7 +528,7 @@ export class VerifaydaService { created_at, updated_at ) VALUES ( gen_random_uuid(), $1::jsonb, $2, $3, $4, $5::jsonb, - 'individual', 'accepted', true, false, + 'individual', 'submitted', true, false, false, 'fayda', NOW(), NOW() ) RETURNING id`, @@ -516,6 +547,60 @@ export class VerifaydaService { } } + private async createFaydaSession( + iamUserId: string, + ): Promise<{ token: string; refreshToken: string; requiresPassword: boolean }> { + const rows = await this.dataSource.query<{ + id: string; + email: string; + name: { en: string; am: string } | null; + username: string; + phone_number: string | null; + has_set_password: boolean; + status: string; + }[]>( + `SELECT id, email, name, username, phone_number, has_set_password, status + FROM iam.users WHERE id = $1 LIMIT 1`, + [iamUserId], + ); + if (!rows.length) throw new Error(`IAM user ${iamUserId} not found`); + const u = rows[0]; + + const userInfo = { + id: u.id, + email: u.email ?? '', + name: u.name ?? { en: '', am: '' }, + userType: 'individual', + status: u.status, + hasSetPassword: u.has_set_password, + isPhoneNumberVerified: false, + hasFinishedRegistration: false, + hasFinishedDMSOnboarding: false, + username: u.username, + phoneNumber: u.phone_number ?? '', + roles: [], + permissions: [], + employee: [], + }; + + const sessions = await this.dataSource.query<{ id: string }[]>( + `INSERT INTO iam.sessions + (id, email, device, "userInfo", expiry_time, refresh_count, status, user_id) + VALUES (gen_random_uuid(), $1, 'fayda-verify', $2::jsonb, NOW() + INTERVAL '1 day', 0, 'ACTIVE', $3) + ON CONFLICT (user_id, device) DO UPDATE + SET status = 'ACTIVE', "userInfo" = EXCLUDED."userInfo", + expiry_time = NOW() + INTERVAL '1 day', updated_at = NOW() + RETURNING id`, + [u.email ?? '', JSON.stringify(userInfo), iamUserId], + ); + + const sessionId = sessions[0].id; + const token = generateToken({ id: sessionId }); + const refreshToken = generateRefreshToken({ id: sessionId }); + + return { token, refreshToken, requiresPassword: !u.has_set_password }; + } + private async markSessionFailed( state: string, errorCode: string, From e2dbd272226342e6a3314681a771914a24c0c835 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Fri, 26 Jun 2026 14:41:31 +0300 Subject: [PATCH 3/3] Update pnpm-lock.yaml --- pnpm-lock.yaml | 52 +++++++++++++++----------------------------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e78ee926..205ca2bd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -479,10 +479,10 @@ importers: version: 8.1.6 '@tria-plc/api-common': specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz - version: file:local-packages/tria-plc-api-common-1.4.3.tgz(e6b80acddd4bb7fc40e438635b24d1bc) + version: file:local-packages/tria-plc-api-common-1.4.3.tgz(c061d697b8a1e15b1d1aba893da7b0e6) '@tria-plc/iamapi-common': - specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.4.tgz - version: file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(c97ba831ddde82920910406ab5262991) + specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.6.tgz + version: file:local-packages/tria-plc-iamapi-common-0.7.6.tgz(c97ba831ddde82920910406ab5262991) '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 @@ -907,7 +907,7 @@ importers: version: 9.1.2(eslint@8.57.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + version: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-react: specifier: ^7.37.1 version: 7.37.5(eslint@8.57.1) @@ -4065,28 +4065,6 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.4.tgz': - resolution: {integrity: sha512-6Ot921laEp3rZZBXDFX+gL7nPKEyHIRJaHSIP1i+seG20+PCCGA/QHDglcJXSgF5ccnpxBdlxmI/BZbYu7LV/A==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.4.tgz} - version: 0.7.4 - engines: {node: '>=20'} - peerDependencies: - '@nestjs/axios': ^4.0.0 - '@nestjs/common': ^11.0.0 - '@nestjs/core': ^11.0.0 - '@nestjs/jwt': ^11.0.0 - '@nestjs/microservices': ^11.0.0 - '@nestjs/passport': ^11.0.0 - '@nestjs/swagger': ^11.0.0 - '@nestjs/throttler': ^6.0.0 - '@nestjs/typeorm': ^11.0.0 - '@tria-plc/api-common': '*' - axios: ^1.9.0 - class-transformer: ^0.5.1 - class-validator: ^0.14.1 - reflect-metadata: ^0.2.0 - rxjs: ^7.8.0 - typeorm: ^0.3.0 - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.6.tgz': resolution: {integrity: sha512-AEYNqqP3Iu26N09LdZ6hBFSKqceKdIkedWNFMe3rau5CoflyrzWdoBUIcnFFbQlR9lRywj03HDkxK+MKNlExLA==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.6.tgz} version: 0.7.6 @@ -15213,7 +15191,7 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} - '@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(e6b80acddd4bb7fc40e438635b24d1bc)': + '@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(c061d697b8a1e15b1d1aba893da7b0e6)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -15224,7 +15202,7 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(c97ba831ddde82920910406ab5262991) + '@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.6.tgz(c97ba831ddde82920910406ab5262991) argon2: 0.43.1 axios: 1.17.0 change-case: 5.4.4 @@ -15301,7 +15279,7 @@ snapshots: - debug - supports-color - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(c97ba831ddde82920910406ab5262991)': + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.6.tgz(578386f46cf99fd4720e3e99f196f69e)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -15309,10 +15287,10 @@ snapshots: '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) - '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(e6b80acddd4bb7fc40e438635b24d1bc) + '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(f4d5d43aaace93ac25ed0343d18ebc9e) api-common: 1.2.2 argon2: 0.43.1 axios: 1.17.0 @@ -15336,7 +15314,7 @@ snapshots: - '@faker-js/faker' - supports-color - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.6.tgz(578386f46cf99fd4720e3e99f196f69e)': + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.6.tgz(c97ba831ddde82920910406ab5262991)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -15344,10 +15322,10 @@ snapshots: '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) - '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(f4d5d43aaace93ac25ed0343d18ebc9e) + '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(c061d697b8a1e15b1d1aba893da7b0e6) api-common: 1.2.2 argon2: 0.43.1 axios: 1.17.0 @@ -17874,7 +17852,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -17908,7 +17886,7 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -17923,7 +17901,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9