Boarding, payment methods, journey direction on seat hold, and more updates

This commit is contained in:
Stephanos A
2026-06-29 08:44:38 +03:00
parent 81ae99cee3
commit c6e56d1c4f
65 changed files with 6437 additions and 1425 deletions

View File

@@ -1,6 +1,7 @@
import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto } from './routes.dto';
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
@Injectable()
export class RoutesService {
@@ -93,8 +94,28 @@ export class RoutesService {
}
async deleteRoute(id: string) {
const route = await this.prisma.route.findUnique({ where: { id } });
const route = await this.prisma.route.findUnique({
where: { id },
include: {
schedules: true,
stops: true
}
});
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 (constraints.length > 0) {
throw new DeleteOperationException('Route', `${route.code} (${route.name})`, constraints);
}
await this.prisma.route.delete({ where: { id } });
return { deleted: true, id };
}

View File

@@ -3,6 +3,8 @@ import { PrismaService } from '../../common/prisma.service';
import { RoutesService } from './routes.service';
import { FareEngineService } from '../fare-engine/fare-engine.service';
import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto } from './schedules.dto';
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
import { parseEthiopianTime, startOfDayEAT, startOfNextDayEAT } from '../../common/utils/timezone.utils';
@Injectable()
export class SchedulesService {
@@ -13,7 +15,7 @@ export class SchedulesService {
) { }
async bulkGenerateSchedules(dto: BulkCreateSchedulesDto) {
const startDate = new Date(dto.startDateTime);
const startDate = parseEthiopianTime(dto.startDateTime);
const endDate = new Date(startDate.getTime() + dto.forNextDays * 24 * 60 * 60 * 1000);
const errors: string[] = [];
const scheduleIds: string[] = [];
@@ -66,8 +68,8 @@ export class SchedulesService {
const where: any = {};
if (dto.date) {
const date = new Date(dto.date);
const nextDay = new Date(date.getTime() + 86_400_000);
const date = parseEthiopianTime(dto.date);
const nextDay = startOfNextDayEAT(date);
where.departureAt = { gte: date, lt: nextDay };
}
if (dto.routeId) where.routeId = dto.routeId;
@@ -93,8 +95,9 @@ export class SchedulesService {
}
async createSchedule(dto: CreateScheduleDto) {
const dep = new Date(dto.departureAt);
const arr = new Date(dto.arrivalAt);
// Parse dates in local Ethiopian time (EAT - UTC+3)
const dep = parseEthiopianTime(dto.departureAt);
const arr = parseEthiopianTime(dto.arrivalAt);
if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');
const route = await this.prisma.route.findUnique({
@@ -105,10 +108,9 @@ export class SchedulesService {
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');
const depDate = new Date(dep);
depDate.setHours(0, 0, 0, 0);
const nextDay = new Date(depDate);
nextDay.setDate(nextDay.getDate() + 1);
// Check for existing schedule on the same day (local Ethiopian time)
const depDate = startOfDayEAT(dep);
const nextDay = startOfNextDayEAT(dep);
const existingSchedule = await this.prisma.trainSchedule.findFirst({
where: { trainId: dto.trainId, routeId: dto.routeId, departureAt: { gte: depDate, lt: nextDay } },
@@ -240,8 +242,9 @@ export class SchedulesService {
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
if (!schedule) throw new NotFoundException('Schedule not found');
const dep = new Date(dto.departureAt);
const arr = new Date(dto.arrivalAt);
// Parse dates in local Ethiopian time (EAT - UTC+3)
const dep = parseEthiopianTime(dto.departureAt);
const arr = parseEthiopianTime(dto.arrivalAt);
if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');
const route = await this.prisma.route.findUnique({
@@ -308,16 +311,59 @@ export class SchedulesService {
async deleteSchedule(id: string) {
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id },
include: { _count: { select: { bookings: true } } },
include: {
_count: { select: { bookings: true } },
train: true,
originStation: true,
destinationStation: true
},
});
if (!schedule) throw new NotFoundException('Schedule not found');
const constraints = [];
if ((schedule as any)._count.bookings > 0) {
throw new BadRequestException(
`Cannot delete schedule. It has ${(schedule as any)._count.bookings} booking(s). Cancel all bookings before deleting.`,
);
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 } });
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
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 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 } }
});
}
return this.prisma.trainSchedule.delete({ where: { id } });
}
@@ -338,8 +384,8 @@ export class SchedulesService {
return this.prisma.tripStopTime.update({
where: { scheduleId_sequence: { scheduleId, sequence } },
data: {
plannedArrivalAt: dto.plannedArrivalAt ? new Date(dto.plannedArrivalAt) : undefined,
plannedDepartureAt: dto.plannedDepartureAt ? new Date(dto.plannedDepartureAt) : undefined,
plannedArrivalAt: dto.plannedArrivalAt ? parseEthiopianTime(dto.plannedArrivalAt) : undefined,
plannedDepartureAt: dto.plannedDepartureAt ? parseEthiopianTime(dto.plannedDepartureAt) : undefined,
status: dto.status,
},
include: { station: true },
@@ -353,8 +399,8 @@ export class SchedulesService {
...rest,
tripId: scheduleId,
nationality,
validFrom: new Date(validFrom),
validUntil: validUntil ? new Date(validUntil) : null,
validFrom: parseEthiopianTime(validFrom),
validUntil: validUntil ? parseEthiopianTime(validUntil) : null,
},
include: { seatClass: true },
});
@@ -371,8 +417,8 @@ export class SchedulesService {
...rest,
...(scheduleId !== undefined && { tripId: scheduleId }),
...(nationality !== undefined && { nationality }),
...(validFrom && { validFrom: new Date(validFrom) }),
...(validUntil !== undefined && { validUntil: validUntil ? new Date(validUntil) : null }),
...(validFrom && { validFrom: parseEthiopianTime(validFrom) }),
...(validUntil !== undefined && { validUntil: validUntil ? parseEthiopianTime(validUntil) : null }),
},
include: { seatClass: true },
});
@@ -390,8 +436,8 @@ export class SchedulesService {
return this.prisma.segmentFareRule.create({
data: {
...rest,
validFrom: new Date(validFrom),
validUntil: validUntil ? new Date(validUntil) : null,
validFrom: parseEthiopianTime(validFrom),
validUntil: validUntil ? parseEthiopianTime(validUntil) : null,
},
include: { seatClass: true, route: true },
});
@@ -415,8 +461,8 @@ export class SchedulesService {
where: { id },
data: {
...rest,
validFrom: validFrom ? new Date(validFrom) : undefined,
validUntil: validUntil ? new Date(validUntil) : null,
validFrom: validFrom ? parseEthiopianTime(validFrom) : undefined,
validUntil: validUntil ? parseEthiopianTime(validUntil) : null,
},
include: { seatClass: true, route: true },
});
@@ -523,8 +569,8 @@ export class SchedulesService {
const updateData: any = {};
if (dto.departureAt || dto.arrivalAt) {
const dep = dto.departureAt ? new Date(dto.departureAt) : new Date(schedule.departureAt);
const arr = dto.arrivalAt ? new Date(dto.arrivalAt) : new Date(schedule.arrivalAt);
const dep = dto.departureAt ? parseEthiopianTime(dto.departureAt) : new Date(schedule.departureAt);
const arr = dto.arrivalAt ? parseEthiopianTime(dto.arrivalAt) : new Date(schedule.arrivalAt);
if (arr <= dep) throw new BadRequestException('Arrival time must be after departure time');
updateData.departureAt = dep;
updateData.arrivalAt = arr;