mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
Merge pull request #646 from Tria-plc/freight_feature/usermanagement
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
|
* caller then computes its own times as before — nothing changes for the
|
||||||
* single-schedule case).
|
* single-schedule case).
|
||||||
*
|
*
|
||||||
* The anchor is preferably a still-PRE_WINDOW sibling (its times are the live
|
* The anchor is the sibling that best represents where the GROUP currently is
|
||||||
* group clock). If every sibling has already opened, we still copy the earliest
|
* on its shared clock, so a train created mid-cycle joins the group AT its
|
||||||
* sibling's frozen times so the new train lines up with the group the customer
|
* current phase (with the group's exact doc-review / payment deadlines) instead
|
||||||
* already sees rather than drifting onto its own `now`-based clock.
|
* 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(
|
private async findGroupWindowAnchor(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
@@ -460,37 +464,67 @@ export class TrainSchedulingService {
|
|||||||
if (siblings.length === 0) return null;
|
if (siblings.length === 0) return null;
|
||||||
const withWindow = siblings.filter((s) => s.windowOpensAt != null);
|
const withWindow = siblings.filter((s) => s.windowOpensAt != null);
|
||||||
if (withWindow.length === 0) return 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 pending = withWindow.filter((s) => s.windowPhase === 'PRE_WINDOW');
|
||||||
const pool = pending.length > 0 ? pending : withWindow;
|
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) =>
|
return pool.reduce((earliest, s) =>
|
||||||
s.windowOpensAt!.getTime() < earliest.windowOpensAt!.getTime() ? s : earliest,
|
s.windowOpensAt!.getTime() < earliest.windowOpensAt!.getTime() ? s : earliest,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The window fields (open/close times + frozen rule snapshot) an anchor sibling
|
* The full window state an anchor sibling hands down to a schedule JOINING its
|
||||||
* hands down to the rest of its group. The shared open/close instants make every
|
* group. Copies not just the open/close times + frozen rule snapshot but the
|
||||||
* schedule in the group advance through the SAME open, doc-review, payment, and
|
* anchor's LIVE PHASE and every phase-end timestamp (docReviewEndsAt,
|
||||||
* close instants on the shared 10s tick — doc-review/payment ends are derived
|
* paymentPhaseEndsAt, bookingCycleNo, bookingWindowStatus). This is what makes a
|
||||||
* live from these shared times during phase advance, so they fall in sync.
|
* 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
|
* `targetDeparture` is the JOINING schedule's own departure: every time is
|
||||||
* are clamped to it so a group whose trains depart at different times of the
|
* clamped to it so a group whose trains depart at different times of the same
|
||||||
* same day never hands an earlier-departing train a window that outlives its
|
* day never hands an earlier-departing train a deadline that outlives its
|
||||||
* departure (computeImport/ExportWindowTimes clamp to departure at source; this
|
* departure (computeImport/ExportWindowTimes clamp to departure at source; this
|
||||||
* preserves that invariant when the anchor departed later). A window that would
|
* preserves that invariant when the anchor departed later).
|
||||||
* be entirely after this train's departure collapses to a zero-length window at
|
|
||||||
* departure — truthful, not a window that never closes.
|
|
||||||
*/
|
*/
|
||||||
private groupWindowFieldsFrom(anchor: TrainSchedule, targetDeparture: Date) {
|
private groupWindowFieldsFrom(anchor: TrainSchedule, targetDeparture: Date) {
|
||||||
const cap = targetDeparture.getTime();
|
const cap = targetDeparture.getTime();
|
||||||
const clamp = (d: Date | null | undefined): Date | null =>
|
const clamp = (d: Date | null | undefined): Date | null =>
|
||||||
d == null ? null : d.getTime() > cap ? targetDeparture : d;
|
d == null ? null : d.getTime() > cap ? targetDeparture : d;
|
||||||
return {
|
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),
|
windowOpensAt: clamp(anchor.windowOpensAt),
|
||||||
windowClosesAt: clamp(anchor.windowClosesAt),
|
windowClosesAt: clamp(anchor.windowClosesAt),
|
||||||
|
docReviewEndsAt: clamp(anchor.docReviewEndsAt),
|
||||||
|
docReviewCompletedAt: clamp(anchor.docReviewCompletedAt),
|
||||||
|
paymentPhaseEndsAt: clamp(anchor.paymentPhaseEndsAt),
|
||||||
|
// Frozen rule snapshot.
|
||||||
ruleWindowOpenHour: anchor.ruleWindowOpenHour,
|
ruleWindowOpenHour: anchor.ruleWindowOpenHour,
|
||||||
ruleWindowCloseHour: anchor.ruleWindowCloseHour,
|
ruleWindowCloseHour: anchor.ruleWindowCloseHour,
|
||||||
ruleWindowDurationHours: anchor.ruleWindowDurationHours,
|
ruleWindowDurationHours: anchor.ruleWindowDurationHours,
|
||||||
|
|||||||
Reference in New Issue
Block a user