Production checklists, issue fixes

This commit is contained in:
Stephanos A
2026-07-01 15:04:38 +03:00
parent 0015f645cf
commit 103117d88b
20 changed files with 100 additions and 200 deletions

View File

@@ -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);
}
}
}

View File

@@ -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 });
}

View File

@@ -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',
},
},
};
});
}
}

View File

@@ -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);
}
}

View File

@@ -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';

View File

@@ -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 } });

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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' } });
}
}
}