diff --git a/apps/edr-passenger-api/.env.example b/apps/edr-passenger-api/.env.example index a4774af84..328023c28 100644 --- a/apps/edr-passenger-api/.env.example +++ b/apps/edr-passenger-api/.env.example @@ -3,6 +3,7 @@ NODE_ENV=development PORT=4000 # Database (Prisma) — owns the `passenger` schema in edr_database +# Production: append ?sslmode=require&connection_limit=10&pool_timeout=20 to enforce SSL and connection pooling DATABASE_URL=postgresql://edr:edr_secret@localhost:5432/edr_database?schema=passenger # Database (TypeORM / @tria-plc IAM) — shared `iam` schema in the SAME edr_database. @@ -32,14 +33,14 @@ FRONTEND_URL=http://localhost:5174 BACK_OFFICE_URL=http://localhost:5184 # JWT (legacy passenger auth — being replaced by IAM) -JWT_SECRET=edr-platform-secret-change-in-production +# REQUIRED in production — use a random 32+ character string (e.g. openssl rand -hex 32) +JWT_SECRET= JWT_EXPIRES_IN=7d -# @tria-plc IAM token contract — the package's JwtGuard/verifyToken + AuthService sign/verify with -# these. MUST match the IAM issuer's secret in shared deployments. (Expiry strings use jsonwebtoken/ms.) -JWT_ACCESS_TOKEN_SECRET=dev-iam-access-secret-change-me +# @tria-plc IAM token contract — REQUIRED in production. MUST match the IAM issuer's secret. +JWT_ACCESS_TOKEN_SECRET= JWT_ACCESS_TOKEN_EXPIRES=1h -JWT_REFRESH_TOKEN_SECRET=dev-iam-refresh-secret-change-me +JWT_REFRESH_TOKEN_SECRET= JWT_REFRESH_TOKEN_EXPIRES=7d # SendGrid @@ -157,7 +158,7 @@ FAYDA_ACR_VALUES=mosip:idp:acr:generated-code FAYDA_CLAIMS_LOCALES=en am FAYDA_SESSION_TTL_MINUTES=10 -GITHUB_PACKAGE_TOKEN= +GITHUB_PACKAGE_TOKEN= # --- Notification broker (RabbitMQ) ----------------------------------------------------------------- # Set RABBITMQ_ENABLED=false to skip connection entirely (dev without a local broker). 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/notifications/sms-client.service.ts b/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts index 04188842d..a2bbbad99 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 @@ -26,7 +26,7 @@ export class SmsClientService implements OnApplicationBootstrap { this.logger.log("connected to SMS service"); }) .catch((err) => { - console.error("Error happened at SMS service", err); + this.logger.error('Error happened at SMS service', err); }); } 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/segments/trip-progress.service.ts b/apps/edr-passenger-api/src/modules/segments/trip-progress.service.ts index f67808385..57b5a10cf 100644 --- a/apps/edr-passenger-api/src/modules/segments/trip-progress.service.ts +++ b/apps/edr-passenger-api/src/modules/segments/trip-progress.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../common/prisma.service'; import { EnhancedSeatsService } from './enhanced-seats.service'; import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; @@ -6,6 +6,7 @@ import { Cron, CronExpression } from '@nestjs/schedule'; @Injectable() export class TripProgressService { + private readonly logger = new Logger(TripProgressService.name); constructor( private prisma: PrismaService, private enhancedSeatsService: EnhancedSeatsService, @@ -154,10 +155,10 @@ export class TripProgressService { try { const result = await this.enhancedSeatsService.expireHolds(); if (result.expiredHolds > 0) { - console.log(`Expired ${result.expiredHolds} holds, released ${result.releasedSeats.length} seats`); + this.logger.log(`Expired ${result.expiredHolds} holds, released ${result.releasedSeats.length} seats`); } } catch (error) { - console.error('Error expiring holds:', error); + this.logger.error('Error expiring holds:', error); } } 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/.env.example b/apps/edr-passenger-web/backoffice/.env.example index 5263b3a36..058069154 100644 --- a/apps/edr-passenger-web/backoffice/.env.example +++ b/apps/edr-passenger-web/backoffice/.env.example @@ -5,5 +5,5 @@ NEXT_PUBLIC_API_URL=https://your-api-domain.com NEXT_PUBLIC_IAM_ENABLED=false NEXT_PUBLIC_IAM_API_URL=https://iam.tria-plc.com/api -# GitHub Packages Token -GITHUB_PACKAGE_TOKEN=$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 +# GitHub Packages Token (required to install @tria-plc/* private packages) +GITHUB_PACKAGE_TOKEN= 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 ? ( <>
-
- -
-
-
-

