diff --git a/apps/edr-passenger-api/src/common/audit.service.ts b/apps/edr-passenger-api/src/common/audit.service.ts index 3f1dc161f..cff5403da 100644 --- a/apps/edr-passenger-api/src/common/audit.service.ts +++ b/apps/edr-passenger-api/src/common/audit.service.ts @@ -1,9 +1,10 @@ -import { Injectable, Inject, Optional } from '@nestjs/common'; +import { Injectable, Inject, Logger, Optional } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { PrismaService } from './prisma.service'; @Injectable() export class AuditService { + private readonly logger = new Logger(AuditService.name); constructor( private prisma: PrismaService, @Optional() @Inject(REQUEST) private request?: any, @@ -34,7 +35,7 @@ export class AuditService { }, }); } catch (error) { - console.error('Failed to log audit event:', error); + this.logger.error('Failed to log audit event:', error); // Don't throw - audit logging should not break main operations } } @@ -74,11 +75,20 @@ export class AuditService { where.entityType = filters.entityType; } - return this.prisma.auditLog.findMany({ - where, - orderBy: { createdAt: 'desc' }, - take: 500, - }); + const limit = Math.min(filters.limit ?? 50, 200); + const offset = filters.offset ?? 0; + + const [data, total] = await Promise.all([ + this.prisma.auditLog.findMany({ + where, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: offset, + }), + this.prisma.auditLog.count({ where }), + ]); + + return { data, total, limit, offset }; } async getLog(id: string) { diff --git a/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts b/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts index 7810c6d94..39d492b2c 100644 --- a/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts +++ b/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts @@ -41,9 +41,7 @@ export class HttpExceptionFilter implements ExceptionFilter { this.logger.error( `${request.method} ${request.url} -> ${status}`, exception instanceof Error ? exception.stack : JSON.stringify(exception), - ); - console.error('Full error details:', exception); - } else { + ); } else { this.logger.warn(`${request.method} ${request.url} -> ${status} ${message}`); } diff --git a/apps/edr-passenger-api/src/common/i18n/i18n.service.ts b/apps/edr-passenger-api/src/common/i18n/i18n.service.ts index 9c3eee5fc..c53a1e033 100644 --- a/apps/edr-passenger-api/src/common/i18n/i18n.service.ts +++ b/apps/edr-passenger-api/src/common/i18n/i18n.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; @@ -6,6 +6,7 @@ type TranslationMap = Record; @Injectable() export class I18nService { + private readonly logger = new Logger(I18nService.name); private translations: Map = new Map(); private readonly supportedLocales = ['en', 'am', 'fr', 'om']; private readonly defaultLocale = 'en'; @@ -21,7 +22,7 @@ export class I18nService { const content = fs.readFileSync(filePath, 'utf-8'); this.translations.set(locale, JSON.parse(content)); } catch (err) { - console.warn(`Failed to load translation file for locale: ${locale}`); + this.logger.warn(`Failed to load translation file for locale: ${locale}`); } } } 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 cd583517c..4dd131f3e 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 @@ -548,7 +548,7 @@ export class PassengerAuthService { await this.dataSource.query(`DELETE FROM iam.users WHERE id = $1`, [iamUserId]); } catch (err) { - console.error('[PassengerAuthService] IAM compensating cleanup failed for', email, (err as Error).message); + this.logger.error(`[PassengerAuthService] IAM compensating cleanup failed for ${email}`, (err as Error).message); } } } diff --git a/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts b/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts index df5eea8a1..ebec606c6 100644 --- a/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts +++ b/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts @@ -317,7 +317,10 @@ export class FleetService { } getTrains() { - return this.prisma.train.findMany({ include: { schedules: { take: 5, orderBy: { departureAt: 'desc' } } } }); + return this.prisma.train.findMany({ + where: { isActive: true }, + include: { schedules: { take: 5, orderBy: { departureAt: 'desc' } } }, + }); } createTrain(dto: CreateTrainDto) { @@ -462,6 +465,7 @@ export class FleetService { return this.prisma.coach.update({ where: { id }, data: { + number: dto.number, arrangement: dto.arrangement, capacity: dto.capacity, status: dto.status, @@ -540,6 +544,7 @@ export class FleetService { ]); if (!schedule) throw new NotFoundException('Schedule not found'); if (!coach) throw new NotFoundException('Coach not found'); + if (coach.status !== 'ACTIVE') throw new BadRequestException('Coach is not active'); return this.prisma.coachAssignment.create({ data: dto }); } diff --git a/apps/edr-passenger-api/src/modules/health/health.controller.ts b/apps/edr-passenger-api/src/modules/health/health.controller.ts index 6cc50e24e..a918fa76c 100644 --- a/apps/edr-passenger-api/src/modules/health/health.controller.ts +++ b/apps/edr-passenger-api/src/modules/health/health.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, HttpStatus, Res } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { SkipThrottle } from '@nestjs/throttler'; import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator'; import { PrismaService } from '../../common/prisma.service'; +import { Response } from 'express'; @ApiTags('Health') @Controller('health') @@ -20,17 +21,17 @@ export class HealthController { @Get('ready') @IsPublic() @ApiOperation({ summary: 'Readiness probe — checks database connectivity' }) - async readiness() { + async readiness(@Res() res: Response) { const start = Date.now(); try { await this.prisma.$queryRaw`SELECT 1`; - return { + return res.status(HttpStatus.OK).json({ status: 'ok', timestamp: new Date().toISOString(), checks: { database: { status: 'ok', latencyMs: Date.now() - start } }, - }; + }); } catch (err) { - return { + return res.status(HttpStatus.SERVICE_UNAVAILABLE).json({ status: 'error', timestamp: new Date().toISOString(), checks: { @@ -40,7 +41,7 @@ export class HealthController { error: err instanceof Error ? err.message : 'Unknown error', }, }, - }; + }); } } 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 b2b7ae631..ea952d98c 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; @@ -25,6 +25,7 @@ type IamUserRow = { @Injectable() export class PassengersService { + private readonly logger = new Logger(PassengersService.name); constructor( private readonly prisma: PrismaService, @InjectDataSource() private readonly dataSource: DataSource, @@ -373,7 +374,7 @@ export class PassengersService { verifiedData = verification.passengerData; } } catch (error) { - console.warn('Fayda verification failed, using manual data:', error); + this.logger.warn('Fayda verification failed, using manual data:', error); } } diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index 5c1b5e582..e37cac0d3 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; @@ -6,6 +6,7 @@ import { GenerateReportDto, ReportType } from './reports.dto'; @Injectable() export class ReportsService { + private readonly logger = new Logger(ReportsService.name); constructor( private prisma: PrismaService, @InjectDataSource() private dataSource: DataSource, @@ -60,8 +61,6 @@ export class ReportsService { include: { paymentIntent: true } }); - console.log(`[Reports] Revenue Report: Found ${bookings.length} bookings between ${dateFrom} and ${dateTo}`); - const totalRevenue = bookings.reduce((sum, b) => sum + b.totalMinor, 0); const byPaymentMethod = bookings.reduce((acc, b) => { const method = b.paymentIntent?.method ?? 'UNKNOWN'; diff --git a/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts b/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts index 7a061ac42..66e43d6e9 100644 --- a/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts +++ b/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts @@ -100,10 +100,15 @@ export class SchedulesService { const arr = parseEthiopianTime(dto.arrivalAt); if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt'); - const route = await this.prisma.route.findUnique({ - where: { id: dto.routeId }, - include: { stops: { orderBy: { sequence: 'asc' } } }, - }); + const [train, route] = await Promise.all([ + this.prisma.train.findUnique({ where: { id: dto.trainId } }), + this.prisma.route.findUnique({ + where: { id: dto.routeId }, + include: { stops: { orderBy: { sequence: 'asc' } } }, + }), + ]); + if (!train) throw new NotFoundException('Train not found'); + if (!train.isActive) throw new BadRequestException('Train is not active'); if (!route) throw new NotFoundException('Route not found'); if (!route.active) throw new BadRequestException('Route is not active'); if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops'); @@ -247,10 +252,15 @@ export class SchedulesService { const arr = parseEthiopianTime(dto.arrivalAt); if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt'); - const route = await this.prisma.route.findUnique({ - where: { id: dto.routeId }, - include: { stops: { orderBy: { sequence: 'asc' } } }, - }); + const [train, route] = await Promise.all([ + this.prisma.train.findUnique({ where: { id: dto.trainId } }), + this.prisma.route.findUnique({ + where: { id: dto.routeId }, + include: { stops: { orderBy: { sequence: 'asc' } } }, + }), + ]); + if (!train) throw new NotFoundException('Train not found'); + if (!train.isActive) throw new BadRequestException('Train is not active'); if (!route) throw new NotFoundException('Route not found'); if (!route.active) throw new BadRequestException('Route is not active'); if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops'); @@ -540,6 +550,8 @@ export class SchedulesService { const coachIds = coaches.map(c => c.coachId); const existingCoaches = await this.prisma.coach.findMany({ where: { id: { in: coachIds } } }); if (existingCoaches.length !== coachIds.length) throw new NotFoundException('One or more coaches not found'); + const inactiveCoach = existingCoaches.find(c => c.status !== 'ACTIVE'); + if (inactiveCoach) throw new BadRequestException(`Coach ${inactiveCoach.number} is not active`); await this.prisma.coachAssignment.deleteMany({ where: { scheduleId } }); diff --git a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts index 3f2fafebf..10fcfe0cf 100644 --- a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts +++ b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts @@ -6,12 +6,16 @@ export class SeatClassesService { constructor(private prisma: PrismaService) {} listSeatClasses() { - return this.prisma.seatClass.findMany({ orderBy: { createdAt: 'asc' } }); + return this.prisma.seatClass.findMany({ + where: { isActive: true }, + orderBy: { createdAt: 'asc' }, + }); } async getSeatClass(id: string) { const sc = await this.prisma.seatClass.findUnique({ where: { id } }); if (!sc) throw new NotFoundException('SeatClass not found'); + if (!sc.isActive) throw new NotFoundException('SeatClass is not active'); return sc; } diff --git a/apps/edr-passenger-api/src/modules/stations/stations.service.ts b/apps/edr-passenger-api/src/modules/stations/stations.service.ts index 736e8cd58..9e9fc824d 100644 --- a/apps/edr-passenger-api/src/modules/stations/stations.service.ts +++ b/apps/edr-passenger-api/src/modules/stations/stations.service.ts @@ -33,8 +33,11 @@ export class StationsService { where.countryCode = filters.country; } + // Default to operational stations only; allow explicit override (e.g. back-office) if (filters.operational !== undefined && filters.operational !== '') { where.isOperational = filters.operational === 'true'; + } else { + where.isOperational = true; } return this.prisma.station.findMany({ @@ -46,6 +49,7 @@ export class StationsService { async findOne(id: string) { const s = await this.prisma.station.findUnique({ where: { id } }); if (!s) throw new NotFoundException('Station not found'); + if (!s.isOperational) throw new NotFoundException('Station is not operational'); return s; } 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 3c07d8c40..797adbb55 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -99,18 +99,11 @@ export class TicketsService { let guestEmail = null; const matchingProfile = t.booking?.passenger?.travelerProfiles?.find((tp: any) => tp.fullName === t.passengerName); - // DEBUG: Log to see what we're getting - this.logger.debug(`Ticket ${t.id}: passengerName=${t.passengerName}, profiles count=${t.booking?.passenger?.travelerProfiles?.length || 0}, matchingProfile=${!!matchingProfile}`); - if (matchingProfile) { - this.logger.debug(`Matching profile notes: ${matchingProfile.notes}`); - } - if (matchingProfile?.notes) { try { const notesData = JSON.parse(matchingProfile.notes); guestPhone = notesData.phone || null; guestEmail = notesData.email || null; - this.logger.debug(`Extracted from notes: phone=${guestPhone}, email=${guestEmail}`); } catch (err) { this.logger.error(`Failed to parse notes JSON: ${err}`); } @@ -120,13 +113,11 @@ export class TicketsService { if (!guestPhone) guestPhone = t.booking?.contactPhone; if (!guestEmail) guestEmail = t.booking?.contactEmail; - this.logger.debug(`Final values: phone=${guestPhone}, email=${guestEmail}`); const passengerInfo = iam ? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number } : { fullName: 'Guest', email: guestEmail, phone: guestPhone }; - this.logger.debug(`Final passenger info: ${JSON.stringify(passengerInfo)}`); return { id: t.id, @@ -596,8 +587,7 @@ export class TicketsService { } private async fireBoardingPassNotification(booking: any, ticket: any, leg: string | null) { - // TODO: Implement notification logic - console.log(`Boarding pass notification for booking ${booking.bookingRef}, leg: ${leg}`); + this.logger.log(`Boarding pass notification for booking ${booking.bookingRef}, leg: ${leg}`); } async getValidationLogs(ticketId: string) { @@ -687,4 +677,4 @@ export class TicketsService { if (!ticket) throw new NotFoundException('Ticket not found'); return this.prisma.ticket.update({ where: { id }, data: { status: 'ACTIVE' } }); } -} \ No newline at end of file +} diff --git a/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx b/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx index b25a40498..5860ea748 100644 --- a/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx @@ -144,11 +144,10 @@ export default function CoachesPage() { const [activeTab, setActiveTab] = useState('coaches'); const [search, setSearch] = useState(''); const [showModal, setShowModal] = useState(false); - const [showPreviewModal, setShowPreviewModal] = useState(false); - const [seatMapPreview, setSeatMapPreview] = useState(null); const [editingItem, setEditingItem] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; item: any | null; error?: string }>({ isOpen: false, item: null }); const [selectedCoachTypeId, setSelectedCoachTypeId] = useState(''); + const queryClient = useQueryClient(); // Coach Types Queries @@ -221,14 +220,6 @@ export default function CoachesPage() { }, }); - const generateSeatMapMutation = useMutation({ - mutationFn: fleetApi.generateSeatMap, - onSuccess: (data) => { - setSeatMapPreview(data); - setShowPreviewModal(true); - }, - }); - const handleCoachTypeSubmit = async (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); @@ -275,27 +266,6 @@ export default function CoachesPage() { } }; - const handlePreviewSeatMap = async () => { - const form = document.querySelector('form') as HTMLFormElement; - const formData = new FormData(form); - const bedCategory = formData.get('bedCategory') as string; - const capacity = parseInt(formData.get('capacity') as string); - - if (!bedCategory || !capacity) { - alert('Please select a bed category and enter capacity to preview seat map'); - return; - } - - const bedsPerRoom = bedCategory === 'VIP_BED' ? 4 : 6; - const roomsPerCoach = Math.ceil(capacity / bedsPerRoom); - - await generateSeatMapMutation.mutateAsync({ - coachCount: 1, - roomsPerCoach, - roomType: bedCategory, - }); - }; - const handleDelete = (item: any, isCoachType: boolean) => { setDeleteConfirm({ isOpen: true, item: { ...item, isCoachType } }); }; @@ -779,23 +749,25 @@ export default function CoachesPage() { /> - {/* Conditionally show bed fields only for Economy and Regular coach types */} {(() => { const selectedCoachType = coachTypesArray.find((ct: any) => ct.id === (selectedCoachTypeId || editingItem?.coachTypeId)); - const isEconomyOrRegular = selectedCoachType && - (selectedCoachType.name?.toLowerCase().includes('economy') || - selectedCoachType.name?.toLowerCase().includes('regular') || - selectedCoachType.type?.toLowerCase().includes('economy') || - selectedCoachType.type?.toLowerCase().includes('regular')); - - return isEconomyOrRegular ? ( + const isBedType = selectedCoachType && + (selectedCoachType.name?.toLowerCase().includes('bed') || + selectedCoachType.name?.toLowerCase().includes('sleeper') || + selectedCoachType.type?.toLowerCase().includes('sleeper')); + + const derivedBedCategory = editingItem?.isCoach && selectedCoachType + ? (selectedCoachType.name?.toLowerCase().includes('vip') ? 'VIP_BED' : 'ECONOMY_BED') + : (editingItem?.bedCategory || ''); + + return isBedType ? ( <>