usage-admin.ts58 lines · main
1import { and, desc, eq, gte } from 'drizzle-orm';
2
3import { getDb } from '../db/client.js';
4import { usageEvents, type UsageEvent } from '../db/schema.js';
5
6/**
7 * Operator-facing read of usage_events. Filterable by polarPushStatus
8 * (pending / pushed / skipped) so an admin can spot a backlog. Most
9 * recent first; the unique index covers the order-by + filter.
10 */
11export async function listUsageEvents(args: {
12 limit?: number;
13 status?: string | undefined;
14}): Promise<UsageEvent[]> {
15 const db = getDb();
16 const limit = Math.min(Math.max(args.limit ?? 200, 1), 1000);
17 const status = args.status;
18 if (status === 'pending' || status === 'pushed' || status === 'skipped') {
19 return db
20 .select()
21 .from(usageEvents)
22 .where(eq(usageEvents.polarPushStatus, status))
23 .orderBy(desc(usageEvents.periodStart))
24 .limit(limit);
25 }
26 return db
27 .select()
28 .from(usageEvents)
29 .orderBy(desc(usageEvents.periodStart))
30 .limit(limit);
31}
32
33/**
34 * Flip recently-skipped usage_events rows back to 'pending' so the Polar
35 * push worker drains them on the next tick. Used as the recovery path
36 * after an operator fixes a config gap (e.g. set the missing meter id)
37 * — see docs/runbooks/polar-metering-setup.md §5. Bounded by a sinceDays
38 * window so a runaway click doesn't re-push years of stale rows that
39 * a customer already paid for through a manual reconciliation.
40 */
41export async function retrySkippedUsageEvents(args: {
42 sinceDays: number;
43}): Promise<{ retried: number }> {
44 // Clamp the window to a reasonable range — we never want to re-push
45 // anything older than the meta-DB retention itself, and same-day
46 // retries are the most common operator action.
47 const days = Math.min(Math.max(args.sinceDays, 1), 90);
48 const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
49 const db = getDb();
50 const result = await db
51 .update(usageEvents)
52 .set({ polarPushStatus: 'pending', polarPushedAt: null })
53 .where(
54 and(eq(usageEvents.polarPushStatus, 'skipped'), gte(usageEvents.periodStart, since)),
55 )
56 .returning({ id: usageEvents.id });
57 return { retried: result.length };
58}