mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 23:35:42 +00:00
Adding train delay minutes and also disabling no schedule days in the booking selection
This commit is contained in:
@@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, ParseInt
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
|
||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||
import { SchedulesService } from './schedules.service';
|
||||
import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto, BulkSchedulesResponseDto, TripStatus } from './schedules.dto';
|
||||
import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto, BulkSchedulesResponseDto, TripStatus, ApplyDelayDto } from './schedules.dto';
|
||||
import { PassengerAdmin, PassengerStaff } from '../../common/passenger-guards';
|
||||
import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry';
|
||||
|
||||
@@ -171,6 +171,22 @@ export class SchedulesController {
|
||||
@Body() dto: UpdateStopTimeDto,
|
||||
) { return this.service.updateStop(id, sequence, dto); }
|
||||
|
||||
@Post(':id/delay')
|
||||
@PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Report a delay — pushes every downstream stop\'s planned times (and check-in cutoffs) back by the same amount',
|
||||
description: `Shifts plannedArrivalAt/plannedDepartureAt on every stop not yet BOARDED/COMPLETED (or from fromSequence
|
||||
onward, if given) by delayMinutes. Since check-in cutoffs are derived directly from these planned
|
||||
times, this is the only action needed for booking closure to reflect the delay — no separate cutoff
|
||||
update. Also shifts the schedule's own departureAt/arrivalAt when the origin stop is included, and
|
||||
records the accumulated delay on the schedule's live status. Does not change schedule/stop status.`,
|
||||
})
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Schedule with shifted stop times' })
|
||||
applyDelay(@Param('id') id: string, @Body() dto: ApplyDelayDto) {
|
||||
return this.service.applyDelay(id, dto);
|
||||
}
|
||||
|
||||
@Put(':scheduleId/fares/:seatClassId')
|
||||
@PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({
|
||||
|
||||
@@ -113,6 +113,14 @@ export class UpdateScheduleStatusDto {
|
||||
@ApiProperty({ enum: TripStatus, example: TripStatus.EN_ROUTE }) @IsEnum(TripStatus) status: TripStatus;
|
||||
}
|
||||
|
||||
export class ApplyDelayDto {
|
||||
@ApiProperty({ example: 60, description: 'Minutes to shift downstream stop times by. Negative to correct an over-reported delay.' })
|
||||
@IsInt() delayMinutes: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 3, description: 'Only shift stops from this sequence onward. Omit to default to every stop not yet BOARDED/COMPLETED.' })
|
||||
@IsOptional() @IsInt() @Min(1) fromSequence?: number;
|
||||
}
|
||||
|
||||
export class BulkCreateSchedulesDto {
|
||||
@ApiProperty({ example: 'train-uuid', description: 'Train UUID' }) @IsString() trainId: string;
|
||||
@ApiProperty({ example: 'route-uuid', description: 'Route UUID' }) @IsString() routeId: string;
|
||||
|
||||
@@ -5,9 +5,10 @@ import { RoutesController } from './routes.controller';
|
||||
import { RoutesService } from './routes.service';
|
||||
import { FareEngineModule } from '../fare-engine/fare-engine.module';
|
||||
import { AuditModule } from '../../common/audit.module';
|
||||
import { LiveModule } from '../live/live.module';
|
||||
|
||||
@Module({
|
||||
imports: [FareEngineModule, AuditModule],
|
||||
imports: [FareEngineModule, AuditModule, LiveModule],
|
||||
controllers: [RoutesController, SchedulesController],
|
||||
providers: [RoutesService, SchedulesService],
|
||||
exports: [RoutesService, SchedulesService],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user