mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Fix checkin time cutoff
This commit is contained in:
@@ -4,6 +4,7 @@ import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto, SetRouteCoachTemplateD
|
||||
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
|
||||
import { AuditService } from '../../common/audit.service';
|
||||
import { parseEthiopianTime } from '../../common/utils/timezone.utils';
|
||||
import { computePlannedStopTimes } from '../../common/utils/schedule-times.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RoutesService {
|
||||
@@ -148,6 +149,20 @@ export class RoutesService {
|
||||
travelMinutesToStop: s.travelMinutesToStop ?? null,
|
||||
})),
|
||||
});
|
||||
|
||||
// Propagate new stop timing to all future schedules on this route so that
|
||||
// per-stop check-in cutoffs reflect the updated travelMinutesToStop values.
|
||||
const futureSchedules = await this.prisma.trainSchedule.findMany({
|
||||
where: { routeId: id, status: { in: ['SCHEDULED', 'BOARDING'] }, departureAt: { gt: new Date() } },
|
||||
select: { id: true, departureAt: true, arrivalAt: true },
|
||||
});
|
||||
const stopsForTiming = dto.stops
|
||||
.map(s => ({ sequence: s.sequence, distanceKm: s.distanceKm ?? null, travelMinutesToStop: s.travelMinutesToStop ?? null, checkinMinutesBefore: s.checkinMinutesBefore ?? null }))
|
||||
.sort((a, b) => a.sequence - b.sequence);
|
||||
for (const sched of futureSchedules) {
|
||||
const times = computePlannedStopTimes({ id, stops: stopsForTiming }, new Date(sched.departureAt), new Date(sched.arrivalAt));
|
||||
await this.applyRouteToSchedule(id, sched.id, Object.fromEntries(times.map(t => [t.sequence, t])));
|
||||
}
|
||||
}
|
||||
|
||||
await this.auditService.log({ action: 'UPDATE', entityType: 'Route', entityId: id, newData: { name: dto.name, active: dto.active } });
|
||||
|
||||
@@ -154,6 +154,12 @@ export class SchedulesController {
|
||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean })
|
||||
deleteSchedule(@Param('id') id: string, @Query('cascade') cascade?: string) { return this.service.deleteSchedule(id, cascade === 'true'); }
|
||||
|
||||
@Post(':id/recalculate-stops')
|
||||
@PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Recompute TripStopTime records from current route travelMinutesToStop values' })
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
recalculateStops(@Param('id') id: string) { return this.service.recalculateStopTimes(id); }
|
||||
|
||||
@Get(':id/stops')
|
||||
@IsPublic()
|
||||
@ApiOperation({ summary: 'List all stops for a schedule' })
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateSchedule
|
||||
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
|
||||
import { parseEthiopianTime, startOfDayEAT, startOfNextDayEAT } from '../../common/utils/timezone.utils';
|
||||
import { AuditService } from '../../common/audit.service';
|
||||
import { computePlannedStopTimes } from '../../common/utils/schedule-times.utils';
|
||||
|
||||
@Injectable()
|
||||
export class SchedulesService {
|
||||
@@ -18,44 +19,6 @@ export class SchedulesService {
|
||||
private auditService: AuditService,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Computes each stop's planned arrival/departure time by walking the route in sequence
|
||||
* order and accumulating `RouteStop.travelMinutesToStop` (minutes of travel from the
|
||||
* previous stop). Falls back to distance-proportional interpolation over `distanceKm` for
|
||||
* any stop missing `travelMinutesToStop`. The last stop is always locked to the confirmed
|
||||
* overall `arr` regardless of the accumulated cursor, so schedule.arrivalAt stays
|
||||
* authoritative even if per-stop estimates drift.
|
||||
*/
|
||||
private computePlannedTimes(
|
||||
route: { id: string; stops: { sequence: number; distanceKm: number | null; travelMinutesToStop: number | null }[] },
|
||||
dep: Date,
|
||||
arr: Date,
|
||||
) {
|
||||
const totalDuration = arr.getTime() - dep.getTime();
|
||||
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
|
||||
|
||||
let cursor = dep;
|
||||
return route.stops.map((stop, index) => {
|
||||
if (index === 0) {
|
||||
cursor = dep;
|
||||
} else if (index === route.stops.length - 1) {
|
||||
cursor = arr;
|
||||
} else if (stop.travelMinutesToStop != null) {
|
||||
cursor = new Date(cursor.getTime() + stop.travelMinutesToStop * 60_000);
|
||||
} else {
|
||||
const stopDistance = stop.distanceKm || 0;
|
||||
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
|
||||
cursor = new Date(dep.getTime() + totalDuration * progress);
|
||||
this.logger.warn(`Route ${route.id} stop seq ${stop.sequence} missing travelMinutesToStop; falling back to distance interpolation`);
|
||||
}
|
||||
return {
|
||||
sequence: stop.sequence,
|
||||
plannedArrivalAt: index === 0 ? undefined : cursor.toISOString(),
|
||||
plannedDepartureAt: index === route.stops.length - 1 ? undefined : cursor.toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async bulkGenerateSchedules(dto: BulkCreateSchedulesDto) {
|
||||
const startDate = parseEthiopianTime(dto.startDateTime);
|
||||
const endDate = new Date(startDate.getTime() + dto.forNextDays * 24 * 60 * 60 * 1000);
|
||||
@@ -169,7 +132,7 @@ export class SchedulesService {
|
||||
|
||||
let plannedTimes = dto.plannedTimes;
|
||||
if (!plannedTimes || plannedTimes.length === 0) {
|
||||
plannedTimes = this.computePlannedTimes(route, dep, arr);
|
||||
plannedTimes = computePlannedStopTimes(route, dep, arr);
|
||||
}
|
||||
|
||||
const providedSeqs = new Set((plannedTimes ?? []).map(t => t.sequence));
|
||||
@@ -340,7 +303,7 @@ export class SchedulesService {
|
||||
|
||||
let plannedTimes = dto.plannedTimes;
|
||||
if (!plannedTimes || plannedTimes.length === 0) {
|
||||
plannedTimes = this.computePlannedTimes(route, dep, arr);
|
||||
plannedTimes = computePlannedStopTimes(route, dep, arr);
|
||||
}
|
||||
|
||||
const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
|
||||
@@ -634,6 +597,24 @@ export class SchedulesService {
|
||||
return { synced, errors };
|
||||
}
|
||||
|
||||
async recalculateStopTimes(scheduleId: string) {
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: scheduleId },
|
||||
include: { route: { include: { stops: { orderBy: { sequence: 'asc' } } } } },
|
||||
});
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
if (!schedule.routeId || !schedule.route) throw new BadRequestException('Schedule has no associated route');
|
||||
|
||||
const plannedTimes = computePlannedStopTimes(
|
||||
schedule.route,
|
||||
new Date(schedule.departureAt),
|
||||
new Date(schedule.arrivalAt),
|
||||
);
|
||||
const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
|
||||
await this.routesService.applyRouteToSchedule(schedule.routeId, scheduleId, plannedTimesMap);
|
||||
return { recalculated: true, scheduleId, stopCount: plannedTimes.length };
|
||||
}
|
||||
|
||||
async assignCoaches(scheduleId: string, coaches: Array<{ coachId: string; positionNumber: number }>) {
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: scheduleId } });
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
@@ -700,7 +681,7 @@ export class SchedulesService {
|
||||
include: { stops: { orderBy: { sequence: 'asc' } } },
|
||||
});
|
||||
if (route && route.stops.length >= 2) {
|
||||
const plannedTimes = this.computePlannedTimes(route, dep, arr);
|
||||
const plannedTimes = computePlannedStopTimes(route, dep, arr);
|
||||
const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
|
||||
await this.routesService.applyRouteToSchedule(schedule.routeId, id, plannedTimesMap);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user