mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
improve group window
This commit is contained in:
@@ -440,10 +440,14 @@ export class TrainSchedulingService {
|
||||
* caller then computes its own times as before — nothing changes for the
|
||||
* single-schedule case).
|
||||
*
|
||||
* The anchor is preferably a still-PRE_WINDOW sibling (its times are the live
|
||||
* group clock). If every sibling has already opened, we still copy the earliest
|
||||
* sibling's frozen times so the new train lines up with the group the customer
|
||||
* already sees rather than drifting onto its own `now`-based clock.
|
||||
* The anchor is the sibling that best represents where the GROUP currently is
|
||||
* on its shared clock, so a train created mid-cycle joins the group AT its
|
||||
* current phase (with the group's exact doc-review / payment deadlines) instead
|
||||
* of restarting the whole cycle on its own `now`. When the group has advanced
|
||||
* past PRE_WINDOW we pick the MOST-ADVANCED live (non-DONE) sibling — that is
|
||||
* the phase the joiner must adopt to see the same "N minutes left" the group
|
||||
* already shows. When every sibling is still PRE_WINDOW we pick the
|
||||
* earliest-opening one (the group's frozen open clock).
|
||||
*/
|
||||
private async findGroupWindowAnchor(
|
||||
manager: EntityManager,
|
||||
@@ -460,37 +464,67 @@ export class TrainSchedulingService {
|
||||
if (siblings.length === 0) return null;
|
||||
const withWindow = siblings.filter((s) => s.windowOpensAt != null);
|
||||
if (withWindow.length === 0) return null;
|
||||
|
||||
// A group whose window is live (some sibling has moved past PRE_WINDOW but is
|
||||
// not yet DONE) — the joiner must land in that same phase on the same clock.
|
||||
const PHASE_ORDER = ['PRE_WINDOW', 'OPEN', 'DOC_REVIEW', 'PAYMENT'];
|
||||
const live = withWindow.filter(
|
||||
(s) => s.windowPhase != null && PHASE_ORDER.includes(s.windowPhase) &&
|
||||
s.windowPhase !== 'PRE_WINDOW',
|
||||
);
|
||||
if (live.length > 0) {
|
||||
// Most-advanced phase leads; ties broken by earliest open for determinism.
|
||||
return live.reduce((best, s) => {
|
||||
const a = PHASE_ORDER.indexOf(s.windowPhase!);
|
||||
const b = PHASE_ORDER.indexOf(best.windowPhase!);
|
||||
if (a !== b) return a > b ? s : best;
|
||||
return s.windowOpensAt!.getTime() < best.windowOpensAt!.getTime() ? s : best;
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise the whole group is still PRE_WINDOW — earliest-opening sibling
|
||||
// defines the group clock (the one a customer would have seen first).
|
||||
const pending = withWindow.filter((s) => s.windowPhase === 'PRE_WINDOW');
|
||||
const pool = pending.length > 0 ? pending : withWindow;
|
||||
// Earliest-opening sibling defines the group clock — deterministic and the
|
||||
// one a customer would have seen first.
|
||||
return pool.reduce((earliest, s) =>
|
||||
s.windowOpensAt!.getTime() < earliest.windowOpensAt!.getTime() ? s : earliest,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The window fields (open/close times + frozen rule snapshot) an anchor sibling
|
||||
* hands down to the rest of its group. The shared open/close instants make every
|
||||
* schedule in the group advance through the SAME open, doc-review, payment, and
|
||||
* close instants on the shared 10s tick — doc-review/payment ends are derived
|
||||
* live from these shared times during phase advance, so they fall in sync.
|
||||
* The full window state an anchor sibling hands down to a schedule JOINING its
|
||||
* group. Copies not just the open/close times + frozen rule snapshot but the
|
||||
* anchor's LIVE PHASE and every phase-end timestamp (docReviewEndsAt,
|
||||
* paymentPhaseEndsAt, bookingCycleNo, bookingWindowStatus). This is what makes a
|
||||
* train created mid-cycle show the EXACT SAME "N minutes left" as the rest of
|
||||
* the group: it enters directly at the group's current phase with the group's
|
||||
* shared payment deadline, instead of restarting PRE_WINDOW→…→PAYMENT on its own
|
||||
* `now` and stamping its own later `paymentPhaseEndsAt`.
|
||||
*
|
||||
* `targetDeparture` is the JOINING schedule's own departure: the shared times
|
||||
* are clamped to it so a group whose trains depart at different times of the
|
||||
* same day never hands an earlier-departing train a window that outlives its
|
||||
* `targetDeparture` is the JOINING schedule's own departure: every time is
|
||||
* clamped to it so a group whose trains depart at different times of the same
|
||||
* day never hands an earlier-departing train a deadline that outlives its
|
||||
* departure (computeImport/ExportWindowTimes clamp to departure at source; this
|
||||
* preserves that invariant when the anchor departed later). A window that would
|
||||
* be entirely after this train's departure collapses to a zero-length window at
|
||||
* departure — truthful, not a window that never closes.
|
||||
* preserves that invariant when the anchor departed later).
|
||||
*/
|
||||
private groupWindowFieldsFrom(anchor: TrainSchedule, targetDeparture: Date) {
|
||||
const cap = targetDeparture.getTime();
|
||||
const clamp = (d: Date | null | undefined): Date | null =>
|
||||
d == null ? null : d.getTime() > cap ? targetDeparture : d;
|
||||
return {
|
||||
// Live phase + its deadlines — a mid-cycle joiner lands here directly.
|
||||
windowPhase: anchor.windowPhase,
|
||||
bookingWindowStatus:
|
||||
anchor.bookingWindowStatus === 'FULL'
|
||||
? 'OPEN'
|
||||
: anchor.bookingWindowStatus,
|
||||
bookingCycleNo: anchor.bookingCycleNo,
|
||||
windowOpensAt: clamp(anchor.windowOpensAt),
|
||||
windowClosesAt: clamp(anchor.windowClosesAt),
|
||||
docReviewEndsAt: clamp(anchor.docReviewEndsAt),
|
||||
docReviewCompletedAt: clamp(anchor.docReviewCompletedAt),
|
||||
paymentPhaseEndsAt: clamp(anchor.paymentPhaseEndsAt),
|
||||
// Frozen rule snapshot.
|
||||
ruleWindowOpenHour: anchor.ruleWindowOpenHour,
|
||||
ruleWindowCloseHour: anchor.ruleWindowCloseHour,
|
||||
ruleWindowDurationHours: anchor.ruleWindowDurationHours,
|
||||
|
||||
Reference in New Issue
Block a user