mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
Commiting stop based booking
This commit is contained in:
@@ -81,17 +81,23 @@ export class TasksService {
|
||||
byRoute.get(stop.routeId)!.push(stop.stationId);
|
||||
}
|
||||
|
||||
// Arrival basis: each stop's own estimated arrival time, not its departure. The first
|
||||
// stop of a route has no arrival (nothing to arrive at), so it falls back to its
|
||||
// departure — expressed below as COALESCE(plannedArrivalAt, plannedDepartureAt).
|
||||
let reopenedCount = 0;
|
||||
let checkinClosedCount = 0;
|
||||
for (const [mins, byRoute] of byMins) {
|
||||
const cutoffAt = new Date(now.getTime() + mins * 60 * 1000);
|
||||
for (const [routeId, stationIds] of byRoute) {
|
||||
// Revert first: if the cutoff was reduced, stops that were prematurely closed
|
||||
// should reopen (departure is still beyond the new cutoff window).
|
||||
// should reopen (arrival is still beyond the new cutoff window).
|
||||
const reverted = await this.prisma.tripStopTime.updateMany({
|
||||
where: {
|
||||
status: 'CHECKIN_CLOSED',
|
||||
plannedDepartureAt: { gt: cutoffAt },
|
||||
OR: [
|
||||
{ plannedArrivalAt: { gt: cutoffAt } },
|
||||
{ AND: [{ plannedArrivalAt: null }, { plannedDepartureAt: { gt: cutoffAt } }] },
|
||||
],
|
||||
stationId: { in: stationIds },
|
||||
schedule: { routeId },
|
||||
},
|
||||
@@ -103,7 +109,10 @@ export class TasksService {
|
||||
const closed = await this.prisma.tripStopTime.updateMany({
|
||||
where: {
|
||||
status: 'OPEN',
|
||||
plannedDepartureAt: { lte: cutoffAt },
|
||||
OR: [
|
||||
{ plannedArrivalAt: { lte: cutoffAt } },
|
||||
{ AND: [{ plannedArrivalAt: null }, { plannedDepartureAt: { lte: cutoffAt } }] },
|
||||
],
|
||||
stationId: { in: stationIds },
|
||||
schedule: { routeId },
|
||||
},
|
||||
@@ -169,8 +178,8 @@ export class TasksService {
|
||||
include: {
|
||||
originStation: { select: { name: true } },
|
||||
destinationStation: { select: { name: true } },
|
||||
stopTimes: { select: { stationId: true, plannedDepartureAt: true } },
|
||||
route: { select: { checkinMinutesBefore: true } },
|
||||
stopTimes: { select: { stationId: true, plannedArrivalAt: true, plannedDepartureAt: true } },
|
||||
route: { select: { checkinMinutesBefore: true, stops: { select: { stationId: true, checkinMinutesBefore: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -179,12 +188,17 @@ export class TasksService {
|
||||
for (const booking of bookings) {
|
||||
try {
|
||||
const createdAt = booking.createdAt as Date;
|
||||
// Use the booking's origin-segment departure and the route's own check-in window.
|
||||
// Use the booking's origin-segment estimated arrival (falling back to its departure
|
||||
// for the first stop) and that stop's own check-in window (falling back to the route
|
||||
// default), same resolution as holdSeats/search.
|
||||
const originStop = (booking.schedule as any).stopTimes?.find(
|
||||
(s: any) => s.stationId === (booking as any).originStationId,
|
||||
);
|
||||
const dep = (originStop?.plannedDepartureAt ?? booking.schedule.departureAt) as Date;
|
||||
const checkinMinutes = (booking.schedule as any).route?.checkinMinutesBefore ?? 30;
|
||||
const dep = (originStop?.plannedArrivalAt ?? originStop?.plannedDepartureAt ?? booking.schedule.departureAt) as Date;
|
||||
const originRouteStop = (booking.schedule as any).route?.stops?.find(
|
||||
(s: any) => s.stationId === (booking as any).originStationId,
|
||||
);
|
||||
const checkinMinutes = originRouteStop?.checkinMinutesBefore ?? (booking.schedule as any).route?.checkinMinutesBefore ?? 30;
|
||||
if (dep <= now) continue; // segment has already departed; cancel job handles clean-up
|
||||
const paymentDeadline = computePaymentDeadline(createdAt, dep, checkinMinutes);
|
||||
const totalWindowMs = paymentDeadline.getTime() - createdAt.getTime();
|
||||
@@ -230,13 +244,28 @@ export class TasksService {
|
||||
|
||||
// ── Cancel bookings whose payment deadline has passed ─────────────────────
|
||||
private async cancelExpiredPendingBookings(now: Date) {
|
||||
const twoHoursAgo = new Date(now.getTime() - MAX_PAYMENT_HOURS * 60 * 60 * 1000);
|
||||
const departureCutoff = new Date(now.getTime() + CUTOFF_MINUTES * 60 * 1000);
|
||||
const twoHoursAgo = new Date(now.getTime() - MAX_PAYMENT_HOURS * 60 * 60 * 1000);
|
||||
|
||||
// payment_deadline = MIN(createdAt + 2h, departureAt - 30min)
|
||||
// The departure pre-filter below is a query-scoping optimization only — the real
|
||||
// deadline check happens per-row further down. It must be widened to the largest
|
||||
// configured checkinMinutes across all routes/stops, or a booking on a route with a
|
||||
// cutoff bigger than the CUTOFF_MINUTES default would never even be fetched here,
|
||||
// silently never getting auto-cancelled.
|
||||
const [maxRouteCutoff, maxStopCutoff] = await Promise.all([
|
||||
this.prisma.route.aggregate({ _max: { checkinMinutesBefore: true } }),
|
||||
this.prisma.routeStop.aggregate({ _max: { checkinMinutesBefore: true } }),
|
||||
]);
|
||||
const effectiveMaxCutoffMinutes = Math.max(
|
||||
CUTOFF_MINUTES,
|
||||
maxRouteCutoff._max.checkinMinutesBefore ?? 0,
|
||||
maxStopCutoff._max.checkinMinutesBefore ?? 0,
|
||||
);
|
||||
const departureCutoff = new Date(now.getTime() + effectiveMaxCutoffMinutes * 60 * 1000);
|
||||
|
||||
// payment_deadline = MIN(createdAt + 2h, segment_arrival - checkinMinutes)
|
||||
// Deadline is reached when either branch of the MIN is in the past:
|
||||
// (a) createdAt ≤ now - 2h → 2-hour max window elapsed
|
||||
// (b) departureAt ≤ now + 30min → departure within 30 min
|
||||
// (a) createdAt ≤ now - 2h → 2-hour max window elapsed
|
||||
// (b) departureAt ≤ now + effectiveMaxCutoff → within the widest possible cutoff window
|
||||
const expiredBookings = await this.prisma.booking.findMany({
|
||||
where: {
|
||||
status: 'PENDING_PAYMENT',
|
||||
@@ -250,8 +279,8 @@ export class TasksService {
|
||||
include: {
|
||||
originStation: { select: { name: true } },
|
||||
destinationStation: { select: { name: true } },
|
||||
stopTimes: { select: { stationId: true, plannedDepartureAt: true } },
|
||||
route: { select: { checkinMinutesBefore: true } },
|
||||
stopTimes: { select: { stationId: true, plannedArrivalAt: true, plannedDepartureAt: true } },
|
||||
route: { select: { checkinMinutesBefore: true, stops: { select: { stationId: true, checkinMinutesBefore: true } } } },
|
||||
},
|
||||
},
|
||||
paymentIntent: { select: { method: true } },
|
||||
@@ -264,14 +293,19 @@ export class TasksService {
|
||||
for (const booking of expiredBookings) {
|
||||
try {
|
||||
// Re-verify exact deadline to avoid racing with a concurrent payment confirmation.
|
||||
// Use the booking's origin-segment departure for the deadline so that a B→C booking
|
||||
// on an A→B→C→D schedule gets the correct payment window anchored to B, not A.
|
||||
// Use the booking's origin-segment estimated arrival (falling back to its departure
|
||||
// for the first stop) and that stop's own check-in window, so a B→C booking on an
|
||||
// A→B→C→D schedule gets the correct payment window anchored to B, not A.
|
||||
const createdAt = booking.createdAt as Date;
|
||||
const originStop = (booking.schedule as any).stopTimes?.find(
|
||||
(s: any) => s.stationId === (booking as any).originStationId,
|
||||
);
|
||||
const dep = (originStop?.plannedDepartureAt ?? booking.schedule.departureAt) as Date;
|
||||
const paymentDeadline = computePaymentDeadline(createdAt, dep);
|
||||
const dep = (originStop?.plannedArrivalAt ?? originStop?.plannedDepartureAt ?? booking.schedule.departureAt) as Date;
|
||||
const originRouteStop = (booking.schedule as any).route?.stops?.find(
|
||||
(s: any) => s.stationId === (booking as any).originStationId,
|
||||
);
|
||||
const checkinMinutes = originRouteStop?.checkinMinutesBefore ?? (booking.schedule as any).route?.checkinMinutesBefore ?? CUTOFF_MINUTES;
|
||||
const paymentDeadline = computePaymentDeadline(createdAt, dep, checkinMinutes);
|
||||
if (now < paymentDeadline) continue;
|
||||
|
||||
// 1a. Release held seats (Journey rows are the occupancy source of truth once paid)
|
||||
|
||||
Reference in New Issue
Block a user