mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 07:10:57 +00:00
Package passenger numbers and pricing updates
This commit is contained in:
@@ -118,4 +118,10 @@ export class BookPackageDto {
|
||||
@ApiProperty({ type: [BookPackagePassengerDto] })
|
||||
@IsArray() @ValidateNested({ each: true }) @Type(() => BookPackagePassengerDto)
|
||||
passengers: BookPackagePassengerDto[];
|
||||
|
||||
/** Number of adult passengers (≥5 years). Derived from passengers array if omitted. */
|
||||
@ApiPropertyOptional({ example: 2 }) @IsOptional() @IsInt() @Min(1) adultCount?: number;
|
||||
|
||||
/** Number of child passengers (<5 years). Derived from passengers array if omitted. */
|
||||
@ApiPropertyOptional({ example: 1 }) @IsOptional() @IsInt() @Min(0) childCount?: number;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,32 @@ import { Currency } from '@prisma/client';
|
||||
import { BookingsService } from '../bookings/bookings.service';
|
||||
import { GuestBookingService } from '../bookings/guest-booking.service';
|
||||
|
||||
/** Package-specific fare rules */
|
||||
const PKG_MAX_ADULTS = 5;
|
||||
const PKG_MAX_CHILDREN = 2;
|
||||
const PKG_CHILD_FARE_RATIO = 0.1;
|
||||
|
||||
function calculatePackageFareBreakdown(
|
||||
priceMinor: number,
|
||||
isRoundTrip: boolean,
|
||||
adultCount: number,
|
||||
childCount: number,
|
||||
) {
|
||||
const multiplier = isRoundTrip ? 2 : 1;
|
||||
const adultFareMinor = priceMinor * multiplier;
|
||||
const childFareMinor = Math.round(adultFareMinor * PKG_CHILD_FARE_RATIO);
|
||||
const totalMinor = adultCount * adultFareMinor + childCount * childFareMinor;
|
||||
return { adultFareMinor, childFareMinor, totalMinor, multiplier };
|
||||
}
|
||||
|
||||
function deriveAge(dateOfBirth: string | Date): number {
|
||||
const today = new Date();
|
||||
const dob = new Date(dateOfBirth);
|
||||
let age = today.getFullYear() - dob.getFullYear();
|
||||
if (today < new Date(today.getFullYear(), dob.getMonth(), dob.getDate())) age--;
|
||||
return age;
|
||||
}
|
||||
|
||||
function generateRef(): string {
|
||||
return 'PKG-' + Array.from({ length: 6 }, () =>
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Math.floor(Math.random() * 26)],
|
||||
@@ -41,14 +67,19 @@ export class PackagesService {
|
||||
const tier = pkg.priceTiers.find(t => t.id === tierId);
|
||||
if (!tier) throw new NotFoundException('Price tier not found');
|
||||
|
||||
const passengerCount = adultCount + childCount;
|
||||
if (passengerCount < 1) throw new BadRequestException('At least one passenger required');
|
||||
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 passengerCount = adultCount + childCount;
|
||||
const remaining = tier.availableSeats - tier.bookedSeats;
|
||||
if (passengerCount > remaining)
|
||||
throw new BadRequestException(`Only ${remaining} seat(s) remaining in the ${tier.label} tier`);
|
||||
|
||||
const totalMinor = tier.priceMinor * passengerCount;
|
||||
const isRoundTrip = !!pkg.returnScheduleId;
|
||||
const { adultFareMinor, childFareMinor, totalMinor } = calculatePackageFareBreakdown(
|
||||
tier.priceMinor, isRoundTrip, adultCount, childCount,
|
||||
);
|
||||
|
||||
// Resolve the seatClassId and coachTypeId that matches this tier's seatType from the outbound schedule coaches
|
||||
let seatClassId: string | null = null;
|
||||
@@ -60,7 +91,6 @@ export class PackagesService {
|
||||
);
|
||||
if (sc) { seatClassId = sc.id; coachTypeId = a.coach.coachTypeId ?? a.coach.coachType?.id ?? null; break; }
|
||||
}
|
||||
// Fallback: use the first coach assignment's coachTypeId if no match found
|
||||
if (!coachTypeId && pkg.outboundSchedule.coachAssignments.length > 0) {
|
||||
const first = pkg.outboundSchedule.coachAssignments[0];
|
||||
coachTypeId = first.coach.coachTypeId ?? first.coach.coachType?.id ?? null;
|
||||
@@ -77,7 +107,12 @@ export class PackagesService {
|
||||
adultCount,
|
||||
childCount,
|
||||
passengerCount,
|
||||
pricePerPassengerMinor: tier.priceMinor,
|
||||
isRoundTrip,
|
||||
pricePerAdultMinor: adultFareMinor,
|
||||
pricePerChildMinor: childFareMinor,
|
||||
childFareNote: `Children pay ${PKG_CHILD_FARE_RATIO * 100}% of adult fare`,
|
||||
maxAdults: PKG_MAX_ADULTS,
|
||||
maxChildren: PKG_MAX_CHILDREN,
|
||||
totalMinor,
|
||||
currency: tier.currency,
|
||||
remainingSeats: remaining,
|
||||
@@ -297,13 +332,30 @@ export class PackagesService {
|
||||
const tier = pkg.priceTiers.find((t) => t.id === dto.priceTierId);
|
||||
if (!tier) throw new NotFoundException('Price tier not found');
|
||||
|
||||
const passengerCount = dto.passengers.length;
|
||||
// Derive adult/child counts from the passengers array (dateOfBirth-based)
|
||||
let adultCount = 0, childCount = 0;
|
||||
for (const p of dto.passengers) {
|
||||
if (p.dateOfBirth && deriveAge(p.dateOfBirth) < 5) childCount++;
|
||||
else adultCount++;
|
||||
}
|
||||
// Allow explicit override from mobile app (e.g. when dateOfBirth is not provided per passenger)
|
||||
if (dto.adultCount !== undefined) adultCount = dto.adultCount;
|
||||
if (dto.childCount !== undefined) childCount = dto.childCount;
|
||||
|
||||
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 passengerCount = adultCount + childCount;
|
||||
const remaining = tier.availableSeats - tier.bookedSeats;
|
||||
if (passengerCount > remaining) {
|
||||
throw new BadRequestException(`Only ${remaining} seats remaining in the ${tier.label} tier`);
|
||||
}
|
||||
|
||||
const totalMinor = tier.priceMinor * passengerCount;
|
||||
const isRoundTrip = !!pkg.returnScheduleId;
|
||||
const { adultFareMinor, childFareMinor, totalMinor } = calculatePackageFareBreakdown(
|
||||
tier.priceMinor, isRoundTrip, adultCount, childCount,
|
||||
);
|
||||
const displayCurrency = (dto.displayCurrency as Currency) ?? Currency.ETB;
|
||||
const displayTotalMinor =
|
||||
displayCurrency !== Currency.ETB
|
||||
@@ -354,7 +406,21 @@ export class PackagesService {
|
||||
}),
|
||||
]);
|
||||
|
||||
return booking;
|
||||
return {
|
||||
...booking,
|
||||
fareBreakdown: {
|
||||
isRoundTrip,
|
||||
adultCount,
|
||||
adultFareMinor,
|
||||
childCount,
|
||||
childFareMinor,
|
||||
childFareNote: `Children pay ${PKG_CHILD_FARE_RATIO * 100}% of adult fare`,
|
||||
totalMinor,
|
||||
currency: 'ETB',
|
||||
displayCurrency,
|
||||
displayTotalMinor,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getMyBookings(passengerId: string) {
|
||||
|
||||
Reference in New Issue
Block a user