Commiting stop based booking

This commit is contained in:
Muluhabt
2026-07-23 20:11:11 +03:00
parent 8bba882710
commit 64e1130f7d
14 changed files with 354 additions and 147 deletions

View File

@@ -10,7 +10,7 @@ import { CurrencyService } from "../currency/currency.service";
import { FareEngineService } from "../fare-engine/fare-engine.service";
import { SegmentsService } from "../segments/segments.service";
import { resolveCurrencyFromNationality } from "../fare-engine/fare-engine.dto";
import { Currency } from "@prisma/client";
import { Currency, Prisma } from "@prisma/client";
const POINTS_TO_MINOR = 10;
@@ -220,12 +220,18 @@ export class SearchService {
const totalPassengers = adultCount + (childCount ?? 0);
const NEEDED = 3;
const baseWhere = {
status: "SCHEDULED",
// Include BOARDING alongside SCHEDULED: BOARDING is just an operational display status the
// schedule-level cron sets on a fixed 30-min-before-departure timer (see tasks.service.ts) —
// it does NOT mean booking is closed. The actual booking cutoff is per-stop and configurable
// (RouteStop/Route.checkinMinutesBefore), enforced below by buildScheduleResult's own live
// check against each stop's estimated arrival/departure. Excluding BOARDING here would
// silently impose a hidden, non-configurable 30-minute cutoff on top of that.
const baseWhere: Prisma.TrainScheduleWhereInput = {
status: { in: ["SCHEDULED", "BOARDING", "EN_ROUTE"] },
isPackageOnly: false,
stopTimes: { some: { stationId: originStationId } },
coachAssignments: { some: {} },
} as const;
};
// Fetch candidates before and after in parallel; take more than needed to
// account for routes that don't serve the destination or have no availability.
@@ -300,23 +306,21 @@ export class SearchService {
const nextDay = new Date(
`${String(y)}-${String(m).padStart(2, "0")}-${String(d + 1).padStart(2, "0")}T00:00:00+03:00`,
);
const now = new Date();
const totalPassengers = adultCount + (childCount ?? 0);
// Use now as the lower bound for today so we don't fetch schedules that have
// already fully departed. The per-segment cutoff check in buildScheduleResult
// handles the exact check using each stop's own plannedDepartureAt.
const isToday =
now.getFullYear() === y &&
now.getMonth() === m - 1 &&
now.getDate() === d;
const earliest = isToday ? now : date;
// Match on the schedule's own departure DATE only — do NOT use `now` as a lower bound here.
// A schedule whose origin has already departed (EN_ROUTE) can still have a later stop (e.g.
// Lebu, Adama) whose own cutoff hasn't passed; using the overall departureAt as a floor would
// wrongly exclude the whole schedule for those still-bookable downstream segments. The
// per-segment cutoff check in buildScheduleResult is the sole authority for whether THIS
// specific origin stop is still bookable, using each stop's own estimated arrival/departure.
const schedules = await this.prisma.trainSchedule.findMany({
where: {
status: "SCHEDULED",
// EN_ROUTE/BOARDING included alongside SCHEDULED — these are operational display
// statuses, not booking-closed signals (see comment on searchAlternatives' baseWhere).
status: { in: ["SCHEDULED", "BOARDING", "EN_ROUTE"] },
isPackageOnly: false,
departureAt: { gte: earliest, lt: nextDay },
departureAt: { gte: date, lt: nextDay },
stopTimes: { some: { stationId: originStationId } },
coachAssignments: { some: {} },
},
@@ -368,7 +372,8 @@ export class SearchService {
const [leg1Schedules, allCandidates] = await Promise.all([
this.prisma.trainSchedule.findMany({
where: {
status: "SCHEDULED",
// BOARDING included alongside SCHEDULED — see comment on searchAlternatives' baseWhere.
status: { in: ["SCHEDULED", "BOARDING", "EN_ROUTE"] },
isPackageOnly: false,
departureAt: { gte: dayStart, lt: dayEnd },
stopTimes: { some: { stationId: originStationId } },
@@ -378,7 +383,7 @@ export class SearchService {
}),
this.prisma.trainSchedule.findMany({
where: {
status: "SCHEDULED",
status: { in: ["SCHEDULED", "BOARDING", "EN_ROUTE"] },
isPackageOnly: false,
departureAt: { gte: dayStart, lt: leg2WindowEnd },
coachAssignments: { some: {} },
@@ -547,13 +552,14 @@ export class SearchService {
if (!originStop || !destStop || originStop.sequence >= destStop.sequence)
return null;
// Segment-level cutoff: use the origin stop's planned departure, not the
// Segment-level cutoff: use the origin stop's own estimated arrival time, not the
// schedule's overall departureAt (which is station A's time). This lets
// B→D remain bookable even after A→D closes.
// B→D remain bookable even after A→D closes. The first stop has no arrival (nothing
// to arrive at), so it falls back to its own departure.
// Cutoff resolution: stop-level override → route default → 30 min fallback.
const now = new Date();
const segmentDepartureAt =
originStop.plannedDepartureAt ?? schedule.departureAt;
originStop.plannedArrivalAt ?? originStop.plannedDepartureAt ?? schedule.departureAt;
const routeStop = schedule.route?.stops?.find(
(s) => s.stationId === originStationId,
);