mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
175 lines
6.7 KiB
TypeScript
175 lines
6.7 KiB
TypeScript
import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
|
|
import { PrismaService } from '../../common/prisma.service';
|
|
import { RoutesService } from './routes.service';
|
|
import { CreateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto } from './schedules.dto';
|
|
|
|
@Injectable()
|
|
export class SchedulesService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private routesService: RoutesService,
|
|
) {}
|
|
|
|
// ── Schedule CRUD ──────────────────────────────────────────────────────────
|
|
|
|
async listSchedules(dto: ListSchedulesDto) {
|
|
const where: any = {};
|
|
|
|
if (dto.date) {
|
|
const date = new Date(dto.date);
|
|
const nextDay = new Date(date.getTime() + 86_400_000);
|
|
where.departureAt = { gte: date, lt: nextDay };
|
|
}
|
|
if (dto.routeId) where.routeId = dto.routeId;
|
|
if (dto.trainId) where.trainId = dto.trainId;
|
|
if (dto.status) where.status = dto.status;
|
|
|
|
return this.prisma.trainSchedule.findMany({
|
|
where,
|
|
include: {
|
|
train: true,
|
|
originStation: true,
|
|
destinationStation: true,
|
|
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
|
|
_count: { select: { coachAssignments: true, bookings: true } },
|
|
},
|
|
orderBy: { departureAt: 'asc' },
|
|
});
|
|
}
|
|
|
|
async createSchedule(dto: CreateScheduleDto) {
|
|
const dep = new Date(dto.departureAt);
|
|
const arr = new Date(dto.arrivalAt);
|
|
if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');
|
|
|
|
// Validate route exists and has stops
|
|
const route = await this.prisma.route.findUnique({
|
|
where: { id: dto.routeId },
|
|
include: { stops: { orderBy: { sequence: 'asc' } } },
|
|
});
|
|
if (!route) throw new NotFoundException('Route not found');
|
|
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');
|
|
|
|
// Validate all route stop sequences are covered by plannedTimes
|
|
const providedSeqs = new Set(dto.plannedTimes.map(t => t.sequence));
|
|
const missingSeqs = route.stops.map(s => s.sequence).filter(seq => !providedSeqs.has(seq));
|
|
if (missingSeqs.length > 0) {
|
|
throw new BadRequestException(`Missing planned times for stop sequences: ${missingSeqs.join(', ')}`);
|
|
}
|
|
|
|
// Derive origin and destination from first and last route stop
|
|
const firstStop = route.stops[0];
|
|
const lastStop = route.stops[route.stops.length - 1];
|
|
|
|
const schedule = await this.prisma.trainSchedule.create({
|
|
data: {
|
|
trainId: dto.trainId,
|
|
routeId: dto.routeId,
|
|
originStationId: firstStop.stationId,
|
|
destinationStationId: lastStop.stationId,
|
|
departureAt: dep,
|
|
arrivalAt: arr,
|
|
durationMinutes: Math.round((arr.getTime() - dep.getTime()) / 60_000),
|
|
stopsCount: Math.max(0, route.stops.length - 2),
|
|
},
|
|
include: { train: true, originStation: true, destinationStation: true },
|
|
});
|
|
|
|
// Copy route stops into TripStopTime with the provided planned times
|
|
const plannedTimesMap = Object.fromEntries(
|
|
dto.plannedTimes.map(t => [t.sequence, t]),
|
|
);
|
|
await this.routesService.applyRouteToSchedule(dto.routeId, schedule.id, plannedTimesMap);
|
|
|
|
return this.getSchedule(schedule.id);
|
|
}
|
|
|
|
async getSchedule(id: string) {
|
|
const schedule = await this.prisma.trainSchedule.findUnique({
|
|
where: { id },
|
|
include: {
|
|
train: true,
|
|
originStation: true,
|
|
destinationStation: true,
|
|
coachAssignments: {
|
|
include: { coach: { include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] }, seatClass: true } } },
|
|
orderBy: { positionNumber: 'asc' },
|
|
},
|
|
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
|
|
},
|
|
});
|
|
if (!schedule) throw new NotFoundException('Schedule not found');
|
|
return schedule;
|
|
}
|
|
|
|
updateScheduleStatus(id: string, dto: UpdateScheduleStatusDto) {
|
|
return this.prisma.trainSchedule.update({ where: { id }, data: { status: dto.status } });
|
|
}
|
|
|
|
// ── Stop Times (per-schedule overrides) ───────────────────────────────────
|
|
|
|
getStops(scheduleId: string) {
|
|
return this.prisma.tripStopTime.findMany({
|
|
where: { scheduleId },
|
|
include: { station: true },
|
|
orderBy: { sequence: 'asc' },
|
|
});
|
|
}
|
|
|
|
async updateStop(scheduleId: string, sequence: number, dto: UpdateStopTimeDto) {
|
|
const stop = await this.prisma.tripStopTime.findUnique({
|
|
where: { scheduleId_sequence: { scheduleId, sequence } },
|
|
});
|
|
if (!stop) throw new NotFoundException(`Stop at sequence ${sequence} not found on schedule`);
|
|
|
|
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,
|
|
status: dto.status,
|
|
},
|
|
include: { station: true },
|
|
});
|
|
}
|
|
|
|
// ── Fare Rules ─────────────────────────────────────────────────────────────
|
|
|
|
createFareRule(dto: CreateFareRuleDto) {
|
|
const { validFrom, validUntil, scheduleId, ...rest } = dto;
|
|
return this.prisma.fareRule.create({
|
|
data: {
|
|
...rest,
|
|
tripId: scheduleId,
|
|
validFrom: new Date(validFrom),
|
|
validUntil: validUntil ? new Date(validUntil) : null,
|
|
},
|
|
});
|
|
}
|
|
|
|
async getFare(scheduleId: string, seatClassName: string) {
|
|
const schedule = await this.prisma.trainSchedule.findUnique({
|
|
where: { id: scheduleId },
|
|
include: { originStation: true, destinationStation: true },
|
|
});
|
|
if (!schedule) throw new NotFoundException('Schedule not found');
|
|
|
|
const route = `${schedule.originStation.code}-${schedule.destinationStation.code}`;
|
|
const seatClass = await this.prisma.seatClass.findFirst({ where: { name: seatClassName } });
|
|
const now = new Date();
|
|
|
|
const rule = await this.prisma.fareRule.findFirst({
|
|
where: {
|
|
seatClassId: seatClass?.id,
|
|
validFrom: { lte: now },
|
|
OR: [{ tripId: scheduleId }, { route }, { tripId: null, route: null }],
|
|
AND: [{ OR: [{ validUntil: null }, { validUntil: { gte: now } }] }],
|
|
},
|
|
orderBy: { validFrom: 'desc' },
|
|
});
|
|
|
|
return rule ?? { baseFareMinor: 45000, currency: 'ETB', seatClassName };
|
|
}
|
|
}
|