mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
Package booking new rules update, cascade delete option updates
This commit is contained in:
@@ -470,10 +470,11 @@ export class BookingsController {
|
||||
@ApiOperation({
|
||||
description: 'Permanently deletes a booking record'
|
||||
})
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
||||
@ApiResponse({ status: 200, description: 'Booking deleted successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
||||
delete(@Param('id') id: string) {
|
||||
return this.service.delete(id);
|
||||
delete(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.delete(id, cascade === 'true');
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
|
||||
@@ -31,9 +31,10 @@ function resolvePackageRoundTripTotal(
|
||||
if (!booking.packageId || booking.bookingType !== 'ROUND_TRIP' || !tierPriceMinor) {
|
||||
return booking.totalMinor;
|
||||
}
|
||||
// First child per adult is free; additional children pay full adult fare
|
||||
const adultFareMinor = tierPriceMinor * 2;
|
||||
const childFareMinor = Math.round(adultFareMinor * 0.1);
|
||||
return adultCount * adultFareMinor + childCount * childFareMinor;
|
||||
const paidChildren = Math.max(0, childCount - adultCount);
|
||||
return adultCount * adultFareMinor + paidChildren * adultFareMinor;
|
||||
}
|
||||
|
||||
function calculateAge(dateOfBirth: Date): number {
|
||||
@@ -557,7 +558,10 @@ export class BookingsService {
|
||||
if (p.category === PassengerCategory.ADULT) {
|
||||
fareMinor = fareCalculation.baseFareMinor;
|
||||
} else if (dto.packageId) {
|
||||
fareMinor = Math.round(fareCalculation.baseFareMinor * 0.1);
|
||||
// Free children (first per adult) get fareMinor=0; paid children pay full adult fare.
|
||||
// passengersWithFares is built in adult-first order so we track paid children by count.
|
||||
const childIdx = passengersWithFares.filter(x => x.category !== PassengerCategory.ADULT).length;
|
||||
fareMinor = childIdx < adultCount ? 0 : fareCalculation.baseFareMinor;
|
||||
} else {
|
||||
if (!freeChildUsed) { fareMinor = 0; freeChildUsed = true; }
|
||||
else fareMinor = fareCalculation.baseFareMinor;
|
||||
@@ -704,8 +708,11 @@ export class BookingsService {
|
||||
outboundFareMinor = outboundFare.baseFareMinor;
|
||||
returnFareMinor = returnFare.baseFareMinor;
|
||||
} else if (dto.packageId) {
|
||||
outboundFareMinor = Math.round(outboundFare.baseFareMinor * 0.1);
|
||||
returnFareMinor = Math.round(returnFare.baseFareMinor * 0.1);
|
||||
// Free children (first per adult) get fareMinor=0; paid children pay full adult fare.
|
||||
const childIdx = passengersWithFares.filter(x => x.category !== PassengerCategory.ADULT).length;
|
||||
const isFreeChild = childIdx < adultCount;
|
||||
outboundFareMinor = isFreeChild ? 0 : outboundFare.baseFareMinor;
|
||||
returnFareMinor = isFreeChild ? 0 : returnFare.baseFareMinor;
|
||||
} else {
|
||||
if (!outboundFreeChildUsed) { outboundFareMinor = 0; outboundFreeChildUsed = true; }
|
||||
else outboundFareMinor = outboundFare.baseFareMinor;
|
||||
@@ -1255,19 +1262,19 @@ export class BookingsService {
|
||||
childCount: number,
|
||||
) {
|
||||
const tier = await this.prisma.packagePriceTier.findUniqueOrThrow({ where: { id: priceTierId } });
|
||||
// For round-trip packages the caller splits the tier price across legs, so
|
||||
// priceMinor here is already the per-leg amount. Children pay 10% of adult fare.
|
||||
const childFareMinor = Math.round(tier.priceMinor * 0.1);
|
||||
// First child per adult travels free (no seat); additional children pay full adult fare.
|
||||
const freeChildrenCount = Math.min(childCount, adultCount);
|
||||
const paidChildrenCount = Math.max(0, childCount - adultCount);
|
||||
const adultFareMinor = tier.priceMinor * adultCount;
|
||||
const childTotalMinor = childFareMinor * childCount;
|
||||
const childTotalMinor = tier.priceMinor * paidChildrenCount;
|
||||
const totalBaseFareMinor = adultFareMinor + childTotalMinor;
|
||||
return {
|
||||
baseFareMinor: tier.priceMinor,
|
||||
adultCount,
|
||||
adultFareMinor,
|
||||
childCount,
|
||||
freeChildrenCount: 0,
|
||||
paidChildrenCount: childCount,
|
||||
freeChildrenCount,
|
||||
paidChildrenCount,
|
||||
childFareMinor: childTotalMinor,
|
||||
totalBaseFareMinor,
|
||||
discountMinor: 0,
|
||||
@@ -1555,21 +1562,51 @@ export class BookingsService {
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
async delete(id: string, cascade = false) {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { id }, include: { seats: true } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
|
||||
// Check usage before allowing deletion
|
||||
const usage = await this.checkBookingUsage(id);
|
||||
if (usage.isInUse && usage.constraints) {
|
||||
throw new DeleteOperationException('Booking', booking.bookingRef, usage.constraints);
|
||||
|
||||
if (!cascade) {
|
||||
const usage = await this.checkBookingUsage(id);
|
||||
if (usage.isInUse && usage.constraints) {
|
||||
throw new DeleteOperationException('Booking', booking.bookingRef, usage.constraints);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.seatsService.releaseSeats(booking.id);
|
||||
|
||||
|
||||
if (cascade) {
|
||||
// Delete all child records that reference this booking (no onDelete: Cascade in schema)
|
||||
const paymentIntent = await this.prisma.paymentIntent.findUnique({ where: { bookingId: id } });
|
||||
if (paymentIntent) {
|
||||
await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: paymentIntent.id } });
|
||||
await this.prisma.paymentIntent.delete({ where: { bookingId: id } });
|
||||
}
|
||||
const tickets = await this.prisma.ticket.findMany({ where: { bookingId: id }, select: { id: true } });
|
||||
for (const t of tickets) {
|
||||
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: t.id } });
|
||||
}
|
||||
await this.prisma.ticket.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.bookingModification.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.agentBooking.deleteMany({ where: { bookingId: id } });
|
||||
const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: id }, select: { id: true } });
|
||||
for (const fo of foodOrders) {
|
||||
await this.prisma.foodOrderItem.deleteMany({ where: { orderId: fo.id } });
|
||||
}
|
||||
await this.prisma.foodOrder.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.baggageBooking.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: id } });
|
||||
const journey = await this.prisma.journey.findUnique({ where: { bookingId: id } });
|
||||
if (journey) {
|
||||
await this.prisma.journeySegment.deleteMany({ where: { journeyId: journey.id } });
|
||||
await this.prisma.journey.delete({ where: { bookingId: id } });
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.booking.delete({ where: { id } });
|
||||
|
||||
|
||||
return { deleted: true, bookingRef: booking.bookingRef };
|
||||
}
|
||||
|
||||
|
||||
@@ -76,10 +76,11 @@ export class FleetController {
|
||||
@Delete('classes/:id')
|
||||
@ApiOperation({ summary: 'Delete a class' })
|
||||
@ApiParam({ name: 'id', description: 'Class UUID' })
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
||||
@ApiResponse({ status: 200, description: 'Class deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Class not found' })
|
||||
deleteClass(@Param('id') id: string) {
|
||||
return this.service.deleteClass(id);
|
||||
deleteClass(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.deleteClass(id, cascade === 'true');
|
||||
}
|
||||
|
||||
// Seat Class Endpoints (DEPRECATED - use Classes endpoints instead)
|
||||
@@ -112,10 +113,11 @@ export class FleetController {
|
||||
@Delete('seat-classes/:id')
|
||||
@ApiOperation({ summary: 'Delete a class (DEPRECATED - use /fleet/classes)' })
|
||||
@ApiParam({ name: 'id', description: 'Class UUID' })
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
||||
@ApiResponse({ status: 200, description: 'Class deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Class not found' })
|
||||
deleteSeatClass(@Param('id') id: string) {
|
||||
return this.service.deleteClass(id);
|
||||
deleteSeatClass(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.deleteClass(id, cascade === 'true');
|
||||
}
|
||||
|
||||
// Train Endpoints
|
||||
@@ -147,10 +149,11 @@ export class FleetController {
|
||||
@Delete('trains/:id')
|
||||
@ApiOperation({ summary: 'Delete a train service' })
|
||||
@ApiParam({ name: 'id', description: 'Train UUID' })
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
||||
@ApiResponse({ status: 200, description: 'Train deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Train not found' })
|
||||
deleteTrain(@Param('id') id: string) {
|
||||
return this.service.deleteTrain(id);
|
||||
deleteTrain(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.deleteTrain(id, cascade === 'true');
|
||||
}
|
||||
|
||||
@Patch('trains/:id/restore')
|
||||
@@ -309,10 +312,11 @@ export class FleetController {
|
||||
@Delete('coaches/:id')
|
||||
@ApiOperation({ summary: 'Delete a coach' })
|
||||
@ApiParam({ name: 'id', description: 'Coach UUID' })
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
||||
@ApiResponse({ status: 200, description: 'Coach deleted successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Coach not found' })
|
||||
deleteCoach(@Param('id') id: string) {
|
||||
return this.service.deleteCoach(id);
|
||||
deleteCoach(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.deleteCoach(id, cascade === 'true');
|
||||
}
|
||||
|
||||
@Post('assignments')
|
||||
|
||||
@@ -273,7 +273,7 @@ export class FleetService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteClass(id: string) {
|
||||
async deleteClass(id: string, cascade = false) {
|
||||
const seatClass = await this.prisma.seatClass.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -284,19 +284,27 @@ export class FleetService {
|
||||
});
|
||||
if (!seatClass) throw new NotFoundException('Seat class not found');
|
||||
|
||||
const totalFareRules = seatClass.fareRules.length + seatClass.routeFareRules.length + seatClass.segmentFares.length;
|
||||
const constraints = [];
|
||||
|
||||
if (totalFareRules > 0) {
|
||||
constraints.push({
|
||||
entityName: 'fare rule',
|
||||
count: totalFareRules,
|
||||
action: 'delete' as const
|
||||
});
|
||||
if (!cascade) {
|
||||
const totalFareRules = seatClass.fareRules.length + seatClass.routeFareRules.length + seatClass.segmentFares.length;
|
||||
const constraints = [];
|
||||
|
||||
if (totalFareRules > 0) {
|
||||
constraints.push({
|
||||
entityName: 'fare rule',
|
||||
count: totalFareRules,
|
||||
action: 'delete' as const
|
||||
});
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Seat Class', seatClass.name, constraints);
|
||||
}
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Seat Class', seatClass.name, constraints);
|
||||
|
||||
if (cascade) {
|
||||
await this.prisma.fareRule.deleteMany({ where: { seatClassId: id } });
|
||||
await this.prisma.routeFareRule.deleteMany({ where: { seatClassId: id } });
|
||||
await this.prisma.segmentFareRule.deleteMany({ where: { seatClassId: id } });
|
||||
}
|
||||
|
||||
return this.prisma.seatClass.delete({ where: { id } });
|
||||
@@ -354,24 +362,78 @@ export class FleetService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteTrain(id: string) {
|
||||
async deleteTrain(id: string, cascade = false) {
|
||||
const train = await this.prisma.train.findUnique({
|
||||
where: { id },
|
||||
include: { schedules: true },
|
||||
});
|
||||
if (!train) throw new NotFoundException('Train not found');
|
||||
|
||||
const constraints = [];
|
||||
if (train.schedules.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'schedule',
|
||||
count: train.schedules.length,
|
||||
action: 'delete' as const
|
||||
});
|
||||
if (!cascade) {
|
||||
const constraints = [];
|
||||
if (train.schedules.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'schedule',
|
||||
count: train.schedules.length,
|
||||
action: 'delete' as const
|
||||
});
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Train', `${train.number} (${train.name})`, constraints);
|
||||
}
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Train', `${train.number} (${train.name})`, constraints);
|
||||
|
||||
if (cascade && train.schedules.length > 0) {
|
||||
const scheduleIds = train.schedules.map((s: any) => s.id);
|
||||
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.tripLiveStatus.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.menuItem.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.journeySegment.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: { OR: [{ scheduleId: { in: scheduleIds } }, { returnScheduleId: { in: scheduleIds } }] },
|
||||
select: { id: true },
|
||||
});
|
||||
if (bookings.length > 0) {
|
||||
const bookingIds = bookings.map(b => b.id);
|
||||
const tickets = await this.prisma.ticket.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (tickets.length > 0) {
|
||||
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: { in: tickets.map(t => t.id) } } });
|
||||
}
|
||||
await this.prisma.ticket.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (foodOrders.length > 0) {
|
||||
await this.prisma.foodOrderItem.deleteMany({ where: { orderId: { in: foodOrders.map(o => o.id) } } });
|
||||
}
|
||||
await this.prisma.foodOrder.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
const paymentIntents = await this.prisma.paymentIntent.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (paymentIntents.length > 0) {
|
||||
await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: { in: paymentIntents.map(p => p.id) } } });
|
||||
}
|
||||
await this.prisma.paymentIntent.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.agentBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingModification.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.baggageBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.journey.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.booking.deleteMany({ where: { id: { in: bookingIds } } });
|
||||
}
|
||||
|
||||
const packages = await this.prisma.travelPackage.findMany({
|
||||
where: { OR: [{ outboundScheduleId: { in: scheduleIds } }, { returnScheduleId: { in: scheduleIds } }] },
|
||||
select: { id: true },
|
||||
});
|
||||
if (packages.length > 0) {
|
||||
const packageIds = packages.map(p => p.id);
|
||||
await this.prisma.packagePriceTier.deleteMany({ where: { packageId: { in: packageIds } } });
|
||||
await this.prisma.travelPackage.deleteMany({ where: { id: { in: packageIds } } });
|
||||
}
|
||||
|
||||
await this.prisma.trainSchedule.deleteMany({ where: { id: { in: scheduleIds } } });
|
||||
}
|
||||
|
||||
return this.prisma.train.delete({ where: { id } });
|
||||
@@ -477,7 +539,7 @@ export class FleetService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCoach(id: string) {
|
||||
async deleteCoach(id: string, cascade = false) {
|
||||
const coach = await this.prisma.coach.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -493,47 +555,60 @@ export class FleetService {
|
||||
});
|
||||
if (!coach) throw new NotFoundException('Coach not found');
|
||||
|
||||
const constraints = [];
|
||||
|
||||
if ((coach as any).assignments.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'schedule assignment',
|
||||
count: (coach as any).assignments.length,
|
||||
action: 'reassign' as const
|
||||
});
|
||||
if (!cascade) {
|
||||
const constraints = [];
|
||||
|
||||
if ((coach as any).assignments.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'schedule assignment',
|
||||
count: (coach as any).assignments.length,
|
||||
action: 'reassign' as const
|
||||
});
|
||||
}
|
||||
|
||||
const bookedSeats = (coach as any).seats.filter((seat: any) => seat.bookingSeats.length > 0);
|
||||
if (bookedSeats.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'booked seat',
|
||||
count: bookedSeats.length,
|
||||
action: 'complete' as const
|
||||
});
|
||||
}
|
||||
|
||||
const blockedSeats = (coach as any).seats.filter((seat: any) => seat.blocks.length > 0);
|
||||
if (blockedSeats.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'blocked seat',
|
||||
count: blockedSeats.length,
|
||||
action: 'delete' as const
|
||||
});
|
||||
}
|
||||
|
||||
const seatsWithTickets = (coach as any).seats.filter((seat: any) => seat.tickets.length > 0);
|
||||
if (seatsWithTickets.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'seat with issued ticket',
|
||||
count: seatsWithTickets.length,
|
||||
action: 'complete' as const
|
||||
});
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Coach', coach.number, constraints);
|
||||
}
|
||||
}
|
||||
|
||||
const bookedSeats = (coach as any).seats.filter((seat: any) => seat.bookingSeats.length > 0);
|
||||
if (bookedSeats.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'booked seat',
|
||||
count: bookedSeats.length,
|
||||
action: 'complete' as const
|
||||
});
|
||||
if (cascade) {
|
||||
const seatIds = (coach as any).seats.map((s: any) => s.id);
|
||||
if (seatIds.length > 0) {
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { seatId: { in: seatIds } } });
|
||||
await this.prisma.seatBlock.deleteMany({ where: { seatId: { in: seatIds } } });
|
||||
await this.prisma.ticket.deleteMany({ where: { seatId: { in: seatIds } } });
|
||||
}
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { coachId: id } });
|
||||
await this.prisma.routeCoachTemplate.deleteMany({ where: { coachId: id } });
|
||||
}
|
||||
|
||||
const blockedSeats = (coach as any).seats.filter((seat: any) => seat.blocks.length > 0);
|
||||
if (blockedSeats.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'blocked seat',
|
||||
count: blockedSeats.length,
|
||||
action: 'delete' as const
|
||||
});
|
||||
}
|
||||
|
||||
const seatsWithTickets = (coach as any).seats.filter((seat: any) => seat.tickets.length > 0);
|
||||
if (seatsWithTickets.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'seat with issued ticket',
|
||||
count: seatsWithTickets.length,
|
||||
action: 'complete' as const
|
||||
});
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Coach', coach.number, constraints);
|
||||
}
|
||||
|
||||
|
||||
await this.prisma.seat.deleteMany({ where: { coachId: id } });
|
||||
|
||||
return this.prisma.coach.delete({ where: { id } });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -508,10 +508,11 @@ Returns saved passenger details with generated IDs and confirmation.`,
|
||||
summary: 'Delete passenger (admin only)',
|
||||
description: 'Permanently deletes a passenger record and associated data'
|
||||
})
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related bookings and data' })
|
||||
@ApiResponse({ status: 200, description: 'Passenger deleted successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Passenger not found' })
|
||||
deletePassenger(@Param('id') id: string) {
|
||||
return this.service.deletePassenger(id);
|
||||
deletePassenger(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.deletePassenger(id, cascade === 'true');
|
||||
}
|
||||
|
||||
@Get(':id/usage')
|
||||
|
||||
@@ -432,7 +432,7 @@ export class PassengersService {
|
||||
};
|
||||
}
|
||||
|
||||
async deletePassenger(id: string) {
|
||||
async deletePassenger(id: string, cascade = false) {
|
||||
// id may be a TravelerProfile.id (from the list endpoint) or a Passenger.id
|
||||
let passenger = await this.prisma.passenger.findUnique({
|
||||
where: { id },
|
||||
@@ -451,29 +451,67 @@ export class PassengersService {
|
||||
|
||||
const passengerId = passenger.id;
|
||||
|
||||
// Check usage before allowing deletion
|
||||
const usage = await this.checkPassengerUsage(passengerId);
|
||||
if (usage.isInUse && usage.constraints) {
|
||||
const passengerName = (passenger as any).user?.fullName || `Passenger ${passengerId.slice(-8)}`;
|
||||
throw new DeleteOperationException('Passenger', passengerName, usage.constraints);
|
||||
if (!cascade) {
|
||||
const usage = await this.checkPassengerUsage(passengerId);
|
||||
if (usage.isInUse && usage.constraints) {
|
||||
const passengerName = (passenger as any).user?.fullName || `Passenger ${passengerId.slice(-8)}`;
|
||||
throw new DeleteOperationException('Passenger', passengerName, usage.constraints);
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.loyaltyLedgerEntry.deleteMany({ where: { account: { passengerId } } }),
|
||||
this.prisma.loyaltyAccount.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.walletLedgerEntry.deleteMany({ where: { wallet: { passengerId } } }),
|
||||
this.prisma.walletAccount.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.notification.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.travelerProfile.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.savedRoute.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.packageBooking.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.ticket.deleteMany({ where: { booking: { passengerId } } }),
|
||||
this.prisma.bookingSeat.deleteMany({ where: { booking: { passengerId } } }),
|
||||
this.prisma.booking.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.journeySegment.deleteMany({ where: { journey: { passengerId } } }),
|
||||
this.prisma.journey.deleteMany({ where: { passengerId } }),
|
||||
this.prisma.passenger.delete({ where: { id: passengerId } }),
|
||||
]);
|
||||
// Resolve booking IDs first (needed for multi-step child deletion)
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: { passengerId },
|
||||
select: { id: true },
|
||||
});
|
||||
const bookingIds = bookings.map(b => b.id);
|
||||
|
||||
if (bookingIds.length > 0) {
|
||||
const tickets = await this.prisma.ticket.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (tickets.length > 0) {
|
||||
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: { in: tickets.map(t => t.id) } } });
|
||||
}
|
||||
await this.prisma.ticket.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
|
||||
const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (foodOrders.length > 0) {
|
||||
await this.prisma.foodOrderItem.deleteMany({ where: { orderId: { in: foodOrders.map(o => o.id) } } });
|
||||
}
|
||||
await this.prisma.foodOrder.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
|
||||
const paymentIntents = await this.prisma.paymentIntent.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (paymentIntents.length > 0) {
|
||||
await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: { in: paymentIntents.map(p => p.id) } } });
|
||||
}
|
||||
await this.prisma.paymentIntent.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.agentBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingModification.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.baggageBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.journey.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.booking.deleteMany({ where: { id: { in: bookingIds } } });
|
||||
}
|
||||
|
||||
// Package bookings
|
||||
const packageBookings = await this.prisma.packageBooking.findMany({ where: { passengerId }, select: { id: true } });
|
||||
if (packageBookings.length > 0) {
|
||||
const pbIds = packageBookings.map(pb => pb.id);
|
||||
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 } } });
|
||||
}
|
||||
|
||||
await this.prisma.loyaltyLedgerEntry.deleteMany({ where: { account: { passengerId } } });
|
||||
await this.prisma.loyaltyAccount.deleteMany({ where: { passengerId } });
|
||||
await this.prisma.walletLedgerEntry.deleteMany({ where: { wallet: { passengerId } } });
|
||||
await this.prisma.walletAccount.deleteMany({ where: { passengerId } });
|
||||
await this.prisma.notification.deleteMany({ where: { passengerId } });
|
||||
await this.prisma.travelerProfile.deleteMany({ where: { passengerId } });
|
||||
await this.prisma.savedRoute.deleteMany({ where: { passengerId } });
|
||||
await this.prisma.passenger.delete({ where: { id: passengerId } });
|
||||
|
||||
return { deleted: true, passengerId };
|
||||
}
|
||||
|
||||
@@ -51,9 +51,10 @@ Route stops carry distanceKm for fare-by-distance calculations.`,
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Delete a route' })
|
||||
@ApiParam({ name: 'id', description: 'Route UUID' })
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
||||
@ApiResponse({ status: 200, description: 'Route deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Route not found' })
|
||||
deleteRoute(@Param('id') id: string) { return this.service.deleteRoute(id); }
|
||||
deleteRoute(@Param('id') id: string, @Query('cascade') cascade?: string) { return this.service.deleteRoute(id, cascade === 'true'); }
|
||||
|
||||
// ── Route Stops ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export class RoutesService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteRoute(id: string) {
|
||||
async deleteRoute(id: string, cascade = false) {
|
||||
const route = await this.prisma.route.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -120,19 +120,77 @@ export class RoutesService {
|
||||
});
|
||||
if (!route) throw new NotFoundException('Route not found');
|
||||
|
||||
const constraints = [];
|
||||
if (route.schedules.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'schedule',
|
||||
count: route.schedules.length,
|
||||
action: 'delete' as const
|
||||
});
|
||||
if (!cascade) {
|
||||
const constraints = [];
|
||||
if (route.schedules.length > 0) {
|
||||
constraints.push({
|
||||
entityName: 'schedule',
|
||||
count: route.schedules.length,
|
||||
action: 'delete' as const
|
||||
});
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Route', `${route.code} (${route.name})`, constraints);
|
||||
}
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
throw new DeleteOperationException('Route', `${route.code} (${route.name})`, constraints);
|
||||
if (cascade) {
|
||||
const scheduleIds = route.schedules.map(s => s.id);
|
||||
if (scheduleIds.length > 0) {
|
||||
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.tripLiveStatus.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.menuItem.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
await this.prisma.journeySegment.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
|
||||
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: { OR: [{ scheduleId: { in: scheduleIds } }, { returnScheduleId: { in: scheduleIds } }] },
|
||||
select: { id: true },
|
||||
});
|
||||
if (bookings.length > 0) {
|
||||
const bookingIds = bookings.map(b => b.id);
|
||||
const tickets = await this.prisma.ticket.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (tickets.length > 0) {
|
||||
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: { in: tickets.map(t => t.id) } } });
|
||||
}
|
||||
await this.prisma.ticket.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (foodOrders.length > 0) {
|
||||
await this.prisma.foodOrderItem.deleteMany({ where: { orderId: { in: foodOrders.map(o => o.id) } } });
|
||||
}
|
||||
await this.prisma.foodOrder.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
const paymentIntents = await this.prisma.paymentIntent.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (paymentIntents.length > 0) {
|
||||
await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: { in: paymentIntents.map(p => p.id) } } });
|
||||
}
|
||||
await this.prisma.paymentIntent.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.agentBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingModification.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.baggageBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.journey.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.booking.deleteMany({ where: { id: { in: bookingIds } } });
|
||||
}
|
||||
|
||||
const packages = await this.prisma.travelPackage.findMany({
|
||||
where: { OR: [{ outboundScheduleId: { in: scheduleIds } }, { returnScheduleId: { in: scheduleIds } }] },
|
||||
select: { id: true },
|
||||
});
|
||||
if (packages.length > 0) {
|
||||
const packageIds = packages.map(p => p.id);
|
||||
await this.prisma.packagePriceTier.deleteMany({ where: { packageId: { in: packageIds } } });
|
||||
await this.prisma.travelPackage.deleteMany({ where: { id: { in: packageIds } } });
|
||||
}
|
||||
|
||||
await this.prisma.trainSchedule.deleteMany({ where: { id: { in: scheduleIds } } });
|
||||
}
|
||||
await this.prisma.routeStop.deleteMany({ where: { routeId: id } });
|
||||
await this.prisma.routeCoachTemplate.deleteMany({ where: { routeId: id } });
|
||||
}
|
||||
|
||||
|
||||
await this.prisma.route.delete({ where: { id } });
|
||||
return { deleted: true, id };
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ export class SchedulesController {
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Delete a schedule' })
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
deleteSchedule(@Param('id') id: string) { return this.service.deleteSchedule(id); }
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean })
|
||||
deleteSchedule(@Param('id') id: string, @Query('cascade') cascade?: string) { return this.service.deleteSchedule(id, cascade === 'true'); }
|
||||
|
||||
@Get(':id/stops')
|
||||
@IsPublic()
|
||||
|
||||
@@ -332,7 +332,7 @@ export class SchedulesService {
|
||||
return this.prisma.trainSchedule.update({ where: { id }, data: { status: dto.status } });
|
||||
}
|
||||
|
||||
async deleteSchedule(id: string) {
|
||||
async deleteSchedule(id: string, cascade = false) {
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -344,18 +344,20 @@ export class SchedulesService {
|
||||
});
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
|
||||
const constraints = [];
|
||||
if ((schedule as any)._count.bookings > 0) {
|
||||
constraints.push({
|
||||
entityName: 'booking',
|
||||
count: (schedule as any)._count.bookings,
|
||||
action: 'cancel' as const
|
||||
});
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
const scheduleName = `${schedule.train.number} (${schedule.originStation.name} → ${schedule.destinationStation.name})`;
|
||||
throw new DeleteOperationException('Schedule', scheduleName, constraints);
|
||||
if (!cascade) {
|
||||
const constraints = [];
|
||||
if ((schedule as any)._count.bookings > 0) {
|
||||
constraints.push({
|
||||
entityName: 'booking',
|
||||
count: (schedule as any)._count.bookings,
|
||||
action: 'cancel' as const
|
||||
});
|
||||
}
|
||||
|
||||
if (constraints.length > 0) {
|
||||
const scheduleName = `${schedule.train.number} (${schedule.originStation.name} → ${schedule.destinationStation.name})`;
|
||||
throw new DeleteOperationException('Schedule', scheduleName, constraints);
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });
|
||||
@@ -363,30 +365,49 @@ export class SchedulesService {
|
||||
await this.prisma.tripLiveStatus.deleteMany({ where: { scheduleId: id } });
|
||||
await this.prisma.menuItem.deleteMany({ where: { scheduleId: id } });
|
||||
await this.prisma.journeySegment.deleteMany({ where: { scheduleId: id } });
|
||||
|
||||
// Delete travel packages that reference this schedule (required fields cannot be nulled)
|
||||
// First get packages that reference this schedule
|
||||
const packagesToDelete = await this.prisma.travelPackage.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ outboundScheduleId: id },
|
||||
{ returnScheduleId: id }
|
||||
]
|
||||
},
|
||||
select: { id: true }
|
||||
|
||||
// Delete bookings and all their children
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: { OR: [{ scheduleId: id }, { returnScheduleId: id }] },
|
||||
select: { id: true },
|
||||
});
|
||||
if (bookings.length > 0) {
|
||||
const bookingIds = bookings.map(b => b.id);
|
||||
// Leaf tables first
|
||||
const tickets = await this.prisma.ticket.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (tickets.length > 0) {
|
||||
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: { in: tickets.map(t => t.id) } } });
|
||||
}
|
||||
await this.prisma.ticket.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (foodOrders.length > 0) {
|
||||
await this.prisma.foodOrderItem.deleteMany({ where: { orderId: { in: foodOrders.map(o => o.id) } } });
|
||||
}
|
||||
await this.prisma.foodOrder.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
const paymentIntents = await this.prisma.paymentIntent.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
|
||||
if (paymentIntents.length > 0) {
|
||||
await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: { in: paymentIntents.map(p => p.id) } } });
|
||||
}
|
||||
await this.prisma.paymentIntent.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.agentBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingModification.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.baggageBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.journey.deleteMany({ where: { bookingId: { in: bookingIds } } });
|
||||
await this.prisma.booking.deleteMany({ where: { id: { in: bookingIds } } });
|
||||
}
|
||||
|
||||
// Delete travel packages that reference this schedule
|
||||
const packagesToDelete = await this.prisma.travelPackage.findMany({
|
||||
where: { OR: [{ outboundScheduleId: id }, { returnScheduleId: id }] },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Delete price tiers first (they have foreign key to packages)
|
||||
if (packagesToDelete.length > 0) {
|
||||
const packageIds = packagesToDelete.map(p => p.id);
|
||||
await this.prisma.packagePriceTier.deleteMany({
|
||||
where: { packageId: { in: packageIds } }
|
||||
});
|
||||
|
||||
// Now delete the packages
|
||||
await this.prisma.travelPackage.deleteMany({
|
||||
where: { id: { in: packageIds } }
|
||||
});
|
||||
await this.prisma.packagePriceTier.deleteMany({ where: { packageId: { in: packageIds } } });
|
||||
await this.prisma.travelPackage.deleteMany({ where: { id: { in: packageIds } } });
|
||||
}
|
||||
return this.prisma.trainSchedule.delete({ where: { id } });
|
||||
}
|
||||
|
||||
@@ -136,9 +136,10 @@ export class StationsController {
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Delete station' })
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
||||
@ApiResponse({ status: 200, description: 'Station deleted successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Station not found' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.remove(id);
|
||||
remove(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
||||
return this.service.remove(id, cascade === 'true');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class StationsService {
|
||||
return updatedStation;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
async remove(id: string, cascade = false) {
|
||||
const station = await this.prisma.station.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -107,24 +107,34 @@ export class StationsService {
|
||||
});
|
||||
if (!station) throw new NotFoundException('Station not found');
|
||||
|
||||
const [routeStopCount, originCount, destCount, stopTimeCount] = await Promise.all([
|
||||
this.prisma.routeStop.count({ where: { stationId: id } }),
|
||||
this.prisma.trainSchedule.count({ where: { originStationId: id } }),
|
||||
this.prisma.trainSchedule.count({ where: { destinationStationId: id } }),
|
||||
(station as any)._count.stopTimes as number,
|
||||
]);
|
||||
if (!cascade) {
|
||||
const [routeStopCount, originCount, destCount, stopTimeCount] = await Promise.all([
|
||||
this.prisma.routeStop.count({ where: { stationId: id } }),
|
||||
this.prisma.trainSchedule.count({ where: { originStationId: id } }),
|
||||
this.prisma.trainSchedule.count({ where: { destinationStationId: id } }),
|
||||
(station as any)._count.stopTimes as number,
|
||||
]);
|
||||
|
||||
const constraints = [];
|
||||
if (routeStopCount > 0)
|
||||
constraints.push({ entityName: 'route', count: routeStopCount, action: 'delete' as const });
|
||||
const scheduleCount = originCount + destCount;
|
||||
if (scheduleCount > 0)
|
||||
constraints.push({ entityName: 'schedule', count: scheduleCount, action: 'delete' as const });
|
||||
if (stopTimeCount > 0)
|
||||
constraints.push({ entityName: 'stop time', count: stopTimeCount, action: 'delete' as const });
|
||||
const constraints = [];
|
||||
if (routeStopCount > 0)
|
||||
constraints.push({ entityName: 'route', count: routeStopCount, action: 'delete' as const });
|
||||
const scheduleCount = originCount + destCount;
|
||||
if (scheduleCount > 0)
|
||||
constraints.push({ entityName: 'schedule', count: scheduleCount, action: 'delete' as const });
|
||||
if (stopTimeCount > 0)
|
||||
constraints.push({ entityName: 'stop time', count: stopTimeCount, action: 'delete' as const });
|
||||
|
||||
if (constraints.length > 0)
|
||||
throw new DeleteOperationException('Station', `${station.name} (${station.code})`, constraints);
|
||||
if (constraints.length > 0)
|
||||
throw new DeleteOperationException('Station', `${station.name} (${station.code})`, constraints);
|
||||
}
|
||||
|
||||
if (cascade) {
|
||||
await this.prisma.tripStopTime.deleteMany({ where: { stationId: id } });
|
||||
await this.prisma.trainSchedule.deleteMany({
|
||||
where: { OR: [{ originStationId: id }, { destinationStationId: id }] },
|
||||
});
|
||||
await this.prisma.routeStop.deleteMany({ where: { stationId: id } });
|
||||
}
|
||||
|
||||
const deleted = await this.prisma.station.delete({ where: { id } });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user