All files / lib ist-time.ts

100% Statements 6/6
100% Branches 0/0
100% Functions 3/3
100% Lines 6/6

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81                                                                      10x     10x                         5x 5x                     1463x                             1460x    
/**
 * IST date helpers — the one place the API turns an instant into an Indian
 * calendar day.
 *
 * Workers run in UTC and the business runs in India. India has no DST and a
 * fixed +05:30 offset, so IST is reachable by arithmetic: shift by
 * `IST_OFFSET_MS` and read the *UTC* parts of the shifted instant. That is
 * deliberately the whole mechanism — `Intl`/timezone databases are not used,
 * and the Worker's own local time is never read, because both make the result
 * depend on the runtime rather than on the input.
 *
 * The bug this file exists to prevent: between 00:00 and 05:29 IST the IST
 * calendar date is one day *ahead* of the UTC date. Anything that counts "a
 * day" from `new Date().setHours(0,0,0,0)` therefore credits every message
 * sent in those five and a half hours to the previous day — which is five and
 * a half hours out of every twenty-four in which a "today" figure on an ops
 * screen is simply wrong.
 *
 * Provenance: these six exports are the generic half of the deleted
 * `src/lib/rrm/time.ts` (F-21), restored verbatim. The ladder-scheduling half
 * (quiet hours, jitter, `nextSendWindow`) and the financial-year half
 * (`fyStart`/`fyLabel`, for TDS on partner payouts) stay deleted — nothing
 * that survived F-21 needs them. The full original is at
 * `.context/briefs/rescued-rrm-time.ts` if FY reporting ever returns; the
 * 00:30 IST on 1 April boundary case documented there is the subtle part.
 *
 * Not in `packages/utils`: that package's bar is two or more consuming apps
 * (`docs/rules/shared-utilities.md`) and only the API needs this today.
 * Promote it if the marketplace ever asks.
 *
 * Everything public takes and returns `Date` (the codebase boundary type)
 * except `istParts`/`istDayStartMs`, which work in epoch ms.
 */
 
/** IST is UTC+05:30, all year, every year. */
export const IST_OFFSET_MS = 19_800_000;
 
/** One day in ms. Exported so callers do not keep their own copy. */
export const DAY_MS = 86_400_000;
 
export interface IstParts {
	/** 0–23, IST. */
	hour: number;
	/** 0–59. Non-zero on the half hour because the offset is :30, not :00. */
	minute: number;
	/** IST calendar date, `YYYY-MM-DD`. Compare calendar days against this. */
	dateISO: string;
}
 
/** Shift into IST, read the UTC parts. */
export function istParts(ms: number): IstParts {
	const d = new Date(ms + IST_OFFSET_MS);
	return {
		hour: d.getUTCHours(),
		minute: d.getUTCMinutes(),
		dateISO: d.toISOString().slice(0, 10),
	};
}
 
/** Epoch ms of 00:00 IST on the IST day containing `ms`. */
export function istDayStartMs(ms: number): number {
	// Flooring in shifted space is only exact because every IST day is exactly
	// DAY_MS long — no DST means no 23- or 25-hour days to special-case.
	return Math.floor((ms + IST_OFFSET_MS) / DAY_MS) * DAY_MS - IST_OFFSET_MS;
}
 
/**
 * The instant 00:00 IST began for the IST day containing `date`.
 *
 * Returned as a `Date` rather than epoch ms because the callers are day
 * counts — `gte(waMessages.dateCreated, istDayStart(now))` — and Drizzle's
 * `mode:"timestamp"` columns compare against `Date`.
 *
 * Note this is usually 18:30 UTC on the *previous* calendar day. Counting a
 * "day" from UTC midnight instead would credit the 00:00–05:29 IST activity to
 * the wrong day, and understate today's by exactly that much.
 */
export function istDayStart(date: Date): Date {
	return new Date(istDayStartMs(date.getTime()));
}