Files
edr-platform/apps/edr-passenger-api/src/modules/segments/trip-progress.service.ts

226 lines
6.1 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { EnhancedSeatsService } from './enhanced-seats.service';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TripProgressService {
constructor(
private prisma: PrismaService,
private enhancedSeatsService: EnhancedSeatsService,
private eventEmitter: EventEmitter2
) {}
/**
* Update trip progress and trigger seat releases
*/
async updateTripProgress(tripId: string, currentStationId: string, progressPercent: number) {
return this.prisma.$transaction(async (tx) => {
// 1. Update trip live status
await tx.tripLiveStatus.upsert({
where: { scheduleId: tripId },
update: {
currentLocationLabel: currentStationId,
progressPercent,
updatedAt: new Date()
},
create: {
scheduleId: tripId,
state: 'EN_ROUTE',
currentLocationLabel: currentStationId,
progressPercent,
delayMinutes: 0,
updatedAt: new Date()
}
});
// 2. Get station name for comparison
const station = await tx.station.findUnique({
where: { id: currentStationId }
});
if (station) {
// 3. Trigger seat release for passengers reaching destination
const releaseResult = await this.enhancedSeatsService.releaseSeats(tripId, currentStationId);
// 4. Emit progress update event
this.eventEmitter.emit('trip.progress.updated', {
tripId,
currentStation: station.name,
progressPercent,
releasedSeats: releaseResult.releasedSeats
});
return {
tripId,
currentStation: station.name,
progressPercent,
releasedSeats: releaseResult.releasedSeats.length,
updatedAt: new Date()
};
}
return { tripId, currentStation: currentStationId, progressPercent, releasedSeats: 0 };
});
}
/**
* Simulate trip progress (for testing/demo)
*/
async simulateTripProgress(tripId: string) {
const trip = await this.prisma.trainSchedule.findUnique({
where: { id: tripId },
include: {
stopTimes: {
include: { station: true },
orderBy: { sequence: 'asc' }
}
}
});
if (!trip) {
throw new Error('Trip not found');
}
// Simulate progress through each station
for (let i = 0; i < trip.stopTimes.length; i++) {
const stopTime = trip.stopTimes[i];
const progressPercent = Math.round((i / (trip.stopTimes.length - 1)) * 100);
await this.updateTripProgress(tripId, stopTime.stationId, progressPercent);
// Emit station arrival event
this.eventEmitter.emit('trip.station.arrived', {
tripId,
stationId: stopTime.stationId,
stationName: stopTime.station.name,
sequence: stopTime.sequence,
progressPercent
});
// Wait 30 seconds between stations (for demo)
await new Promise(resolve => setTimeout(resolve, 30000));
}
}
/**
* Handle trip completion
*/
@OnEvent('trip.completed')
async handleTripCompleted(payload: { tripId: string }) {
// Release all remaining seats for this trip
const trip = await this.prisma.trainSchedule.findUnique({
where: { id: payload.tripId },
include: {
coachAssignments: {
include: {
coach: {
include: {
seats: {
where: { status: 'BOOKED' }
}
}
}
}
}
}
});
if (trip) {
const bookedSeatIds = trip.coachAssignments.flatMap(assignment =>
assignment.coach.seats.map(seat => seat.id)
);
if (bookedSeatIds.length > 0) {
await this.prisma.seat.updateMany({
where: { id: { in: bookedSeatIds } },
data: { status: 'AVAILABLE' }
});
this.eventEmitter.emit('trip.seats.released', {
tripId: payload.tripId,
releasedSeats: bookedSeatIds
});
}
}
}
/**
* Background job to expire holds every minute
*/
@Cron(CronExpression.EVERY_MINUTE)
async expireHoldsJob() {
try {
const result = await this.enhancedSeatsService.expireHolds();
if (result.expiredHolds > 0) {
console.log(`Expired ${result.expiredHolds} holds, released ${result.releasedSeats.length} seats`);
}
} catch (error) {
console.error('Error expiring holds:', error);
}
}
/**
* Get current trip status with seat availability
*/
async getTripStatus(tripId: string) {
const trip = await this.prisma.trainSchedule.findUnique({
where: { id: tripId },
include: {
liveStatus: true,
stopTimes: {
include: { station: true },
orderBy: { sequence: 'asc' }
},
coachAssignments: {
include: {
coach: {
include: { seats: true }
}
}
}
}
});
if (!trip) {
throw new Error('Trip not found');
}
const seatSummary = {
total: 0,
available: 0,
held: 0,
booked: 0,
blocked: 0
};
trip.coachAssignments.forEach(assignment => {
assignment.coach.seats.forEach(seat => {
seatSummary.total++;
const status = seat.status.toLowerCase() as keyof typeof seatSummary;
if (status in seatSummary) {
seatSummary[status]++;
}
});
});
return {
tripId,
status: trip.status,
currentLocation: trip.liveStatus?.currentLocationLabel,
progressPercent: trip.liveStatus?.progressPercent || 0,
delayMinutes: trip.liveStatus?.delayMinutes || 0,
stations: trip.stopTimes.map(st => ({
id: st.stationId,
name: st.station.name,
sequence: st.sequence,
plannedArrival: st.plannedArrivalAt,
plannedDeparture: st.plannedDepartureAt,
actualArrival: st.actualArrivalAt
})),
seatSummary,
lastUpdated: trip.liveStatus?.updatedAt
};
}
}