mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 13:38:20 +00:00
Package booking new rules update, cascade delete option updates
This commit is contained in:
@@ -142,8 +142,9 @@ export class PackagesController {
|
||||
@UseGuards(IamGuard)
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Delete package (admin)' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.remove(id);
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete even with active bookings' })
|
||||
remove(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.remove(id, cascade === 'true');
|
||||
}
|
||||
|
||||
@Patch(':id/activate')
|
||||
|
||||
@@ -8,9 +8,12 @@ import { GuestBookingService } from '../bookings/guest-booking.service';
|
||||
|
||||
/** Package-specific fare rules */
|
||||
const PKG_MAX_ADULTS = 5;
|
||||
const PKG_CHILDREN_PER_ADULT = 2; // 2 children allowed per adult
|
||||
const PKG_CHILD_FARE_RATIO = 0.1;
|
||||
const PKG_CHILDREN_PER_ADULT = 5; // max 5 children per adult
|
||||
|
||||
/**
|
||||
* First child per adult travels FREE (no seat).
|
||||
* Additional children beyond one per adult pay the full adult fare.
|
||||
*/
|
||||
function calculatePackageFareBreakdown(
|
||||
priceMinor: number,
|
||||
isRoundTrip: boolean,
|
||||
@@ -19,9 +22,10 @@ function calculatePackageFareBreakdown(
|
||||
) {
|
||||
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 };
|
||||
const freeChildren = Math.min(childCount, adultCount);
|
||||
const paidChildren = Math.max(0, childCount - adultCount);
|
||||
const totalMinor = adultCount * adultFareMinor + paidChildren * adultFareMinor;
|
||||
return { adultFareMinor, freeChildren, paidChildren, totalMinor, multiplier };
|
||||
}
|
||||
|
||||
function deriveAge(dateOfBirth: string | Date): number {
|
||||
@@ -71,16 +75,18 @@ export class PackagesService {
|
||||
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;
|
||||
if (passengerCount > remaining)
|
||||
throw new BadRequestException(`Only ${remaining} seat(s) remaining in the ${tier.label} tier`);
|
||||
|
||||
const isRoundTrip = !!pkg.returnScheduleId;
|
||||
const { adultFareMinor, childFareMinor, totalMinor } = calculatePackageFareBreakdown(
|
||||
const { adultFareMinor, freeChildren, paidChildren, totalMinor } = calculatePackageFareBreakdown(
|
||||
tier.priceMinor, isRoundTrip, adultCount, childCount,
|
||||
);
|
||||
|
||||
// Only adults and paid children need seats; free children travel without a seat
|
||||
const seatsNeeded = adultCount + paidChildren;
|
||||
const passengerCount = adultCount + childCount;
|
||||
if (seatsNeeded > remaining)
|
||||
throw new BadRequestException(`Only ${remaining} seat(s) remaining in the ${tier.label} tier`);
|
||||
|
||||
// Resolve the seatClassId and coachTypeId that matches this tier's seatType from the outbound schedule coaches
|
||||
let seatClassId: string | null = tier.seatClassId ?? null;
|
||||
let seatClassName: string | null = null;
|
||||
@@ -113,8 +119,10 @@ export class PackagesService {
|
||||
passengerCount,
|
||||
isRoundTrip,
|
||||
pricePerAdultMinor: adultFareMinor,
|
||||
pricePerChildMinor: childFareMinor,
|
||||
childFareNote: `Children pay ${PKG_CHILD_FARE_RATIO * 100}% of adult fare`,
|
||||
pricePerChildMinor: adultFareMinor, // paid children pay full adult fare
|
||||
freeChildrenCount: freeChildren,
|
||||
paidChildrenCount: paidChildren,
|
||||
childFareNote: `First child per adult travels free (no seat); additional children pay full adult fare`,
|
||||
maxAdults: PKG_MAX_ADULTS,
|
||||
maxChildren: adultCount * PKG_CHILDREN_PER_ADULT,
|
||||
totalMinor,
|
||||
@@ -317,25 +325,31 @@ export class PackagesService {
|
||||
return this.prisma.packagePriceTier.delete({ where: { id: tierId } });
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const pkg = await this.prisma.travelPackage.findUnique({
|
||||
where: { id },
|
||||
include: { bookings: { select: { id: true, status: true } } },
|
||||
});
|
||||
async remove(id: string, cascade = false) {
|
||||
const pkg = await this.prisma.travelPackage.findUnique({ where: { id } });
|
||||
if (!pkg) throw new NotFoundException('Package not found');
|
||||
const hasActive = pkg.bookings.some((b) => b.status === 'PENDING_PAYMENT' || b.status === 'CONFIRMED');
|
||||
if (hasActive) throw new BadRequestException('Cannot delete a package with active bookings');
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const bookingIds = pkg.bookings.map((b) => b.id);
|
||||
if (bookingIds.length > 0) {
|
||||
await tx.packageBookingPassenger.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await tx.packagePaymentIntent.deleteMany({ where: { packageBookingId: { in: bookingIds } } });
|
||||
await tx.packageBooking.deleteMany({ where: { packageId: id } });
|
||||
}
|
||||
await tx.packagePriceTier.deleteMany({ where: { packageId: id } });
|
||||
await tx.travelPackage.delete({ where: { id } });
|
||||
const packageBookings = await this.prisma.packageBooking.findMany({
|
||||
where: { packageId: id },
|
||||
select: { id: true, status: true },
|
||||
});
|
||||
|
||||
if (!cascade) {
|
||||
const hasActive = packageBookings.some(b => b.status === 'PENDING_PAYMENT' || b.status === 'CONFIRMED');
|
||||
if (hasActive) throw new BadRequestException('Cannot delete a package with active bookings. Use cascade=true to force delete.');
|
||||
}
|
||||
|
||||
const pbIds = packageBookings.map(b => b.id);
|
||||
if (pbIds.length > 0) {
|
||||
await this.prisma.packageBookingPassenger.deleteMany({ where: { bookingId: { in: pbIds } } });
|
||||
await this.prisma.packagePaymentIntent.deleteMany({ where: { packageBookingId: { in: pbIds } } });
|
||||
await this.prisma.packageBooking.deleteMany({ where: { id: { in: pbIds } } });
|
||||
}
|
||||
|
||||
// PackageInquiry references packageId and priceTierId
|
||||
await this.prisma.packageInquiry.deleteMany({ where: { packageId: id } });
|
||||
await this.prisma.packagePriceTier.deleteMany({ where: { packageId: id } });
|
||||
await this.prisma.travelPackage.delete({ where: { id } });
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
@@ -379,14 +393,16 @@ export class PackagesService {
|
||||
|
||||
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 isRoundTrip = !!pkg.returnScheduleId;
|
||||
const { adultFareMinor, childFareMinor, totalMinor } = calculatePackageFareBreakdown(
|
||||
const { adultFareMinor, freeChildren, paidChildren, totalMinor } = calculatePackageFareBreakdown(
|
||||
tier.priceMinor, isRoundTrip, adultCount, childCount,
|
||||
);
|
||||
// Only adults and paid children need seats; free children travel without a seat
|
||||
const seatsNeeded = adultCount + paidChildren;
|
||||
if (seatsNeeded > remaining) {
|
||||
throw new BadRequestException(`Only ${remaining} seats remaining in the ${tier.label} tier`);
|
||||
}
|
||||
const displayCurrency = (dto.displayCurrency as Currency) ?? Currency.ETB;
|
||||
const displayTotalMinor =
|
||||
displayCurrency !== Currency.ETB
|
||||
@@ -435,7 +451,7 @@ export class PackagesService {
|
||||
}),
|
||||
this.prisma.packagePriceTier.update({
|
||||
where: { id: dto.priceTierId },
|
||||
data: { bookedSeats: { increment: passengerCount } },
|
||||
data: { bookedSeats: { increment: seatsNeeded } },
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -446,8 +462,10 @@ export class PackagesService {
|
||||
adultCount,
|
||||
adultFareMinor,
|
||||
childCount,
|
||||
childFareMinor,
|
||||
childFareNote: `Children pay ${PKG_CHILD_FARE_RATIO * 100}% of adult fare`,
|
||||
freeChildrenCount: freeChildren,
|
||||
paidChildrenCount: paidChildren,
|
||||
paidChildFareMinor: adultFareMinor,
|
||||
childFareNote: `First child per adult travels free (no seat); additional children pay full adult fare`,
|
||||
totalMinor,
|
||||
currency: 'ETB',
|
||||
displayCurrency,
|
||||
|
||||
Reference in New Issue
Block a user