CBE Birr

-

Bank payment provider

-
- -
- - - - )} - - {activeTab === 'integrations' && ( -
-
-

Verifayda 2.0 Integration

-
-
- - -
-
- - -
-
- - -
-
-
- -
-

Corporate IAM Integration

-
-
- - -
-
- - -
-
- - -
-
-
-
- )} - + {activeTab === 'configurations' && (

Rate Limiting (requests / minute / IP)

diff --git a/apps/edr-passenger-web/backoffice/src/components/ui/ActionButton.tsx b/apps/edr-passenger-web/backoffice/src/components/ui/ActionButton.tsx index e3764ed56..1b70303ec 100644 --- a/apps/edr-passenger-web/backoffice/src/components/ui/ActionButton.tsx +++ b/apps/edr-passenger-web/backoffice/src/components/ui/ActionButton.tsx @@ -49,8 +49,7 @@ export default function ActionButton({ try { setIsLoading(true); await onClick(); - } catch (error) { - console.error('Action failed:', error); + } catch (error) { } finally { setIsLoading(false); } diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/dashboard.ts b/apps/edr-passenger-web/backoffice/src/lib/api/dashboard.ts index 78411b38a..cfd1c681e 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/dashboard.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/dashboard.ts @@ -13,10 +13,14 @@ export const dashboardApi = { const bookingsTotal = bookingsRes?.meta?.total || 0; const passengersTotal = passengersRes?.meta?.total || 0; - // Calculate revenue from bookings - const allBookingsRes = await apiClient.get('/bookings?pageSize=100'); - const allBookings = Array.isArray(allBookingsRes) ? allBookingsRes : allBookingsRes?.items || []; - const totalRevenue = allBookings.reduce((sum: number, b: any) => sum + (b.totalMinor || 0), 0); + // Calculate revenue from confirmed bookings only + const confirmedRes = await apiClient.get('/bookings?pageSize=1000&status=CONFIRMED'); + const boardedRes = await apiClient.get('/bookings?pageSize=1000&status=BOARDED'); + const paidBookings = [ + ...(Array.isArray(confirmedRes) ? confirmedRes : confirmedRes?.items || []), + ...(Array.isArray(boardedRes) ? boardedRes : boardedRes?.items || []), + ]; + const totalRevenue = paidBookings.reduce((sum: number, b: any) => sum + (b.totalMinor || 0), 0); // Calculate average occupancy (placeholder - would need dedicated endpoint) const occupancyRate = Math.floor(Math.random() * 100); // Replace with actual data @@ -29,10 +33,9 @@ export const dashboardApi = { totalTripsToday: 0, activeTrips: 0, cancelledBookings: 0, - averageTicketPrice: allBookings.length > 0 ? totalRevenue / allBookings.length : 0, + averageTicketPrice: paidBookings.length > 0 ? Math.round(totalRevenue / paidBookings.length) : 0, }; } catch (error) { - console.error('Failed to fetch dashboard stats:', error); return { totalBookings: 0, totalRevenue: 0, @@ -51,7 +54,6 @@ export const dashboardApi = { const response = await apiClient.get(`/dashboard/revenue?days=${days}`); return response; } catch (error) { - console.error('Failed to fetch revenue chart:', error); return []; } }, @@ -82,7 +84,6 @@ export const dashboardApi = { paymentIntent: booking.paymentIntent, })); } catch (error) { - console.error('Failed to fetch recent bookings:', error); return []; } }, @@ -92,7 +93,6 @@ export const dashboardApi = { const response = await apiClient.get(`/agents/top?limit=${limit}`); return response || []; } catch (error) { - console.error('Failed to fetch top agents:', error); return []; } }, @@ -102,7 +102,6 @@ export const dashboardApi = { const response = await apiClient.get(`/dashboard/occupancy?days=${days}`); return response || []; } catch (error) { - console.error('Failed to fetch occupancy trend:', error); return []; } }, @@ -112,7 +111,6 @@ export const dashboardApi = { const response = await apiClient.get(`/schedules/upcoming?limit=${limit}`); return response || []; } catch (error) { - console.error('Failed to fetch upcoming trips:', error); return []; } }, @@ -122,7 +120,6 @@ export const dashboardApi = { const response = await apiClient.get('/dashboard/payment-methods'); return response || []; } catch (error) { - console.error('Failed to fetch payment methods:', error); return []; } }, @@ -137,7 +134,6 @@ export const dashboardApi = { loyaltyPoints: 0, }; } catch (error) { - console.error('Failed to fetch passenger stats:', error); return { totalPassengers: 0, newPassengersToday: 0, @@ -157,7 +153,6 @@ export const dashboardApi = { totalAmount: 0, }; } catch (error) { - console.error('Failed to fetch transaction summary:', error); return { totalTransactions: 0, successfulTransactions: 0, @@ -176,7 +171,6 @@ export const dashboardApi = { activePayments: 0, }; } catch (error) { - console.error('Failed to fetch live metrics:', error); return { onlineUsers: 0, activeBookings: 0, diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/routes.ts b/apps/edr-passenger-web/backoffice/src/lib/api/routes.ts index 8a4873606..80c30e08b 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/routes.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/routes.ts @@ -5,7 +5,6 @@ import { PaginatedResponse } from '@edr/types'; export const routesApi = { getAll: async () => { const response = await apiClient.get('/routes'); - console.log('Routes API response:', response); // Handle both direct array and wrapped response if (Array.isArray(response)) { return { items: response }; @@ -24,7 +23,6 @@ export const routesApi = { }, create: (data: any) => { - console.log('Creating route with data:', JSON.stringify(data, null, 2)); return apiClient.post('/routes', data); }, diff --git a/apps/edr-passenger-web/portal/.env.example b/apps/edr-passenger-web/portal/.env.example index 6560533f2..295fbe775 100644 --- a/apps/edr-passenger-web/portal/.env.example +++ b/apps/edr-passenger-web/portal/.env.example @@ -1,9 +1,5 @@ # API Configuration NEXT_PUBLIC_API_URL=https://your-api-domain.com -# Allowlisted destination hosts for the /go D-Money redirect bounce page (comma-separated). -# The /go?url=... page only forwards to https hosts that match one of these (host or subdomain). -NEXT_PUBLIC_DMONEY_ALLOWED_HOSTS=d-money.dj - -# GitHub Packages Token -GITHUB_PACKAGE_TOKEN=$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 \ No newline at end of file +# GitHub Packages Token (required to install @tria-plc/* private packages) +GITHUB_PACKAGE_TOKEN= \ No newline at end of file diff --git a/apps/edr-passenger-web/portal/public/packages/kulubi.jpeg b/apps/edr-passenger-web/portal/public/packages/kulubi.jpeg new file mode 100644 index 000000000..5c11fb5b3 Binary files /dev/null and b/apps/edr-passenger-web/portal/public/packages/kulubi.jpeg differ diff --git a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx index 4b46859d6..a15bba6cf 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx @@ -681,7 +681,7 @@ export default function PassengersPage() { try { const response: any = await apiClient.post('/fayda/verification/start', { - purpose: 'PURCHASE', + purpose: 'VERIFY', platform: 'WEB', saveToAccount: index === 0 && isAuthenticated, }); diff --git a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx index bb6340d0a..d173a1263 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx @@ -1532,107 +1532,6 @@ export default function SearchPage() {
- {/* Promotions Section */} -
-
-
-
- 🎁 -

- Offers & Promotions -

-
-
- {/* Wide promo card */} -
-
-
-
-
-
- - Limited Time - -

- 20% Off Weekend -
- Travel -

-

- Book any weekend journey and save 20%. Valid for all seat - classes. -

-
-
- - Valid until 31 Dec 2024 - - - Book now - -
-
-
- - {/* Narrow promo cards */} -
-
-
-
-
-
- - New - -

- Family Package -

-

- 4 tickets for the price of 3 -

-
-
- - Learn more - -
-
-
- -
-
-
-
-
- - Student - -

- Student Discount -

-

- 15% off with valid student ID -

-
-
- - Learn more - -
-
-
-
-
-
-
-
-