usage-admin.ts58 lines · main
| 1 | import { and, desc, eq, gte } from 'drizzle-orm'; |
| 2 | |
| 3 | import { getDb } from '../db/client.js'; |
| 4 | import { 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 | */ |
| 11 | export 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 | */ |
| 41 | export 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 | } |