1+Added: import { cycleBounds } from "./cycle";
2+Added: import { addInvoiceLineItem } from "./invoice";
3+Added: import { computeOverage } from "./overage";
4+Added: import { getQuota } from "./quota";
5+Added: import { getUsage } from "./usage";
6+Added: import { logger } from "../logger";
7+Added:
8+Added: /**
9+Added: * Runs once when a workspace's cycle ends. Overage is settled here, on the
10+Added: * closed cycle's final count, never while calls are still coming in.
11+Added: */
12+Added: export async function closeCycle(workspace: { id: string; billingAnchor: Date; stripeCustomerId: string }, cycleStart: Date) {
13+Added: const [calls, quota] = await Promise.all([getUsage(workspace.id, cycleStart), getQuota(workspace.id)]);
14+Added: const overage = computeOverage(calls, quota.includedCalls, quota.overageCentsPer1k);
15+Added: if (overage.cents > 0) {
16+Added: await addInvoiceLineItem(workspace.stripeCustomerId, {
17+Added: description: `API overage: ${overage.blocks.toLocaleString("en-US")} × 1,000 calls`,
18+Added: amountCents: overage.cents,
19+Added: idempotencyKey: `overage:${workspace.id}:${cycleStart.toISOString()}`,
20+Added: });
21+Added: }
22+Added: logger.info({ workspaceId: workspace.id, calls, overageCents: overage.cents }, "cycle closed");
23+Added: return { next: cycleBounds(workspace.billingAnchor, new Date(cycleStart.getTime() + 32 * 86_400_000)).start };
24+Added: }