Adding train delay minutes and also disabling no schedule days in the booking selection

This commit is contained in:
Muluhabt
2026-07-27 14:42:57 +03:00
parent d175514f85
commit d71f6dc1b3
19 changed files with 845 additions and 54 deletions

View File

@@ -2,10 +2,11 @@ import { Injectable, Logger, NotFoundException, BadRequestException } from '@nes
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 { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto, ApplyDelayDto } from './schedules.dto';
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
import { parseEthiopianTime, startOfDayEAT, startOfNextDayEAT } from '../../common/utils/timezone.utils';
import { AuditService } from '../../common/audit.service';
import { LiveService } from '../live/live.service';
@Injectable()
export class SchedulesService {
@@ -16,6 +17,7 @@ export class SchedulesService {
private routesService: RoutesService,
private fareEngine: FareEngineService,
private auditService: AuditService,
private liveService: LiveService,
) { }
/**
@@ -126,6 +128,7 @@ export class SchedulesService {
include: { coach: true },
orderBy: { positionNumber: 'asc' },
},
liveStatus: { select: { delayMinutes: true } },
_count: { select: { coachAssignments: true, bookings: true } },
},
orderBy: { departureAt: 'asc' },
@@ -246,6 +249,7 @@ export class SchedulesService {
orderBy: { positionNumber: 'asc' },
},
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
liveStatus: { select: { delayMinutes: true } },
},
});
if (!schedule) throw new NotFoundException('Schedule not found');
@@ -462,6 +466,70 @@ export class SchedulesService {
});
}
/**
* Shifts stored planned times additively rather than reusing updateSchedulePartial's
* recompute-from-route-interpolation path — that path also guards `departureAt must be in the
* future`, which a delay report for an already-departed/EN_ROUTE train would legitimately
* fail. Check-in cutoffs (resolveCheckinCutoff, SeatsService.holdSeats) are both derived
* directly from TripStopTime.plannedArrivalAt/plannedDepartureAt at read time, so shifting the
* stored values here is the entire fix — neither of those needs to change.
*/
async applyDelay(scheduleId: string, dto: ApplyDelayDto) {
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: scheduleId } });
if (!schedule) throw new NotFoundException('Schedule not found');
const stopWhere: any = { scheduleId };
if (dto.fromSequence != null) {
stopWhere.sequence = { gte: dto.fromSequence };
} else {
// Default: only stops the train hasn't reached yet — a delay report must not retroactively
// move a stop that's already BOARDED/COMPLETED.
stopWhere.status = { notIn: ['BOARDED', 'COMPLETED'] };
}
const stopsToShift = await this.prisma.tripStopTime.findMany({ where: stopWhere });
const shiftMs = dto.delayMinutes * 60_000;
const includesOrigin = stopsToShift.some((s) => s.sequence === 1);
await this.prisma.$transaction(async (tx) => {
for (const stop of stopsToShift) {
await tx.tripStopTime.update({
where: { id: stop.id },
data: {
plannedArrivalAt: stop.plannedArrivalAt ? new Date(stop.plannedArrivalAt.getTime() + shiftMs) : undefined,
plannedDepartureAt: stop.plannedDepartureAt ? new Date(stop.plannedDepartureAt.getTime() + shiftMs) : undefined,
},
});
}
// Origin stop shifted → the schedule's own departureAt/arrivalAt drive search's day-window
// queries and the displayed departure time, so they must move too (both together, so
// durationMinutes stays correct).
if (includesOrigin) {
await tx.trainSchedule.update({
where: { id: scheduleId },
data: {
departureAt: new Date(schedule.departureAt.getTime() + shiftMs),
arrivalAt: new Date(schedule.arrivalAt.getTime() + shiftMs),
},
});
}
});
const currentLive = await this.prisma.tripLiveStatus.findUnique({ where: { scheduleId } });
const accumulatedDelayMinutes = Math.max(0, (currentLive?.delayMinutes ?? 0) + dto.delayMinutes);
await this.liveService.updateLiveStatus(scheduleId, { delayMinutes: accumulatedDelayMinutes });
await this.auditService.log({
action: 'UPDATE',
entityType: 'Schedule',
entityId: scheduleId,
newData: { delayMinutes: dto.delayMinutes, fromSequence: dto.fromSequence, accumulatedDelayMinutes },
});
return this.getSchedule(scheduleId);
}
async upsertScheduleFare(
scheduleId: string,
seatClassId: string,