fix(stats): finalize snapshots on the verse-day window

A verse date is a player's local calendar day, spanning D-1 10:00Z
(UTC+14) through D+1 12:00Z (UTC-12). The old UTC end-of-day cutoff both
dropped late-night finishes in negative timezones and could persist a
partial day.

- Scope snapshot loads by the verse date column; use completedBefore only
  to pin an in-progress point-in-time build.
- Gate persistence on isDayClosed (D+1 12:00Z); open days are computed
  live and never written.
- Build the cron snapshot for latestClosedDate instead of EST yesterday,
  and read the same date for the community page.
- Add all-time best single day to snapshot totals.
- Bump PAYLOAD_SCHEMA_VERSION to 4 and extend snapshot/window tests.
This commit is contained in:
George Powell committed 2026-09-28 20:21:47 -04:00
1 parent 45ffa39f2a
commit a562ee3cc9
10 files changed
+198 -52

No files matched your search

+69
View File
@@ -11,9 +11,11 @@ import {
computePayload,
computePlayerState,
ensureSnapshot,
loadStatsData,
PAYLOAD_SCHEMA_VERSION,
type LoadedData
} from '$lib/server/stats/snapshot';
import { endOfVerseWindow, isDayClosed, latestClosedDate } from '$lib/server/stats/dates';
import { SETTINGS_DEFAULTS, type CompletionRow } from '$lib/types/stats';
import crypto from 'node:crypto';
@@ -72,6 +74,20 @@ describe('computePayload point-in-time cutoff', () => {
expect(payload.kpis.completionRate).not.toBeNull();
});
test('all-time best day picks the highest count and ignores future days', () => {
const payload = computePayload('2026-09-10', settings, {
...data,
completions: [
c('A', '2026-09-01'),
c('A', '2026-09-02'),
c('B', '2026-09-02'),
c('C', '2026-09-02'),
c('D', '2026-09-11') // future relative to asOfDate
]
});
expect(payload.totals.bestDay).toEqual({ date: '2026-09-02', count: 3 });
});
test('payload has all lab sections', () => {
const payload = computePayload('2026-09-10', settings, data);
expect(payload.retention.cohorts.length).toBeGreaterThan(0);
@@ -138,4 +154,57 @@ describe('snapshot persistence', () => {
const snap = await ensureSnapshot('2026-01-10', {}, testDb);
expect(snap.payload.meta?.schemaVersion).toBe(PAYLOAD_SCHEMA_VERSION);
});
test('groups completions by verse date, not a UTC 24h window', async () => {
// A player in a UTC-negative timezone finishing "Jan 10" late at night
// lands on Jan 11 04:00Z. The old UTC end-of-day cutoff dropped them.
await testDb.insert(dailyCompletions).values({
id: crypto.randomUUID(),
anonymousId: 'late',
date: '2026-01-10',
guessCount: 4,
completedAt: new Date('2026-01-11T04:00:00Z')
});
const { snapshot } = await buildSnapshot('2026-01-10', { kind: 'lazy' }, testDb);
expect(snapshot.payload.activity.dau).toBe(1);
});
test('isDayClosed gates persistence for in-progress days', async () => {
await seed();
const open = new Date().toISOString().slice(0, 10);
await buildSnapshot(open, { kind: 'lazy' }, testDb);
await buildSnapshot(open, { kind: 'lazy', replace: true }, testDb);
const live = await ensureSnapshot(open, {}, testDb);
expect(live.date).toBe(open);
// None of the open-day builds may leave a persisted row behind.
expect(await testDb.select().from(statsSnapshot)).toHaveLength(0);
});
});
describe('verse day window', () => {
test('closes at D+1 12:00Z (UTC-12 has finished the local day)', () => {
expect(endOfVerseWindow('2026-01-10').toISOString()).toBe('2026-01-11T12:00:00.000Z');
expect(isDayClosed('2026-01-10', new Date('2026-01-11T11:59:59Z'))).toBe(false);
expect(isDayClosed('2026-01-10', new Date('2026-01-11T12:00:00Z'))).toBe(true);
});
test('latestClosedDate reflects the window lag', () => {
expect(latestClosedDate(new Date('2026-01-11T11:00:00Z'))).toBe('2026-01-09');
expect(latestClosedDate(new Date('2026-01-11T13:00:00Z'))).toBe('2026-01-10');
});
test('completedBefore pins a point-in-time build', async () => {
await testDb.delete(analyticsEvents);
await testDb.delete(dailyCompletions);
await testDb.insert(dailyCompletions).values([
{ id: crypto.randomUUID(), anonymousId: 'early', date: '2026-01-10', guessCount: 1, completedAt: new Date('2026-01-10T05:00:00Z') },
{ id: crypto.randomUUID(), anonymousId: 'late', date: '2026-01-10', guessCount: 2, completedAt: new Date('2026-01-11T04:00:00Z') }
]);
const pinned = await loadStatsData('2026-01-10', testDb, {
completedBefore: new Date('2026-01-10T12:00:00Z')
});
expect(pinned.completions.map((c) => c.anonymousId)).toEqual(['early']);
const full = await loadStatsData('2026-01-10', testDb);
expect(full.completions).toHaveLength(2);
});
});