mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 21:43:27 +00:00
auto allocation and batch managemnt, tracking the train
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { BATCH_TIMEZONE } from './booking-batch.constants';
|
||||
|
||||
/** EAT intake boundaries — cron runs at these hours; each window spans to the next. */
|
||||
export const BATCH_WINDOW_START_HOURS = [7, 10, 13, 16, 19, 22] as const;
|
||||
|
||||
export interface BatchWindow {
|
||||
key: string;
|
||||
label: string;
|
||||
start: Date;
|
||||
end: Date;
|
||||
}
|
||||
|
||||
type EatDateParts = {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
hour: number;
|
||||
minute: number;
|
||||
};
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
timeZone: BATCH_TIMEZONE,
|
||||
});
|
||||
|
||||
const timeFmt = new Intl.DateTimeFormat('en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: BATCH_TIMEZONE,
|
||||
});
|
||||
|
||||
function eatParts(date: Date): EatDateParts {
|
||||
const parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: BATCH_TIMEZONE,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).formatToParts(date);
|
||||
|
||||
const get = (type: Intl.DateTimeFormatPartTypes) =>
|
||||
Number(parts.find((p) => p.type === type)?.value ?? 0);
|
||||
|
||||
return {
|
||||
year: get('year'),
|
||||
month: get('month'),
|
||||
day: get('day'),
|
||||
hour: get('hour'),
|
||||
minute: get('minute'),
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a UTC Date for a given EAT local wall-clock time on a calendar day. */
|
||||
function eatToUtc(
|
||||
year: number,
|
||||
month: number,
|
||||
day: number,
|
||||
hour: number,
|
||||
minute = 0,
|
||||
): Date {
|
||||
// EAT is UTC+3 year-round (no DST). Binary search would be safer across DST zones;
|
||||
// for Africa/Addis_Ababa the offset is fixed.
|
||||
const utcMs = Date.UTC(year, month - 1, day, hour - 3, minute, 0, 0);
|
||||
return new Date(utcMs);
|
||||
}
|
||||
|
||||
function formatWindowLabel(start: Date, end: Date, endHourLabel?: string): string {
|
||||
const endTime = endHourLabel ?? timeFmt.format(new Date(end.getTime() - 60_000));
|
||||
return `${dateFmt.format(start)} · ${timeFmt.format(start)} – ${endTime} EAT`;
|
||||
}
|
||||
|
||||
function windowFromEatStart(
|
||||
year: number,
|
||||
month: number,
|
||||
day: number,
|
||||
startHour: number,
|
||||
): BatchWindow {
|
||||
const start = eatToUtc(year, month, day, startHour);
|
||||
let endYear = year;
|
||||
let endMonth = month;
|
||||
let endDay = day;
|
||||
let endHour: number;
|
||||
let endHourLabel: string;
|
||||
|
||||
const idx = BATCH_WINDOW_START_HOURS.indexOf(startHour as (typeof BATCH_WINDOW_START_HOURS)[number]);
|
||||
if (idx === BATCH_WINDOW_START_HOURS.length - 1) {
|
||||
endHour = 7;
|
||||
endHourLabel = '07:00';
|
||||
const next = new Date(eatToUtc(year, month, day, 0));
|
||||
next.setUTCDate(next.getUTCDate() + 1);
|
||||
const nextParts = eatParts(next);
|
||||
endYear = nextParts.year;
|
||||
endMonth = nextParts.month;
|
||||
endDay = nextParts.day;
|
||||
} else {
|
||||
endHour = BATCH_WINDOW_START_HOURS[idx + 1];
|
||||
endHourLabel = `${String(endHour).padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
const end = eatToUtc(endYear, endMonth, endDay, endHour);
|
||||
return {
|
||||
key: start.toISOString(),
|
||||
start,
|
||||
end,
|
||||
label: formatWindowLabel(start, end, endHourLabel),
|
||||
};
|
||||
}
|
||||
|
||||
/** Which 3h EAT intake window a timestamp (e.g. fullyExecutedAt) belongs to. */
|
||||
export function getBatchWindowForTimestamp(date: Date): BatchWindow {
|
||||
const { year, month, day, hour } = eatParts(date);
|
||||
|
||||
if (hour < 7) {
|
||||
const prev = new Date(eatToUtc(year, month, day, 0));
|
||||
prev.setUTCDate(prev.getUTCDate() - 1);
|
||||
const prevParts = eatParts(prev);
|
||||
return windowFromEatStart(prevParts.year, prevParts.month, prevParts.day, 22);
|
||||
}
|
||||
|
||||
let startHour: (typeof BATCH_WINDOW_START_HOURS)[number] = 7;
|
||||
for (const h of BATCH_WINDOW_START_HOURS) {
|
||||
if (hour >= h) startHour = h;
|
||||
}
|
||||
|
||||
return windowFromEatStart(year, month, day, startHour);
|
||||
}
|
||||
|
||||
/** All six intake windows for an EAT calendar day (includes overnight 22:00–07:00). */
|
||||
export function listBatchWindowsForDate(reference: Date): BatchWindow[] {
|
||||
const { year, month, day } = eatParts(reference);
|
||||
return BATCH_WINDOW_START_HOURS.map((startHour) =>
|
||||
windowFromEatStart(year, month, day, startHour),
|
||||
);
|
||||
}
|
||||
|
||||
export function compareBatchWindows(a: BatchWindow, b: BatchWindow): number {
|
||||
return a.start.getTime() - b.start.getTime();
|
||||
}
|
||||
|
||||
/** Schedule-day windows plus any extra windows that contain booking timestamps (cross-day). */
|
||||
export function listBatchWindowsForBookings(
|
||||
timestamps: Array<Date | null | undefined>,
|
||||
referenceDate: Date,
|
||||
): BatchWindow[] {
|
||||
const byKey = new Map<string, BatchWindow>();
|
||||
for (const w of listBatchWindowsForDate(referenceDate)) {
|
||||
byKey.set(w.key, w);
|
||||
}
|
||||
for (const ts of timestamps) {
|
||||
if (!ts) continue;
|
||||
const w = getBatchWindowForTimestamp(ts);
|
||||
byKey.set(w.key, w);
|
||||
}
|
||||
return [...byKey.values()].sort(compareBatchWindows);
|
||||
}
|
||||
|
||||
/** Group items by batch window key; items without a timestamp go to `pendingKey`. */
|
||||
export function groupByBatchWindow<T>(
|
||||
items: T[],
|
||||
getTimestamp: (item: T) => Date | null | undefined,
|
||||
referenceDate: Date,
|
||||
pendingKey = 'pending-contract',
|
||||
): Map<string, { window: BatchWindow | null; items: T[] }> {
|
||||
const timestamps = items.map(getTimestamp);
|
||||
const windows = listBatchWindowsForBookings(timestamps, referenceDate);
|
||||
const map = new Map<string, { window: BatchWindow | null; items: T[] }>();
|
||||
|
||||
for (const w of windows) {
|
||||
map.set(w.key, { window: w, items: [] });
|
||||
}
|
||||
map.set(pendingKey, { window: null, items: [] });
|
||||
|
||||
for (const item of items) {
|
||||
const ts = getTimestamp(item);
|
||||
if (!ts) {
|
||||
map.get(pendingKey)!.items.push(item);
|
||||
continue;
|
||||
}
|
||||
const w = getBatchWindowForTimestamp(ts);
|
||||
if (!map.has(w.key)) {
|
||||
map.set(w.key, { window: w, items: [] });
|
||||
}
|
||||
map.get(w.key)!.items.push(item);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
Reference in New Issue
Block a user