UAT issues resolution

This commit is contained in:
Stephanos A
2026-07-06 16:30:48 +03:00
parent 7b9f15fa58
commit 9b6423392b
18 changed files with 467 additions and 233 deletions

View File

@@ -32,10 +32,6 @@ export class CurrenciesService {
async createCurrency(dto: CreateCurrencyDto) {
const { code, name, symbol, baseCurrencyCode = 'ETB', exchangeRate } = dto;
if (!['ETB', 'USD', 'DJF'].includes(code.toUpperCase())) {
throw new BadRequestException('Unsupported currency code');
}
if (exchangeRate <= 0) {
throw new BadRequestException('Exchange rate must be positive');
}

View File

@@ -3,6 +3,9 @@ import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreatePriceTierDto {
@ApiPropertyOptional({ description: 'SeatClass ID to link this tier to a specific seat class' })
@IsOptional() @IsUUID() seatClassId?: string;
@ApiProperty({ example: 'HSC' })
@IsString() seatType: string;
@@ -31,6 +34,7 @@ export class UpdateInquiryStatusDto {
}
export class UpdatePriceTierDto {
@ApiPropertyOptional() @IsOptional() @IsUUID() seatClassId?: string;
@ApiPropertyOptional() @IsOptional() @IsString() seatType?: string;
@ApiPropertyOptional() @IsOptional() @IsString() label?: string;
@ApiPropertyOptional() @IsOptional() @IsInt() @Min(0) priceMinor?: number;

View File

@@ -8,7 +8,7 @@ import { GuestBookingService } from '../bookings/guest-booking.service';
/** Package-specific fare rules */
const PKG_MAX_ADULTS = 5;
const PKG_MAX_CHILDREN = 2;
const PKG_CHILDREN_PER_ADULT = 2; // 2 children allowed per adult
const PKG_CHILD_FARE_RATIO = 0.1;
function calculatePackageFareBreakdown(
@@ -68,7 +68,8 @@ export class PackagesService {
if (adultCount < 1) throw new BadRequestException('At least one adult passenger required');
if (adultCount > PKG_MAX_ADULTS) throw new BadRequestException(`Maximum ${PKG_MAX_ADULTS} adults allowed per package booking`);
if (childCount > PKG_MAX_CHILDREN) throw new BadRequestException(`Maximum ${PKG_MAX_CHILDREN} children allowed per package booking`);
const maxChildren = adultCount * PKG_CHILDREN_PER_ADULT;
if (childCount > maxChildren) throw new BadRequestException(`Maximum ${PKG_CHILDREN_PER_ADULT} children per adult (${maxChildren} for ${adultCount} adult${adultCount !== 1 ? 's' : ''}) allowed per package booking`);
const passengerCount = adultCount + childCount;
const remaining = tier.availableSeats - tier.bookedSeats;
@@ -81,14 +82,17 @@ export class PackagesService {
);
// Resolve the seatClassId and coachTypeId that matches this tier's seatType from the outbound schedule coaches
let seatClassId: string | null = null;
let seatClassId: string | null = tier.seatClassId ?? null;
let seatClassName: string | null = null;
let coachTypeId: string | null = null;
for (const a of pkg.outboundSchedule.coachAssignments) {
const sc = a.coach.coachType?.seatClasses?.find(
(s: any) => s.name.toLowerCase().includes(tier.seatType.toLowerCase()) ||
tier.seatType.toLowerCase().includes(s.name.toLowerCase()),
);
if (sc) { seatClassId = sc.id; coachTypeId = a.coach.coachTypeId ?? a.coach.coachType?.id ?? null; break; }
const sc = seatClassId
? a.coach.coachType?.seatClasses?.find((s: any) => s.id === seatClassId)
: a.coach.coachType?.seatClasses?.find(
(s: any) => s.name.toLowerCase().includes(tier.seatType.toLowerCase()) ||
tier.seatType.toLowerCase().includes(s.name.toLowerCase()),
);
if (sc) { seatClassId = sc.id; seatClassName = sc.name; coachTypeId = a.coach.coachTypeId ?? a.coach.coachType?.id ?? null; break; }
}
if (!coachTypeId && pkg.outboundSchedule.coachAssignments.length > 0) {
const first = pkg.outboundSchedule.coachAssignments[0];
@@ -102,6 +106,7 @@ export class PackagesService {
tierLabel: tier.label,
seatType: tier.seatType,
seatClassId,
seatClassName,
coachTypeId,
adultCount,
childCount,
@@ -111,7 +116,7 @@ export class PackagesService {
pricePerChildMinor: childFareMinor,
childFareNote: `Children pay ${PKG_CHILD_FARE_RATIO * 100}% of adult fare`,
maxAdults: PKG_MAX_ADULTS,
maxChildren: PKG_MAX_CHILDREN,
maxChildren: adultCount * PKG_CHILDREN_PER_ADULT,
totalMinor,
currency: tier.currency,
remainingSeats: remaining,
@@ -203,7 +208,7 @@ export class PackagesService {
const pkg = await this.prisma.travelPackage.findUnique({
where: { id },
include: {
priceTiers: true,
priceTiers: { include: { seatClass: { include: { coachType: true } } } },
outboundSchedule: {
include: {
originStation: true,
@@ -369,7 +374,8 @@ export class PackagesService {
if (adultCount < 1) throw new BadRequestException('At least one adult passenger required');
if (adultCount > PKG_MAX_ADULTS) throw new BadRequestException(`Maximum ${PKG_MAX_ADULTS} adults allowed per package booking`);
if (childCount > PKG_MAX_CHILDREN) throw new BadRequestException(`Maximum ${PKG_MAX_CHILDREN} children allowed per package booking`);
const maxChildrenBook = adultCount * PKG_CHILDREN_PER_ADULT;
if (childCount > maxChildrenBook) throw new BadRequestException(`Maximum ${PKG_CHILDREN_PER_ADULT} children per adult (${maxChildrenBook} for ${adultCount} adult${adultCount !== 1 ? 's' : ''}) allowed per package booking`);
const passengerCount = adultCount + childCount;
const remaining = tier.availableSeats - tier.bookedSeats;
@@ -398,6 +404,8 @@ export class PackagesService {
contactPhone: dto.contactPhone,
promoCode: dto.promoCode,
passengerCount,
adultCount,
childCount,
totalMinor,
currency: 'ETB',
displayCurrency,

View File

@@ -59,6 +59,7 @@ export class UpdateScheduleDto {
@ApiPropertyOptional({ example: '2026-06-15T20:00:00Z', description: 'Scheduled arrival at the last stop (destination)' }) @IsOptional() @IsDateString() arrivalAt?: string;
@ApiPropertyOptional({ enum: TripStatus, example: TripStatus.SCHEDULED }) @IsOptional() @IsEnum(TripStatus) status?: TripStatus;
@ApiPropertyOptional({ type: Array, description: 'List of coaches to assign' }) @IsOptional() @IsArray() coaches?: Array<{ coachId: string; positionNumber: number }>;
@ApiPropertyOptional({ example: false, description: 'Exclude from public search (reserved for packages)' }) @IsOptional() isPackageOnly?: boolean;
}
export class UpdateStopTimeDto {

View File

@@ -632,6 +632,7 @@ export class SchedulesService {
}
if (dto.status) updateData.status = dto.status;
if (dto.isPackageOnly !== undefined) updateData.isPackageOnly = dto.isPackageOnly;
if (Object.keys(updateData).length > 0) {
await this.prisma.trainSchedule.update({ where: { id }, data: updateData });

View File

@@ -157,6 +157,7 @@ export class SearchService {
const schedules = await this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
isPackageOnly: false,
OR: [
{ departureAt: { gte: windowStart, lt: requestedDate } },
{ departureAt: { gte: requestedNextDay < now ? now : requestedNextDay, lt: windowEnd } },
@@ -192,6 +193,7 @@ export class SearchService {
const schedules = await this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
isPackageOnly: false,
departureAt: { gte: date < now ? now : date, lt: nextDay },
stopTimes: { some: { stationId: originStationId } },
},
@@ -230,6 +232,7 @@ export class SearchService {
this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
isPackageOnly: false,
departureAt: { gte: dayStart, lt: dayEnd },
stopTimes: { some: { stationId: originStationId } },
},
@@ -238,6 +241,7 @@ export class SearchService {
this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
isPackageOnly: false,
departureAt: { gte: dayStart, lt: leg2WindowEnd },
},
include: SCHEDULE_INCLUDE,