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 }; } }