diff --git a/CONTEXT.md b/CONTEXT.md index 1df829658d1..5c8cb51c835 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,7 +2,7 @@ ## Scope -Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contract defines Code Reviewer, Security Agent, Cost Insights, Auto Routing, and Kilo Pass for Organizations language plus ownership boundaries used across review execution, analytics, sync, web, email, remediation, billing alerts, model selection, tests, and product documentation. +Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contract defines Code Reviewer, Security Agent, Auto Routing, and Kilo Pass for Organizations language plus ownership boundaries used across review execution, analytics, sync, web, email, remediation, billing alerts, model selection, tests, and product documentation. Covered domains must use this contract's canonical terms in code, docs, task descriptions, tests, and agent outputs. Do not introduce a synonym for a governed concept without updating this contract. Scoped `AGENTS.md` files link to this contract instead of copying it. @@ -15,7 +15,7 @@ Covered domains must use this contract's canonical terms in code, docs, task des | **Security Sync** | Dependabot synchronization, finding persistence, notification eligibility, recipient intent materialization, and durable notification state | `services/security-sync/` | Event state remains owner-scoped; email sending does not occur inside finding persistence transactions | | **Security Agent Email Delivery** | Dispatch-time revalidation, email rendering, owner-aware links, and Mailgun delivery | `apps/web/src/app/api/internal/security-agent/`, `apps/web/src/lib/email.ts`, `apps/web/src/emails/` | Accepts notification identity only and loads current data before sending | | **Shared Security Notification Policy** | Canonical config parsing, defaults, severity thresholds, and pure event eligibility rules | `packages/worker-utils/src/security-notification-policy.ts` | Web and Worker must use same policy contract | -| **Cost Insights** | Retiring. Spend Alerts policy, alert history, and owner-scoped spend alerting | `apps/web/src/lib/cost-insights/`, `apps/web/src/app/api/cron/cost-insights-*` | All Cost Insights UI, routes, and tRPC procedures are removed; remaining evaluation, rollup, and retention code is being retired next | +| **Cost Insights** | Retired. No owned behaviour remains | `apps/web/src/components/cost-insights/CostInsightsDiscontinuedNotice.tsx` | Only the discontinued-notice routes remain; the unused `cost_insight_*` tables are scheduled to be dropped | | **Auto Routing** | Efficient model-pool settings, shared benchmark profiles, and per-request model selection | `packages/auto-routing-contracts/`, `services/auto-routing/`, `services/auto-routing-benchmark/`, `apps/web/src/components/auto-routing/` | Personal and organization settings constrain the existing `kilo-auto/efficient` model | | **Kilo Pass for Organizations** | Organization-owned Kilo Pass agreements, term versions, purchased pass capacity, sub-org allocation, and pooled bonus unlock behavior | Organization billing, seats, credits, sub-orgs, and Kilo Pass org administration surfaces | Separate source of truth from personal Kilo Pass subscriptions | @@ -41,21 +41,9 @@ Covered domains must use this contract's canonical terms in code, docs, task des | **Email Delivery** | Attempt to render and send one Security Agent Notification through Mailgun | Referring to provider side effect, retry, or acceptance | Notification event | | **Security Finding Activity Event** | Immutable record of one material user, system-policy, or source-driven action or outcome that changes or explains a Security Finding | Referring to evidence included in a Security Agent Audit Report | Page view, unchanged sync observation, queue claim, heartbeat | | **Security Agent Audit Report** | Owner-scoped, period-bounded audit view of Security Finding Activity Events grouped by Security Finding | Referring to the interactive audit report | Generic audit-log export, activity dump | -| **Cost Insights** | Retired product surface for viewing spend evidence, configuring Spend Alerts, and acting on Cost Suggestions | Referring to the removed surface or to remaining server-side spend-alert code awaiting removal | Spend Protection, Cost Controls, new Cost Insights UI work | -| **Spend Alerts** | Owner-scoped alerting capability for unusual or excessive Credit spend | Referring to alert evaluation, emails, banners, settings, or notification policy | Spend Protection, hard limit, spend blocker | -| **Cost Suggestion** | Optional owner-scoped recommendation based on observed Credit spend that may improve cost efficiency through an eligible Coding Plan or Kilo Pass | Referring to recommendation evaluation, dashboard cards, emails, CTA destinations, dismissal, or settings | Alert, warning, guaranteed savings, automatic optimization | -| **Suggestion dismissal** | Authorized owner action that hides one Cost Suggestion without changing billing or future suggestion eligibility | Referring to dismissing a recommendation | Alert acknowledgment, unsubscribe, disable suggestions | -| **Spend owner** | Personal user or organization whose credit balance is charged for Credit spend | Referring to the Spend Alerts policy and evaluation boundary | Account when personal/org ambiguity matters | | **Efficient model pool** | The exact model and thinking-variant pairs that `kilo-auto/efficient` may consider for an owner | Referring to either the platform default or an owner-configured pool | Custom mode, custom efficient model | | **Pool entry** | One concrete model paired with its canonical thinking-variant key, or the model's default only when it exposes no variants | Referring to benchmark and routing identity inside an Efficient model pool | Model alone, normalized effort | | **Benchmark profile** | Globally shared, current benchmark results for one Pool entry | Referring to readiness and measured routing evidence reused across owners | User benchmark, pool benchmark | -| **Spend Anomaly Alert** | Spend Alert triggered when short-window owner Credit spend exceeds that owner's normal usage pattern | Referring to hourly burst-detection Spend Alerts | Low-balance alert, threshold alert | -| **Variable Credit spend** | Credit spend created by request-metered product usage such as token usage or metered tool/API usage | Referring to spend that can burst unexpectedly during active usage | Scheduled Credit spend | -| **Scheduled Credit spend** | Predictable Credit spend created by subscription-like purchases, renewals, or hosting deductions | Referring to expected recurring or explicitly purchased credit deductions | Variable Credit spend | -| **Spend Threshold Alert** | Spend Alert triggered when owner Credit spend crosses a configured rolling 24-hour, rolling 7-day, or rolling 30-day spend threshold | Referring to threshold notification identity or review | Warning threshold, critical threshold, quota | -| **Spend threshold** | Optional configured rolling 24-hour, rolling 7-day, or rolling 30-day owner Credit-spend amount for Spend Threshold Alerts | Referring to any supported threshold window | Hard limit, budget cap, daily quota | -| **Alert acknowledgment** | Authorized owner action that marks the current alert episode as reviewed | Referring to review without changing settings | Email open, page view, passive acknowledge | -| **Cost Insight Event** | Durable owner-scoped record of Spend Alert notifications, reviews, configuration changes, and disablement | Referring to 90-day Cost Insights history | Raw usage row, provider log | | **Kilo Pass for Organizations** | Organization-owned Kilo Pass product that buys pass capacity for parent-org paid seats and allocates pooled credit capacity into parent or direct child containers | Referring to org Kilo Pass agreements, purchases, allocations, and admin workflows | Personal Kilo Pass, user Kilo Pass subscription | | **Kilo Pass org agreement** | Durable org-owned source of truth for one organization's Kilo Pass terms, version, purchase channel, and lifecycle | Preserving self-serve and manual org Kilo Pass terms | Personal subscription row, seat purchase row | | **Kilo Pass term version** | Immutable versioned commercial and credit-grant rules attached to a Kilo Pass org agreement; standard and dedicated custom versions are allowed | Referring to legacy upfront-bonus terms, default bonus-after-base terms, or future bonus changes | Promo flag, mutable agreement fields, plan metadata | @@ -96,7 +84,6 @@ Covered domains must use this contract's canonical terms in code, docs, task des - An **Email Delivery** realizes a durable **Security Agent Notification** and may be retried without creating new event identity. - A **Security Remediation** belongs to one **Security Finding** and can have one or more **Security Remediation Attempts**. - A **Security Finding Activity Event** belongs to one Security Agent owner and one Security Finding, including after that finding is deleted. -- **Spend Alerts** and **Cost Suggestions** belong to exactly one **Spend owner**: one personal user or one organization. - An **Efficient model pool** belongs to one personal user or organization; an organization pool overrides members' personal pools, while no configured pool inherits the next available setting and then the platform default. - A **Pool entry** has at most one current **Benchmark profile** for a benchmark-engine version, shared by every owner that selects that entry. - `kilo-auto/efficient` considers only ready, request-compatible Pool entries from the effective Efficient model pool; its balanced failure fallback remains outside pool membership. @@ -167,81 +154,9 @@ Covered domains must use this contract's canonical terms in code, docs, task des - **Kilo Pass processing runs** use leased idempotent retries, metrics, and manual replay. Blocked window shows persistent subscription-page status and sends one deduplicated email to current parent organization owners and billing managers. - The product does not support creating new custom or manual agreements. Platform admins may maintain imported legacy agreements by setting paid-through intervals, designating manual processing, scheduling commercial transitions, issuing audited compensation, or manually retrying; each mutation records actor, reason, before/after values, and timestamp. - Organization subscription page supports initial direct-child distribution before first issuance, then separates current immutable issuance snapshot from next **Kilo Pass allocation plan** and effective date; it shows purchased, parent-default, direct-child allocated, paid-through, pending-cancellation, blocked, and overallocated state. -- **Cost Insights is being retired.** Its UI is removed: no Cost Insights routes, sidebar entries, dashboards, settings screens, banners, or tRPC procedures exist, and none may be added. Remaining server-side spend-alert evaluation, rollup maintenance, notification, and event-retention code is scheduled for removal, so treat the rules below as describing code awaiting deletion rather than a live product surface. -- All Credit spend charged to a **Spend owner** counts toward that owner's Spend Alerts and Cost Suggestion evaluation. -- **Cost Suggestions** are enabled by default, independent from Spend Alerts, and recommend an eligible Coding Plan or Kilo Pass when observed Credit spend indicates potential cost-efficiency benefit. -- Cost Suggestions are advisory. They do not guarantee savings, automatically purchase or change subscriptions, or alter spend behavior. -- Every active Cost Suggestion provides one destination CTA and a **Suggestion dismissal** action. -- Suggestion dismissal hides that specific recommendation, creates a **Cost Insight Event**, and does not disable future materially different Cost Suggestions. -- Disabling Cost Suggestions suppresses new suggestion emails and active dashboard suggestions but preserves prior suggestion activity history. -- **Spend Alerts** are inactive until a **Spend owner** explicitly enables them. -- **Spend Anomaly Alerts** are enabled by default whenever **Spend Alerts** are enabled, and Spend owners may opt out independently. -- First enable immediately evaluates each enabled alert sub-option: anomaly state plus configured rolling 24-hour, rolling 7-day, and rolling 30-day **Spend threshold** state. -- First enable can create alert email and banner when current spend already crosses enabled alert state. -- **Spend Alerts** are alert-only. They do not block spend, pause usage, throttle usage, suppress auto-top-up, reject paid requests, or return Spend Alerts-specific HTTP 402 responses. -- Existing low-balance and depleted-credit billing behavior remains separate from **Spend Alerts**. -- **Spend Anomaly Alerts** detect hourly Credit-spend bursts, not only daily spend increases. -- **Spend Anomaly Alerts** evaluate bursty **Variable Credit spend** separately from predictable **Scheduled Credit spend**. -- **Spend Anomaly Alerts** use a Postgres owner-hourly spend rollup, not warehouse-only hourly analytics. -- Spend Alerts hourly rollups are maintained for all **Spend owners**, including owners who have not enabled Spend Alerts. -- V1 **Spend Anomaly Alert** sensitivity is product-managed and fixed; users cannot configure sensitivity, custom multipliers, or custom floors. -- Default **Spend Anomaly Alert** baseline is trailing 7-day hourly p95 **Variable Credit spend**. -- Spend Anomaly Alert baseline uses completed prior UTC-hour buckets and excludes the current UTC hour. -- Owners with at least 24 completed hourly buckets use available-history p95 even before 7 full days exist. -- Owners without at least 24 hourly baseline buckets use a starter current-hour **Variable Credit spend** floor for **Spend Anomaly Alerts**. -- **Spend Anomaly Alerts** may fire at most once per owner per hour while anomalous spend persists. -- **Spend Threshold Alerts** use separate notification identity from **Spend Anomaly Alerts** and from each other threshold window. -- Spend owners may configure independent optional rolling 24-hour, rolling 7-day, and rolling 30-day **Spend thresholds**, stored as microdollars and displayed with cent precision. -- The threshold windows are exact half-open intervals: `[asOf - 24h, asOf)`, `[asOf - 7d, asOf)`, and `[asOf - 30d, asOf)`. -- Each **Spend Threshold Alert** evaluates all owner Credit spend in its configured rolling window, including **Variable Credit spend** and **Scheduled Credit spend**. -- Each threshold window maintains separate crossing, review, recovery, and notification identity; it fires once per below-to-above crossing and may fire again only after spend in that window drops below its threshold and later crosses it again. -- **Alert acknowledgment** reviews the current anomaly or threshold episode without requiring settings changes. -- Active threshold alerts expose snapshotted top drivers from the exact evaluated rolling window across Variable and Scheduled Credit spend. -- Spend Alert emails are no longer sent. Recipient rows are still created and are drained to `skipped`. Historically, recipient selection was admin-gated for personal owners only; organization recipients were any membership with role `owner` or `billing_manager`, without a platform-admin or active-status filter. -- Spend Alerts store owner-scoped **Cost Insight Events** separately from per-recipient notification delivery rows. -- Spend Alerts snapshot intended notification recipients at event creation and revalidate recipient access before delivery. -- Spend Alerts notification delivery rows are deleted with their parent **Cost Insight Event** after 90 days. -- **Spend Alerts** produce no user-visible output. Email sending and the in-app banner were both removed, and no mobile or push notifications exist. -- The removed Cost Insights routes serve a discontinued notice so old bookmarks and previously sent alert emails do not land on a 404. -- Cost Insights retains and displays 90 days of **Cost Insight Events**. -- **Cost Insight Events** include configuration changes, anomaly alerts, threshold alerts, reviews, and disablement. -- Cost Insight Event history remains fixed to 90 days even though hourly rollups are retained indefinitely. -- Cost Insight Events are deleted after 90 days rather than merely hidden. -- Cost Insight Event retention is enforced by daily app cron deletion. -- **Cost Insight Events** store summarized decision snapshots such as threshold, rolling spend totals, current-hour variable spend, baseline, and top driver dimensions. They do not copy raw request rows. -- Alert **Cost Insight Events** snapshot top 5 spend drivers at event creation time. -- Cost Insight Events store direct evaluated settings in snapshots and do not require config version tracking in v1. -- Spend Alert config events store changed fields plus resulting key settings, not full config snapshots. -- Kilo platform admins may inspect organization Spend Alerts under existing admin patterns, but v1 disable and settings changes require owner or billing-manager authority. -- Spend Alert config and review actions do not require reason text in v1; events record actor, action, old and new values where applicable, and timestamp. -- Disabling Spend Alerts keeps the owner config row disabled rather than deleting it. -- Re-enabling Spend Alerts reuses existing saved settings unless an authorized manager changes them. -- Re-enabling Spend Alerts immediately evaluates each enabled sub-option: current-hour anomaly state and all three configured rolling spend threshold windows. -- While Spend Alerts are disabled, settings changes save only and do not evaluate controls, create events, or send emails. -- Active Spend Anomaly Alerts snapshot and expose the top current-hour Variable Credit spend drivers with their UTC-hour evidence window. -- Spend Alerts owner state stores active episode dedupe and review state separately from 90-day event history. -- Spend Alerts owner state stores minimal current episode markers for anomaly hour, threshold crossing state, and review status. -- Spend Alerts use dedicated normalized storage for owner configuration, owner state, hourly spend rollups, and **Cost Insight Events**. -- Enabling Spend Alerts uses already-maintained owner hourly rollups for baseline data, with Postgres source-of-truth backfill or repair when rollups are missing. -- Threshold evaluation falls back to exact canonical Postgres source data when rolling 7-day or rolling 30-day rollup coverage is incomplete. -- Spend Alerts store owner-hour totals separately from compact owner-hour driver buckets. -- Spend Alerts owner-hour totals record all Credit spend and label spend category so anomaly evaluation can distinguish **Variable Credit spend** from **Scheduled Credit spend**. -- Spend Alerts owner-hour totals are keyed by spend category, with separate rows for Variable Credit spend and Scheduled Credit spend. -- Spend Alerts owner-hour buckets use UTC hour start timestamps. -- Spend Alerts driver buckets group owner-hour spend by compact dimensions such as product or feature, model or provider, and actor user where applicable. -- Spend Alerts driver buckets are keyed by spend category as well as source and driver dimensions. -- Spend Alerts driver buckets use controlled taxonomy values, with `other` for unknown source classification. -- V1 Spend Alerts source taxonomy is `ai_gateway`, `kiloclaw`, `coding_plan`, and `other`. -- Spend Alerts owner-hour totals and driver buckets are retained indefinitely in v1. -- Spend Alerts driver buckets may retain actor user IDs indefinitely because soft-deleted user rows are anonymized. Driver buckets and event snapshots must not copy actor email or actor display name. -- Spend Alerts store actor user IDs in driver buckets for both personal and organization spend; UI resolves member labels from current user rows at render time. -- Spend Alerts driver buckets store total spend and contributing spend-record count. -- Every Credit spend path updates the Spend Alerts hourly rollup atomically with spend recording. -- Credit spend must not commit unless the corresponding Spend Alerts hourly rollup update also commits. -- Spend Alerts evaluation runs asynchronously after Credit spend updates and through an hourly sweep that catches missed evaluations and rolling-window transitions. -- Async Spend Alerts evaluation uses current config at evaluation time. -- V1 Spend Alerts evaluation runs in `apps/web` through post-spend async execution and an app cron hourly sweep. -- Organization **Spend Alerts** are managed by organization owners and billing managers who are also Kilo platform admins during initial rollout. +- **Cost Insights is retired.** No Cost Insights code runs: the UI, tRPC procedures, cron sweeps, spend capture, evaluation, alerting, and rollup maintenance are all removed, and none may be reintroduced. The `cost_insight_*` tables still exist but are no longer read or written, and are scheduled to be dropped. +- Credit spend paths record spend and usage only. They no longer maintain an hourly spend rollup, so spend recording no longer depends on Cost Insights availability. +- The former Cost Insights routes serve a discontinued notice so old bookmarks and previously sent alert emails do not land on a 404. - A **Security Finding Activity Event** falls into a report period based on when Kilo recorded or applied it. External source timestamps are supporting evidence and do not determine report inclusion. - A **Security Agent Audit Report** groups every matching reportable **Security Finding Activity Event** recorded by Kilo in the selected period. - V1 reports persisted SLA evidence only when it can do so from trustworthy recorded data. It does not calculate historical SLA compliance percentages or introduce new SLA lifecycle semantics. @@ -274,7 +189,6 @@ Covered domains must use this contract's canonical terms in code, docs, task des - Do not call organization members or billing managers **Notification Recipients** unless they also hold current organization `owner` role. - Treat "all activity" in a **Security Agent Audit Report** as all material actions and outcomes recorded by Kilo, not every internal processing step or an attestation that legacy history is exhaustive. Exclude reads, unchanged sync observations, queue claims, heartbeats, and retries with no new finding-level outcome. - A rollout baseline event records current state at actual capture time for an existing Security Finding; it is not a synthetic creation event and must not be backdated. -- Use **Cost Insights** for the user-facing surface, **Spend Alerts** for the alerting capability, and **Cost Suggestion** for optional cost-efficiency recommendations. Do not call this feature Spend Protection or Cost Controls. - Use **Kilo Pass for Organizations** for the org-owned product. Do not describe org agreements as personal Kilo Pass subscriptions. - Use **Kilo Pass org agreement** for the durable org-owned source of truth. Do not use seat purchase rows as the Kilo Pass source of truth. - Use existing `tier_19`, `tier_49`, and `tier_199` identifiers for **Kilo Pass org tier**. Do not create per-member tiers or separate org tier identifiers without a later product decision. @@ -319,9 +233,6 @@ Covered domains must use this contract's canonical terms in code, docs, task des - Keep normal bonus unlock transactional with canonical spend recording and issuance-level idempotency. Do not rely on asynchronous or scheduled evaluation as primary unlock path. - Treat bonus eligibility as period-bounded by agreement-relative **Kilo Pass issuance window**. Do not carry a locked bonus threshold into later windows or count later spend toward multiple issuance bonuses. - Apply bonus-credit expiry at next agreement-relative issuance boundary, matching personal Kilo Pass period behavior. Do not apply same period expiry to base credits. -- Do not describe a Cost Suggestion as an alert, warning, guaranteed savings, automatic optimization, or required action. -- Use **Spend Threshold Alert** and **Spend threshold** for the independent rolling 24-hour, rolling 7-day, and rolling 30-day threshold windows. Do not introduce warning or critical threshold tiers. -- Keep Spend Alerts alert-only. Do not describe them as spend blocks, hard limits, pauses, throttles, or request admission controls. - Use **Efficient model pool** for the candidate set and **Pool entry** for an exact model and canonical thinking variant. Do not introduce a second efficient mode or model ID. - Treat **Benchmark profiles** as global evidence, never owner-specific benchmark results. @@ -347,23 +258,14 @@ Covered domains must use this contract's canonical terms in code, docs, task des - **Security Agent** owns product policy, settings, permissions, and user-visible finding/remediation outcomes. - **Security Sync** owns finding synchronization, notification event admission, recipient intent materialization, deduplication, and durable state transitions. - **Security Agent Email Delivery** may revalidate and deliver an existing notification but must not create notification eligibility or copy mutable finding data into Worker request. -- Spend Alerts email delivery may retry per-recipient delivery rows but must not create duplicate owner-scoped Cost Insight Events. -- Spend Alerts email delivery must not send to an organization recipient who no longer has authorized access at dispatch time. -- Treat active Spend Alerts banner visibility as current owner state, not notification-recipient history. - **Shared Security Notification Policy** defines common parsing and pure eligibility behavior; it does not perform persistence or recipient lookup. - Cross-context dispatch sends only stable notification ID from **Security Sync** to authenticated **Security Agent Email Delivery** boundary. -- **Spend Alerts** evaluate personal and organization Credit spend at the owner boundary, not per product by default. - **Auto Routing** owns effective pool resolution and per-request selection. The benchmark worker owns Benchmark profile measurement and publication; web owns catalog validation, permissions, and settings UI. - **Kilo Pass for Organizations** owns org agreements, term versions, purchased pass capacity, allocation plans, issuance snapshots, and pooled bonus-after-base behavior. Personal Kilo Pass owns existing user subscriptions and user-global threshold behavior. - Org seat purchases may determine eligible seat counts for Kilo Pass for Organizations, but seat purchase rows are not the Kilo Pass agreement source of truth. -- Do not assume Spend Alerts apply to owners who have not opted in. -- Do not make Spend Alerts block, pause, throttle, suppress auto-top-up, reject paid requests, or emit Spend Alerts-specific HTTP 402 responses. -- Do not make Spend Alerts depend on Snowflake-only usage analytics for detection. -- During initial rollout, treat organization owners and billing managers as authorized managers for organization Spend Alerts only when they are also Kilo platform admins. ## Decision References -- `.specs/cost-insights.md` defines Cost Insights and Spend Alerts business rules. - `.specs/kilo-pass.md` defines current personal Kilo Pass behavior. - `docs/adr/0003-org-kilo-pass-source-of-truth.md` records org-owned agreement, provider-anchored issuance, allocation, bonus, migration, and operational decisions. - `.plans/code-review-analytics.md` defines prospective Review Analytics collection, taxonomy, persistence, and metric semantics. diff --git a/apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts b/apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts deleted file mode 100644 index 49b1ae6e163..00000000000 --- a/apps/web/src/app/api/cron/cost-insights-hourly/route.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { NextRequest } from 'next/server'; - -jest.mock('@/lib/config.server', () => ({ CRON_SECRET: 'cron-secret' })); -jest.mock('@/lib/cost-insights/jobs', () => ({ runCostInsightHourlySweep: jest.fn() })); -const mockSentryLog = jest.fn(); -jest.mock('@/lib/utils.server', () => ({ sentryLogger: jest.fn(() => mockSentryLog) })); - -import { runCostInsightHourlySweep } from '@/lib/cost-insights/jobs'; -import type { CostInsightHourlySweepSummary } from '@/lib/cost-insights/jobs'; -import { GET, maxDuration } from './route'; - -const mockRunCostInsightHourlySweep = jest.mocked(runCostInsightHourlySweep); - -function summary( - failedOwners: Array<{ owner: { type: 'user'; id: string }; error: string }> = [] -): CostInsightHourlySweepSummary { - return { - evaluatedOwners: 2, - failedOwners, - dirtyEvaluations: { - claimed: 1, - evaluatedOwners: [{ type: 'user' as const, id: 'user-1' }], - failedOwners: [], - evaluationDurationMs: 25, - rawCanonicalFallbackCount: 0, - rollupDegradedIntervalCount: 0, - }, - rollupRepairs: { - claimed: 0, - repaired: 0, - failed: [], - }, - dailyUsageRollupRepairs: { - claimed: 0, - repaired: 0, - failed: [], - }, - notifications: { - claimed: 1, - sent: 1, - skipped: 0, - terminalized: 0, - failed: 0, - }, - dirtyQueueDepthBefore: 3, - dirtyQueueDepthAfter: 1, - evaluationDurationMs: 50, - rawCanonicalFallbackCount: 1, - rollupDegradedIntervalCount: 2, - alreadyRunning: false, - deadlineReached: false, - ownerCycleComplete: true, - cycleId: 'cycle-1', - }; -} - -describe('GET /api/cron/cost-insights-hourly', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('rejects invalid cron authorization', async () => { - const response = await GET( - new NextRequest('http://localhost:3000/api/cron/cost-insights-hourly', { - headers: { authorization: 'Bearer wrong-secret' }, - }) - ); - - expect(response.status).toBe(401); - await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' }); - expect(mockRunCostInsightHourlySweep).not.toHaveBeenCalled(); - }); - - test('returns failure status and telemetry for a partial owner failure', async () => { - mockRunCostInsightHourlySweep.mockResolvedValue( - summary([{ owner: { type: 'user', id: 'user-2' }, error: 'evaluation failed' }]) - ); - - const response = await GET( - new NextRequest('http://localhost:3000/api/cron/cost-insights-hourly', { - headers: { authorization: 'Bearer cron-secret' }, - }) - ); - const body = await response.json(); - - expect(response.status).toBe(500); - expect(body).toMatchObject({ success: false, partialFailure: true }); - expect(mockSentryLog).toHaveBeenCalledWith( - 'Cost Insights hourly sweep completed', - expect.objectContaining({ - evaluatedOwnerCount: 2, - dirtyQueueDepthBefore: 3, - rawCanonicalFallbackCount: 1, - rollupDegradedIntervalCount: 2, - }) - ); - expect(mockSentryLog).toHaveBeenCalledWith( - 'Cost Insights hourly sweep completed with partial failures', - expect.objectContaining({ failedOwnerCount: 1 }) - ); - }); - - test('returns success only when all work succeeds', async () => { - mockRunCostInsightHourlySweep.mockResolvedValue(summary()); - - const response = await GET( - new NextRequest('http://localhost:3000/api/cron/cost-insights-hourly', { - headers: { authorization: 'Bearer cron-secret' }, - }) - ); - - expect(response.status).toBe(200); - await expect(response.json()).resolves.toMatchObject({ - success: true, - partialFailure: false, - }); - expect(mockSentryLog).toHaveBeenCalledWith( - 'Cost Insights hourly sweep completed', - expect.objectContaining({ - evaluatedOwnerCount: 2, - deadlineReached: false, - }) - ); - }); - - test('returns failure status when a rollup repair fails', async () => { - const result = summary(); - result.rollupRepairs.failed.push({ - owner: { type: 'user', id: 'user-3' }, - hourStart: '2026-07-13T10:00:00.000Z', - error: 'repair failed', - }); - mockRunCostInsightHourlySweep.mockResolvedValue(result); - - const response = await GET( - new NextRequest('http://localhost:3000/api/cron/cost-insights-hourly', { - headers: { authorization: 'Bearer cron-secret' }, - }) - ); - - expect(response.status).toBe(500); - await expect(response.json()).resolves.toMatchObject({ success: false, partialFailure: true }); - expect(mockSentryLog).toHaveBeenCalledWith( - 'Cost Insights hourly sweep completed with partial failures', - expect.objectContaining({ failedRollupRepairCount: 1 }) - ); - }); - - test('returns failure status and telemetry when a daily usage rollup repair fails', async () => { - const result = summary(); - result.dailyUsageRollupRepairs.failed.push({ - usageId: '00000000-0000-4000-8000-000000000001', - kiloUserId: 'user-3', - organizationId: null, - usageDate: '2026-07-13', - error: 'postgres:55P03', - }); - mockRunCostInsightHourlySweep.mockResolvedValue(result); - - const response = await GET( - new NextRequest('http://localhost:3000/api/cron/cost-insights-hourly', { - headers: { authorization: 'Bearer cron-secret' }, - }) - ); - - expect(response.status).toBe(500); - await expect(response.json()).resolves.toMatchObject({ success: false, partialFailure: true }); - expect(mockSentryLog).toHaveBeenCalledWith( - 'Cost Insights hourly sweep completed with partial failures', - expect.objectContaining({ failedDailyUsageRollupRepairCount: 1 }) - ); - }); - - test('exports a bounded function duration for resumable sweeps', () => { - expect(maxDuration).toBe(300); - }); -}); diff --git a/apps/web/src/app/api/cron/cost-insights-hourly/route.ts b/apps/web/src/app/api/cron/cost-insights-hourly/route.ts deleted file mode 100644 index 9a25e77e365..00000000000 --- a/apps/web/src/app/api/cron/cost-insights-hourly/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NextResponse } from 'next/server'; - -import { db } from '@/lib/drizzle'; -import { CRON_SECRET } from '@/lib/config.server'; -import { runCostInsightHourlySweep } from '@/lib/cost-insights/jobs'; -import { isCronAuthorizationValid } from '@/lib/cron-auth'; -import { sentryLogger } from '@/lib/utils.server'; - -if (!CRON_SECRET) { - throw new Error('CRON_SECRET is not configured in environment variables'); -} - -export const maxDuration = 300; - -export async function GET(request: Request) { - const authHeader = request.headers.get('authorization'); - if (!isCronAuthorizationValid(authHeader, CRON_SECRET)) { - sentryLogger( - 'cron', - 'warning' - )( - 'SECURITY: Invalid cost-insights-hourly CRON authorization attempt: ' + - (authHeader ? 'Invalid authorization header' : 'Missing authorization header') - ); - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const summary = await runCostInsightHourlySweep(db); - const dailyUsageRollupRepairs = summary.dailyUsageRollupRepairs; - sentryLogger('cron', 'info')('Cost Insights hourly sweep completed', { - evaluatedOwnerCount: summary.evaluatedOwners, - failedOwnerCount: summary.failedOwners.length, - dirtyQueueDepthBefore: summary.dirtyQueueDepthBefore, - dirtyQueueDepthAfter: summary.dirtyQueueDepthAfter, - dirtyClaimedCount: summary.dirtyEvaluations.claimed, - rollupRepairClaimedCount: summary.rollupRepairs.claimed, - rollupRepairCompletedCount: summary.rollupRepairs.repaired, - rollupRepairFailedCount: summary.rollupRepairs.failed.length, - dailyUsageRollupRepairClaimedCount: dailyUsageRollupRepairs.claimed, - dailyUsageRollupRepairCompletedCount: dailyUsageRollupRepairs.repaired, - dailyUsageRollupRepairFailedCount: dailyUsageRollupRepairs.failed.length, - evaluationDurationMs: summary.evaluationDurationMs, - rawCanonicalFallbackCount: summary.rawCanonicalFallbackCount, - rollupDegradedIntervalCount: summary.rollupDegradedIntervalCount, - notificationClaimedCount: summary.notifications.claimed, - deadlineReached: summary.deadlineReached, - ownerCycleComplete: summary.ownerCycleComplete, - alreadyRunning: summary.alreadyRunning, - }); - const hasFailures = - summary.failedOwners.length > 0 || - summary.rollupRepairs.failed.length > 0 || - dailyUsageRollupRepairs.failed.length > 0 || - summary.notifications.failed > 0 || - summary.notifications.terminalized > 0; - if (hasFailures) { - sentryLogger('cron', 'error')('Cost Insights hourly sweep completed with partial failures', { - failedOwnerCount: summary.failedOwners.length, - failedRollupRepairCount: summary.rollupRepairs.failed.length, - failedDailyUsageRollupRepairCount: dailyUsageRollupRepairs.failed.length, - failedNotificationCount: summary.notifications.failed, - terminalizedNotificationCount: summary.notifications.terminalized, - }); - } - - return NextResponse.json( - { - success: !hasFailures, - partialFailure: hasFailures, - complete: summary.ownerCycleComplete, - deadlineReached: summary.deadlineReached, - alreadyRunning: summary.alreadyRunning, - summary, - timestamp: new Date().toISOString(), - }, - { status: hasFailures ? 500 : 200 } - ); -} diff --git a/apps/web/src/app/api/cron/cost-insights-retention/route.test.ts b/apps/web/src/app/api/cron/cost-insights-retention/route.test.ts deleted file mode 100644 index 828a33f7cf3..00000000000 --- a/apps/web/src/app/api/cron/cost-insights-retention/route.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextRequest } from 'next/server'; - -jest.mock('@/lib/config.server', () => ({ CRON_SECRET: 'cron-secret' })); -jest.mock('@/lib/cost-insights/jobs', () => ({ - runCostInsightEventRetentionCleanup: jest.fn(), -})); -const mockSentryLog = jest.fn(); -jest.mock('@/lib/utils.server', () => ({ sentryLogger: jest.fn(() => mockSentryLog) })); - -import { runCostInsightEventRetentionCleanup } from '@/lib/cost-insights/jobs'; -import { GET } from './route'; - -const mockRunCostInsightEventRetentionCleanup = jest.mocked(runCostInsightEventRetentionCleanup); - -describe('GET /api/cron/cost-insights-retention', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('rejects invalid cron authorization', async () => { - const response = await GET( - new NextRequest('http://localhost:3000/api/cron/cost-insights-retention', { - headers: { authorization: 'Bearer wrong-secret' }, - }) - ); - - expect(response.status).toBe(401); - await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' }); - expect(mockRunCostInsightEventRetentionCleanup).not.toHaveBeenCalled(); - }); - - test('runs retention cleanup for valid cron authorization', async () => { - mockRunCostInsightEventRetentionCleanup.mockResolvedValue({ - deletedEvents: 3, - cutoff: '2026-04-07T00:00:00.000Z', - }); - - const response = await GET( - new NextRequest('http://localhost:3000/api/cron/cost-insights-retention', { - headers: { authorization: 'Bearer cron-secret' }, - }) - ); - - expect(response.status).toBe(200); - await expect(response.json()).resolves.toMatchObject({ - success: true, - summary: { deletedEvents: 3 }, - }); - expect(mockRunCostInsightEventRetentionCleanup).toHaveBeenCalledTimes(1); - }); -}); diff --git a/apps/web/src/app/api/cron/cost-insights-retention/route.ts b/apps/web/src/app/api/cron/cost-insights-retention/route.ts deleted file mode 100644 index d2d65eb1514..00000000000 --- a/apps/web/src/app/api/cron/cost-insights-retention/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextResponse } from 'next/server'; - -import { db } from '@/lib/drizzle'; -import { CRON_SECRET } from '@/lib/config.server'; -import { runCostInsightEventRetentionCleanup } from '@/lib/cost-insights/jobs'; -import { isCronAuthorizationValid } from '@/lib/cron-auth'; -import { sentryLogger } from '@/lib/utils.server'; - -if (!CRON_SECRET) { - throw new Error('CRON_SECRET is not configured in environment variables'); -} - -export async function GET(request: Request) { - const authHeader = request.headers.get('authorization'); - if (!isCronAuthorizationValid(authHeader, CRON_SECRET)) { - sentryLogger( - 'cron', - 'warning' - )( - 'SECURITY: Invalid cost-insights-retention CRON authorization attempt: ' + - (authHeader ? 'Invalid authorization header' : 'Missing authorization header') - ); - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const summary = await runCostInsightEventRetentionCleanup(db); - - return NextResponse.json( - { - success: true, - summary, - timestamp: new Date().toISOString(), - }, - { status: 200 } - ); -} diff --git a/apps/web/src/app/api/cron/usage-daily-rollup-repairs/route.test.ts b/apps/web/src/app/api/cron/usage-daily-rollup-repairs/route.test.ts new file mode 100644 index 00000000000..a5733bf366e --- /dev/null +++ b/apps/web/src/app/api/cron/usage-daily-rollup-repairs/route.test.ts @@ -0,0 +1,91 @@ +import { NextRequest } from 'next/server'; + +jest.mock('@/lib/config.server', () => ({ CRON_SECRET: 'cron-secret' })); +jest.mock('@/lib/ai-gateway/usage-daily-rollup-repairs', () => ({ + processPendingDailyUsageRollupRepairs: jest.fn(), +})); +const mockSentryLog = jest.fn(); +jest.mock('@/lib/utils.server', () => ({ sentryLogger: jest.fn(() => mockSentryLog) })); + +import { processPendingDailyUsageRollupRepairs } from '@/lib/ai-gateway/usage-daily-rollup-repairs'; +import { GET, maxDuration } from './route'; + +const mockProcessPendingDailyUsageRollupRepairs = jest.mocked( + processPendingDailyUsageRollupRepairs +); + +function request(secret: string): NextRequest { + return new NextRequest('http://localhost:3000/api/cron/usage-daily-rollup-repairs', { + headers: { authorization: `Bearer ${secret}` }, + }); +} + +describe('GET /api/cron/usage-daily-rollup-repairs', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('rejects invalid cron authorization', async () => { + const response = await GET(request('wrong-secret')); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' }); + expect(mockProcessPendingDailyUsageRollupRepairs).not.toHaveBeenCalled(); + }); + + test('drains the repair queue with a bounded limit and reports success', async () => { + mockProcessPendingDailyUsageRollupRepairs.mockResolvedValue({ + claimed: 2, + repaired: 2, + failed: [], + }); + + const response = await GET(request('cron-secret')); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + success: true, + partialFailure: false, + summary: { claimed: 2, repaired: 2 }, + }); + expect(mockProcessPendingDailyUsageRollupRepairs).toHaveBeenCalledWith(expect.anything(), { + limit: 20, + }); + expect(mockSentryLog).toHaveBeenCalledWith( + 'Usage daily rollup repairs completed', + expect.objectContaining({ claimedCount: 2, repairedCount: 2, failedCount: 0 }) + ); + }); + + test('returns failure status and telemetry when a repair fails', async () => { + mockProcessPendingDailyUsageRollupRepairs.mockResolvedValue({ + claimed: 2, + repaired: 1, + failed: [ + { + usageId: '00000000-0000-4000-8000-000000000001', + kiloUserId: 'user-3', + organizationId: null, + usageDate: '2026-07-13', + error: 'postgres:55P03', + }, + ], + }); + + const response = await GET(request('cron-secret')); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toMatchObject({ + success: false, + partialFailure: true, + }); + expect(mockSentryLog).toHaveBeenCalledWith( + 'Usage daily rollup repairs completed with partial failures', + expect.objectContaining({ failedCount: 1 }) + ); + }); + + test('exports a bounded function duration', () => { + expect(maxDuration).toBe(300); + }); +}); diff --git a/apps/web/src/app/api/cron/usage-daily-rollup-repairs/route.ts b/apps/web/src/app/api/cron/usage-daily-rollup-repairs/route.ts new file mode 100644 index 00000000000..3cf49330b64 --- /dev/null +++ b/apps/web/src/app/api/cron/usage-daily-rollup-repairs/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from 'next/server'; + +import { db } from '@/lib/drizzle'; +import { CRON_SECRET } from '@/lib/config.server'; +import { processPendingDailyUsageRollupRepairs } from '@/lib/ai-gateway/usage-daily-rollup-repairs'; +import { isCronAuthorizationValid } from '@/lib/cron-auth'; +import { sentryLogger } from '@/lib/utils.server'; + +if (!CRON_SECRET) { + throw new Error('CRON_SECRET is not configured in environment variables'); +} + +/** + * Drains `microdollar_usage_daily_repairs` and rebuilds the affected + * `microdollar_usage_daily` rows. Every billable usage row enqueues a repair + * signal, and a claim coalesces all pending signals for one owner and UTC day, + * so the limit bounds distinct owner-days rebuilt per run rather than requests. + * + * This ran inside the Cost Insights hourly sweep until that feature was retired. + * The limit and hourly schedule match the previous behaviour. + */ +const DAILY_USAGE_ROLLUP_REPAIR_LIMIT = 20; + +export const maxDuration = 300; + +export async function GET(request: Request) { + const authHeader = request.headers.get('authorization'); + if (!isCronAuthorizationValid(authHeader, CRON_SECRET)) { + sentryLogger( + 'cron', + 'warning' + )( + 'SECURITY: Invalid usage-daily-rollup-repairs CRON authorization attempt: ' + + (authHeader ? 'Invalid authorization header' : 'Missing authorization header') + ); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const summary = await processPendingDailyUsageRollupRepairs(db, { + limit: DAILY_USAGE_ROLLUP_REPAIR_LIMIT, + }); + sentryLogger('cron', 'info')('Usage daily rollup repairs completed', { + claimedCount: summary.claimed, + repairedCount: summary.repaired, + failedCount: summary.failed.length, + }); + + const hasFailures = summary.failed.length > 0; + if (hasFailures) { + sentryLogger('cron', 'error')('Usage daily rollup repairs completed with partial failures', { + failedCount: summary.failed.length, + }); + } + + return NextResponse.json( + { + success: !hasFailures, + partialFailure: hasFailures, + summary, + timestamp: new Date().toISOString(), + }, + { status: hasFailures ? 500 : 200 } + ); +} diff --git a/apps/web/src/lib/ai-gateway/processUsage.test.ts b/apps/web/src/lib/ai-gateway/processUsage.test.ts index 64691a1dd9b..1fcc3533148 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.test.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.test.ts @@ -26,10 +26,6 @@ import { createReadStream } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { db } from '@/lib/drizzle'; import { - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, - cost_insight_rollup_degraded_intervals, - cost_insight_rollup_repairs, microdollar_usage, microdollar_usage_daily, microdollar_usage_daily_repairs, @@ -613,34 +609,6 @@ describe('logMicrodollarUsage', () => { expect(usageRecord?.has_error).toBe(false); expect(usageRecord?.created_at).toBeTruthy(); expect(metadataRecord?.created_at).toBe(usageRecord?.created_at); - - await waitForExpectation(async () => { - const [total] = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); - expect(total).toMatchObject({ - owned_by_organization_id: null, - spend_category: 'variable', - total_microdollars: 500, - spend_record_count: 1, - }); - - const [driver] = await db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, user.id)); - expect(driver).toMatchObject({ - source: 'ai_gateway', - product_key: 'vscode-extension', - feature_key: 'chat_completions', - model_or_plan_key: 'anthropic/claude-3.7-sonnet', - provider_key: 'Provider', - actor_user_id: user.id, - total_microdollars: 500, - spend_record_count: 1, - }); - }); }); test('stores session_id when provided', async () => { @@ -748,12 +716,6 @@ describe('logMicrodollarUsage', () => { expect(usageRecord?.has_error).toBe(true); expect(usageRecord?.model).toBe('openai/gpt-4.1'); expect(metadataRecord?.has_middle_out_transform).toBe(false); - - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); - expect(totals).toHaveLength(0); }); test('keeps AI source rows when legacy usage owner does not exist', async () => { @@ -773,87 +735,11 @@ describe('logMicrodollarUsage', () => { .from(microdollar_usage) .where(eq(microdollar_usage.kilo_user_id, missingUserId)); expect(sourceRows).toHaveLength(1); - - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, missingUserId)); - expect(totals).toHaveLength(0); - - const degradedIntervals = await db - .select() - .from(cost_insight_rollup_degraded_intervals) - .where(eq(cost_insight_rollup_degraded_intervals.source, 'ai_gateway')); - expect(degradedIntervals).toHaveLength(0); } finally { consoleErrorSpy.mockRestore(); } }); - test('keeps usage and balance write and enqueues owner-hour repair after capture fails', async () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const user = await insertTestUser({ - id: ` test-cost-insight-failure-user-${crypto.randomUUID()} `, - microdollars_used: 0, - google_user_email: 'cost-insight-failure@example.com', - }); - const organization = await createTestOrganization( - 'Cost insight capture failure organization', - user.id, - 10_000 - ); - const { core, metadata } = await defineMicrodollarUsage(); - - try { - const result = await insertUsageRecord( - { ...core, kilo_user_id: user.id, organization_id: organization.id, cost: 1000 }, - metadata - ); - - expect(result).toMatchObject({ usageId: core.id, newMicrodollarsUsed: null }); - - const sourceRows = await db - .select() - .from(microdollar_usage) - .where(eq(microdollar_usage.kilo_user_id, user.id)); - expect(sourceRows).toHaveLength(1); - - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_organization_id, organization.id)); - expect(totals).toHaveLength(0); - - const degradedIntervals = await db - .select() - .from(cost_insight_rollup_degraded_intervals) - .where(eq(cost_insight_rollup_degraded_intervals.source, 'ai_gateway')); - expect(degradedIntervals).toHaveLength(0); - - const repairs = await db - .select() - .from(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.owned_by_organization_id, organization.id)); - expect(repairs).toHaveLength(1); - expect(repairs[0].usage_id).toBe(core.id); - expect(new Date(repairs[0].hour_start).toISOString()).toBe( - new Date(Math.floor(Date.parse(core.created_at) / 3_600_000) * 3_600_000).toISOString() - ); - expect(Date.parse(repairs[0].next_attempt_at)).toBeGreaterThan(Date.parse(core.created_at)); - } finally { - await db - .delete(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.owned_by_organization_id, organization.id)); - consoleErrorSpy.mockRestore(); - } - }); - test('stores 3 usage records with overlapping data and tests metadata deduplication', async () => { const user = await insertTestUser({ id: 'test-dedup-user', @@ -1041,17 +927,6 @@ describe('logMicrodollarUsage', () => { kilo_user_id: user.id, microdollar_usage: 500, }); - - const [total] = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_organization_id, organization.id)); - expect(total).toMatchObject({ - owned_by_user_id: null, - spend_category: 'variable', - total_microdollars: 500, - spend_record_count: 1, - }); }); }); diff --git a/apps/web/src/lib/ai-gateway/processUsage.ts b/apps/web/src/lib/ai-gateway/processUsage.ts index d3dfeebd7ae..86430155a76 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.ts @@ -34,10 +34,6 @@ import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import { sentryLogger } from '@/lib/utils.server'; import { maybeIssueKiloPassBonusFromUsageThreshold } from '@/lib/kilo-pass/usage-triggered-bonus'; import { getEffectiveKiloPassThreshold } from '@/lib/kilo-pass/threshold'; -import { - acknowledgeCostInsightRollupCapture, - enqueueCostInsightRollupRepair, -} from '@/lib/cost-insights/rollup-repairs'; import { runBestEffortPostCommitTasks, type BestEffortPostCommitTask, @@ -84,17 +80,6 @@ import { type KiloExclusiveModel, } from '@/lib/ai-gateway/providers/kilo-exclusive-model'; import { calculateCustomCost_mUsd } from '@/lib/ai-gateway/custom-pricing'; -import { - acquireCostInsightOwnerHourLock, - acquireCostInsightOwnerHourSharedLock, - captureCostInsightSpend, - getCostInsightUtcHourStart, -} from '@kilocode/db/cost-insights-rollups'; -import { - getAiGatewayCostInsightFeatureKey, - getAiGatewayCostInsightProductKey, -} from '@/lib/cost-insights/canonical-sources'; -import { scheduleCostInsightEvaluationAfterSpend } from '@/lib/cost-insights/evaluation'; import { enqueueDailyUsageRollupRepair } from './usage-daily-rollup-repairs'; import { recordOrganizationConsumption } from '@/lib/kilo-pass-org/consumption'; @@ -471,16 +456,6 @@ async function insertUsageTransaction( ): Promise { return db.transaction(async tx => { await setUsageTransactionIdleTimeout(tx); - if (coreUsageFields.cost > 0) { - const owner = coreUsageFields.organization_id - ? { type: 'organization' as const, id: coreUsageFields.organization_id } - : { type: 'user' as const, id: coreUsageFields.kilo_user_id }; - await acquireCostInsightOwnerHourSharedLock( - tx, - owner, - getCostInsightUtcHourStart(coreUsageFields.created_at) - ); - } const inserted = await insertUsageAndMetadataWithBalanceUpdate( tx, coreUsageFields, @@ -509,45 +484,10 @@ async function insertUsageTransaction( createdAt: coreUsageFields.created_at, }); } - if (coreUsageFields.cost > 0) { - const owner = coreUsageFields.organization_id - ? { type: 'organization' as const, id: coreUsageFields.organization_id } - : { type: 'user' as const, id: coreUsageFields.kilo_user_id }; - await enqueueCostInsightRollupRepair(tx, { - usageId: coreUsageFields.id, - owner, - occurredAt: coreUsageFields.created_at, - }); - } return { inserted }; }); } -const COST_INSIGHT_CAPTURE_LOCK_TIMEOUT_MS = 2_000; -const COST_INSIGHT_CAPTURE_STATEMENT_TIMEOUT_MS = 5_000; -const COST_INSIGHT_CAPTURE_IDLE_TRANSACTION_TIMEOUT_MS = 10_000; - -async function setCostInsightCaptureTimeouts(tx: UsageStatementExecutor): Promise { - await tx.execute(sql` - SELECT - pg_catalog.set_config( - 'lock_timeout', - ${`${COST_INSIGHT_CAPTURE_LOCK_TIMEOUT_MS}ms`}, - true - ), - pg_catalog.set_config( - 'statement_timeout', - ${`${COST_INSIGHT_CAPTURE_STATEMENT_TIMEOUT_MS}ms`}, - true - ), - pg_catalog.set_config( - 'idle_in_transaction_session_timeout', - ${`${COST_INSIGHT_CAPTURE_IDLE_TRANSACTION_TIMEOUT_MS}ms`}, - true - ) - `); -} - function getPostgresErrorCode(error: unknown): string | null { if (typeof error !== 'object' || error === null) return null; if ('code' in error && typeof error.code === 'string' && /^[0-9A-Z]{5}$/.test(error.code)) { @@ -570,25 +510,6 @@ function reportPostCommitFailure( }); } -function scheduleCostInsightEvaluationSafely( - owner: Parameters[0], - usageId: string -): void { - try { - scheduleCostInsightEvaluationAfterSpend(owner); - } catch (error) { - reportPostCommitFailure( - 'post-commit cost insight evaluation scheduling failed', - error, - { - source: 'postCommitCostInsightEvaluationScheduling', - ownerType: owner.type, - }, - usageId - ); - } -} - function organizationUsageMutationTask( usage: MicrodollarUsage, result: UsageStatementResult @@ -610,66 +531,11 @@ function organizationUsageMutationTask( }; } -function costInsightSpendCaptureTask( - usage: MicrodollarUsage, - metadataFields: UsageMetaData -): BestEffortPostCommitTask | null { - if (usage.cost <= 0) return null; - - const owner = usage.organization_id - ? { type: 'organization' as const, id: usage.organization_id } - : { type: 'user' as const, id: usage.kilo_user_id }; - - return { - run: async () => { - await db.transaction(async tx => { - await setCostInsightCaptureTimeouts(tx); - await acquireCostInsightOwnerHourLock( - tx, - owner, - getCostInsightUtcHourStart(usage.created_at) - ); - if (!(await acknowledgeCostInsightRollupCapture(tx, usage.id))) return; - await captureCostInsightSpend(tx, { - owner, - actorUserId: usage.kilo_user_id, - occurredAt: usage.created_at, - amountMicrodollars: usage.cost, - category: 'variable', - source: 'ai_gateway', - productKey: getAiGatewayCostInsightProductKey(metadataFields.feature), - featureKey: getAiGatewayCostInsightFeatureKey(metadataFields.api_kind), - modelOrPlanKey: usage.requested_model || usage.model || 'other', - providerKey: usage.inference_provider || usage.provider || 'other', - }); - }); - scheduleCostInsightEvaluationSafely(owner, usage.id); - }, - reportError: async error => { - reportPostCommitFailure( - 'post-commit cost insight spend capture failed', - error, - { - source: 'postCommitCostInsightSpendCapture', - spendCategory: 'variable', - spendSource: 'ai_gateway', - ownerType: owner.type, - }, - usage.id - ); - }, - }; -} - async function runPostCommitUsageWork( usage: MicrodollarUsage, - metadataFields: UsageMetaData, result: UsageStatementResult ): Promise { - const tasks = [ - organizationUsageMutationTask(usage, result), - costInsightSpendCaptureTask(usage, metadataFields), - ].filter(task => task !== null); + const tasks = [organizationUsageMutationTask(usage, result)].filter(task => task !== null); await runBestEffortPostCommitTasks(tasks); } @@ -733,7 +599,7 @@ export async function insertUsageRecord( } ); - await runPostCommitUsageWork(coreUsageFields, metadataFields, result.inserted); + await runPostCommitUsageWork(coreUsageFields, result.inserted); scheduleKiloPassBonusIfNeeded(coreUsageFields, result.inserted); return { usageId: result.inserted.usageId, diff --git a/apps/web/src/lib/cost-insights/spend-writer-audit.test.ts b/apps/web/src/lib/ai-gateway/spend-writer-audit.test.ts similarity index 97% rename from apps/web/src/lib/cost-insights/spend-writer-audit.test.ts rename to apps/web/src/lib/ai-gateway/spend-writer-audit.test.ts index 06319ee0aae..45864d10ec6 100644 --- a/apps/web/src/lib/cost-insights/spend-writer-audit.test.ts +++ b/apps/web/src/lib/ai-gateway/spend-writer-audit.test.ts @@ -41,7 +41,7 @@ function listTypeScriptFiles(directory: string): string[] { return files; } -describe('Cost Insights Credit-spend writer audit', () => { +describe('Credit-spend writer audit', () => { test('requires every direct microdollars_used increment to have an explicit classification', () => { const detectedWriters = sourceRoots .flatMap(sourceRoot => listTypeScriptFiles(join(repositoryRoot, sourceRoot))) diff --git a/apps/web/src/lib/ai-gateway/usage-post-commit-work.test.ts b/apps/web/src/lib/ai-gateway/usage-post-commit-work.test.ts index 0dd9cd2e6b3..1a0369b48fe 100644 --- a/apps/web/src/lib/ai-gateway/usage-post-commit-work.test.ts +++ b/apps/web/src/lib/ai-gateway/usage-post-commit-work.test.ts @@ -17,7 +17,7 @@ function deferred(): { promise: Promise; resolve: () => void } { } describe('runBestEffortPostCommitTasks', () => { - test('awaits personal Cost Insights transaction through commit and connection release', async () => { + test('awaits a task transaction through commit and connection release', async () => { const transactionMayCommit = deferred(); const events: string[] = []; const reportError = jest.fn((_error: unknown): void => {}); diff --git a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts index d185cf5ff7f..a5565c89548 100644 --- a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts +++ b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts @@ -1,6 +1,5 @@ /* eslint-disable drizzle/enforce-delete-with-where */ import { eq } from 'drizzle-orm'; -import type { CaptureCostInsightSpendInput } from '@kilocode/db/cost-insights-rollups'; import { runCodingPlanBillingLifecycleCron } from '@/lib/coding-plans/billing-lifecycle-cron'; import { subscribeToCodingPlan, uploadKeysToInventory } from '@/lib/coding-plans'; @@ -13,8 +12,6 @@ import { coding_plan_key_inventory, coding_plan_subscriptions, coding_plan_terms, - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, credit_transactions, kilocode_users, } from '@kilocode/db/schema'; @@ -22,15 +19,6 @@ import { jest.mock('@/lib/autoTopUp', () => ({ maybePerformAutoTopUp: jest.fn(async () => undefined), })); -jest.mock('@kilocode/db/cost-insights-rollups', () => ({ - captureCostInsightSpend: jest.fn(async () => undefined), - COST_INSIGHT_CODING_PLAN_PRODUCT_KEY: 'coding-plan', -})); - -const captureCostInsightSpendMock = jest.requireMock<{ - captureCostInsightSpend: jest.Mock, [unknown, CaptureCostInsightSpendInput]>; -}>('@kilocode/db/cost-insights-rollups').captureCostInsightSpend; - const PLAN_ID = 'minimax-token-plan-plus'; const MAX_PLAN_ID = 'minimax-token-plan-max'; const BYTEPLUS_PLAN_ID = 'byteplus-coding-plan-team-lite'; @@ -66,9 +54,6 @@ async function createSubscription( afterEach(async () => { jest.mocked(maybePerformAutoTopUp).mockClear(); - captureCostInsightSpendMock.mockClear(); - await db.delete(cost_insight_owner_hour_driver_buckets); - await db.delete(cost_insight_owner_hour_totals); await db.delete(coding_plan_terms); await db.delete(coding_plan_subscriptions); await db.delete(byok_api_keys); @@ -79,8 +64,7 @@ afterEach(async () => { describe('Coding Plan billing lifecycle cron', () => { it('renews atomically with a charged term and retains assigned access', async () => { - const { user, subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); - captureCostInsightSpendMock.mockClear(); + const { subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); const summary = await runCodingPlanBillingLifecycleCron(db); const [subscription] = await db @@ -112,22 +96,6 @@ describe('Coding Plan billing lifecycle cron', () => { createdAt: expect.any(String), }, ]); - expect(captureCostInsightSpendMock).toHaveBeenCalledWith(expect.anything(), { - owner: { type: 'user', id: user.id }, - actorUserId: user.id, - occurredAt: expect.any(String), - amountMicrodollars: COST_MICRODOLLARS, - category: 'scheduled', - source: 'coding_plan', - productKey: 'coding-plan', - featureKey: 'renewal', - modelOrPlanKey: PLAN_ID, - providerKey: 'minimax', - }); - const captureInput = captureCostInsightSpendMock.mock.calls[0]?.[1] as - | { occurredAt: string } - | undefined; - expect(new Date(renewalTransaction[0].createdAt).toISOString()).toBe(captureInput?.occurredAt); expect(credential.status).toBe('assigned'); }); @@ -194,34 +162,6 @@ describe('Coding Plan billing lifecycle cron', () => { expect(renewal).toEqual([{ description: 'Coding plan renewal: BytePlus Coding Plan Lite' }]); }); - it('rolls back renewal when scheduled-spend capture fails', async () => { - const { user, subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); - captureCostInsightSpendMock.mockClear(); - captureCostInsightSpendMock.mockImplementationOnce(async () => { - throw new Error('rollup unavailable'); - }); - - const summary = await runCodingPlanBillingLifecycleCron(db); - const [updatedUser] = await db - .select({ used: kilocode_users.microdollars_used }) - .from(kilocode_users) - .where(eq(kilocode_users.id, user.id)); - const terms = await db - .select() - .from(coding_plan_terms) - .where(eq(coding_plan_terms.subscription_id, subscriptionId)); - const renewalTransactions = await db - .select() - .from(credit_transactions) - .where(eq(credit_transactions.description, 'Coding plan renewal: MiniMax Token Plan Plus')); - - expect(summary.errors).toBe(1); - expect(summary.renewals).toBe(0); - expect(updatedUser.used).toBe(COST_MICRODOLLARS); - expect(terms.map(term => term.kind)).toEqual(['activation']); - expect(renewalTransactions).toHaveLength(0); - }); - it('renews after the subscriber deletes the installed MiniMax BYOK key', async () => { const { subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2); const [before] = await db diff --git a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts index 796d9649610..9336e2e3bd0 100644 --- a/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts +++ b/apps/web/src/lib/coding-plans/billing-lifecycle-cron.ts @@ -6,13 +6,8 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { maybePerformAutoTopUp } from '@/lib/autoTopUp'; import { getCodingPlanPrice } from '@/lib/coding-plans/pricing'; -import { scheduleCostInsightEvaluationAfterSpend } from '@/lib/cost-insights/evaluation'; import { maybeIssueKiloPassBonusFromUsageThreshold } from '@/lib/kilo-pass/usage-triggered-bonus'; import { sentryLogger } from '@/lib/utils.server'; -import { - captureCostInsightSpend, - COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, -} from '@kilocode/db/cost-insights-rollups'; import { byok_api_keys, coding_plan_key_inventory, @@ -352,18 +347,6 @@ async function processRenewal( original_baseline_microdollars_used: row.microdollars_used, created_at: occurredAt, }); - await captureCostInsightSpend(tx, { - owner: { type: 'user', id: row.user_id }, - actorUserId: row.user_id, - occurredAt, - amountMicrodollars: row.cost_microdollars, - category: 'scheduled', - source: 'coding_plan', - productKey: COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, - featureKey: 'renewal', - modelOrPlanKey: row.plan_id, - providerKey: row.provider_id, - }); await tx.insert(coding_plan_terms).values({ subscription_id: row.id, user_id: row.user_id, @@ -445,8 +428,5 @@ async function processRenewal( } return 'terminated'; }); - if (result === 'renewed') { - scheduleCostInsightEvaluationAfterSpend({ type: 'user', id: selectedRow.user_id }); - } return result; } diff --git a/apps/web/src/lib/coding-plans/index.test.ts b/apps/web/src/lib/coding-plans/index.test.ts index 8cefec72145..3bef258192a 100644 --- a/apps/web/src/lib/coding-plans/index.test.ts +++ b/apps/web/src/lib/coding-plans/index.test.ts @@ -1,6 +1,5 @@ /* eslint-disable drizzle/enforce-delete-with-where */ import { eq } from 'drizzle-orm'; -import type { CaptureCostInsightSpendInput } from '@kilocode/db/cost-insights-rollups'; import { encryptApiKey } from '@/lib/ai-gateway/byok/encryption'; import { BYOK_ENCRYPTION_KEY } from '@/lib/config.server'; @@ -27,21 +26,10 @@ import { coding_plan_key_inventory, coding_plan_subscriptions, coding_plan_terms, - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, credit_transactions, kilocode_users, } from '@kilocode/db/schema'; -jest.mock('@kilocode/db/cost-insights-rollups', () => ({ - captureCostInsightSpend: jest.fn(async () => undefined), - COST_INSIGHT_CODING_PLAN_PRODUCT_KEY: 'coding-plan', -})); - -const captureCostInsightSpendMock = jest.requireMock<{ - captureCostInsightSpend: jest.Mock, [unknown, CaptureCostInsightSpendInput]>; -}>('@kilocode/db/cost-insights-rollups').captureCostInsightSpend; - const PLAN_ID = 'minimax-token-plan-plus'; const MAX_PLAN_ID = 'minimax-token-plan-max'; const ULTRA_PLAN_ID = 'minimax-token-plan-ultra'; @@ -74,9 +62,6 @@ async function createUserWithBalance(microdollars: number) { } afterEach(async () => { - captureCostInsightSpendMock.mockClear(); - await db.delete(cost_insight_owner_hour_driver_buckets); - await db.delete(cost_insight_owner_hour_totals); await db.delete(coding_plan_terms); await db.delete(coding_plan_subscriptions); await db.delete(byok_api_keys); @@ -196,22 +181,7 @@ describe('coding plans', () => { expect(terms).toHaveLength(1); expect(terms[0].kind).toBe('activation'); expect(terms[0].cost_microdollars).toBe(COST_MICRODOLLARS); - expect(captureCostInsightSpendMock).toHaveBeenCalledWith(expect.anything(), { - owner: { type: 'user', id: user.id }, - actorUserId: user.id, - occurredAt: expect.any(String), - amountMicrodollars: COST_MICRODOLLARS, - category: 'scheduled', - source: 'coding_plan', - productKey: 'coding-plan', - featureKey: 'activation', - modelOrPlanKey: PLAN_ID, - providerKey: PROVIDER_ID, - }); - const captureInput = captureCostInsightSpendMock.mock.calls[0]?.[1] as - | { occurredAt: string } - | undefined; - expect(new Date(deduction.created_at).toISOString()).toBe(captureInput?.occurredAt); + expect(deduction.amount_microdollars).toBe(-COST_MICRODOLLARS); expect(managedKey.provider_id).toBe(PROVIDER_ID); expect(managedKey.management_source).toBe('coding_plan'); expect(inventoryKey.status).toBe('assigned'); @@ -239,7 +209,6 @@ describe('coding plans', () => { expect(terms).toHaveLength(1); expect(assigned).toHaveLength(1); expect(updatedUser.microdollars_used).toBe(COST_MICRODOLLARS); - expect(captureCostInsightSpendMock).toHaveBeenCalledTimes(1); }); it('rejects a new purchase while an active subscription exists', async () => { @@ -397,33 +366,6 @@ describe('coding plans', () => { expect(unchargedUser.microdollars_used).toBe(0); }); - it('rolls back activation when scheduled-spend capture fails', async () => { - const user = await createUserWithBalance(COST_MICRODOLLARS); - await seedInventoryKey(); - captureCostInsightSpendMock.mockImplementationOnce(async () => { - throw new Error('rollup unavailable'); - }); - - await expect(subscribeToCodingPlan(user.id, PLAN_ID, 'rollup-failure')).rejects.toThrow( - 'rollup unavailable' - ); - - const [updatedUser] = await db - .select() - .from(kilocode_users) - .where(eq(kilocode_users.id, user.id)); - const transactions = await db.select().from(credit_transactions); - const terms = await db.select().from(coding_plan_terms); - const subscriptions = await db.select().from(coding_plan_subscriptions); - const [inventory] = await db.select().from(coding_plan_key_inventory); - - expect(updatedUser.microdollars_used).toBe(0); - expect(transactions).toHaveLength(0); - expect(terms).toHaveLength(0); - expect(subscriptions).toHaveLength(0); - expect(inventory.status).toBe('available'); - }); - it('rejects activation when the personal MiniMax BYOK slot is occupied', async () => { const user = await createUserWithBalance(COST_MICRODOLLARS); await seedInventoryKey(); diff --git a/apps/web/src/lib/coding-plans/index.ts b/apps/web/src/lib/coding-plans/index.ts index c7eb053803e..6e92303cc0f 100644 --- a/apps/web/src/lib/coding-plans/index.ts +++ b/apps/web/src/lib/coding-plans/index.ts @@ -13,14 +13,9 @@ import { validateCodingPlanCredential, } from '@/lib/coding-plans/inventory-validation'; import { getCodingPlanPrice, type CodingPlanId } from '@/lib/coding-plans/pricing'; -import { scheduleCostInsightEvaluationAfterSpend } from '@/lib/cost-insights/evaluation'; import { db } from '@/lib/drizzle'; import { maybeIssueKiloPassBonusFromUsageThreshold } from '@/lib/kilo-pass/usage-triggered-bonus'; import { sentryLogger } from '@/lib/utils.server'; -import { - captureCostInsightSpend, - COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, -} from '@kilocode/db/cost-insights-rollups'; import { byok_api_keys, coding_plan_availability_intents, @@ -173,19 +168,6 @@ export async function subscribeToCodingPlan( original_baseline_microdollars_used: lockedUser.microdollars_used, created_at: periodStartIso, }); - await captureCostInsightSpend(tx, { - owner: { type: 'user', id: userId }, - actorUserId: userId, - occurredAt: periodStartIso, - amountMicrodollars: plan.costMicrodollars, - category: 'scheduled', - source: 'coding_plan', - productKey: COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, - featureKey: 'activation', - modelOrPlanKey: plan.planId, - providerKey: plan.providerId, - }); - const { rows: inventoryRows } = await tx.execute<{ id: string; encrypted_api_key: { iv: string; data: string; authTag: string } | null; @@ -267,7 +249,6 @@ export async function subscribeToCodingPlan( if (outcome.charged) { await evaluateUsageBonus(userId); - scheduleCostInsightEvaluationAfterSpend({ type: 'user', id: userId }); } logInfo('Coding plan purchase processed', { user_id: userId, diff --git a/apps/web/src/lib/cost-insights-source-indexes-script.test.ts b/apps/web/src/lib/cost-insights-source-indexes-script.test.ts deleted file mode 100644 index ceadf6e9059..00000000000 --- a/apps/web/src/lib/cost-insights-source-indexes-script.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { - parseCostInsightsSourceIndexScriptArgs, - provisionCostInsightsSourceIndexes, -} from '../scripts/db/cost-insights-source-indexes'; -import type { SQL } from 'drizzle-orm'; -import { PgDialect } from 'drizzle-orm/pg-core'; - -describe('Cost Insights source index operator', () => { - test('defaults to dry-run and parses execute', () => { - expect(parseCostInsightsSourceIndexScriptArgs([])).toEqual({ execute: false }); - expect(parseCostInsightsSourceIndexScriptArgs(['--execute'])).toEqual({ execute: true }); - expect(() => parseCostInsightsSourceIndexScriptArgs(['--execute', '--again'])).toThrow( - 'Unknown arguments' - ); - }); - - test('dry-run inspects both indexes without DDL', async () => { - const statements: string[] = []; - const dialect = new PgDialect(); - const fakeDb = { - execute: async (query: SQL) => { - statements.push(dialect.sqlToQuery(query).sql); - return { rows: [] }; - }, - }; - const log = jest.spyOn(console, 'log').mockImplementation(() => undefined); - - try { - await provisionCostInsightsSourceIndexes(fakeDb as never, { execute: false }); - } finally { - log.mockRestore(); - } - - expect(statements).toHaveLength(2); - expect(statements.every(statement => statement.includes('FROM pg_catalog.pg_index'))).toBe( - true - ); - }); - - test('creates missing indexes concurrently and verifies catalog state', async () => { - const statements: string[] = []; - const dialect = new PgDialect(); - const validIndexes = new Map(); - const fakeDb = { - execute: async (query: SQL) => { - const compiled = dialect.sqlToQuery(query); - statements.push(compiled.sql); - if (compiled.sql.includes('FROM pg_catalog.pg_index')) { - const indexName = String(compiled.params[0]); - const tableName = validIndexes.get(indexName); - return { - rows: tableName - ? [ - { - schema_name: 'public', - index_name: indexName, - table_schema_name: 'public', - table_name: tableName, - is_valid: true, - is_ready: true, - }, - ] - : [], - }; - } - const createdIndex = compiled.sql.match( - /CREATE INDEX CONCURRENTLY IF NOT EXISTS "([^"]+)" ON "([^"]+)"/ - ); - if (createdIndex) validIndexes.set(createdIndex[1], createdIndex[2]); - return { rows: [] }; - }, - }; - const log = jest.spyOn(console, 'log').mockImplementation(() => undefined); - - try { - await provisionCostInsightsSourceIndexes(fakeDb as never, { execute: true }); - } finally { - log.mockRestore(); - } - - expect(statements.filter(statement => statement.startsWith('CREATE INDEX'))).toEqual([ - 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_coding_plan_terms_credit_transaction" ON "coding_plan_terms" ("credit_transaction_id")', - 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_microdollar_usage_org_created_at" ON "microdollar_usage" ("organization_id", "created_at") WHERE "organization_id" IS NOT NULL', - ]); - expect( - statements.filter(statement => statement.includes('FROM pg_catalog.pg_index')) - ).toHaveLength(4); - }); - - test('drops and rebuilds invalid concurrent index attempts', async () => { - const statements: string[] = []; - const dialect = new PgDialect(); - const indexName = 'idx_microdollar_usage_org_created_at'; - const indexTables = new Map([ - ['IDX_coding_plan_terms_credit_transaction', 'coding_plan_terms'], - ['idx_microdollar_usage_org_created_at', 'microdollar_usage'], - ]); - let state: 'invalid' | 'missing' | 'valid' = 'invalid'; - const fakeDb = { - execute: async (query: SQL) => { - const compiled = dialect.sqlToQuery(query); - statements.push(compiled.sql); - if (compiled.sql.includes('FROM pg_catalog.pg_index')) { - const queriedIndexName = String(compiled.params[0]); - const tableName = indexTables.get(queriedIndexName); - if (queriedIndexName !== indexName) { - return { - rows: [ - { - schema_name: 'public', - index_name: queriedIndexName, - table_schema_name: 'public', - table_name: tableName, - is_valid: true, - is_ready: true, - }, - ], - }; - } - return { - rows: - state === 'missing' - ? [] - : [ - { - schema_name: 'public', - index_name: indexName, - table_schema_name: 'public', - table_name: tableName, - is_valid: state === 'valid', - is_ready: state === 'valid', - }, - ], - }; - } - if (compiled.sql.startsWith('DROP INDEX')) state = 'missing'; - if (compiled.sql.includes(`"${indexName}"`) && compiled.sql.startsWith('CREATE INDEX')) { - state = 'valid'; - } - return { rows: [] }; - }, - }; - const log = jest.spyOn(console, 'log').mockImplementation(() => undefined); - - try { - await provisionCostInsightsSourceIndexes(fakeDb as never, { execute: true }); - } finally { - log.mockRestore(); - } - - expect(statements.filter(statement => statement.startsWith('DROP INDEX'))).toEqual([ - 'DROP INDEX CONCURRENTLY IF EXISTS "public"."idx_microdollar_usage_org_created_at"', - ]); - }); -}); diff --git a/apps/web/src/lib/cost-insights/canonical-sources.test.ts b/apps/web/src/lib/cost-insights/canonical-sources.test.ts deleted file mode 100644 index 47bb47099fe..00000000000 --- a/apps/web/src/lib/cost-insights/canonical-sources.test.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { - aggregateCanonicalCostInsightDrivers, - aggregateNormalizedCanonicalCostInsightDrivers, - loadCanonicalCostInsightAggregationsByHour, - loadCanonicalCostInsightAggregationsByIntervals, - mapAiGatewayCanonicalDriver, - mapCodingPlanCanonicalDriver, - mapExaCanonicalDriver, - mapKiloClawCanonicalDriver, - parseSafeDatabaseInteger, - type CostInsightQueryExecutor, -} from './canonical-sources'; - -const userOwner = { type: 'user', id: 'user-1' } as const; - -describe('Cost Insights canonical source mapping', () => { - test('maps AI Gateway dimensions with requested model and inference provider precedence', () => { - const mapped = mapAiGatewayCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - feature: 'cloud-agent', - apiKind: 'responses', - requestedModel: 'requested/model', - resolvedModel: 'resolved/model', - inferenceProvider: 'inference-provider', - gatewayProvider: 'gateway-provider', - totalMicrodollars: 17, - spendRecordCount: 1, - }); - - expect(mapped.driver).toMatchObject({ - category: 'variable', - source: 'ai_gateway', - productKey: 'cloud-agent', - featureKey: 'responses', - modelOrPlanKey: 'requested/model', - providerKey: 'inference-provider', - actorUserId: 'user-1', - }); - expect(mapped.unknownTaxonomyValues).toEqual([]); - }); - - test('maps absent AI attribution to other sentinels', () => { - const mapped = mapAiGatewayCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - feature: null, - apiKind: null, - requestedModel: null, - resolvedModel: null, - inferenceProvider: null, - gatewayProvider: null, - totalMicrodollars: 5, - spendRecordCount: 1, - }); - - expect(mapped.driver).toMatchObject({ - productKey: 'other', - featureKey: 'other', - modelOrPlanKey: 'other', - providerKey: 'other', - }); - expect(mapped.unknownTaxonomyValues).toEqual([]); - }); - - test('reports unknown AI and Exa taxonomy while preserving bounded fallback mapping', () => { - const ai = mapAiGatewayCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - feature: 'unregistered-feature', - apiKind: 'unknown-operation', - requestedModel: 'model', - resolvedModel: null, - inferenceProvider: 'provider', - gatewayProvider: null, - totalMicrodollars: 8, - spendRecordCount: 2, - }); - const exa = mapExaCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - path: '/future-path', - totalMicrodollars: 3, - spendRecordCount: 1, - }); - - expect(ai.driver.productKey).toBe('other'); - expect(ai.driver.featureKey).toBe('other'); - expect(ai.unknownTaxonomyValues).toHaveLength(2); - expect(exa.driver).toMatchObject({ - source: 'other', - productKey: 'exa', - featureKey: 'other', - modelOrPlanKey: 'other', - providerKey: 'exa', - }); - expect(exa.unknownTaxonomyValues).toEqual([ - expect.objectContaining({ sourceFamily: 'exa', field: 'feature_key' }), - ]); - }); - - test('keeps allowlisted Exa path as canonical operation key', () => { - const mapped = mapExaCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - path: '/search', - totalMicrodollars: 3, - spendRecordCount: 1, - }); - - expect(mapped.driver.featureKey).toBe('search'); - expect(mapped.unknownTaxonomyValues).toEqual([]); - }); - - test('maps Coding Plan and pure-credit KiloClaw scheduled spend', () => { - const codingPlan = mapCodingPlanCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - termKind: 'renewal', - planId: 'minimax-annual', - providerId: 'minimax', - totalMicrodollars: 100, - spendRecordCount: 1, - }); - const kiloClaw = mapKiloClawCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - isCommit: true, - featureKey: 'renewal', - totalMicrodollars: 200, - spendRecordCount: 1, - }); - - expect(codingPlan.driver).toMatchObject({ - category: 'scheduled', - source: 'coding_plan', - productKey: 'coding-plan', - featureKey: 'renewal', - modelOrPlanKey: 'minimax-annual', - providerKey: 'minimax', - }); - expect(kiloClaw).toMatchObject({ - category: 'scheduled', - source: 'kiloclaw', - productKey: 'kiloclaw-hosting', - featureKey: 'renewal', - modelOrPlanKey: 'commit', - providerKey: 'other', - }); - expect( - mapKiloClawCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - isCommit: false, - featureKey: 'legacy-description', - totalMicrodollars: 1, - spendRecordCount: 1, - }).featureKey - ).toBe('other'); - }); - - test('aggregates identical normalized drivers and owner totals deterministically', async () => { - const input = mapExaCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - path: '/answer', - totalMicrodollars: 4, - spendRecordCount: 1, - }).driver; - const aggregation = await aggregateCanonicalCostInsightDrivers([ - input, - { ...input, totalMicrodollars: 6, spendRecordCount: 2 }, - ]); - - expect(aggregation.totals).toEqual([ - expect.objectContaining({ - owner: userOwner, - category: 'variable', - totalMicrodollars: 10, - spendRecordCount: 3, - }), - ]); - expect(aggregation.drivers).toEqual([ - expect.objectContaining({ - totalMicrodollars: 10, - spendRecordCount: 3, - driverKey: expect.stringMatching(/^[0-9a-f]{64}$/), - }), - ]); - }); - - test('rejects matching driver digests with different canonical dimensions', async () => { - const input = mapExaCanonicalDriver({ - owner: userOwner, - actorUserId: 'user-1', - path: '/answer', - totalMicrodollars: 4, - spendRecordCount: 1, - }).driver; - const normalized = await aggregateCanonicalCostInsightDrivers([input]); - const driver = normalized.drivers[0]; - if (!driver) throw new Error('Expected normalized canonical driver.'); - - expect(() => - aggregateNormalizedCanonicalCostInsightDrivers([ - driver, - { ...driver, providerKey: 'different-provider' }, - ]) - ).toThrow('Canonical Cost Insights driver digest collision.'); - }); - - test('keeps multi-hour canonical source aggregates separated by UTC hour', async () => { - const execute = jest - .fn() - .mockResolvedValueOnce({ - rows: [ - { - hour_start: '2026-06-01 00:00:00+00', - owned_by_user_id: 'user-1', - owned_by_organization_id: null, - actor_user_id: 'user-1', - raw_product_key: null, - raw_feature_key: null, - requested_model: 'model', - resolved_model: null, - inference_provider: 'provider', - gateway_provider: null, - total_microdollars: '4', - spend_record_count: '1', - }, - { - hour_start: '2026-06-01 01:00:00+00', - owned_by_user_id: 'user-1', - owned_by_organization_id: null, - actor_user_id: 'user-1', - raw_product_key: null, - raw_feature_key: null, - requested_model: 'model', - resolved_model: null, - inference_provider: 'provider', - gateway_provider: null, - total_microdollars: '6', - spend_record_count: '2', - }, - ], - }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }); - const executor = { execute } as unknown as CostInsightQueryExecutor; - - const hourly = await loadCanonicalCostInsightAggregationsByHour(executor, { - startInclusive: '2026-06-01T00:00:00.000Z', - endExclusive: '2026-06-01T02:00:00.000Z', - }); - - expect(execute).toHaveBeenCalledTimes(4); - expect(hourly).toEqual([ - expect.objectContaining({ - hourStart: '2026-06-01T00:00:00.000Z', - totals: [expect.objectContaining({ totalMicrodollars: 4, spendRecordCount: 1 })], - }), - expect.objectContaining({ - hourStart: '2026-06-01T01:00:00.000Z', - totals: [expect.objectContaining({ totalMicrodollars: 6, spendRecordCount: 2 })], - }), - ]); - }); - - test('loads separated intervals with four source queries and excludes envelope-only rows', async () => { - const row = (hour_start: string, total_microdollars: string) => ({ - hour_start, - owned_by_user_id: 'user-1', - owned_by_organization_id: null, - actor_user_id: 'user-1', - raw_product_key: null, - raw_feature_key: null, - requested_model: 'model', - resolved_model: null, - inference_provider: 'provider', - gateway_provider: null, - total_microdollars, - spend_record_count: '1', - }); - const execute = jest - .fn() - .mockResolvedValueOnce({ - rows: [ - row('2026-06-01 00:00:00+00', '4'), - row('2026-06-01 01:00:00+00', '99'), - row('2026-06-01 02:00:00+00', '6'), - ], - }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }); - - const hourly = await loadCanonicalCostInsightAggregationsByIntervals( - { execute } as unknown as CostInsightQueryExecutor, - { - owner: userOwner, - intervals: [ - { - startInclusive: '2026-06-01T00:00:00.000Z', - endExclusive: '2026-06-01T01:00:00.000Z', - }, - { - startInclusive: '2026-06-01T02:00:00.000Z', - endExclusive: '2026-06-01T03:00:00.000Z', - }, - ], - } - ); - - expect(execute).toHaveBeenCalledTimes(4); - expect(hourly).toEqual([ - expect.objectContaining({ - hourStart: '2026-06-01T00:00:00.000Z', - totals: [expect.objectContaining({ totalMicrodollars: 4 })], - }), - expect.objectContaining({ - hourStart: '2026-06-01T02:00:00.000Z', - totals: [expect.objectContaining({ totalMicrodollars: 6 })], - }), - ]); - }); - - test('normalizes touching intervals and rejects invalid interval timestamps', async () => { - const execute = jest - .fn() - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }); - await loadCanonicalCostInsightAggregationsByIntervals( - { execute } as unknown as CostInsightQueryExecutor, - { - owner: userOwner, - intervals: [ - { - startInclusive: '2026-06-01T01:00:00.000Z', - endExclusive: '2026-06-01T02:00:00.000Z', - }, - { - startInclusive: '2026-06-01T00:00:00.000Z', - endExclusive: '2026-06-01T01:00:00.000Z', - }, - ], - } - ); - expect(execute).toHaveBeenCalledTimes(4); - - await expect( - loadCanonicalCostInsightAggregationsByIntervals( - { execute } as unknown as CostInsightQueryExecutor, - { - owner: userOwner, - intervals: [ - { - startInclusive: 'invalid', - endExclusive: '2026-06-01T01:00:00.000Z', - }, - ], - } - ) - ).rejects.toThrow('startInclusive must be a valid timestamp'); - }); - - test('rejects unsafe database integers instead of rounding them', () => { - expect(() => parseSafeDatabaseInteger('9007199254740992', 'unsafe aggregate')).toThrow( - 'safe-integer range' - ); - }); -}); diff --git a/apps/web/src/lib/cost-insights/canonical-sources.ts b/apps/web/src/lib/cost-insights/canonical-sources.ts deleted file mode 100644 index e0746f0284a..00000000000 --- a/apps/web/src/lib/cost-insights/canonical-sources.ts +++ /dev/null @@ -1,1081 +0,0 @@ -import { GatewayApiKindSchema } from '@kilocode/db'; -import type { CostInsightSpendCategory, CostInsightSpendSource } from '@kilocode/db/schema-types'; -import { - buildCostInsightDriver, - COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, - COST_INSIGHT_DRIVER_FALLBACK, - COST_INSIGHT_EXA_PRODUCT_KEY, - COST_INSIGHT_KILOCLAW_PRODUCT_KEY, - type CostInsightSpendOwner, -} from '@kilocode/db/cost-insights-rollups'; -import { - api_kind, - coding_plan_subscriptions, - coding_plan_terms, - credit_transactions, - exa_usage_log, - feature, - microdollar_usage, - microdollar_usage_metadata, -} from '@kilocode/db/schema'; -import { sql, type SQL } from 'drizzle-orm'; - -import type { db } from '@/lib/drizzle'; -import { EXA_ALLOWED_PATHS, getExaCostInsightFeatureKey } from '@/lib/exa-paths'; -import { validateFeatureHeader } from '@/lib/feature-detection'; - -export const COST_INSIGHT_ROLLUP_VERSION = 1; -export const COST_INSIGHT_OTHER_DRIVER_KEY = COST_INSIGHT_DRIVER_FALLBACK; -export const COST_INSIGHT_DIRECT_GATEWAY_PRODUCT_KEY = 'direct-gateway'; -export { - COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, - COST_INSIGHT_EXA_PRODUCT_KEY, - COST_INSIGHT_KILOCLAW_PRODUCT_KEY, -}; - -export const COST_INSIGHT_EXA_FEATURE_KEYS = EXA_ALLOWED_PATHS; -const HOUR_MS = 60 * 60 * 1_000; -const TIMESTAMP_WITH_TIMEZONE_PATTERN = /(?:Z|[+-]\d{2}(?::?\d{2})?)$/i; - -export type { CostInsightSpendCategory, CostInsightSpendSource }; -export type CostInsightQueryExecutor = Pick; - -export type CanonicalCostInsightDriverInput = { - owner: CostInsightSpendOwner; - category: CostInsightSpendCategory; - source: CostInsightSpendSource; - productKey: string; - featureKey: string; - modelOrPlanKey: string; - providerKey: string; - actorUserId: string; - totalMicrodollars: number; - spendRecordCount: number; -}; - -export type CanonicalCostInsightDriverAggregate = CanonicalCostInsightDriverInput & { - driverKey: string; -}; - -export type CanonicalCostInsightOwnerTotal = { - owner: CostInsightSpendOwner; - category: CostInsightSpendCategory; - totalMicrodollars: number; - spendRecordCount: number; -}; - -export type CostInsightUnknownTaxonomyValue = { - sourceFamily: 'ai_gateway' | 'exa' | 'coding_plan' | 'kiloclaw'; - field: 'product_key' | 'feature_key' | 'term_kind'; - value: string; - spendRecordCount: number; -}; - -export type CanonicalCostInsightAggregation = { - totals: CanonicalCostInsightOwnerTotal[]; - drivers: CanonicalCostInsightDriverAggregate[]; - unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; -}; - -export type CanonicalCostInsightHourAggregation = CanonicalCostInsightAggregation & { - hourStart: string; -}; - -type CanonicalRange = { - startInclusive: string; - endExclusive: string; -}; - -export type CanonicalCostInsightInterval = CanonicalRange; - -type RawAiAggregate = { - hour_start: string | Date; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - actor_user_id: string; - raw_product_key: string | null; - raw_feature_key: string | null; - requested_model: string | null; - resolved_model: string | null; - inference_provider: string | null; - gateway_provider: string | null; - total_microdollars: string | number | bigint; - spend_record_count: string | number | bigint; -}; - -type RawExaAggregate = { - hour_start: string | Date; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - actor_user_id: string; - raw_feature_key: string; - total_microdollars: string | number | bigint; - spend_record_count: string | number | bigint; -}; - -type RawCodingPlanAggregate = { - hour_start: string | Date; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - actor_user_id: string; - plan_id: string; - provider_id: string; - term_kind: string; - total_microdollars: string | number | bigint; - spend_record_count: string | number | bigint; -}; - -type RawKiloClawAggregate = { - hour_start: string | Date; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - actor_user_id: string; - feature_key: string; - model_or_plan_key: string; - total_microdollars: string | number | bigint; - spend_record_count: string | number | bigint; -}; - -type RawCanonicalTotal = { - spend_category: CostInsightSpendCategory; - total_microdollars: string | number | bigint; - spend_record_count: string | number | bigint; -}; - -function requireCanonicalRange(range: CanonicalRange): void { - const start = Date.parse(requireUtcTimestamp(range.startInclusive, 'startInclusive')); - const end = Date.parse(requireUtcTimestamp(range.endExclusive, 'endExclusive')); - if (start >= end) { - throw new Error('Cost Insights canonical source range must be a non-empty half-open range.'); - } -} - -function normalizeCanonicalIntervals( - intervals: readonly [CanonicalCostInsightInterval, ...CanonicalCostInsightInterval[]] -): CanonicalCostInsightInterval[] { - const normalized = intervals - .map(interval => { - requireCanonicalRange(interval); - return { - startInclusive: requireUtcTimestamp(interval.startInclusive, 'startInclusive'), - endExclusive: requireUtcTimestamp(interval.endExclusive, 'endExclusive'), - }; - }) - .sort((left, right) => Date.parse(left.startInclusive) - Date.parse(right.startInclusive)); - const merged: CanonicalCostInsightInterval[] = []; - for (const interval of normalized) { - const previous = merged.at(-1); - if (!previous || Date.parse(previous.endExclusive) < Date.parse(interval.startInclusive)) { - merged.push(interval); - continue; - } - if (Date.parse(interval.endExclusive) > Date.parse(previous.endExclusive)) { - merged[merged.length - 1] = { ...previous, endExclusive: interval.endExclusive }; - } - } - return merged; -} - -function intervalMembershipPredicate( - timestampColumn: SQL, - intervals: readonly CanonicalCostInsightInterval[] -): SQL { - const values = sql.join( - intervals.map( - interval => - sql`(${interval.startInclusive}::timestamptz, ${interval.endExclusive}::timestamptz)` - ), - sql`, ` - ); - return sql`EXISTS ( - SELECT 1 - FROM (VALUES ${values}) AS canonical_intervals(start_inclusive, end_exclusive) - WHERE ${timestampColumn} >= canonical_intervals.start_inclusive - AND ${timestampColumn} < canonical_intervals.end_exclusive - )`; -} - -function canonicalHourIntersectsIntervals( - rawHourStart: string | Date, - intervals: readonly CanonicalCostInsightInterval[] -): boolean { - const hourStart = Date.parse(normalizeCanonicalHour(rawHourStart)); - const hourEnd = hourStart + HOUR_MS; - return intervals.some( - interval => - Date.parse(interval.startInclusive) < hourEnd && Date.parse(interval.endExclusive) > hourStart - ); -} - -export function requireUtcTimestamp(value: string, fieldName: string): string { - const timestamp = Date.parse(value); - if (!TIMESTAMP_WITH_TIMEZONE_PATTERN.test(value) || !Number.isFinite(timestamp)) { - throw new Error(`${fieldName} must be a valid timestamp with an explicit UTC offset.`); - } - return new Date(timestamp).toISOString(); -} - -export function requireUtcHour(value: string, fieldName: string): string { - const timestamp = Date.parse(requireUtcTimestamp(value, fieldName)); - if (timestamp % HOUR_MS !== 0) { - throw new Error(`${fieldName} must be an exact UTC hour.`); - } - return new Date(timestamp).toISOString(); -} - -export function parseSafeDatabaseInteger( - value: string | number | bigint, - fieldName: string -): number { - const parsed = typeof value === 'bigint' ? Number(value) : Number(value); - if (!Number.isSafeInteger(parsed)) { - throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); - } - return parsed; -} - -function addSafeInteger(left: number, right: number, fieldName: string): number { - const result = left + right; - if (!Number.isSafeInteger(result)) { - throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); - } - return result; -} - -function normalizeCanonicalHour(value: string | Date): string { - const timestamp = value instanceof Date ? value.getTime() : Date.parse(value); - if (!Number.isFinite(timestamp) || timestamp % HOUR_MS !== 0) { - throw new Error('Canonical Cost Insights source row has an invalid UTC hour.'); - } - return new Date(timestamp).toISOString(); -} - -function ownerFromColumns( - ownedByUserId: string | null, - ownedByOrganizationId: string | null -): CostInsightSpendOwner { - if (ownedByOrganizationId && !ownedByUserId) { - return { type: 'organization', id: ownedByOrganizationId }; - } - if (ownedByUserId && !ownedByOrganizationId) { - return { type: 'user', id: ownedByUserId }; - } - throw new Error('Canonical Cost Insights source row must resolve to exactly one Spend owner.'); -} - -function ownerIdentity(owner: CostInsightSpendOwner): string { - return `${owner.type}:${owner.id}`; -} - -function ownerPredicate(params: { - owner: CostInsightSpendOwner | undefined; - userColumn: SQL; - organizationColumn: SQL; -}): SQL { - if (!params.owner) { - return sql`TRUE`; - } - if (params.owner.type === 'organization') { - return sql`${params.organizationColumn} = ${params.owner.id}`; - } - return sql`${params.organizationColumn} IS NULL AND ${params.userColumn} = ${params.owner.id}`; -} - -function pureCreditKiloClawPredicate(transactionAlias: SQL): SQL { - return sql`( - ${transactionAlias}.credit_category LIKE 'kiloclaw-subscription:%' - OR ${transactionAlias}.credit_category LIKE 'kiloclaw-subscription-commit:%' - )`; -} - -function appendUnknownTaxonomyValue( - target: CostInsightUnknownTaxonomyValue[], - value: CostInsightUnknownTaxonomyValue -): void { - const existing = target.find( - candidate => - candidate.sourceFamily === value.sourceFamily && - candidate.field === value.field && - candidate.value === value.value - ); - if (existing) { - existing.spendRecordCount = addSafeInteger( - existing.spendRecordCount, - value.spendRecordCount, - 'unknown taxonomy spend record count' - ); - return; - } - target.push(value); -} - -export function getAiGatewayCostInsightProductKey(featureValue: string | null): string { - return validateFeatureHeader(featureValue) ?? COST_INSIGHT_OTHER_DRIVER_KEY; -} - -export function getAiGatewayCostInsightFeatureKey(apiKindValue: string | null): string { - const parsedApiKind = GatewayApiKindSchema.safeParse(apiKindValue); - return parsedApiKind.success ? parsedApiKind.data : COST_INSIGHT_OTHER_DRIVER_KEY; -} - -export function mapAiGatewayCanonicalDriver(params: { - owner: CostInsightSpendOwner; - actorUserId: string; - feature: string | null; - apiKind: string | null; - requestedModel: string | null; - resolvedModel: string | null; - inferenceProvider: string | null; - gatewayProvider: string | null; - totalMicrodollars: number; - spendRecordCount: number; -}): { - driver: CanonicalCostInsightDriverInput; - unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; -} { - const unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[] = []; - const productKey = getAiGatewayCostInsightProductKey(params.feature); - if (params.feature && productKey === COST_INSIGHT_OTHER_DRIVER_KEY) { - unknownTaxonomyValues.push({ - sourceFamily: 'ai_gateway', - field: 'product_key', - value: params.feature, - spendRecordCount: params.spendRecordCount, - }); - } - - const featureKey = getAiGatewayCostInsightFeatureKey(params.apiKind); - if (params.apiKind && featureKey === COST_INSIGHT_OTHER_DRIVER_KEY) { - unknownTaxonomyValues.push({ - sourceFamily: 'ai_gateway', - field: 'feature_key', - value: params.apiKind, - spendRecordCount: params.spendRecordCount, - }); - } - - return { - driver: { - owner: params.owner, - category: 'variable', - source: 'ai_gateway', - productKey, - featureKey, - modelOrPlanKey: - params.requestedModel || params.resolvedModel || COST_INSIGHT_OTHER_DRIVER_KEY, - providerKey: - params.inferenceProvider || params.gatewayProvider || COST_INSIGHT_OTHER_DRIVER_KEY, - actorUserId: params.actorUserId, - totalMicrodollars: params.totalMicrodollars, - spendRecordCount: params.spendRecordCount, - }, - unknownTaxonomyValues, - }; -} - -export function mapExaCanonicalDriver(params: { - owner: CostInsightSpendOwner; - actorUserId: string; - path: string; - totalMicrodollars: number; - spendRecordCount: number; -}): { - driver: CanonicalCostInsightDriverInput; - unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; -} { - const featureKey = getExaCostInsightFeatureKey(params.path); - return { - driver: { - owner: params.owner, - category: 'variable', - source: 'other', - productKey: COST_INSIGHT_EXA_PRODUCT_KEY, - featureKey, - modelOrPlanKey: COST_INSIGHT_OTHER_DRIVER_KEY, - providerKey: COST_INSIGHT_EXA_PRODUCT_KEY, - actorUserId: params.actorUserId, - totalMicrodollars: params.totalMicrodollars, - spendRecordCount: params.spendRecordCount, - }, - unknownTaxonomyValues: - featureKey !== COST_INSIGHT_OTHER_DRIVER_KEY - ? [] - : [ - { - sourceFamily: 'exa', - field: 'feature_key', - value: params.path, - spendRecordCount: params.spendRecordCount, - }, - ], - }; -} - -export function mapCodingPlanCanonicalDriver(params: { - owner: CostInsightSpendOwner; - actorUserId: string; - termKind: string; - planId: string; - providerId: string; - totalMicrodollars: number; - spendRecordCount: number; -}): { - driver: CanonicalCostInsightDriverInput; - unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; -} { - const isKnownKind = params.termKind === 'activation' || params.termKind === 'renewal'; - return { - driver: { - owner: params.owner, - category: 'scheduled', - source: 'coding_plan', - productKey: COST_INSIGHT_CODING_PLAN_PRODUCT_KEY, - featureKey: isKnownKind ? params.termKind : COST_INSIGHT_OTHER_DRIVER_KEY, - modelOrPlanKey: params.planId, - providerKey: params.providerId, - actorUserId: params.actorUserId, - totalMicrodollars: params.totalMicrodollars, - spendRecordCount: params.spendRecordCount, - }, - unknownTaxonomyValues: isKnownKind - ? [] - : [ - { - sourceFamily: 'coding_plan', - field: 'term_kind', - value: params.termKind, - spendRecordCount: params.spendRecordCount, - }, - ], - }; -} - -export function mapKiloClawCanonicalDriver(params: { - owner: CostInsightSpendOwner; - actorUserId: string; - isCommit: boolean; - featureKey: string; - totalMicrodollars: number; - spendRecordCount: number; -}): CanonicalCostInsightDriverInput { - return { - owner: params.owner, - category: 'scheduled', - source: 'kiloclaw', - productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, - featureKey: - params.featureKey === 'enrollment' || params.featureKey === 'renewal' - ? params.featureKey - : COST_INSIGHT_OTHER_DRIVER_KEY, - modelOrPlanKey: params.isCommit ? 'commit' : 'standard', - providerKey: COST_INSIGHT_OTHER_DRIVER_KEY, - actorUserId: params.actorUserId, - totalMicrodollars: params.totalMicrodollars, - spendRecordCount: params.spendRecordCount, - }; -} - -function driverDimensionsMatch( - left: CanonicalCostInsightDriverAggregate, - right: CanonicalCostInsightDriverAggregate -): boolean { - return ( - left.source === right.source && - left.productKey === right.productKey && - left.featureKey === right.featureKey && - left.modelOrPlanKey === right.modelOrPlanKey && - left.providerKey === right.providerKey && - left.actorUserId === right.actorUserId - ); -} - -export function aggregateNormalizedCanonicalCostInsightDrivers( - inputs: CanonicalCostInsightDriverAggregate[] -): { - totals: CanonicalCostInsightOwnerTotal[]; - drivers: CanonicalCostInsightDriverAggregate[]; -} { - const totals = new Map(); - const drivers = new Map(); - - for (const input of inputs) { - if (!Number.isSafeInteger(input.totalMicrodollars) || input.totalMicrodollars <= 0) { - throw new Error('Canonical Cost Insights amount must be a positive safe integer.'); - } - if (!Number.isSafeInteger(input.spendRecordCount) || input.spendRecordCount <= 0) { - throw new Error('Canonical Cost Insights record count must be a positive safe integer.'); - } - - const totalKey = `${ownerIdentity(input.owner)}:${input.category}`; - const driverIdentity = `${totalKey}:${input.driverKey}`; - const priorDriver = drivers.get(driverIdentity); - if (priorDriver && !driverDimensionsMatch(priorDriver, input)) { - throw new Error('Canonical Cost Insights driver digest collision.'); - } - - const priorTotal = totals.get(totalKey); - if (priorTotal) { - priorTotal.totalMicrodollars = addSafeInteger( - priorTotal.totalMicrodollars, - input.totalMicrodollars, - 'canonical total microdollars' - ); - priorTotal.spendRecordCount = addSafeInteger( - priorTotal.spendRecordCount, - input.spendRecordCount, - 'canonical total spend record count' - ); - } else { - totals.set(totalKey, { - owner: input.owner, - category: input.category, - totalMicrodollars: input.totalMicrodollars, - spendRecordCount: input.spendRecordCount, - }); - } - - if (priorDriver) { - priorDriver.totalMicrodollars = addSafeInteger( - priorDriver.totalMicrodollars, - input.totalMicrodollars, - 'canonical driver microdollars' - ); - priorDriver.spendRecordCount = addSafeInteger( - priorDriver.spendRecordCount, - input.spendRecordCount, - 'canonical driver spend record count' - ); - } else { - drivers.set(driverIdentity, { ...input }); - } - } - - return { - totals: [...totals.values()].sort(compareCanonicalTotals), - drivers: [...drivers.values()].sort(compareCanonicalDrivers), - }; -} - -export async function aggregateCanonicalCostInsightDrivers( - inputs: CanonicalCostInsightDriverInput[] -): Promise<{ - totals: CanonicalCostInsightOwnerTotal[]; - drivers: CanonicalCostInsightDriverAggregate[]; -}> { - const normalizedInputs: CanonicalCostInsightDriverAggregate[] = []; - for (const input of inputs) { - const normalizedDriver = await buildCostInsightDriver({ - source: input.source, - productKey: input.productKey, - featureKey: input.featureKey, - modelOrPlanKey: input.modelOrPlanKey, - providerKey: input.providerKey, - actorUserId: input.actorUserId, - }); - normalizedInputs.push({ - ...input, - ...normalizedDriver, - }); - } - return aggregateNormalizedCanonicalCostInsightDrivers(normalizedInputs); -} - -function compareCanonicalTotals( - left: CanonicalCostInsightOwnerTotal, - right: CanonicalCostInsightOwnerTotal -): number { - return ( - ownerIdentity(left.owner).localeCompare(ownerIdentity(right.owner)) || - left.category.localeCompare(right.category) - ); -} - -function compareCanonicalDrivers( - left: CanonicalCostInsightDriverAggregate, - right: CanonicalCostInsightDriverAggregate -): number { - return ( - ownerIdentity(left.owner).localeCompare(ownerIdentity(right.owner)) || - left.category.localeCompare(right.category) || - left.driverKey.localeCompare(right.driverKey) - ); -} - -async function loadAiAggregates( - executor: CostInsightQueryExecutor, - range: CanonicalRange, - owner: CostInsightSpendOwner | undefined, - intervalPredicate: SQL = sql`TRUE` -): Promise { - const result = await executor.execute(sql` - SELECT - date_trunc('hour', ${microdollar_usage.created_at}, 'UTC') AS hour_start, - CASE WHEN ${microdollar_usage.organization_id} IS NULL - THEN ${microdollar_usage.kilo_user_id} ELSE NULL END AS owned_by_user_id, - ${microdollar_usage.organization_id} AS owned_by_organization_id, - ${microdollar_usage.kilo_user_id} AS actor_user_id, - ${feature.feature} AS raw_product_key, - ${api_kind.api_kind} AS raw_feature_key, - ${microdollar_usage.requested_model} AS requested_model, - ${microdollar_usage.model} AS resolved_model, - ${microdollar_usage.inference_provider} AS inference_provider, - ${microdollar_usage.provider} AS gateway_provider, - SUM(${microdollar_usage.cost})::text AS total_microdollars, - COUNT(*)::text AS spend_record_count - FROM ${microdollar_usage} - LEFT JOIN ${microdollar_usage_metadata} - ON ${microdollar_usage_metadata.id} = ${microdollar_usage.id} - LEFT JOIN ${feature} - ON ${feature.feature_id} = ${microdollar_usage_metadata.feature_id} - LEFT JOIN ${api_kind} - ON ${api_kind.api_kind_id} = ${microdollar_usage_metadata.api_kind_id} - WHERE ${microdollar_usage.created_at} >= ${range.startInclusive} - AND ${microdollar_usage.created_at} < ${range.endExclusive} - AND ${intervalPredicate} - AND ${microdollar_usage.cost} > 0 - AND ${ownerPredicate({ - owner, - userColumn: sql`${microdollar_usage.kilo_user_id}`, - organizationColumn: sql`${microdollar_usage.organization_id}`, - })} - GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 - `); - return result.rows; -} - -async function loadExaAggregates( - executor: CostInsightQueryExecutor, - range: CanonicalRange, - owner: CostInsightSpendOwner | undefined, - intervalPredicate: SQL = sql`TRUE` -): Promise { - const result = await executor.execute(sql` - SELECT - date_trunc('hour', ${exa_usage_log.created_at}, 'UTC') AS hour_start, - CASE WHEN ${exa_usage_log.organization_id} IS NULL - THEN ${exa_usage_log.kilo_user_id} ELSE NULL END AS owned_by_user_id, - ${exa_usage_log.organization_id} AS owned_by_organization_id, - ${exa_usage_log.kilo_user_id} AS actor_user_id, - ${exa_usage_log.path} AS raw_feature_key, - SUM(${exa_usage_log.cost_microdollars})::text AS total_microdollars, - COUNT(*)::text AS spend_record_count - FROM ${exa_usage_log} - WHERE ${exa_usage_log.created_at} >= ${range.startInclusive} - AND ${exa_usage_log.created_at} < ${range.endExclusive} - AND ${intervalPredicate} - AND ${exa_usage_log.charged_to_balance} = TRUE - AND ${exa_usage_log.cost_microdollars} > 0 - AND ${ownerPredicate({ - owner, - userColumn: sql`${exa_usage_log.kilo_user_id}`, - organizationColumn: sql`${exa_usage_log.organization_id}`, - })} - GROUP BY 1, 2, 3, 4, 5 - `); - return result.rows; -} - -async function loadCodingPlanAggregates( - executor: CostInsightQueryExecutor, - range: CanonicalRange, - owner: CostInsightSpendOwner | undefined, - intervalPredicate: SQL = sql`TRUE` -): Promise { - const result = await executor.execute(sql` - SELECT - date_trunc('hour', ${credit_transactions.created_at}, 'UTC') AS hour_start, - CASE WHEN ${credit_transactions.organization_id} IS NULL - THEN ${credit_transactions.kilo_user_id} ELSE NULL END AS owned_by_user_id, - ${credit_transactions.organization_id} AS owned_by_organization_id, - ${coding_plan_terms.user_id} AS actor_user_id, - ${coding_plan_terms.plan_id} AS plan_id, - ${coding_plan_subscriptions.provider_id} AS provider_id, - ${coding_plan_terms.kind} AS term_kind, - SUM(-${credit_transactions.amount_microdollars})::text AS total_microdollars, - COUNT(*)::text AS spend_record_count - FROM ${coding_plan_terms} - INNER JOIN ${credit_transactions} - ON ${credit_transactions.id} = ${coding_plan_terms.credit_transaction_id} - INNER JOIN ${coding_plan_subscriptions} - ON ${coding_plan_subscriptions.id} = ${coding_plan_terms.subscription_id} - WHERE ${credit_transactions.created_at} >= ${range.startInclusive} - AND ${credit_transactions.created_at} < ${range.endExclusive} - AND ${intervalPredicate} - AND ${credit_transactions.amount_microdollars} < 0 - AND ${ownerPredicate({ - owner, - userColumn: sql`${credit_transactions.kilo_user_id}`, - organizationColumn: sql`${credit_transactions.organization_id}`, - })} - GROUP BY 1, 2, 3, 4, 5, 6, 7 - `); - return result.rows; -} - -async function loadKiloClawAggregates( - executor: CostInsightQueryExecutor, - range: CanonicalRange, - owner: CostInsightSpendOwner | undefined, - intervalPredicate: SQL = sql`TRUE` -): Promise { - const transactionAlias = sql.raw('ct'); - const result = await executor.execute(sql` - WITH matching_transactions AS ( - SELECT - date_trunc('hour', ct.created_at, 'UTC') AS hour_start, - CASE WHEN ct.organization_id IS NULL THEN ct.kilo_user_id ELSE NULL END - AS owned_by_user_id, - ct.organization_id AS owned_by_organization_id, - ct.kilo_user_id AS actor_user_id, - CASE WHEN ct.credit_category LIKE 'kiloclaw-subscription-commit:%' - THEN 'commit' ELSE 'standard' END AS model_or_plan_key, - CASE - WHEN ct.description IN ( - 'KiloClaw standard enrollment', - 'KiloClaw commit enrollment' - ) THEN 'enrollment' - WHEN ct.description IN ( - 'KiloClaw standard renewal', - 'KiloClaw commit renewal' - ) THEN 'renewal' - ELSE 'other' - END AS feature_key, - -ct.amount_microdollars AS amount_microdollars - FROM ${credit_transactions} ct - WHERE ct.created_at >= ${range.startInclusive} - AND ct.created_at < ${range.endExclusive} - AND ${intervalPredicate} - AND ct.amount_microdollars < 0 - AND ${pureCreditKiloClawPredicate(transactionAlias)} - AND ${ownerPredicate({ - owner, - userColumn: sql.raw('ct.kilo_user_id'), - organizationColumn: sql.raw('ct.organization_id'), - })} - ) - SELECT - hour_start, - owned_by_user_id, - owned_by_organization_id, - actor_user_id, - feature_key, - model_or_plan_key, - SUM(amount_microdollars)::text AS total_microdollars, - COUNT(*)::text AS spend_record_count - FROM matching_transactions - GROUP BY 1, 2, 3, 4, 5, 6 - `); - return result.rows; -} - -type CanonicalHourAccumulator = { - inputs: CanonicalCostInsightDriverInput[]; - unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; -}; - -function getCanonicalHourAccumulator( - accumulators: Map, - rawHourStart: string | Date -): CanonicalHourAccumulator { - const hourStart = normalizeCanonicalHour(rawHourStart); - const existing = accumulators.get(hourStart); - if (existing) return existing; - const created = { inputs: [], unknownTaxonomyValues: [] }; - accumulators.set(hourStart, created); - return created; -} - -function appendMappedCanonicalDriver( - accumulator: CanonicalHourAccumulator, - mapped: { - driver: CanonicalCostInsightDriverInput; - unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[]; - } -): void { - accumulator.inputs.push(mapped.driver); - for (const unknown of mapped.unknownTaxonomyValues) { - appendUnknownTaxonomyValue(accumulator.unknownTaxonomyValues, unknown); - } -} - -export async function loadCanonicalCostInsightAggregationsByHour( - executor: CostInsightQueryExecutor, - params: CanonicalRange & { - owner?: CostInsightSpendOwner; - intervals?: readonly CanonicalCostInsightInterval[]; - } -): Promise { - requireCanonicalRange(params); - const intervals = params.intervals; - const accumulators = new Map(); - - const aiRows = await loadAiAggregates( - executor, - params, - params.owner, - intervals - ? intervalMembershipPredicate(sql`${microdollar_usage.created_at}`, intervals) - : undefined - ); - for (const row of aiRows) { - if (intervals && !canonicalHourIntersectsIntervals(row.hour_start, intervals)) continue; - appendMappedCanonicalDriver( - getCanonicalHourAccumulator(accumulators, row.hour_start), - mapAiGatewayCanonicalDriver({ - owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), - actorUserId: row.actor_user_id, - feature: row.raw_product_key, - apiKind: row.raw_feature_key, - requestedModel: row.requested_model, - resolvedModel: row.resolved_model, - inferenceProvider: row.inference_provider, - gatewayProvider: row.gateway_provider, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'AI Gateway canonical microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'AI Gateway canonical record count' - ), - }) - ); - } - - const exaRows = await loadExaAggregates( - executor, - params, - params.owner, - intervals ? intervalMembershipPredicate(sql`${exa_usage_log.created_at}`, intervals) : undefined - ); - for (const row of exaRows) { - if (intervals && !canonicalHourIntersectsIntervals(row.hour_start, intervals)) continue; - appendMappedCanonicalDriver( - getCanonicalHourAccumulator(accumulators, row.hour_start), - mapExaCanonicalDriver({ - owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), - actorUserId: row.actor_user_id, - path: row.raw_feature_key, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'Exa canonical microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'Exa canonical record count' - ), - }) - ); - } - - const codingPlanRows = await loadCodingPlanAggregates( - executor, - params, - params.owner, - intervals - ? intervalMembershipPredicate(sql`${credit_transactions.created_at}`, intervals) - : undefined - ); - for (const row of codingPlanRows) { - if (intervals && !canonicalHourIntersectsIntervals(row.hour_start, intervals)) continue; - appendMappedCanonicalDriver( - getCanonicalHourAccumulator(accumulators, row.hour_start), - mapCodingPlanCanonicalDriver({ - owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), - actorUserId: row.actor_user_id, - termKind: row.term_kind, - planId: row.plan_id, - providerId: row.provider_id, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'Coding Plan canonical microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'Coding Plan canonical record count' - ), - }) - ); - } - - const kiloClawRows = await loadKiloClawAggregates( - executor, - params, - params.owner, - intervals ? intervalMembershipPredicate(sql.raw('ct.created_at'), intervals) : undefined - ); - for (const row of kiloClawRows) { - if (intervals && !canonicalHourIntersectsIntervals(row.hour_start, intervals)) continue; - getCanonicalHourAccumulator(accumulators, row.hour_start).inputs.push( - mapKiloClawCanonicalDriver({ - owner: ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id), - actorUserId: row.actor_user_id, - isCommit: row.model_or_plan_key === 'commit', - featureKey: row.feature_key, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'KiloClaw canonical microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'KiloClaw canonical record count' - ), - }) - ); - } - - const hourly: CanonicalCostInsightHourAggregation[] = []; - for (const hourStart of [...accumulators.keys()].sort()) { - const accumulator = accumulators.get(hourStart); - if (!accumulator) continue; - hourly.push({ - hourStart, - ...(await aggregateCanonicalCostInsightDrivers(accumulator.inputs)), - unknownTaxonomyValues: accumulator.unknownTaxonomyValues, - }); - } - return hourly; -} - -export async function loadCanonicalCostInsightAggregationsByIntervals( - executor: CostInsightQueryExecutor, - params: { - owner: CostInsightSpendOwner; - intervals: readonly [CanonicalCostInsightInterval, ...CanonicalCostInsightInterval[]]; - } -): Promise { - const intervals = normalizeCanonicalIntervals(params.intervals); - const first = intervals[0]; - const last = intervals.at(-1); - if (!first || !last) { - throw new Error('Cost Insights canonical source intervals must not be empty.'); - } - return await loadCanonicalCostInsightAggregationsByHour(executor, { - owner: params.owner, - startInclusive: first.startInclusive, - endExclusive: last.endExclusive, - intervals, - }); -} - -export async function loadCanonicalCostInsightAggregation( - executor: CostInsightQueryExecutor, - params: CanonicalRange & { owner?: CostInsightSpendOwner } -): Promise { - const hourly = await loadCanonicalCostInsightAggregationsByHour(executor, params); - const unknownTaxonomyValues: CostInsightUnknownTaxonomyValue[] = []; - for (const hour of hourly) { - for (const unknown of hour.unknownTaxonomyValues) { - appendUnknownTaxonomyValue(unknownTaxonomyValues, unknown); - } - } - return { - ...aggregateNormalizedCanonicalCostInsightDrivers(hourly.flatMap(hour => hour.drivers)), - unknownTaxonomyValues, - }; -} - -export async function getCanonicalOwnerSpendTotals( - executor: CostInsightQueryExecutor, - params: CanonicalRange & { owner: CostInsightSpendOwner } -): Promise<{ - variableMicrodollars: number; - scheduledMicrodollars: number; - variableRecordCount: number; - scheduledRecordCount: number; -}> { - requireCanonicalRange(params); - const owner = params.owner; - const kiloClawAlias = sql.raw('ct'); - const result = await executor.execute(sql` - WITH canonical_totals AS ( - SELECT - 'variable'::text AS spend_category, - SUM(${microdollar_usage.cost}) AS total_microdollars, - COUNT(*) AS spend_record_count - FROM ${microdollar_usage} - WHERE ${microdollar_usage.created_at} >= ${params.startInclusive} - AND ${microdollar_usage.created_at} < ${params.endExclusive} - AND ${microdollar_usage.cost} > 0 - AND ${ownerPredicate({ - owner, - userColumn: sql`${microdollar_usage.kilo_user_id}`, - organizationColumn: sql`${microdollar_usage.organization_id}`, - })} - UNION ALL - SELECT - 'variable'::text, - SUM(${exa_usage_log.cost_microdollars}), - COUNT(*) - FROM ${exa_usage_log} - WHERE ${exa_usage_log.created_at} >= ${params.startInclusive} - AND ${exa_usage_log.created_at} < ${params.endExclusive} - AND ${exa_usage_log.charged_to_balance} = TRUE - AND ${exa_usage_log.cost_microdollars} > 0 - AND ${ownerPredicate({ - owner, - userColumn: sql`${exa_usage_log.kilo_user_id}`, - organizationColumn: sql`${exa_usage_log.organization_id}`, - })} - UNION ALL - SELECT - 'scheduled'::text, - SUM(-${credit_transactions.amount_microdollars}), - COUNT(*) - FROM ${coding_plan_terms} - INNER JOIN ${credit_transactions} - ON ${credit_transactions.id} = ${coding_plan_terms.credit_transaction_id} - WHERE ${credit_transactions.created_at} >= ${params.startInclusive} - AND ${credit_transactions.created_at} < ${params.endExclusive} - AND ${credit_transactions.amount_microdollars} < 0 - AND ${ownerPredicate({ - owner, - userColumn: sql`${credit_transactions.kilo_user_id}`, - organizationColumn: sql`${credit_transactions.organization_id}`, - })} - UNION ALL - SELECT - 'scheduled'::text, - SUM(-ct.amount_microdollars), - COUNT(*) - FROM ${credit_transactions} ct - WHERE ct.created_at >= ${params.startInclusive} - AND ct.created_at < ${params.endExclusive} - AND ct.amount_microdollars < 0 - AND ${pureCreditKiloClawPredicate(kiloClawAlias)} - AND ${ownerPredicate({ - owner, - userColumn: sql.raw('ct.kilo_user_id'), - organizationColumn: sql.raw('ct.organization_id'), - })} - ) - SELECT - spend_category, - COALESCE(SUM(total_microdollars), 0)::text AS total_microdollars, - COALESCE(SUM(spend_record_count), 0)::text AS spend_record_count - FROM canonical_totals - GROUP BY spend_category - `); - - let variableMicrodollars = 0; - let scheduledMicrodollars = 0; - let variableRecordCount = 0; - let scheduledRecordCount = 0; - for (const row of result.rows) { - const amount = parseSafeDatabaseInteger(row.total_microdollars, 'canonical owner microdollars'); - const count = parseSafeDatabaseInteger(row.spend_record_count, 'canonical owner record count'); - if (row.spend_category === 'variable') { - variableMicrodollars = amount; - variableRecordCount = count; - } else if (row.spend_category === 'scheduled') { - scheduledMicrodollars = amount; - scheduledRecordCount = count; - } - } - return { - variableMicrodollars, - scheduledMicrodollars, - variableRecordCount, - scheduledRecordCount, - }; -} diff --git a/apps/web/src/lib/cost-insights/cost-insights-rollups-script.test.ts b/apps/web/src/lib/cost-insights/cost-insights-rollups-script.test.ts deleted file mode 100644 index d2c02d8f89d..00000000000 --- a/apps/web/src/lib/cost-insights/cost-insights-rollups-script.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { parseCostInsightRollupScriptArgs } from '@/scripts/db/cost-insights-rollups'; - -describe('Cost Insights rollup operator arguments', () => { - test('defaults to dry-run reconciliation', () => { - expect( - parseCostInsightRollupScriptArgs([ - '--start-hour', - '2026-06-01T00:00:00.000Z', - '--end-hour', - '2026-06-01T02:00:00.000Z', - '--max-hours', - '2', - ]) - ).toEqual({ - execute: false, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - maxHours: 2, - sleepMs: 0, - }); - }); - - test('parses explicit execution and pacing', () => { - expect( - parseCostInsightRollupScriptArgs([ - '--execute', - '--start-hour', - '2026-06-01T00:00:00.000Z', - '--end-hour', - '2026-06-01T02:00:00.000Z', - '--max-hours', - '24', - '--sleep-ms', - '250', - ]) - ).toMatchObject({ execute: true, maxHours: 24, sleepMs: 250 }); - }); - - test('parses one-time live-capture coverage initialization only for execution', () => { - expect( - parseCostInsightRollupScriptArgs([ - '--execute', - '--live-capture-start-hour', - '2026-06-01T02:00:00.000Z', - '--start-hour', - '2026-06-01T00:00:00.000Z', - '--end-hour', - '2026-06-01T02:00:00.000Z', - '--max-hours', - '2', - ]) - ).toMatchObject({ liveCaptureStartHour: '2026-06-01T02:00:00.000Z' }); - - expect(() => - parseCostInsightRollupScriptArgs([ - '--live-capture-start-hour', - '2026-06-01T02:00:00.000Z', - '--start-hour', - '2026-06-01T00:00:00.000Z', - '--end-hour', - '2026-06-01T02:00:00.000Z', - '--max-hours', - '2', - ]) - ).toThrow('--live-capture-start-hour requires --execute'); - }); - - test('rejects ranges beyond explicit maximum', () => { - expect(() => - parseCostInsightRollupScriptArgs([ - '--start-hour', - '2026-06-01T00:00:00.000Z', - '--end-hour', - '2026-06-01T03:00:00.000Z', - '--max-hours', - '2', - ]) - ).toThrow('Requested range must contain 1-2 UTC hours'); - }); - - test('rejects non-hour-aligned bounds', () => { - expect(() => - parseCostInsightRollupScriptArgs([ - '--start-hour', - '2026-06-01T00:30:00.000Z', - '--end-hour', - '2026-06-01T02:00:00.000Z', - '--max-hours', - '2', - ]) - ).toThrow('exact UTC hour'); - }); -}); diff --git a/apps/web/src/lib/cost-insights/evaluation.integration.test.ts b/apps/web/src/lib/cost-insights/evaluation.integration.test.ts deleted file mode 100644 index 75589235a03..00000000000 --- a/apps/web/src/lib/cost-insights/evaluation.integration.test.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { afterEach, describe, expect, test } from '@jest/globals'; -import { captureCostInsightSpend } from '@kilocode/db/cost-insights-rollups'; -import { - cost_insight_evaluation_dirty_owners, - cost_insight_events, - cost_insight_owner_configs, - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, - cost_insight_owner_states, - kilocode_users, - microdollar_usage, -} from '@kilocode/db/schema'; -import { and, eq } from 'drizzle-orm'; - -import { db } from '@/lib/drizzle'; -import { addHours, floorUtcHour } from './policy'; -import { getOwnerRollingDriverEvidenceExact, getOwnerRollingSpendExact } from './spend-repository'; -import { evaluateCostInsightsForOwner, processPendingCostInsightEvaluations } from './evaluation'; - -const testUserIds = new Set(); - -async function createUser(): Promise { - const id = `cost-insights-evaluation-${crypto.randomUUID()}`; - testUserIds.add(id); - await db.insert(kilocode_users).values({ - id, - google_user_email: `${id}@example.com`, - google_user_name: 'Cost Insights Evaluation Test', - google_user_image_url: 'https://example.com/avatar.png', - stripe_customer_id: `cus_${crypto.randomUUID()}`, - }); - return id; -} - -afterEach(async () => { - for (const userId of testUserIds) { - await db.delete(microdollar_usage).where(eq(microdollar_usage.kilo_user_id, userId)); - await db - .delete(cost_insight_evaluation_dirty_owners) - .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)); - await db - .delete(cost_insight_owner_states) - .where(eq(cost_insight_owner_states.owned_by_user_id, userId)); - await db.delete(cost_insight_events).where(eq(cost_insight_events.owned_by_user_id, userId)); - await db - .delete(cost_insight_owner_configs) - .where(eq(cost_insight_owner_configs.owned_by_user_id, userId)); - await db - .delete(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); - await db - .delete(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); - } - testUserIds.clear(); -}); - -describe('Cost Insights evaluation integration', () => { - test('does not create anomaly events when anomaly alerting is opted out', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const asOf = new Date().toISOString(); - const currentHourStart = floorUtcHour(new Date(asOf)); - - await db.insert(cost_insight_owner_configs).values({ - owned_by_user_id: userId, - spend_alerts_enabled: true, - anomaly_alerts_enabled: false, - cost_suggestions_enabled: false, - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt: currentHourStart, - amountMicrodollars: 30_000_000, - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - }); - - const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); - const events = await db - .select() - .from(cost_insight_events) - .where(eq(cost_insight_events.owned_by_user_id, userId)); - - expect(result.anomalyEventCreated).toBe(false); - expect(events).toHaveLength(0); - }); - - test('creates an independent rolling 30-day threshold alert', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const asOf = new Date().toISOString(); - const currentHourStart = floorUtcHour(new Date(asOf)); - const spendAt = new Date(Date.parse(asOf) - 1_000).toISOString(); - const usageId = crypto.randomUUID(); - - await db.insert(cost_insight_owner_configs).values({ - owned_by_user_id: userId, - spend_alerts_enabled: true, - anomaly_alerts_enabled: false, - cost_suggestions_enabled: false, - spend_30_day_threshold_microdollars: 20_000_000, - }); - await db.insert(microdollar_usage).values({ - id: usageId, - kilo_user_id: userId, - cost: 30_000_000, - input_tokens: 1, - output_tokens: 1, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: spendAt, - requested_model: 'anthropic/claude-sonnet-4', - model: 'anthropic/claude-sonnet-4', - provider: 'anthropic', - inference_provider: 'anthropic', - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt: currentHourStart, - amountMicrodollars: 30_000_000, - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - }); - - await expect( - getOwnerRollingSpendExact(db, { - owner, - asOf, - windowHours: 720, - fallbackToCanonical: true, - }) - ).resolves.toMatchObject({ totalMicrodollars: 30_000_000, isComplete: true }); - await expect( - getOwnerRollingDriverEvidenceExact(db, { owner, asOf, windowHours: 720 }) - ).resolves.toMatchObject({ totalMicrodollars: 30_000_000 }); - - const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); - const [event] = await db - .select() - .from(cost_insight_events) - .where( - and( - eq(cost_insight_events.owned_by_user_id, userId), - eq(cost_insight_events.alert_kind, 'threshold_30d') - ) - ); - const [state] = await db - .select() - .from(cost_insight_owner_states) - .where(eq(cost_insight_owner_states.owned_by_user_id, userId)); - - expect(result.threshold30DayEventCreated).toBe(true); - expect(event?.snapshot).toMatchObject({ - thresholdMicrodollars: 20_000_000, - thresholdWindow: 'rolling_30d', - rolling30DayMicrodollars: 30_000_000, - }); - expect(state).toMatchObject({ - rolling_30_day_threshold_crossing_active: true, - active_rolling_30_day_threshold_event_id: event?.id, - rolling_30_day_threshold_reviewed_at: null, - }); - }); - - test('creates an independent rolling 7-day threshold alert', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const asOf = new Date().toISOString(); - const currentHourStart = floorUtcHour(new Date(asOf)); - const spendAt = new Date(Date.parse(asOf) - 1_000).toISOString(); - const usageId = crypto.randomUUID(); - - await db.insert(cost_insight_owner_configs).values({ - owned_by_user_id: userId, - spend_alerts_enabled: true, - anomaly_alerts_enabled: false, - cost_suggestions_enabled: false, - spend_7_day_threshold_microdollars: 20_000_000, - }); - await db.insert(microdollar_usage).values({ - id: usageId, - kilo_user_id: userId, - cost: 30_000_000, - input_tokens: 1, - output_tokens: 1, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: spendAt, - requested_model: 'anthropic/claude-sonnet-4', - model: 'anthropic/claude-sonnet-4', - provider: 'anthropic', - inference_provider: 'anthropic', - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt: currentHourStart, - amountMicrodollars: 30_000_000, - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - }); - - await expect( - getOwnerRollingSpendExact(db, { - owner, - asOf, - windowHours: 7 * 24, - fallbackToCanonical: true, - }) - ).resolves.toMatchObject({ totalMicrodollars: 30_000_000, isComplete: true }); - - const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); - const [event] = await db - .select() - .from(cost_insight_events) - .where( - and( - eq(cost_insight_events.owned_by_user_id, userId), - eq(cost_insight_events.alert_kind, 'threshold_7d') - ) - ); - const [state] = await db - .select() - .from(cost_insight_owner_states) - .where(eq(cost_insight_owner_states.owned_by_user_id, userId)); - - expect(result.threshold7DayEventCreated).toBe(true); - expect(event?.snapshot).toMatchObject({ - thresholdMicrodollars: 20_000_000, - thresholdWindow: 'rolling_7d', - rolling7DayMicrodollars: 30_000_000, - }); - expect(state).toMatchObject({ - rolling_7_day_threshold_crossing_active: true, - active_rolling_7_day_threshold_event_id: event?.id, - rolling_7_day_threshold_reviewed_at: null, - }); - }); - - test('snapshots only current-hour Variable Credit spend drivers for anomaly alerts', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const asOf = new Date().toISOString(); - const currentHourStart = floorUtcHour(new Date(asOf)); - const priorHourStart = addHours(currentHourStart, -1); - - await db.insert(cost_insight_owner_configs).values({ - owned_by_user_id: userId, - spend_alerts_enabled: true, - cost_suggestions_enabled: false, - }); - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 30_000_000, - input_tokens: 1, - output_tokens: 1, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: currentHourStart, - requested_model: 'anthropic/claude-sonnet-4', - model: 'anthropic/claude-sonnet-4', - provider: 'anthropic', - inference_provider: 'anthropic', - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt: currentHourStart, - amountMicrodollars: 30_000_000, - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt: currentHourStart, - amountMicrodollars: 100_000_000, - category: 'scheduled', - source: 'kiloclaw', - productKey: 'kiloclaw', - featureKey: 'renewal', - modelOrPlanKey: 'standard', - providerKey: 'other', - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt: priorHourStart, - amountMicrodollars: 200_000_000, - category: 'variable', - source: 'ai_gateway', - productKey: 'cloud-agent', - featureKey: 'responses', - modelOrPlanKey: 'openai/gpt-4.1', - providerKey: 'openai', - }); - - await evaluateCostInsightsForOwner(db, owner, { asOf }); - - const [event] = await db - .select({ snapshot: cost_insight_events.snapshot }) - .from(cost_insight_events) - .where( - and( - eq(cost_insight_events.owned_by_user_id, userId), - eq(cost_insight_events.event_type, 'anomaly_alert') - ) - ); - expect(event?.snapshot.topDriversWindow).toEqual({ - startInclusive: currentHourStart, - endExclusive: asOf, - spendCategory: 'variable', - }); - expect(event?.snapshot.topDrivers).toEqual([ - expect.objectContaining({ - spendCategory: 'variable', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - totalMicrodollars: 30_000_000, - }), - ]); - }); - - test('uses historical asOf for partial-hour anomaly amount and drivers', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const hourStart = '2026-06-26T10:00:00.000Z'; - const asOf = '2026-06-26T10:30:00.000Z'; - - await db.insert(cost_insight_owner_configs).values({ - owned_by_user_id: userId, - spend_alerts_enabled: true, - cost_suggestions_enabled: false, - }); - for (const spend of [ - { - occurredAt: '2026-06-26T10:15:00.000Z', - amountMicrodollars: 30_000_000, - model: 'anthropic/claude-sonnet-4', - }, - { - occurredAt: '2026-06-26T10:45:00.000Z', - amountMicrodollars: 100_000_000, - model: 'openai/gpt-4.1', - }, - ]) { - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: spend.amountMicrodollars, - input_tokens: 1, - output_tokens: 1, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: spend.occurredAt, - requested_model: spend.model, - model: spend.model, - provider: spend.model.startsWith('openai') ? 'openai' : 'anthropic', - inference_provider: spend.model.startsWith('openai') ? 'openai' : 'anthropic', - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt: spend.occurredAt, - amountMicrodollars: spend.amountMicrodollars, - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: spend.model, - providerKey: spend.model.startsWith('openai') ? 'openai' : 'anthropic', - }); - } - - const result = await evaluateCostInsightsForOwner(db, owner, { asOf }); - const [event] = await db - .select({ snapshot: cost_insight_events.snapshot }) - .from(cost_insight_events) - .where(eq(cost_insight_events.owned_by_user_id, userId)); - - expect(result.anomalyEventCreated).toBe(true); - expect(event?.snapshot).toMatchObject({ - currentHourVariableMicrodollars: 30_000_000, - topDriversWindow: { - startInclusive: hourStart, - endExclusive: asOf, - spendCategory: 'variable', - }, - topDrivers: [ - expect.objectContaining({ - modelOrPlanKey: 'anthropic/claude-sonnet-4', - totalMicrodollars: 30_000_000, - }), - ], - }); - }); - - test('hourly recovery evaluates the just-completed hour at rollover', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const completedHourStart = '2026-06-26T10:00:00.000Z'; - const completedHourEnd = '2026-06-26T11:00:00.000Z'; - const asOf = '2026-06-26T11:05:00.000Z'; - const occurredAt = '2026-06-26T10:59:59.000Z'; - - await db.insert(cost_insight_owner_configs).values({ - owned_by_user_id: userId, - spend_alerts_enabled: true, - cost_suggestions_enabled: false, - }); - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 30_000_000, - input_tokens: 1, - output_tokens: 1, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: occurredAt, - requested_model: 'anthropic/claude-sonnet-4', - model: 'anthropic/claude-sonnet-4', - provider: 'anthropic', - inference_provider: 'anthropic', - }); - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt, - amountMicrodollars: 30_000_000, - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - }); - - const result = await evaluateCostInsightsForOwner(db, owner, { - asOf, - recoverCompletedHour: true, - }); - const [event] = await db - .select({ snapshot: cost_insight_events.snapshot }) - .from(cost_insight_events) - .where(eq(cost_insight_events.owned_by_user_id, userId)); - - expect(result.recoveredAnomalyEventCreated).toBe(true); - expect(result.anomalyEventCreated).toBe(false); - expect(event?.snapshot.topDriversWindow).toEqual({ - startInclusive: completedHourStart, - endExclusive: completedHourEnd, - spendCategory: 'variable', - }); - }); - - test('coalesces multiple spend captures into one durable owner evaluation', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const occurredAt = '2026-06-26T10:15:00.000Z'; - - await db.insert(cost_insight_owner_configs).values({ - owned_by_user_id: userId, - spend_alerts_enabled: false, - cost_suggestions_enabled: false, - }); - for (let index = 0; index < 3; index += 1) { - await captureCostInsightSpend(db, { - owner, - actorUserId: userId, - occurredAt, - amountMicrodollars: 1_000_000, - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - }); - } - - const [dirtyOwner] = await db - .select() - .from(cost_insight_evaluation_dirty_owners) - .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)); - expect(dirtyOwner?.generation).toBe(3); - expect(Date.parse(dirtyOwner?.next_attempt_at ?? '')).toBeGreaterThan(Date.now()); - - await db - .update(cost_insight_evaluation_dirty_owners) - .set({ next_attempt_at: '2026-06-26T10:29:00.000Z' }) - .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)); - - await expect( - processPendingCostInsightEvaluations(db, { owner, asOf: '2026-06-26T10:30:00.000Z' }) - ).resolves.toMatchObject({ - claimed: 1, - evaluatedOwners: [owner], - failedOwners: [], - rawCanonicalFallbackCount: 0, - }); - await expect( - db - .select() - .from(cost_insight_evaluation_dirty_owners) - .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)) - ).resolves.toHaveLength(0); - }); -}); diff --git a/apps/web/src/lib/cost-insights/evaluation.ts b/apps/web/src/lib/cost-insights/evaluation.ts deleted file mode 100644 index e9a8bf0572f..00000000000 --- a/apps/web/src/lib/cost-insights/evaluation.ts +++ /dev/null @@ -1,800 +0,0 @@ -import { createHash } from 'node:crypto'; - -import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; -import { after } from 'next/server'; -import type { CostInsightSpendCategory, CostInsightSpendSource } from '@kilocode/db/schema-types'; -import { sql } from 'drizzle-orm'; - -import { db } from '@/lib/drizzle'; -import { - getOwnerHourlySpend, - getOwnerHourDriverEvidence, - getOwnerRollingDriverEvidenceExact, - getOwnerRollingSpendExact, - type OwnerTopSpendDriver, -} from './spend-repository'; -import { - addDays, - addHours, - calculateAnomalyPolicy, - floorUtcHour, - microdollarsToUsd, - MICRODOLLARS_PER_USD, -} from './policy'; -import { - clearCostInsightThresholdEpisode, - createCostInsightEvent, - createCostInsightNotificationDeliveries, - getCostInsightDashboardState, - getCostInsightOwnerConfig, - listActiveCostInsightSuggestions, - listCostInsightNotificationRecipientUserIds, - markCostInsightAnomalyEpisode, - markCostInsightEvaluation, - markCostInsightThresholdEpisode, - upsertCostInsightActiveSuggestion, - type CostInsightDatabase, - type CostInsightRootDatabase, - type CostInsightThresholdAlertKind, -} from './repository'; -import { dispatchPendingCostInsightNotifications } from './notifications'; -import { isCodingPlanSuggestionEligible } from './suggestion-eligibility'; - -const SUGGESTION_MIN_VARIABLE_MICRODOLLARS = 50 * MICRODOLLARS_PER_USD; -const SUGGESTION_MIN_TOTAL_MICRODOLLARS = 100 * MICRODOLLARS_PER_USD; -const KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS = 199 * MICRODOLLARS_PER_USD; -const KILO_PASS_EXPERT_BONUS_MICRODOLLARS = 79_600_000; - -type AlertTopDriverSnapshot = { - spendCategory: CostInsightSpendCategory; - source: CostInsightSpendSource; - productKey: string; - featureKey: string; - modelOrPlanKey: string; - providerKey: string; - actorUserId: string | null; - totalMicrodollars: number; - spendRecordCount: number; -}; - -export type CostInsightEvaluationSummary = { - owner: CostInsightSpendOwner; - evaluatedAt: string; - anomalyEventCreated: boolean; - recoveredAnomalyEventCreated: boolean; - thresholdEventCreated: boolean; - threshold7DayEventCreated: boolean; - threshold30DayEventCreated: boolean; - suggestionCreated: boolean; - durationMs: number; - rawCanonicalFallbackCount: number; - rollupDegradedIntervalCount: number; -}; - -const thresholdWindowDescriptors = { - threshold: { - windowHours: 24, - windowLabel: '24-hour', - snapshotWindow: 'rolling_24h', - rollingSnapshot: (microdollars: number) => ({ rolling24HourMicrodollars: microdollars }), - }, - threshold_7d: { - windowHours: 7 * 24, - windowLabel: '7-day', - snapshotWindow: 'rolling_7d', - rollingSnapshot: (microdollars: number) => ({ rolling7DayMicrodollars: microdollars }), - }, - threshold_30d: { - windowHours: 30 * 24, - windowLabel: '30-day', - snapshotWindow: 'rolling_30d', - rollingSnapshot: (microdollars: number) => ({ rolling30DayMicrodollars: microdollars }), - }, -} satisfies Record< - CostInsightThresholdAlertKind, - { - windowHours: number; - windowLabel: string; - snapshotWindow: 'rolling_24h' | 'rolling_7d' | 'rolling_30d'; - rollingSnapshot: ( - microdollars: number - ) => - | { rolling24HourMicrodollars: number } - | { rolling7DayMicrodollars: number } - | { rolling30DayMicrodollars: number }; - } ->; - -function topDriverSnapshot(drivers: OwnerTopSpendDriver[]): AlertTopDriverSnapshot[] { - return drivers.slice(0, 5).map(driver => ({ - spendCategory: driver.category, - source: driver.source, - productKey: driver.productKey, - featureKey: driver.featureKey, - modelOrPlanKey: driver.modelOrPlanKey, - providerKey: driver.providerKey, - actorUserId: driver.actorUserId, - totalMicrodollars: driver.totalMicrodollars, - spendRecordCount: driver.spendRecordCount, - })); -} - -function usdLabel(microdollars: number): string { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(microdollarsToUsd(microdollars)); -} - -function roundedUsdLabel(microdollars: number): string { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - maximumFractionDigits: 0, - }).format(microdollarsToUsd(microdollars)); -} - -function suggestionKey(parts: string[]): string { - return createHash('sha256').update(parts.join('\0')).digest('hex'); -} - -function sentenceLabel(value: string): string { - return value - .split(/[-_:/.]+/) - .filter(Boolean) - .map(part => - part.toLowerCase() === 'cli' ? 'CLI' : part.charAt(0).toUpperCase() + part.slice(1) - ) - .join(' '); -} - -export async function getCostInsightAnomalyPolicy( - database: CostInsightDatabase, - owner: CostInsightSpendOwner, - currentHourStart: string -) { - const baselineStart = addHours(currentHourStart, -24 * 7); - const hourly = await getOwnerHourlySpend(database, { - owner, - startHour: baselineStart, - endHourExclusive: currentHourStart, - }); - return calculateAnomalyPolicy( - hourly - .filter(hour => hour.isCovered && hour.variableMicrodollars !== null) - .map(hour => hour.variableMicrodollars ?? 0) - ); -} - -async function maybeCreateAnomalyAlert(params: { - database: CostInsightDatabase; - owner: CostInsightSpendOwner; - hourStart: string; - intervalEnd: string; - variableMicrodollars: number; - anomalyPolicy: Awaited>; - topDrivers: OwnerTopSpendDriver[]; -}): Promise { - if (params.variableMicrodollars < params.anomalyPolicy.thresholdMicrodollars) { - return false; - } - - const dashboardState = await getCostInsightDashboardState(params.database, params.owner); - if ( - dashboardState.state?.activeAnomalyHourStart && - new Date(dashboardState.state.activeAnomalyHourStart).toISOString() === params.hourStart - ) { - return false; - } - - const snapshot = { - currentHourVariableMicrodollars: params.variableMicrodollars, - anomalyBaselineMicrodollars: params.anomalyPolicy.baselineMicrodollars, - anomalyThresholdMicrodollars: params.anomalyPolicy.thresholdMicrodollars, - topDrivers: topDriverSnapshot(params.topDrivers), - topDriversWindow: { - startInclusive: params.hourStart, - endExclusive: params.intervalEnd, - spendCategory: 'variable' as const, - }, - }; - const event = await createCostInsightEvent(params.database, { - owner: params.owner, - eventType: 'anomaly_alert', - alertKind: 'anomaly', - title: 'Spend Anomaly Alert', - description: `Usage-based spend reached ${usdLabel(params.variableMicrodollars)} during the evaluated UTC hour.`, - snapshot, - dedupeKey: `anomaly:${params.hourStart}`, - }); - if (!event.created) return false; - - await markCostInsightAnomalyEpisode(params.database, { - owner: params.owner, - eventId: event.id, - hourStart: params.hourStart, - snapshot, - }); - await createCostInsightNotificationDeliveries( - params.database, - event.id, - await listCostInsightNotificationRecipientUserIds(params.database, params.owner) - ); - return true; -} - -async function evaluateAnomalyInterval(params: { - database: CostInsightDatabase; - owner: CostInsightSpendOwner; - hourStart: string; - intervalEnd: string; -}): Promise<{ - eventCreated: boolean; - rawCanonicalFallbackCount: number; - rollupDegradedIntervalCount: number; -}> { - if (Date.parse(params.intervalEnd) <= Date.parse(params.hourStart)) { - return { - eventCreated: false, - rawCanonicalFallbackCount: 0, - rollupDegradedIntervalCount: 0, - }; - } - - const anomalyPolicy = await getCostInsightAnomalyPolicy( - params.database, - params.owner, - params.hourStart - ); - const evidence = await getOwnerHourDriverEvidence(params.database, { - owner: params.owner, - hourStart: params.hourStart, - intervalEnd: params.intervalEnd, - category: 'variable', - }); - const eventCreated = await maybeCreateAnomalyAlert({ - database: params.database, - owner: params.owner, - hourStart: params.hourStart, - intervalEnd: params.intervalEnd, - variableMicrodollars: evidence.variableMicrodollars, - anomalyPolicy, - topDrivers: evidence.topDrivers, - }); - return { - eventCreated, - rawCanonicalFallbackCount: evidence.usedCanonicalFallback ? 1 : 0, - rollupDegradedIntervalCount: evidence.degradedIntervalCount ?? 0, - }; -} - -async function maybeCreateThresholdAlert(params: { - database: CostInsightDatabase; - owner: CostInsightSpendOwner; - asOf: string; - alertKind: CostInsightThresholdAlertKind; - thresholdMicrodollars: number | null; - rollingMicrodollars: number | null; -}): Promise { - if (params.thresholdMicrodollars === null) { - await clearCostInsightThresholdEpisode(params.database, params.owner, null, params.alertKind); - return false; - } - if (params.rollingMicrodollars === null) return false; - - const dashboardState = await getCostInsightDashboardState(params.database, params.owner); - const crossingActive = (() => { - if (params.alertKind === 'threshold_7d') - return dashboardState.state?.threshold7DayCrossingActive; - if (params.alertKind === 'threshold_30d') { - return dashboardState.state?.threshold30DayCrossingActive; - } - return dashboardState.state?.thresholdCrossingActive; - })(); - if (params.rollingMicrodollars < params.thresholdMicrodollars) { - if (crossingActive) { - await clearCostInsightThresholdEpisode( - params.database, - params.owner, - params.asOf, - params.alertKind - ); - } - return false; - } - - if (crossingActive) return false; - - const evidence = await getOwnerRollingDriverEvidenceExact(params.database, { - owner: params.owner, - asOf: params.asOf, - windowHours: thresholdWindowDescriptors[params.alertKind].windowHours, - }); - if (evidence.totalMicrodollars < params.thresholdMicrodollars) return false; - - const descriptor = thresholdWindowDescriptors[params.alertKind]; - const snapshot = { - thresholdMicrodollars: params.thresholdMicrodollars, - thresholdWindow: descriptor.snapshotWindow, - ...descriptor.rollingSnapshot(evidence.totalMicrodollars), - topDrivers: topDriverSnapshot(evidence.topDrivers), - topDriversWindow: { - startInclusive: evidence.windowStart, - endExclusive: evidence.asOf, - }, - }; - const event = await createCostInsightEvent(params.database, { - owner: params.owner, - eventType: 'threshold_crossed', - alertKind: params.alertKind, - title: `${descriptor.windowLabel} Spend Threshold Alert`, - description: `Rolling ${descriptor.windowLabel} Credit spend crossed ${usdLabel(params.thresholdMicrodollars)}.`, - snapshot, - dedupeKey: `${params.alertKind}:${params.thresholdMicrodollars}:${params.asOf}`, - }); - if (!event.created) return false; - - await markCostInsightThresholdEpisode(params.database, { - owner: params.owner, - eventId: event.id, - crossedAt: params.asOf, - alertKind: params.alertKind, - snapshot, - }); - await createCostInsightNotificationDeliveries( - params.database, - event.id, - await listCostInsightNotificationRecipientUserIds(params.database, params.owner) - ); - return true; -} - -async function maybeCreateCostSuggestion(params: { - database: CostInsightDatabase; - owner: CostInsightSpendOwner; - topDrivers: OwnerTopSpendDriver[]; - evidenceWindowStart: string; - evidenceWindowEnd: string; - observedMicrodollars: number; -}): Promise { - const activeSuggestions = await listActiveCostInsightSuggestions(params.database, params.owner); - if (activeSuggestions.length > 0) return false; - - const topDriver = params.topDrivers[0]; - - const codingPlanCandidate = isCodingPlanSuggestionEligible( - params.owner, - topDriver, - SUGGESTION_MIN_VARIABLE_MICRODOLLARS - ); - const kiloPassCandidate = - params.owner.type === 'user' && - params.observedMicrodollars >= SUGGESTION_MIN_TOTAL_MICRODOLLARS; - - const suggestion = codingPlanCandidate - ? (() => { - const driverLabel = - topDriver.modelOrPlanKey !== 'other' - ? sentenceLabel(topDriver.modelOrPlanKey) - : sentenceLabel(topDriver.productKey); - return { - suggestionKind: 'coding_plan' as const, - suggestionKey: suggestionKey([ - params.owner.type, - params.owner.id, - 'coding_plan', - params.evidenceWindowEnd.slice(0, 10), - topDriver.source, - topDriver.productKey, - topDriver.modelOrPlanKey, - ]), - title: `Consider a Coding Plan for ${driverLabel}`, - description: `A Coding Plan may improve cost efficiency for recurring ${driverLabel} usage.`, - ctaLabel: 'View subscriptions', - ctaHref: '/subscriptions', - observedMicrodollars: topDriver.totalMicrodollars, - benefitLabel: 'Plan option', - benefitDetail: 'Compare Coding Plans', - }; - })() - : kiloPassCandidate - ? { - suggestionKind: 'kilo_pass' as const, - suggestionKey: suggestionKey([ - params.owner.type, - params.owner.id, - 'kilo_pass', - params.evidenceWindowEnd.slice(0, 10), - String(params.observedMicrodollars), - ]), - title: 'Get more credits with Kilo Pass Expert', - description: `The plan includes ${roundedUsdLabel(KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS)} in paid credits plus up to ${usdLabel(KILO_PASS_EXPERT_BONUS_MICRODOLLARS)} in free bonus credits.`, - ctaLabel: 'View Kilo Pass Expert', - ctaHref: '/subscriptions/kilo-pass', - observedMicrodollars: params.observedMicrodollars, - benefitLabel: 'Expert plan', - benefitDetail: `${roundedUsdLabel(KILO_PASS_EXPERT_MONTHLY_MICRODOLLARS)}/mo + up to ${usdLabel( - KILO_PASS_EXPERT_BONUS_MICRODOLLARS - )} bonus`, - } - : null; - - if (!suggestion) return false; - - const upserted = await upsertCostInsightActiveSuggestion(params.database, { - owner: params.owner, - suggestionKind: suggestion.suggestionKind, - suggestionKey: suggestion.suggestionKey, - title: suggestion.title, - description: suggestion.description, - ctaLabel: suggestion.ctaLabel, - ctaHref: suggestion.ctaHref, - evidenceWindowStart: params.evidenceWindowStart, - evidenceWindowEnd: params.evidenceWindowEnd, - observedMicrodollars: suggestion.observedMicrodollars, - benefitLabel: suggestion.benefitLabel, - benefitDetail: suggestion.benefitDetail, - }); - if (!upserted.created) return false; - - await createCostInsightEvent(params.database, { - owner: params.owner, - eventType: 'suggestion_created', - suggestionKind: suggestion.suggestionKind, - activeSuggestionId: upserted.id, - title: 'Cost Suggestion created', - description: suggestion.title, - snapshot: { - suggestion: { - suggestionKey: suggestion.suggestionKey, - evidenceWindowStart: params.evidenceWindowStart, - evidenceWindowEnd: params.evidenceWindowEnd, - observedMicrodollars: suggestion.observedMicrodollars, - ctaHref: suggestion.ctaHref, - }, - }, - dedupeKey: `suggestion:${suggestion.suggestionKey}`, - }); - return true; -} - -async function evaluateCostInsightsForOwnerLocked( - database: CostInsightDatabase, - owner: CostInsightSpendOwner, - options: { asOf?: string; recoverCompletedHour?: boolean } = {} -): Promise { - const startedAt = performance.now(); - const requestedAsOf = options.asOf ?? new Date().toISOString(); - const asOfTimestamp = Date.parse(requestedAsOf); - if (!Number.isFinite(asOfTimestamp)) throw new Error('Cost Insights evaluation asOf is invalid.'); - const asOf = new Date(asOfTimestamp).toISOString(); - const currentHourStart = floorUtcHour(new Date(asOf)); - const suggestionWindowEnd = asOf; - const suggestionWindowStart = addDays(suggestionWindowEnd, -7); - - const config = await getCostInsightOwnerConfig(database, owner); - - let anomalyEventCreated = false; - let recoveredAnomalyEventCreated = false; - let thresholdEventCreated = false; - let threshold7DayEventCreated = false; - let threshold30DayEventCreated = false; - let suggestionCreated = false; - let rawCanonicalFallbackCount = 0; - let rollupDegradedIntervalCount = 0; - - if (config?.spend_alerts_enabled) { - if (config.anomaly_alerts_enabled) { - if (options.recoverCompletedHour) { - const completedHourStart = addHours(currentHourStart, -1); - const recoveredAnomaly = await evaluateAnomalyInterval({ - database, - owner, - hourStart: completedHourStart, - intervalEnd: currentHourStart, - }); - recoveredAnomalyEventCreated = recoveredAnomaly.eventCreated; - rawCanonicalFallbackCount += recoveredAnomaly.rawCanonicalFallbackCount; - rollupDegradedIntervalCount += recoveredAnomaly.rollupDegradedIntervalCount; - } - const anomaly = await evaluateAnomalyInterval({ - database, - owner, - hourStart: currentHourStart, - intervalEnd: asOf, - }); - anomalyEventCreated = anomaly.eventCreated; - rawCanonicalFallbackCount += anomaly.rawCanonicalFallbackCount; - rollupDegradedIntervalCount += anomaly.rollupDegradedIntervalCount; - } - if (config.spend_threshold_microdollars !== null) { - const rolling24HourSpend = await getOwnerRollingSpendExact(database, { - owner, - asOf, - windowHours: 24, - }); - thresholdEventCreated = await maybeCreateThresholdAlert({ - database, - owner, - asOf, - alertKind: 'threshold', - thresholdMicrodollars: config.spend_threshold_microdollars, - rollingMicrodollars: rolling24HourSpend.totalMicrodollars, - }); - } - if (config.spend_7_day_threshold_microdollars !== null) { - const rolling7DaySpend = await getOwnerRollingSpendExact(database, { - owner, - asOf, - windowHours: 7 * 24, - fallbackToCanonical: true, - }); - threshold7DayEventCreated = await maybeCreateThresholdAlert({ - database, - owner, - asOf, - alertKind: 'threshold_7d', - thresholdMicrodollars: config.spend_7_day_threshold_microdollars, - rollingMicrodollars: rolling7DaySpend.totalMicrodollars, - }); - } - if (config.spend_30_day_threshold_microdollars !== null) { - const rolling30DaySpend = await getOwnerRollingSpendExact(database, { - owner, - asOf, - windowHours: 30 * 24, - fallbackToCanonical: true, - }); - threshold30DayEventCreated = await maybeCreateThresholdAlert({ - database, - owner, - asOf, - alertKind: 'threshold_30d', - thresholdMicrodollars: config.spend_30_day_threshold_microdollars, - rollingMicrodollars: rolling30DaySpend.totalMicrodollars, - }); - } - } - - if (config?.cost_suggestions_enabled ?? true) { - const suggestionEvidence = await getOwnerRollingDriverEvidenceExact(database, { - owner, - asOf, - windowHours: 7 * 24, - }); - suggestionCreated = await maybeCreateCostSuggestion({ - database, - owner, - topDrivers: suggestionEvidence.topDrivers, - evidenceWindowStart: suggestionWindowStart, - evidenceWindowEnd: suggestionWindowEnd, - observedMicrodollars: suggestionEvidence.totalMicrodollars, - }); - } - - await markCostInsightEvaluation(database, owner, asOf); - return { - owner, - evaluatedAt: asOf, - anomalyEventCreated, - recoveredAnomalyEventCreated, - thresholdEventCreated, - threshold7DayEventCreated, - threshold30DayEventCreated, - suggestionCreated, - durationMs: Math.round(performance.now() - startedAt), - rawCanonicalFallbackCount, - rollupDegradedIntervalCount, - }; -} - -export async function evaluateCostInsightsForOwner( - database: CostInsightRootDatabase, - owner: CostInsightSpendOwner, - options: { asOf?: string; recoverCompletedHour?: boolean } = {} -): Promise { - return await database.transaction(async tx => { - const lockKey = `cost-insights-evaluation:${owner.type}:${owner.id}`; - await tx.execute( - sql`SELECT pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(${lockKey}, 0))` - ); - return await evaluateCostInsightsForOwnerLocked(tx, owner, options); - }); -} - -const COST_INSIGHT_EVALUATION_LEASE_MINUTES = 5; - -type CostInsightClaimedDirtyOwner = { - id: string; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - generation: string | number | bigint; - claim_token: string; -}; - -export type CostInsightDirtyEvaluationSummary = { - claimed: number; - evaluatedOwners: CostInsightSpendOwner[]; - failedOwners: Array<{ owner: CostInsightSpendOwner; error: string }>; - evaluationDurationMs: number; - rawCanonicalFallbackCount: number; - rollupDegradedIntervalCount: number; -}; - -function ownerFromDirtyRow(row: CostInsightClaimedDirtyOwner): CostInsightSpendOwner { - if (row.owned_by_user_id) return { type: 'user', id: row.owned_by_user_id }; - if (row.owned_by_organization_id) { - return { type: 'organization', id: row.owned_by_organization_id }; - } - throw new Error('Cost Insights dirty evaluation row has no owner.'); -} - -async function claimDirtyCostInsightOwners( - database: CostInsightRootDatabase, - options: { limit: number; owner?: CostInsightSpendOwner } -): Promise { - const claimToken = crypto.randomUUID(); - const ownerPredicate = options.owner - ? options.owner.type === 'user' - ? sql`dirty_owner.owned_by_user_id = ${options.owner.id} AND dirty_owner.owned_by_organization_id IS NULL` - : sql`dirty_owner.owned_by_organization_id = ${options.owner.id} AND dirty_owner.owned_by_user_id IS NULL` - : sql`TRUE`; - const result = await database.execute(sql` - WITH claimed AS ( - SELECT dirty_owner.id - FROM cost_insight_evaluation_dirty_owners dirty_owner - WHERE dirty_owner.next_attempt_at <= CURRENT_TIMESTAMP - AND ( - dirty_owner.claimed_at IS NULL - OR dirty_owner.claimed_at <= CURRENT_TIMESTAMP - make_interval( - mins => ${COST_INSIGHT_EVALUATION_LEASE_MINUTES} - ) - ) - AND NOT EXISTS ( - SELECT 1 - FROM cost_insight_rollup_repairs repair - WHERE ( - repair.owned_by_user_id = dirty_owner.owned_by_user_id - OR repair.owned_by_organization_id = dirty_owner.owned_by_organization_id - ) - ) - AND ${ownerPredicate} - ORDER BY dirty_owner.dirty_at ASC, dirty_owner.id ASC - LIMIT ${options.limit} - FOR UPDATE SKIP LOCKED - ) - UPDATE cost_insight_evaluation_dirty_owners dirty_owner - SET - claimed_at = CURRENT_TIMESTAMP, - claim_token = ${claimToken}, - attempt_count = dirty_owner.attempt_count + 1, - last_error_redacted = NULL, - updated_at = CURRENT_TIMESTAMP - FROM claimed - WHERE dirty_owner.id = claimed.id - RETURNING - dirty_owner.id, - dirty_owner.owned_by_user_id, - dirty_owner.owned_by_organization_id, - dirty_owner.generation, - dirty_owner.claim_token - `); - return result.rows; -} - -async function completeDirtyCostInsightOwner( - database: CostInsightRootDatabase, - row: CostInsightClaimedDirtyOwner -): Promise { - await database.execute(sql` - WITH removed AS ( - DELETE FROM cost_insight_evaluation_dirty_owners - WHERE id = ${row.id} - AND generation = ${row.generation} - AND claim_token = ${row.claim_token} - RETURNING id - ) - UPDATE cost_insight_evaluation_dirty_owners dirty_owner - SET - claimed_at = NULL, - claim_token = NULL, - attempt_count = 0, - next_attempt_at = CURRENT_TIMESTAMP, - last_error_redacted = NULL, - updated_at = CURRENT_TIMESTAMP - WHERE dirty_owner.id = ${row.id} - AND dirty_owner.claim_token = ${row.claim_token} - AND NOT EXISTS (SELECT 1 FROM removed) - `); -} - -async function failDirtyCostInsightOwner( - database: CostInsightRootDatabase, - row: CostInsightClaimedDirtyOwner, - error: string -): Promise { - await database.execute(sql` - UPDATE cost_insight_evaluation_dirty_owners - SET - claimed_at = NULL, - claim_token = NULL, - next_attempt_at = CURRENT_TIMESTAMP + INTERVAL '5 minutes', - last_error_redacted = ${error.slice(0, 500)}, - updated_at = CURRENT_TIMESTAMP - WHERE id = ${row.id} - AND claim_token = ${row.claim_token} - `); -} - -export async function processPendingCostInsightEvaluations( - database: CostInsightRootDatabase, - options: { - limit?: number; - owner?: CostInsightSpendOwner; - asOf?: string; - recoverCompletedHour?: boolean; - concurrency?: number; - } = {} -): Promise { - const limit = options.limit ?? 25; - const concurrency = Math.max(1, Math.min(options.concurrency ?? 4, limit)); - const summary: CostInsightDirtyEvaluationSummary = { - claimed: 0, - evaluatedOwners: [], - failedOwners: [], - evaluationDurationMs: 0, - rawCanonicalFallbackCount: 0, - rollupDegradedIntervalCount: 0, - }; - - while (summary.claimed < limit) { - const rows = await claimDirtyCostInsightOwners(database, { - limit: Math.min(concurrency, limit - summary.claimed), - owner: options.owner, - }); - if (rows.length === 0) break; - summary.claimed += rows.length; - - await Promise.all( - rows.map(async row => { - const owner = ownerFromDirtyRow(row); - try { - const evaluation = await evaluateCostInsightsForOwner(database, owner, { - asOf: options.asOf, - recoverCompletedHour: options.recoverCompletedHour, - }); - summary.evaluationDurationMs += evaluation.durationMs; - summary.rawCanonicalFallbackCount += evaluation.rawCanonicalFallbackCount; - summary.rollupDegradedIntervalCount += evaluation.rollupDegradedIntervalCount; - await completeDirtyCostInsightOwner(database, row); - if ( - !summary.evaluatedOwners.some( - evaluatedOwner => evaluatedOwner.type === owner.type && evaluatedOwner.id === owner.id - ) - ) { - summary.evaluatedOwners.push(owner); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - await failDirtyCostInsightOwner(database, row, message); - summary.failedOwners.push({ owner, error: message }); - } - }) - ); - } - - return summary; -} - -export function scheduleCostInsightEvaluationAfterSpend(owner: CostInsightSpendOwner): void { - if (process.env.NODE_ENV === 'test') return; - after(async () => { - const evaluations = await processPendingCostInsightEvaluations(db, { limit: 2, owner }); - await dispatchPendingCostInsightNotifications(db, 10); - if (evaluations.failedOwners.length > 0) { - console.error('[cost-insights] post-spend evaluation failed', evaluations.failedOwners[0]); - } - }); -} diff --git a/apps/web/src/lib/cost-insights/hourly-sweep-repository.ts b/apps/web/src/lib/cost-insights/hourly-sweep-repository.ts deleted file mode 100644 index 76e9aaab53c..00000000000 --- a/apps/web/src/lib/cost-insights/hourly-sweep-repository.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { sql } from 'drizzle-orm'; - -import type { CostInsightOwnerCursor, CostInsightRootDatabase } from './repository'; - -const OWNER_EVALUATION_JOB_NAME = 'owner_evaluation'; - -type SweepCheckpointRow = { - cycle_id: string; - cycle_as_of: string | Date; - cohort_created_before: string | Date; - cursor_owner_type: 'user' | 'organization' | null; - cursor_owner_id: string | null; - lease_token: string; -}; - -export type CostInsightHourlySweepLease = { - leaseToken: string; - cycleId: string; - cycleAsOf: string; - cohortCreatedBefore: string; - cursor: CostInsightOwnerCursor | null; -}; - -function normalizeTimestamp(value: string | Date): string { - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); -} - -function cursorFromRow(row: SweepCheckpointRow): CostInsightOwnerCursor | null { - if (!row.cursor_owner_type || !row.cursor_owner_id) return null; - return { ownerType: row.cursor_owner_type, ownerId: row.cursor_owner_id }; -} - -export async function acquireCostInsightHourlySweepLease( - database: CostInsightRootDatabase, - options: { asOf: string; leaseSeconds: number } -): Promise { - const leaseToken = crypto.randomUUID(); - const cycleId = crypto.randomUUID(); - - return await database.transaction(async tx => { - await tx.execute(sql` - INSERT INTO cost_insight_hourly_sweep_checkpoints (job_name) - VALUES (${OWNER_EVALUATION_JOB_NAME}) - ON CONFLICT (job_name) DO NOTHING - `); - const result = await tx.execute(sql` - UPDATE cost_insight_hourly_sweep_checkpoints - SET - cycle_id = COALESCE(cycle_id, ${cycleId}::uuid), - cycle_as_of = COALESCE(cycle_as_of, ${options.asOf}::timestamptz), - cohort_created_before = COALESCE(cohort_created_before, CURRENT_TIMESTAMP), - lease_token = ${leaseToken}::uuid, - lease_expires_at = CURRENT_TIMESTAMP + make_interval(secs => ${options.leaseSeconds}), - started_at = COALESCE(started_at, CURRENT_TIMESTAMP), - updated_at = CURRENT_TIMESTAMP - WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} - AND (lease_token IS NULL OR lease_expires_at <= CURRENT_TIMESTAMP) - RETURNING - cycle_id::text, - cycle_as_of, - cohort_created_before, - cursor_owner_type, - cursor_owner_id, - lease_token::text - `); - const row = result.rows[0]; - if (!row) return null; - return { - leaseToken: row.lease_token, - cycleId: row.cycle_id, - cycleAsOf: normalizeTimestamp(row.cycle_as_of), - cohortCreatedBefore: normalizeTimestamp(row.cohort_created_before), - cursor: cursorFromRow(row), - }; - }); -} - -export async function advanceCostInsightHourlySweepCursor( - database: CostInsightRootDatabase, - leaseToken: string, - cursor: CostInsightOwnerCursor -): Promise { - const result = await database.execute<{ job_name: string }>(sql` - UPDATE cost_insight_hourly_sweep_checkpoints - SET - cursor_owner_type = ${cursor.ownerType}, - cursor_owner_id = ${cursor.ownerId}, - updated_at = CURRENT_TIMESTAMP - WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} - AND lease_token = ${leaseToken}::uuid - RETURNING job_name - `); - return result.rows.length > 0; -} - -export async function completeCostInsightHourlySweepCycle( - database: CostInsightRootDatabase, - leaseToken: string -): Promise { - const result = await database.execute<{ job_name: string }>(sql` - UPDATE cost_insight_hourly_sweep_checkpoints - SET - cycle_id = NULL, - cycle_as_of = NULL, - cohort_created_before = NULL, - cursor_owner_type = NULL, - cursor_owner_id = NULL, - lease_token = NULL, - lease_expires_at = NULL, - started_at = NULL, - last_completed_at = CURRENT_TIMESTAMP, - updated_at = CURRENT_TIMESTAMP - WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} - AND lease_token = ${leaseToken}::uuid - RETURNING job_name - `); - return result.rows.length > 0; -} - -export async function releaseCostInsightHourlySweepLease( - database: CostInsightRootDatabase, - leaseToken: string -): Promise { - await database.execute(sql` - UPDATE cost_insight_hourly_sweep_checkpoints - SET - lease_token = NULL, - lease_expires_at = NULL, - updated_at = CURRENT_TIMESTAMP - WHERE job_name = ${OWNER_EVALUATION_JOB_NAME} - AND lease_token = ${leaseToken}::uuid - `); -} diff --git a/apps/web/src/lib/cost-insights/jobs.ts b/apps/web/src/lib/cost-insights/jobs.ts deleted file mode 100644 index ae2f04cea00..00000000000 --- a/apps/web/src/lib/cost-insights/jobs.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; -import { sql } from 'drizzle-orm'; -import pLimit from 'p-limit'; - -import { evaluateCostInsightsForOwner, processPendingCostInsightEvaluations } from './evaluation'; -import { - acquireCostInsightHourlySweepLease, - advanceCostInsightHourlySweepCursor, - completeCostInsightHourlySweepCycle, - releaseCostInsightHourlySweepLease, -} from './hourly-sweep-repository'; -import { dispatchPendingCostInsightNotifications } from './notifications'; -import { processPendingDailyUsageRollupRepairs } from '../ai-gateway/usage-daily-rollup-repairs'; -import { - listPendingCostInsightRepairOwnerKeys, - processPendingCostInsightRollupRepairs, -} from './rollup-repairs'; -import { - deleteExpiredCostInsightEvents, - listEnabledCostInsightOwnerPage, - type CostInsightDatabase, - type CostInsightRootDatabase, -} from './repository'; - -const DEFAULT_SWEEP_TIME_BUDGET_MS = 240_000; -const OWNER_PAGE_SIZE = 20; -const OWNER_CONCURRENCY = 4; -const DIRTY_OWNER_LIMIT = 20; -const ROLLUP_REPAIR_LIMIT = 2; -const DAILY_USAGE_ROLLUP_REPAIR_LIMIT = 20; -const NOTIFICATION_LIMIT = 25; -const CHECKPOINT_LEASE_SECONDS = 5 * 60; - -export type CostInsightHourlySweepSummary = { - evaluatedOwners: number; - failedOwners: Array<{ owner: CostInsightSpendOwner; error: string }>; - dirtyEvaluations: Awaited>; - rollupRepairs: Awaited>; - dailyUsageRollupRepairs: Awaited>; - notifications: Awaited>; - dirtyQueueDepthBefore: number; - dirtyQueueDepthAfter: number; - evaluationDurationMs: number; - rawCanonicalFallbackCount: number; - rollupDegradedIntervalCount: number; - alreadyRunning: boolean; - deadlineReached: boolean; - ownerCycleComplete: boolean; - cycleId: string | null; -}; - -function ownerKey(owner: CostInsightSpendOwner): string { - return `${owner.type}:${owner.id}`; -} - -export async function runCostInsightHourlySweep( - database: CostInsightRootDatabase, - options: { - asOf?: string; - dirtyOwnerLimit?: number; - rollupRepairLimit?: number; - dailyUsageRollupRepairLimit?: number; - timeBudgetMs?: number; - ownerPageSize?: number; - ownerConcurrency?: number; - notificationLimit?: number; - } = {} -): Promise { - const asOf = options.asOf ?? new Date().toISOString(); - const deadline = performance.now() + (options.timeBudgetMs ?? DEFAULT_SWEEP_TIME_BUDGET_MS); - const ownerConcurrency = options.ownerConcurrency ?? OWNER_CONCURRENCY; - const dirtyQueueDepthBefore = await countCostInsightDirtyOwnerQueueDepth(database); - let rollupRepairs: Awaited> = { - claimed: 0, - repaired: 0, - failed: [], - }; - let dirtyEvaluations: Awaited> = { - claimed: 0, - evaluatedOwners: [], - failedOwners: [], - evaluationDurationMs: 0, - rawCanonicalFallbackCount: 0, - rollupDegradedIntervalCount: 0, - }; - let dailyUsageRollupRepairs: Awaited> = { - claimed: 0, - repaired: 0, - failed: [], - }; - const lease = await acquireCostInsightHourlySweepLease(database, { - asOf, - leaseSeconds: CHECKPOINT_LEASE_SECONDS, - }); - if (lease) { - try { - rollupRepairs = await processPendingCostInsightRollupRepairs(database, { - limit: options.rollupRepairLimit ?? ROLLUP_REPAIR_LIMIT, - }); - dailyUsageRollupRepairs = await processPendingDailyUsageRollupRepairs(database, { - limit: options.dailyUsageRollupRepairLimit ?? DAILY_USAGE_ROLLUP_REPAIR_LIMIT, - }); - dirtyEvaluations = await processPendingCostInsightEvaluations(database, { - limit: options.dirtyOwnerLimit ?? DIRTY_OWNER_LIMIT, - asOf, - recoverCompletedHour: true, - concurrency: ownerConcurrency, - }); - } catch (error) { - await releaseCostInsightHourlySweepLease(database, lease.leaseToken); - throw error; - } - } - const claimedOwnerKeys = new Set( - [ - ...dirtyEvaluations.evaluatedOwners, - ...dirtyEvaluations.failedOwners.map(row => row.owner), - ].map(ownerKey) - ); - const failedOwners: CostInsightHourlySweepSummary['failedOwners'] = [ - ...dirtyEvaluations.failedOwners, - ]; - let evaluatedOwners = dirtyEvaluations.evaluatedOwners.length; - let evaluationDurationMs = dirtyEvaluations.evaluationDurationMs; - let rawCanonicalFallbackCount = dirtyEvaluations.rawCanonicalFallbackCount; - let rollupDegradedIntervalCount = dirtyEvaluations.rollupDegradedIntervalCount; - let alreadyRunning = false; - let deadlineReached = false; - let ownerCycleComplete = false; - let cycleId: string | null = null; - - if (!lease) { - alreadyRunning = true; - } else { - cycleId = lease.cycleId; - const limitOwnerWork = pLimit(ownerConcurrency); - let cursor = lease.cursor; - try { - while (performance.now() < deadline) { - const page = await listEnabledCostInsightOwnerPage(database, { - cohortCreatedBefore: lease.cohortCreatedBefore, - after: cursor, - limit: options.ownerPageSize ?? OWNER_PAGE_SIZE, - }); - if (page.owners.length === 0) { - ownerCycleComplete = true; - await completeCostInsightHourlySweepCycle(database, lease.leaseToken); - break; - } - const pendingRepairOwnerKeys = await listPendingCostInsightRepairOwnerKeys( - database, - page.owners - ); - const outcomes = await Promise.all( - page.owners.map(owner => - limitOwnerWork(async () => { - if (claimedOwnerKeys.has(ownerKey(owner))) return { owner, skipped: true as const }; - if (pendingRepairOwnerKeys.has(ownerKey(owner))) { - return { owner, skipped: true as const }; - } - try { - const evaluation = await evaluateCostInsightsForOwner(database, owner, { - asOf: lease.cycleAsOf, - recoverCompletedHour: true, - }); - return { owner, skipped: false as const, error: null, evaluation }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - await markCostInsightOwnerDirtyForRetry(database, owner, message); - return { owner, skipped: false as const, error: message, evaluation: null }; - } - }) - ) - ); - for (const outcome of outcomes) { - if (outcome.skipped) continue; - if (outcome.error) { - failedOwners.push({ owner: outcome.owner, error: outcome.error }); - } else { - if (!outcome.evaluation) { - throw new Error( - 'Cost Insights sweep evaluation summary missing for successful owner.' - ); - } - evaluatedOwners += 1; - evaluationDurationMs += outcome.evaluation.durationMs; - rawCanonicalFallbackCount += outcome.evaluation.rawCanonicalFallbackCount; - rollupDegradedIntervalCount += outcome.evaluation.rollupDegradedIntervalCount; - } - } - if (page.nextCursor) { - await advanceCostInsightHourlySweepCursor(database, lease.leaseToken, page.nextCursor); - cursor = page.nextCursor; - } - if (!page.hasMore) { - ownerCycleComplete = true; - await completeCostInsightHourlySweepCycle(database, lease.leaseToken); - break; - } - } - deadlineReached = !ownerCycleComplete && performance.now() >= deadline; - } finally { - if (!ownerCycleComplete) { - await releaseCostInsightHourlySweepLease(database, lease.leaseToken); - } - } - } - - return { - evaluatedOwners, - failedOwners, - dirtyEvaluations, - rollupRepairs, - dailyUsageRollupRepairs, - notifications: await dispatchPendingCostInsightNotifications( - database, - options.notificationLimit ?? NOTIFICATION_LIMIT - ), - dirtyQueueDepthBefore, - dirtyQueueDepthAfter: await countCostInsightDirtyOwnerQueueDepth(database), - evaluationDurationMs, - rawCanonicalFallbackCount, - rollupDegradedIntervalCount, - alreadyRunning, - deadlineReached, - ownerCycleComplete, - cycleId, - }; -} - -async function countCostInsightDirtyOwnerQueueDepth( - database: CostInsightRootDatabase -): Promise { - const result = await database.execute<{ count: string | number | bigint }>(sql` - SELECT COUNT(*)::text AS count - FROM cost_insight_evaluation_dirty_owners - `); - const value = Number(result.rows[0]?.count ?? 0); - if (!Number.isSafeInteger(value) || value < 0) { - throw new Error('Cost Insights dirty-owner queue depth is outside the safe integer range.'); - } - return value; -} - -async function markCostInsightOwnerDirtyForRetry( - database: CostInsightRootDatabase, - owner: CostInsightSpendOwner, - error: string -): Promise { - const ownerColumns = - owner.type === 'user' - ? { ownedByUserId: owner.id, ownedByOrganizationId: null } - : { ownedByUserId: null, ownedByOrganizationId: owner.id }; - const conflictTarget = - owner.type === 'user' - ? sql.raw('(owned_by_user_id) WHERE owned_by_organization_id IS NULL') - : sql.raw('(owned_by_organization_id) WHERE owned_by_user_id IS NULL'); - await database.execute(sql` - INSERT INTO cost_insight_evaluation_dirty_owners AS dirty_owner ( - owned_by_user_id, - owned_by_organization_id, - dirty_at, - next_attempt_at, - last_error_redacted - ) VALUES ( - ${ownerColumns.ownedByUserId}, - ${ownerColumns.ownedByOrganizationId}, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP + INTERVAL '5 minutes', - ${error.slice(0, 500)} - ) - ON CONFLICT ${conflictTarget} - DO UPDATE SET - generation = dirty_owner.generation + 1, - dirty_at = CURRENT_TIMESTAMP, - next_attempt_at = CURRENT_TIMESTAMP + INTERVAL '5 minutes', - claimed_at = NULL, - claim_token = NULL, - last_error_redacted = ${error.slice(0, 500)}, - updated_at = CURRENT_TIMESTAMP - `); -} - -export async function runCostInsightEventRetentionCleanup( - database: CostInsightDatabase -): Promise<{ deletedEvents: number; cutoff: string }> { - const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(); - return { - cutoff, - deletedEvents: await deleteExpiredCostInsightEvents(database, cutoff), - }; -} diff --git a/apps/web/src/lib/cost-insights/notifications.integration.test.ts b/apps/web/src/lib/cost-insights/notifications.integration.test.ts deleted file mode 100644 index da4758ab5de..00000000000 --- a/apps/web/src/lib/cost-insights/notifications.integration.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { afterEach, describe, expect, test } from '@jest/globals'; -import { - cost_insight_events, - cost_insight_notification_deliveries, - kilocode_users, -} from '@kilocode/db/schema'; -import { eq } from 'drizzle-orm'; - -import { db } from '@/lib/drizzle'; -import { - claimPendingCostInsightNotificationDeliveries, - dispatchPendingCostInsightNotifications, -} from './notifications'; - -const testUserIds = new Set(); - -async function createDelivery(): Promise<{ deliveryId: string; eventId: string; userId: string }> { - const userId = `cost-insights-notification-${crypto.randomUUID()}`; - testUserIds.add(userId); - await db.insert(kilocode_users).values({ - id: userId, - google_user_email: `${userId}@example.com`, - google_user_name: 'Cost Insights Notification Test', - google_user_image_url: 'https://example.com/avatar.png', - stripe_customer_id: `cus_${crypto.randomUUID()}`, - }); - const [event] = await db - .insert(cost_insight_events) - .values({ - owned_by_user_id: userId, - event_type: 'anomaly_alert', - alert_kind: 'anomaly', - title: 'Spend Anomaly Alert', - description: 'Test alert', - snapshot: {}, - }) - .returning({ id: cost_insight_events.id }); - if (!event) throw new Error('Test event insert returned no row.'); - const [delivery] = await db - .insert(cost_insight_notification_deliveries) - .values({ event_id: event.id, recipient_user_id: userId }) - .returning({ id: cost_insight_notification_deliveries.id }); - if (!delivery) throw new Error('Test delivery insert returned no row.'); - return { deliveryId: delivery.id, eventId: event.id, userId }; -} - -afterEach(async () => { - for (const userId of testUserIds) { - await db.delete(cost_insight_events).where(eq(cost_insight_events.owned_by_user_id, userId)); - await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); - } - testUserIds.clear(); -}); - -describe('Cost Insights notification claims', () => { - test('reclaims a stale sending row after interruption', async () => { - const { deliveryId } = await createDelivery(); - - const firstClaim = await claimPendingCostInsightNotificationDeliveries(db, 1); - expect(firstClaim.rows).toHaveLength(1); - expect(firstClaim.rows[0]?.attempt_count).toBe(1); - - await db - .update(cost_insight_notification_deliveries) - .set({ claimed_at: '2026-01-01T00:00:00.000Z' }) - .where(eq(cost_insight_notification_deliveries.id, deliveryId)); - - const reclaimed = await claimPendingCostInsightNotificationDeliveries(db, 1); - expect(reclaimed.terminalized).toBe(0); - expect(reclaimed.rows).toHaveLength(1); - expect(reclaimed.rows[0]).toMatchObject({ delivery_id: deliveryId, attempt_count: 2 }); - }); - - test('terminalizes a stale sending row after attempt exhaustion', async () => { - const { deliveryId } = await createDelivery(); - await db - .update(cost_insight_notification_deliveries) - .set({ - status: 'sending', - attempt_count: 5, - claimed_at: '2026-01-01T00:00:00.000Z', - }) - .where(eq(cost_insight_notification_deliveries.id, deliveryId)); - - const claim = await claimPendingCostInsightNotificationDeliveries(db, 1); - const [delivery] = await db - .select() - .from(cost_insight_notification_deliveries) - .where(eq(cost_insight_notification_deliveries.id, deliveryId)); - - expect(claim).toMatchObject({ rows: [], terminalized: 1 }); - expect(delivery).toMatchObject({ - status: 'skipped', - attempt_count: 5, - claimed_at: null, - last_error_redacted: 'stale_claim_attempts_exhausted', - }); - }); - - test('skips every claimed delivery instead of sending a discontinued alert', async () => { - const { deliveryId } = await createDelivery(); - - await expect(dispatchPendingCostInsightNotifications(db, 1)).resolves.toMatchObject({ - claimed: 1, - sent: 0, - skipped: 1, - failed: 0, - }); - const [delivery] = await db - .select() - .from(cost_insight_notification_deliveries) - .where(eq(cost_insight_notification_deliveries.id, deliveryId)); - - expect(delivery).toMatchObject({ - status: 'skipped', - sent_at: null, - last_error_redacted: 'feature_discontinued', - }); - }); -}); diff --git a/apps/web/src/lib/cost-insights/notifications.ts b/apps/web/src/lib/cost-insights/notifications.ts deleted file mode 100644 index 9f24fd74b8b..00000000000 --- a/apps/web/src/lib/cost-insights/notifications.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { cost_insight_notification_deliveries } from '@kilocode/db/schema'; -import type { CostInsightAlertKind } from '@kilocode/db/schema-types'; -import { eq, sql } from 'drizzle-orm'; - -import type { CostInsightDatabase } from './repository'; - -const COST_INSIGHT_NOTIFICATION_MAX_ATTEMPTS = 5; -const COST_INSIGHT_NOTIFICATION_LEASE_MINUTES = 15; - -export type CostInsightClaimedDeliveryRow = { - delivery_id: string; - recipient_user_id: string; - recipient_email: string; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - title: string; - description: string; - alert_kind: CostInsightAlertKind | null; - attempt_count: number; - snapshot: unknown; -}; - -export type CostInsightNotificationDispatchSummary = { - claimed: number; - sent: number; - skipped: number; - terminalized: number; - failed: number; -}; - -async function terminalizeExhaustedDeliveryClaims(database: CostInsightDatabase): Promise { - const result = await database.execute<{ id: string }>(sql` - UPDATE cost_insight_notification_deliveries delivery - SET - status = 'skipped', - claimed_at = NULL, - failed_at = NULL, - sent_at = NULL, - last_error_redacted = 'stale_claim_attempts_exhausted', - updated_at = CURRENT_TIMESTAMP - WHERE delivery.status = 'sending' - AND delivery.attempt_count >= ${COST_INSIGHT_NOTIFICATION_MAX_ATTEMPTS} - AND delivery.claimed_at <= CURRENT_TIMESTAMP - make_interval( - mins => ${COST_INSIGHT_NOTIFICATION_LEASE_MINUTES} - ) - RETURNING delivery.id - `); - return result.rows.length; -} - -export async function claimPendingCostInsightNotificationDeliveries( - database: CostInsightDatabase, - limit: number -): Promise<{ rows: CostInsightClaimedDeliveryRow[]; terminalized: number }> { - const terminalized = await terminalizeExhaustedDeliveryClaims(database); - const result = await database.execute(sql` - WITH claimed AS ( - SELECT delivery.id - FROM cost_insight_notification_deliveries delivery - WHERE delivery.attempt_count < ${COST_INSIGHT_NOTIFICATION_MAX_ATTEMPTS} - AND ( - ( - delivery.status IN ('pending', 'failed') - AND delivery.next_attempt_at <= CURRENT_TIMESTAMP - ) - OR ( - delivery.status = 'sending' - AND delivery.claimed_at <= CURRENT_TIMESTAMP - make_interval( - mins => ${COST_INSIGHT_NOTIFICATION_LEASE_MINUTES} - ) - ) - ) - ORDER BY delivery.next_attempt_at ASC, delivery.id ASC - LIMIT ${limit} - FOR UPDATE SKIP LOCKED - ), updated AS ( - UPDATE cost_insight_notification_deliveries delivery - SET - status = 'sending', - claimed_at = CURRENT_TIMESTAMP, - failed_at = NULL, - sent_at = NULL, - last_error_redacted = NULL, - attempt_count = delivery.attempt_count + 1, - updated_at = CURRENT_TIMESTAMP - FROM claimed - WHERE delivery.id = claimed.id - RETURNING delivery.id, delivery.recipient_user_id, delivery.attempt_count - ) - SELECT - updated.id AS delivery_id, - updated.recipient_user_id, - updated.attempt_count, - recipient.google_user_email AS recipient_email, - event.owned_by_user_id, - event.owned_by_organization_id, - event.title, - event.description, - event.alert_kind, - event.snapshot - FROM updated - INNER JOIN cost_insight_events event ON event.id = ( - SELECT delivery.event_id - FROM cost_insight_notification_deliveries delivery - WHERE delivery.id = updated.id - ) - INNER JOIN kilocode_users recipient ON recipient.id = updated.recipient_user_id - ORDER BY updated.id ASC - `); - return { rows: result.rows, terminalized }; -} - -async function markDeliverySkipped( - database: CostInsightDatabase, - deliveryId: string, - reason: string -): Promise { - await database - .update(cost_insight_notification_deliveries) - .set({ - status: 'skipped', - last_error_redacted: reason, - failed_at: null, - sent_at: null, - updated_at: sql`now()`, - }) - .where(eq(cost_insight_notification_deliveries.id, deliveryId)); -} - -export async function dispatchPendingCostInsightNotifications( - database: CostInsightDatabase, - limit = 25 -): Promise { - const claim = await claimPendingCostInsightNotificationDeliveries(database, limit); - const rows = claim.rows; - const summary: CostInsightNotificationDispatchSummary = { - claimed: rows.length, - sent: 0, - skipped: 0, - terminalized: claim.terminalized, - failed: 0, - }; - - // Cost Insights is discontinued, so nothing is delivered. Claimed rows are - // drained to 'skipped' instead of being retried until attempts run out. - for (const row of rows) { - await markDeliverySkipped(database, row.delivery_id, 'feature_discontinued'); - summary.skipped += 1; - } - - return summary; -} diff --git a/apps/web/src/lib/cost-insights/owner.ts b/apps/web/src/lib/cost-insights/owner.ts deleted file mode 100644 index ee4b0e25ebf..00000000000 --- a/apps/web/src/lib/cost-insights/owner.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; -import type { SQL } from 'drizzle-orm'; -import { eq, isNotNull, isNull, sql } from 'drizzle-orm'; -import type { AnyPgColumn } from 'drizzle-orm/pg-core'; - -export type CostInsightOwnerColumns = { - owned_by_user_id: AnyPgColumn; - owned_by_organization_id: AnyPgColumn; -}; - -export type CostInsightOwnerInsertValues = - | { owned_by_user_id: string; owned_by_organization_id: null } - | { owned_by_user_id: null; owned_by_organization_id: string }; - -export function costInsightOwnerInsertValues( - owner: CostInsightSpendOwner -): CostInsightOwnerInsertValues { - return owner.type === 'user' - ? { owned_by_user_id: owner.id, owned_by_organization_id: null } - : { owned_by_user_id: null, owned_by_organization_id: owner.id }; -} - -export function costInsightOwnerWhere( - owner: CostInsightSpendOwner, - columns: CostInsightOwnerColumns -): SQL { - return owner.type === 'user' - ? sql`${columns.owned_by_organization_id} IS NULL AND ${columns.owned_by_user_id} = ${owner.id}` - : sql`${columns.owned_by_user_id} IS NULL AND ${columns.owned_by_organization_id} = ${owner.id}`; -} - -export function costInsightOwnerTargetWhere( - owner: CostInsightSpendOwner, - columns: CostInsightOwnerColumns -): SQL { - return owner.type === 'user' - ? isNull(columns.owned_by_organization_id) - : isNull(columns.owned_by_user_id); -} - -export function costInsightOwnerTargetColumn( - owner: CostInsightSpendOwner, - columns: CostInsightOwnerColumns -): AnyPgColumn { - return owner.type === 'user' ? columns.owned_by_user_id : columns.owned_by_organization_id; -} - -export function costInsightOwnerHasValue(columns: CostInsightOwnerColumns): SQL { - return sql`((${columns.owned_by_user_id} IS NOT NULL)::int + (${columns.owned_by_organization_id} IS NOT NULL)::int) = 1`; -} - -export function costInsightOwnerEquality( - owner: CostInsightSpendOwner, - columns: CostInsightOwnerColumns -): SQL { - return owner.type === 'user' - ? eq(columns.owned_by_user_id, owner.id) - : eq(columns.owned_by_organization_id, owner.id); -} - -export function costInsightOwnerIsSameKind( - owner: CostInsightSpendOwner, - columns: CostInsightOwnerColumns -): SQL { - return owner.type === 'user' - ? isNotNull(columns.owned_by_user_id) - : isNotNull(columns.owned_by_organization_id); -} - -export function costInsightOwnerDisplayType( - owner: CostInsightSpendOwner -): 'personal' | 'organization' { - return owner.type === 'user' ? 'personal' : 'organization'; -} diff --git a/apps/web/src/lib/cost-insights/policy.test.ts b/apps/web/src/lib/cost-insights/policy.test.ts deleted file mode 100644 index 8c95e58d4a2..00000000000 --- a/apps/web/src/lib/cost-insights/policy.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - COST_INSIGHT_ANOMALY_FLOOR_MICRODOLLARS, - COST_INSIGHT_STARTER_ANOMALY_FLOOR_MICRODOLLARS, - MICRODOLLARS_PER_USD, - calculateAnomalyPolicy, - formatSpendThresholdUsd, - parseSpendThresholdUsd, - requireSafeMicrodollars, -} from './policy'; - -describe('Cost Insights policy', () => { - describe('parseSpendThresholdUsd', () => { - it('returns null for blank thresholds', () => { - expect(parseSpendThresholdUsd(null)).toBeNull(); - expect(parseSpendThresholdUsd(' ')).toBeNull(); - }); - - it('parses cents precision to microdollars', () => { - expect(parseSpendThresholdUsd('25')).toBe(25 * MICRODOLLARS_PER_USD); - expect(parseSpendThresholdUsd('25.50')).toBe(25_500_000); - expect(parseSpendThresholdUsd('0.01')).toBe(10_000); - }); - - it('rejects invalid or zero values', () => { - expect(() => parseSpendThresholdUsd('0')).toThrow('greater than $0.00'); - expect(() => parseSpendThresholdUsd('-1')).toThrow('positive USD amount'); - expect(() => parseSpendThresholdUsd('10.001')).toThrow('positive USD amount'); - expect(() => parseSpendThresholdUsd('1,000')).toThrow('positive USD amount'); - }); - - it('accepts the largest cent-precision threshold that converts safely', () => { - expect(parseSpendThresholdUsd('9007199254.74')).toBe(9_007_199_254_740_000); - }); - - it('rejects thresholds whose final microdollar value is unsafe', () => { - expect(() => parseSpendThresholdUsd('9007199254.75')).toThrow( - 'Spend threshold is too large. Enter a smaller USD amount.' - ); - expect(() => parseSpendThresholdUsd('999999999999999999999999.99')).toThrow( - 'Spend threshold is too large. Enter a smaller USD amount.' - ); - }); - }); - - describe('requireSafeMicrodollars', () => { - it('requires a positive safe integer', () => { - expect(requireSafeMicrodollars(1, 'Amount')).toBe(1); - expect(() => requireSafeMicrodollars(0, 'Amount')).toThrow('positive safe integer'); - expect(() => requireSafeMicrodollars(Number.MAX_SAFE_INTEGER + 1, 'Amount')).toThrow( - 'positive safe integer' - ); - }); - }); - - describe('formatSpendThresholdUsd', () => { - it('formats microdollars as fixed cents', () => { - expect(formatSpendThresholdUsd(null)).toBe(''); - expect(formatSpendThresholdUsd(10_000)).toBe('0.01'); - expect(formatSpendThresholdUsd(25_500_000)).toBe('25.50'); - }); - }); - - describe('calculateAnomalyPolicy', () => { - it('uses starter threshold until enough buckets exist', () => { - const policy = calculateAnomalyPolicy([1, 2, 3]); - - expect(policy.mode).toBe('starter'); - expect(policy.thresholdMicrodollars).toBe(COST_INSIGHT_STARTER_ANOMALY_FLOOR_MICRODOLLARS); - }); - - it('uses floor when historical spend is below floor', () => { - const policy = calculateAnomalyPolicy(Array.from({ length: 24 }, () => 1_000_000)); - - expect(policy.mode).toBe('available-history'); - expect(policy.thresholdMicrodollars).toBe(COST_INSIGHT_ANOMALY_FLOOR_MICRODOLLARS); - }); - - it('uses seven-day p95 multiplier when full history exists', () => { - const values = Array.from({ length: 24 * 7 }, (_, index) => (index + 1) * 1_000_000); - const policy = calculateAnomalyPolicy(values); - - expect(policy.mode).toBe('seven-day'); - expect(policy.baselineMicrodollars).toBe(160 * MICRODOLLARS_PER_USD); - expect(policy.thresholdMicrodollars).toBe(480 * MICRODOLLARS_PER_USD); - }); - }); -}); diff --git a/apps/web/src/lib/cost-insights/policy.ts b/apps/web/src/lib/cost-insights/policy.ts deleted file mode 100644 index 2677e724272..00000000000 --- a/apps/web/src/lib/cost-insights/policy.ts +++ /dev/null @@ -1,112 +0,0 @@ -export const MICRODOLLARS_PER_USD = 1_000_000; -export const MICRODOLLARS_PER_CENT = 10_000; - -export const COST_INSIGHT_ANOMALY_MULTIPLIER = 3; -export const COST_INSIGHT_ANOMALY_FLOOR_MICRODOLLARS = 10 * MICRODOLLARS_PER_USD; -export const COST_INSIGHT_STARTER_ANOMALY_FLOOR_MICRODOLLARS = 25 * MICRODOLLARS_PER_USD; -export const COST_INSIGHT_MIN_BASELINE_BUCKETS = 24; - -const THRESHOLD_USD_PATTERN = /^(?:0|[1-9]\d*)(?:\.(\d{1,2}))?$/; - -export type CostInsightBaselineMode = 'starter' | 'available-history' | 'seven-day'; - -export type CostInsightAnomalyPolicy = { - baselineMicrodollars: number; - thresholdMicrodollars: number; - baselineBucketCount: number; - mode: CostInsightBaselineMode; -}; - -export function microdollarsToUsd(microdollars: number): number { - return microdollars / MICRODOLLARS_PER_USD; -} - -export function usdToMicrodollarsFromCents(cents: number): number { - return cents * MICRODOLLARS_PER_CENT; -} - -export function parseSpendThresholdUsd(value: string | null): number | null { - const trimmed = value?.trim() ?? ''; - if (trimmed === '') return null; - const match = THRESHOLD_USD_PATTERN.exec(trimmed); - if (!match) { - throw new Error('Spend threshold must be a positive USD amount with cents precision.'); - } - - const [wholePart, centsPart = ''] = trimmed.split('.'); - const dollars = Number.parseInt(wholePart, 10); - const cents = Number.parseInt(centsPart.padEnd(2, '0') || '0', 10); - const totalCents = dollars * 100 + cents; - if (totalCents <= 0) { - throw new Error('Spend threshold must be greater than $0.00.'); - } - if (!Number.isSafeInteger(totalCents)) { - throw new Error('Spend threshold is too large. Enter a smaller USD amount.'); - } - - const totalMicrodollars = usdToMicrodollarsFromCents(totalCents); - try { - return requireSafeMicrodollars(totalMicrodollars, 'Spend threshold'); - } catch { - throw new Error('Spend threshold is too large. Enter a smaller USD amount.'); - } -} - -export function formatSpendThresholdUsd(value: number | null): string { - if (value === null) return ''; - const cents = Math.round(value / MICRODOLLARS_PER_CENT); - const dollars = Math.floor(cents / 100); - const remainder = cents % 100; - return `${dollars}.${String(remainder).padStart(2, '0')}`; -} - -export function requireSafeMicrodollars(value: number, fieldName: string): number { - if (!Number.isSafeInteger(value) || value <= 0) { - throw new Error(`${fieldName} must be a positive safe integer.`); - } - return value; -} - -export function percentileNearestRank(values: number[], percentile: number): number { - if (values.length === 0) return 0; - const sorted = [...values].sort((left, right) => left - right); - const rank = Math.ceil((percentile / 100) * sorted.length); - return sorted[Math.max(0, rank - 1)] ?? 0; -} - -export function calculateAnomalyPolicy(values: number[]): CostInsightAnomalyPolicy { - const baselineBucketCount = values.length; - if (baselineBucketCount < COST_INSIGHT_MIN_BASELINE_BUCKETS) { - return { - baselineMicrodollars: 0, - thresholdMicrodollars: COST_INSIGHT_STARTER_ANOMALY_FLOOR_MICRODOLLARS, - baselineBucketCount, - mode: 'starter', - }; - } - - const baselineMicrodollars = percentileNearestRank(values, 95); - const thresholdMicrodollars = Math.max( - baselineMicrodollars * COST_INSIGHT_ANOMALY_MULTIPLIER, - COST_INSIGHT_ANOMALY_FLOOR_MICRODOLLARS - ); - return { - baselineMicrodollars, - thresholdMicrodollars, - baselineBucketCount, - mode: baselineBucketCount >= 24 * 7 ? 'seven-day' : 'available-history', - }; -} - -export function floorUtcHour(date: Date): string { - const timestamp = Math.floor(date.getTime() / 3_600_000) * 3_600_000; - return new Date(timestamp).toISOString(); -} - -export function addHours(timestamp: string, hours: number): string { - return new Date(Date.parse(timestamp) + hours * 3_600_000).toISOString(); -} - -export function addDays(timestamp: string, days: number): string { - return addHours(timestamp, days * 24); -} diff --git a/apps/web/src/lib/cost-insights/repository.ts b/apps/web/src/lib/cost-insights/repository.ts deleted file mode 100644 index daa4e82a51a..00000000000 --- a/apps/web/src/lib/cost-insights/repository.ts +++ /dev/null @@ -1,1159 +0,0 @@ -import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; -import { - cost_insight_active_suggestions, - cost_insight_events, - cost_insight_notification_deliveries, - cost_insight_owner_configs, - cost_insight_owner_states, - CostInsightEventSnapshotSchema, - kilocode_users, - organization_memberships, - type CostInsightEventSnapshot, - type CostInsightOwnerConfig, - type CostInsightOwnerState, -} from '@kilocode/db/schema'; -import type { - CostInsightAlertKind, - CostInsightEventType, - CostInsightSuggestionKind, -} from '@kilocode/db/schema-types'; -import { and, count, desc, eq, inArray, isNull, lt, or, sql } from 'drizzle-orm'; - -import type { db, DrizzleTransaction } from '@/lib/drizzle'; -import { - costInsightOwnerInsertValues, - costInsightOwnerTargetColumn, - costInsightOwnerTargetWhere, - costInsightOwnerWhere, -} from './owner'; - -export type CostInsightDatabase = typeof db | DrizzleTransaction; -export type CostInsightRootDatabase = typeof db; -export type CostInsightEventFilter = 'all' | 'alerts' | 'suggestions' | 'reviews' | 'settings'; -export type CostInsightOwnerCursor = { ownerType: CostInsightSpendOwner['type']; ownerId: string }; - -const eventTypesByFilter = { - alerts: ['anomaly_alert', 'threshold_crossed'], - suggestions: ['suggestion_created', 'suggestion_dismissed'], - reviews: ['alert_reviewed'], - settings: ['config_changed', 'disabled'], -} satisfies Record, CostInsightEventType[]>; - -function eventTypesForFilter(filter: CostInsightEventFilter): CostInsightEventType[] | null { - return filter === 'all' ? null : eventTypesByFilter[filter]; -} - -export type CostInsightThresholdAlertKind = Extract< - CostInsightAlertKind, - 'threshold' | 'threshold_7d' | 'threshold_30d' ->; - -export type CostInsightConfigPatch = { - spendAlertsEnabled?: boolean; - anomalyAlertsEnabled?: boolean; - costSuggestionsEnabled?: boolean; - spendThresholdMicrodollars?: number | null; - spend7DayThresholdMicrodollars?: number | null; - spend30DayThresholdMicrodollars?: number | null; -}; - -export type CostInsightEventInput = { - owner: CostInsightSpendOwner; - eventType: CostInsightEventType; - alertKind?: CostInsightAlertKind; - suggestionKind?: CostInsightSuggestionKind; - activeSuggestionId?: string | null; - actorUserId?: string | null; - title: string; - description: string; - snapshot?: CostInsightEventSnapshot; - dedupeKey?: string | null; -}; - -export function parsePersistedCostInsightEventSnapshot( - value: unknown -): CostInsightEventSnapshot | null { - const result = CostInsightEventSnapshotSchema.safeParse(value); - return result.success ? result.data : null; -} - -function validateCostInsightEventSnapshot(value: unknown): CostInsightEventSnapshot { - return CostInsightEventSnapshotSchema.parse(value); -} - -type CostInsightEventWriter = ( - database: CostInsightDatabase, - input: CostInsightEventInput -) => Promise<{ id: string; created: boolean }>; - -export type CostInsightSuggestionInput = { - owner: CostInsightSpendOwner; - suggestionKind: CostInsightSuggestionKind; - suggestionKey: string; - title: string; - description: string; - ctaLabel: string; - ctaHref: string; - evidenceWindowStart: string; - evidenceWindowEnd: string; - observedMicrodollars: number; - benefitLabel: string; - benefitDetail: string; -}; - -export async function getCostInsightOwnerConfig( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const [config] = await database - .select() - .from(cost_insight_owner_configs) - .where(costInsightOwnerWhere(owner, cost_insight_owner_configs)) - .limit(1); - return config ?? null; -} - -export async function getOrCreateCostInsightOwnerConfig( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const existing = await getCostInsightOwnerConfig(database, owner); - if (existing) return existing; - - const [config] = await database - .insert(cost_insight_owner_configs) - .values(costInsightOwnerInsertValues(owner)) - .onConflictDoUpdate({ - target: costInsightOwnerTargetColumn(owner, cost_insight_owner_configs), - targetWhere: costInsightOwnerTargetWhere(owner, cost_insight_owner_configs), - set: { - updated_at: sql`now()`, - }, - }) - .returning(); - if (!config) throw new Error('Cost Insights config upsert returned no row.'); - return config; -} - -export async function updateCostInsightOwnerConfig( - database: CostInsightDatabase, - owner: CostInsightSpendOwner, - patch: CostInsightConfigPatch -): Promise<{ previous: CostInsightOwnerConfig; current: CostInsightOwnerConfig }> { - const previous = await getOrCreateCostInsightOwnerConfig(database, owner); - const nextSpendAlertsEnabled = patch.spendAlertsEnabled ?? previous.spend_alerts_enabled; - - const [current] = await database - .update(cost_insight_owner_configs) - .set({ - spend_alerts_enabled: nextSpendAlertsEnabled, - anomaly_alerts_enabled: patch.anomalyAlertsEnabled ?? previous.anomaly_alerts_enabled, - cost_suggestions_enabled: patch.costSuggestionsEnabled ?? previous.cost_suggestions_enabled, - spend_threshold_microdollars: - patch.spendThresholdMicrodollars === undefined - ? previous.spend_threshold_microdollars - : patch.spendThresholdMicrodollars, - spend_7_day_threshold_microdollars: - patch.spend7DayThresholdMicrodollars === undefined - ? previous.spend_7_day_threshold_microdollars - : patch.spend7DayThresholdMicrodollars, - spend_30_day_threshold_microdollars: - patch.spend30DayThresholdMicrodollars === undefined - ? previous.spend_30_day_threshold_microdollars - : patch.spend30DayThresholdMicrodollars, - spend_alerts_enabled_at: nextSpendAlertsEnabled - ? (previous.spend_alerts_enabled_at ?? sql`now()`) - : null, - updated_at: sql`now()`, - }) - .where(eq(cost_insight_owner_configs.id, previous.id)) - .returning(); - - if (!current) throw new Error('Cost Insights config update returned no row.'); - return { previous, current }; -} - -export function getCostInsightConfigChanges( - previous: CostInsightOwnerConfig, - current: CostInsightOwnerConfig -): Record { - const fields: Record = {}; - const addChange = (key: string, oldValue: unknown, newValue: unknown) => { - if (oldValue !== newValue) fields[key] = { old: oldValue, new: newValue }; - }; - - addChange('spendAlertsEnabled', previous.spend_alerts_enabled, current.spend_alerts_enabled); - addChange( - 'anomalyAlertsEnabled', - previous.anomaly_alerts_enabled, - current.anomaly_alerts_enabled - ); - addChange( - 'costSuggestionsEnabled', - previous.cost_suggestions_enabled, - current.cost_suggestions_enabled - ); - addChange( - 'spendThresholdMicrodollars', - previous.spend_threshold_microdollars, - current.spend_threshold_microdollars - ); - addChange( - 'spend7DayThresholdMicrodollars', - previous.spend_7_day_threshold_microdollars, - current.spend_7_day_threshold_microdollars - ); - addChange( - 'spend30DayThresholdMicrodollars', - previous.spend_30_day_threshold_microdollars, - current.spend_30_day_threshold_microdollars - ); - return fields; -} - -export function getCostInsightSettingsSnapshot(config: CostInsightOwnerConfig) { - return { - spendAlertsEnabled: config.spend_alerts_enabled, - anomalyAlertsEnabled: config.anomaly_alerts_enabled, - costSuggestionsEnabled: config.cost_suggestions_enabled, - spendThresholdMicrodollars: config.spend_threshold_microdollars, - spend7DayThresholdMicrodollars: config.spend_7_day_threshold_microdollars, - spend30DayThresholdMicrodollars: config.spend_30_day_threshold_microdollars, - }; -} - -export async function updateCostInsightSettings( - database: CostInsightRootDatabase, - params: { - owner: CostInsightSpendOwner; - actorUserId: string; - patch: CostInsightConfigPatch; - } -): Promise<{ - previous: CostInsightOwnerConfig; - current: CostInsightOwnerConfig; - hasChanges: boolean; -}> { - return await database.transaction(async transaction => { - const { previous, current } = await updateCostInsightOwnerConfig( - transaction, - params.owner, - params.patch - ); - const changes = getCostInsightConfigChanges(previous, current); - const hasChanges = Object.keys(changes).length > 0; - const disabled = hasChanges && previous.spend_alerts_enabled && !current.spend_alerts_enabled; - - if (disabled) { - await clearCostInsightAlertState(transaction, params.owner); - } else { - if (previous.anomaly_alerts_enabled && !current.anomaly_alerts_enabled) { - await clearCostInsightAnomalyEpisode(transaction, params.owner); - } - if ( - previous.spend_threshold_microdollars !== null && - current.spend_threshold_microdollars === null - ) { - await clearCostInsightThresholdEpisode(transaction, params.owner, null, 'threshold'); - } - if ( - previous.spend_7_day_threshold_microdollars !== null && - current.spend_7_day_threshold_microdollars === null - ) { - await clearCostInsightThresholdEpisode(transaction, params.owner, null, 'threshold_7d'); - } - if ( - previous.spend_30_day_threshold_microdollars !== null && - current.spend_30_day_threshold_microdollars === null - ) { - await clearCostInsightThresholdEpisode(transaction, params.owner, null, 'threshold_30d'); - } - } - - if (disabled) { - await createCostInsightEvent(transaction, { - owner: params.owner, - eventType: 'disabled', - actorUserId: params.actorUserId, - title: 'Spend Alerts turned off', - description: 'Spend Alerts were disabled. Cost evidence remains visible.', - snapshot: { - changedFields: changes, - settings: getCostInsightSettingsSnapshot(current), - }, - }); - } else if (hasChanges && (previous.spend_alerts_enabled || current.spend_alerts_enabled)) { - await createCostInsightEvent(transaction, { - owner: params.owner, - eventType: 'config_changed', - actorUserId: params.actorUserId, - title: 'Cost Insights settings changed', - description: 'Spend Alert settings were updated.', - snapshot: { - changedFields: changes, - settings: getCostInsightSettingsSnapshot(current), - }, - }); - } - - return { previous, current, hasChanges }; - }); -} - -export async function getOrCreateCostInsightOwnerState( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const [state] = await database - .insert(cost_insight_owner_states) - .values(costInsightOwnerInsertValues(owner)) - .onConflictDoUpdate({ - target: costInsightOwnerTargetColumn(owner, cost_insight_owner_states), - targetWhere: costInsightOwnerTargetWhere(owner, cost_insight_owner_states), - set: { - updated_at: sql`now()`, - }, - }) - .returning(); - if (!state) throw new Error('Cost Insights state upsert returned no row.'); - return state; -} - -export async function clearCostInsightAlertState( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const state = await getOrCreateCostInsightOwnerState(database, owner); - await database - .update(cost_insight_owner_states) - .set({ - active_anomaly_event_id: null, - active_anomaly_episode_id: null, - active_anomaly_hour_start: null, - active_anomaly_snapshot: null, - active_anomaly_reviewed_at: null, - threshold_crossing_active: false, - active_threshold_event_id: null, - active_threshold_episode_id: null, - threshold_crossing_started_at: null, - active_threshold_snapshot: null, - threshold_reviewed_at: null, - threshold_recovered_at: null, - rolling_7_day_threshold_crossing_active: false, - active_rolling_7_day_threshold_event_id: null, - active_rolling_7_day_threshold_episode_id: null, - rolling_7_day_threshold_crossing_started_at: null, - active_rolling_7_day_threshold_snapshot: null, - rolling_7_day_threshold_reviewed_at: null, - rolling_7_day_threshold_recovered_at: null, - rolling_30_day_threshold_crossing_active: false, - active_rolling_30_day_threshold_event_id: null, - active_rolling_30_day_threshold_episode_id: null, - rolling_30_day_threshold_crossing_started_at: null, - active_rolling_30_day_threshold_snapshot: null, - rolling_30_day_threshold_reviewed_at: null, - rolling_30_day_threshold_recovered_at: null, - updated_at: sql`now()`, - }) - .where(eq(cost_insight_owner_states.id, state.id)); -} - -export async function clearCostInsightAnomalyEpisode( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const state = await getOrCreateCostInsightOwnerState(database, owner); - await database - .update(cost_insight_owner_states) - .set({ - active_anomaly_event_id: null, - active_anomaly_episode_id: null, - active_anomaly_hour_start: null, - active_anomaly_snapshot: null, - active_anomaly_reviewed_at: null, - updated_at: sql`now()`, - }) - .where(eq(cost_insight_owner_states.id, state.id)); -} - -export async function markCostInsightEvaluation( - database: CostInsightDatabase, - owner: CostInsightSpendOwner, - asOf: string -): Promise { - const state = await getOrCreateCostInsightOwnerState(database, owner); - await database - .update(cost_insight_owner_states) - .set({ last_evaluated_at: asOf, updated_at: sql`now()` }) - .where(eq(cost_insight_owner_states.id, state.id)); -} - -export async function createCostInsightEvent( - database: CostInsightDatabase, - input: CostInsightEventInput -): Promise<{ id: string; created: boolean }> { - const snapshot = validateCostInsightEventSnapshot(input.snapshot ?? {}); - const [event] = await database - .insert(cost_insight_events) - .values({ - ...costInsightOwnerInsertValues(input.owner), - event_type: input.eventType, - alert_kind: input.alertKind ?? null, - suggestion_kind: input.suggestionKind ?? null, - active_suggestion_id: input.activeSuggestionId ?? null, - actor_user_id: input.actorUserId ?? null, - title: input.title, - description: input.description, - snapshot, - dedupe_key: input.dedupeKey ?? null, - }) - .onConflictDoNothing() - .returning({ id: cost_insight_events.id }); - - if (event) return { id: event.id, created: true }; - if (!input.dedupeKey) throw new Error('Cost Insights event insert returned no row.'); - - const [existing] = await database - .select({ id: cost_insight_events.id }) - .from(cost_insight_events) - .where( - and( - costInsightOwnerWhere(input.owner, cost_insight_events), - eq(cost_insight_events.dedupe_key, input.dedupeKey) - ) - ) - .limit(1); - if (!existing) throw new Error('Cost Insights deduped event could not be loaded.'); - return { id: existing.id, created: false }; -} - -export async function createCostInsightNotificationDeliveries( - database: CostInsightDatabase, - eventId: string, - recipientUserIds: string[] -): Promise { - const uniqueRecipientUserIds = [...new Set(recipientUserIds)].sort(); - if (uniqueRecipientUserIds.length === 0) return 0; - const rows = await database - .insert(cost_insight_notification_deliveries) - .values( - uniqueRecipientUserIds.map(recipient_user_id => ({ event_id: eventId, recipient_user_id })) - ) - .onConflictDoNothing() - .returning({ id: cost_insight_notification_deliveries.id }); - return rows.length; -} - -export async function listCostInsightNotificationRecipientUserIds( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - if (owner.type === 'user') { - const [admin] = await database - .select({ id: kilocode_users.id }) - .from(kilocode_users) - .where(and(eq(kilocode_users.id, owner.id), eq(kilocode_users.is_admin, true))) - .limit(1); - return admin ? [admin.id] : []; - } - - const rows = await database - .select({ userId: organization_memberships.kilo_user_id }) - .from(organization_memberships) - .where( - and( - eq(organization_memberships.organization_id, owner.id), - inArray(organization_memberships.role, ['owner', 'billing_manager']) - ) - ) - .orderBy(organization_memberships.kilo_user_id); - return rows.map(row => row.userId); -} - -export async function listEnabledCostInsightOwners( - database: CostInsightDatabase -): Promise { - const rows = await database - .select({ - userId: cost_insight_owner_configs.owned_by_user_id, - organizationId: cost_insight_owner_configs.owned_by_organization_id, - }) - .from(cost_insight_owner_configs) - .where( - or( - eq(cost_insight_owner_configs.spend_alerts_enabled, true), - eq(cost_insight_owner_configs.cost_suggestions_enabled, true) - ) - ) - .orderBy(cost_insight_owner_configs.updated_at, cost_insight_owner_configs.id); - - return rows.map(row => { - if (row.userId) return { type: 'user', id: row.userId }; - if (row.organizationId) return { type: 'organization', id: row.organizationId }; - throw new Error('Cost Insights enabled config row has no owner.'); - }); -} - -type EnabledCostInsightOwnerRow = { - owner_type: CostInsightSpendOwner['type']; - owner_id: string; -}; - -export async function listEnabledCostInsightOwnerPage( - database: CostInsightDatabase, - options: { - cohortCreatedBefore: string; - after?: CostInsightOwnerCursor | null; - limit: number; - } -): Promise<{ - owners: CostInsightSpendOwner[]; - nextCursor: CostInsightOwnerCursor | null; - hasMore: boolean; -}> { - if (!Number.isSafeInteger(options.limit) || options.limit <= 0) { - throw new Error('Cost Insights owner page limit must be a positive safe integer.'); - } - const afterPredicate = options.after - ? sql`(owner_type, owner_id) > (${options.after.ownerType}, ${options.after.ownerId})` - : sql`TRUE`; - const result = await database.execute(sql` - WITH active_owners AS ( - SELECT 'organization'::text AS owner_type, owned_by_organization_id::text AS owner_id - FROM cost_insight_owner_configs - WHERE owned_by_organization_id IS NOT NULL - AND created_at < ${options.cohortCreatedBefore} - AND (spend_alerts_enabled = TRUE OR cost_suggestions_enabled = TRUE) - UNION ALL - SELECT 'user'::text AS owner_type, owned_by_user_id AS owner_id - FROM cost_insight_owner_configs - WHERE owned_by_user_id IS NOT NULL - AND created_at < ${options.cohortCreatedBefore} - AND (spend_alerts_enabled = TRUE OR cost_suggestions_enabled = TRUE) - ) - SELECT owner_type, owner_id - FROM active_owners - WHERE ${afterPredicate} - ORDER BY owner_type ASC, owner_id ASC - LIMIT ${options.limit + 1} - `); - const pageRows = result.rows.slice(0, options.limit); - const owners = pageRows.map(row => ({ type: row.owner_type, id: row.owner_id })); - const last = pageRows.at(-1); - return { - owners, - nextCursor: last - ? { ownerType: last.owner_type, ownerId: last.owner_id } - : (options.after ?? null), - hasMore: result.rows.length > options.limit, - }; -} - -export async function listCostInsightEvents( - database: CostInsightDatabase, - owner: CostInsightSpendOwner, - options: { limit?: number; offset?: number; filter?: CostInsightEventFilter } = {} -) { - const eventTypes = eventTypesForFilter(options.filter ?? 'all'); - const rows = await database - .select({ - id: cost_insight_events.id, - eventType: cost_insight_events.event_type, - alertKind: cost_insight_events.alert_kind, - suggestionKind: cost_insight_events.suggestion_kind, - actorUserId: cost_insight_events.actor_user_id, - actorName: kilocode_users.google_user_name, - title: cost_insight_events.title, - description: cost_insight_events.description, - snapshot: cost_insight_events.snapshot, - occurredAt: cost_insight_events.occurred_at, - }) - .from(cost_insight_events) - .leftJoin(kilocode_users, eq(kilocode_users.id, cost_insight_events.actor_user_id)) - .where( - and( - costInsightOwnerWhere(owner, cost_insight_events), - eventTypes ? inArray(cost_insight_events.event_type, eventTypes) : undefined - ) - ) - .orderBy(desc(cost_insight_events.occurred_at), desc(cost_insight_events.id)) - .limit(options.limit ?? 50) - .offset(options.offset ?? 0); - return rows.map(row => ({ - ...row, - snapshot: parsePersistedCostInsightEventSnapshot(row.snapshot) ?? {}, - })); -} - -export async function countCostInsightEvents( - database: CostInsightDatabase, - owner: CostInsightSpendOwner, - filter: CostInsightEventFilter = 'all' -): Promise { - const eventTypes = eventTypesForFilter(filter); - const [row] = await database - .select({ value: count() }) - .from(cost_insight_events) - .where( - and( - costInsightOwnerWhere(owner, cost_insight_events), - eventTypes ? inArray(cost_insight_events.event_type, eventTypes) : undefined - ) - ); - return row?.value ?? 0; -} - -export async function getCostInsightDashboardState( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -) { - const [state] = await database - .select({ - activeAnomalyEventId: cost_insight_owner_states.active_anomaly_event_id, - activeAnomalyEpisodeId: cost_insight_owner_states.active_anomaly_episode_id, - activeAnomalyHourStart: cost_insight_owner_states.active_anomaly_hour_start, - activeAnomalySnapshot: cost_insight_owner_states.active_anomaly_snapshot, - activeAnomalyReviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at, - activeThresholdEventId: cost_insight_owner_states.active_threshold_event_id, - activeThresholdEpisodeId: cost_insight_owner_states.active_threshold_episode_id, - thresholdCrossingActive: cost_insight_owner_states.threshold_crossing_active, - activeThresholdSnapshot: cost_insight_owner_states.active_threshold_snapshot, - thresholdReviewedAt: cost_insight_owner_states.threshold_reviewed_at, - active7DayThresholdEventId: cost_insight_owner_states.active_rolling_7_day_threshold_event_id, - active7DayThresholdEpisodeId: - cost_insight_owner_states.active_rolling_7_day_threshold_episode_id, - threshold7DayCrossingActive: - cost_insight_owner_states.rolling_7_day_threshold_crossing_active, - active7DayThresholdSnapshot: - cost_insight_owner_states.active_rolling_7_day_threshold_snapshot, - threshold7DayReviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at, - active30DayThresholdEventId: - cost_insight_owner_states.active_rolling_30_day_threshold_event_id, - active30DayThresholdEpisodeId: - cost_insight_owner_states.active_rolling_30_day_threshold_episode_id, - threshold30DayCrossingActive: - cost_insight_owner_states.rolling_30_day_threshold_crossing_active, - active30DayThresholdSnapshot: - cost_insight_owner_states.active_rolling_30_day_threshold_snapshot, - threshold30DayReviewedAt: cost_insight_owner_states.rolling_30_day_threshold_reviewed_at, - lastEvaluatedAt: cost_insight_owner_states.last_evaluated_at, - }) - .from(cost_insight_owner_states) - .where(costInsightOwnerWhere(owner, cost_insight_owner_states)) - .limit(1); - - const eventIds = [ - state?.activeAnomalyEventId, - state?.activeThresholdEventId, - state?.active7DayThresholdEventId, - state?.active30DayThresholdEventId, - ].filter((id): id is string => Boolean(id)); - - const events = - eventIds.length === 0 - ? [] - : await database - .select({ - id: cost_insight_events.id, - event_type: cost_insight_events.event_type, - alert_kind: cost_insight_events.alert_kind, - snapshot: cost_insight_events.snapshot, - }) - .from(cost_insight_events) - .where(inArray(cost_insight_events.id, eventIds)); - - const eventsById = new Map( - events.map(event => [ - event.id, - { ...event, snapshot: parsePersistedCostInsightEventSnapshot(event.snapshot) ?? {} }, - ]) - ); - const activeAnomalyEpisodeId = state?.activeAnomalyEpisodeId ?? state?.activeAnomalyEventId; - if (activeAnomalyEpisodeId) { - const parsedSnapshot = parsePersistedCostInsightEventSnapshot(state?.activeAnomalySnapshot); - if (parsedSnapshot || !eventsById.has(activeAnomalyEpisodeId)) { - eventsById.set(activeAnomalyEpisodeId, { - id: activeAnomalyEpisodeId, - event_type: 'anomaly_alert', - alert_kind: 'anomaly', - snapshot: parsedSnapshot ?? {}, - }); - } - } - const thresholdSnapshots = [ - { - id: state?.activeThresholdEpisodeId, - fallbackId: state?.activeThresholdEventId, - snapshot: state?.activeThresholdSnapshot, - alertKind: 'threshold' as const, - }, - { - id: state?.active7DayThresholdEpisodeId, - fallbackId: state?.active7DayThresholdEventId, - snapshot: state?.active7DayThresholdSnapshot, - alertKind: 'threshold_7d' as const, - }, - { - id: state?.active30DayThresholdEpisodeId, - fallbackId: state?.active30DayThresholdEventId, - snapshot: state?.active30DayThresholdSnapshot, - alertKind: 'threshold_30d' as const, - }, - ]; - for (const threshold of thresholdSnapshots) { - const episodeId = threshold.id ?? threshold.fallbackId; - if (!episodeId) continue; - const parsedSnapshot = parsePersistedCostInsightEventSnapshot(threshold.snapshot); - if (parsedSnapshot || !eventsById.has(episodeId)) { - eventsById.set(episodeId, { - id: episodeId, - event_type: 'threshold_crossed', - alert_kind: threshold.alertKind, - snapshot: parsedSnapshot ?? {}, - }); - } - } - - return { state: state ?? null, events: [...eventsById.values()] }; -} - -export async function markCostInsightAnomalyEpisode( - database: CostInsightDatabase, - params: { - owner: CostInsightSpendOwner; - eventId: string; - hourStart: string; - snapshot: CostInsightEventSnapshot; - } -): Promise { - const snapshot = validateCostInsightEventSnapshot(params.snapshot); - const state = await getOrCreateCostInsightOwnerState(database, params.owner); - await database - .update(cost_insight_owner_states) - .set({ - active_anomaly_event_id: params.eventId, - active_anomaly_episode_id: params.eventId, - active_anomaly_hour_start: params.hourStart, - active_anomaly_snapshot: snapshot, - active_anomaly_reviewed_at: null, - updated_at: sql`now()`, - }) - .where(eq(cost_insight_owner_states.id, state.id)); -} - -export async function markCostInsightThresholdEpisode( - database: CostInsightDatabase, - params: { - owner: CostInsightSpendOwner; - eventId: string; - crossedAt: string; - alertKind: CostInsightThresholdAlertKind; - snapshot: CostInsightEventSnapshot; - } -): Promise { - const snapshot = validateCostInsightEventSnapshot(params.snapshot); - const state = await getOrCreateCostInsightOwnerState(database, params.owner); - const values = (() => { - if (params.alertKind === 'threshold_7d') { - return { - rolling_7_day_threshold_crossing_active: true, - active_rolling_7_day_threshold_event_id: params.eventId, - active_rolling_7_day_threshold_episode_id: params.eventId, - rolling_7_day_threshold_crossing_started_at: params.crossedAt, - active_rolling_7_day_threshold_snapshot: snapshot, - rolling_7_day_threshold_reviewed_at: null, - rolling_7_day_threshold_recovered_at: null, - updated_at: sql`now()`, - }; - } - if (params.alertKind === 'threshold_30d') { - return { - rolling_30_day_threshold_crossing_active: true, - active_rolling_30_day_threshold_event_id: params.eventId, - active_rolling_30_day_threshold_episode_id: params.eventId, - rolling_30_day_threshold_crossing_started_at: params.crossedAt, - active_rolling_30_day_threshold_snapshot: snapshot, - rolling_30_day_threshold_reviewed_at: null, - rolling_30_day_threshold_recovered_at: null, - updated_at: sql`now()`, - }; - } - return { - threshold_crossing_active: true, - active_threshold_event_id: params.eventId, - active_threshold_episode_id: params.eventId, - threshold_crossing_started_at: params.crossedAt, - active_threshold_snapshot: snapshot, - threshold_reviewed_at: null, - threshold_recovered_at: null, - updated_at: sql`now()`, - }; - })(); - await database - .update(cost_insight_owner_states) - .set(values) - .where(eq(cost_insight_owner_states.id, state.id)); -} - -export async function clearCostInsightThresholdEpisode( - database: CostInsightDatabase, - owner: CostInsightSpendOwner, - recoveredAt: string | null, - alertKind: CostInsightThresholdAlertKind = 'threshold' -): Promise { - const state = await getOrCreateCostInsightOwnerState(database, owner); - const values = (() => { - if (alertKind === 'threshold_7d') { - return { - rolling_7_day_threshold_crossing_active: false, - active_rolling_7_day_threshold_event_id: null, - active_rolling_7_day_threshold_episode_id: null, - rolling_7_day_threshold_crossing_started_at: null, - active_rolling_7_day_threshold_snapshot: null, - rolling_7_day_threshold_reviewed_at: null, - rolling_7_day_threshold_recovered_at: recoveredAt, - updated_at: sql`now()`, - }; - } - if (alertKind === 'threshold_30d') { - return { - rolling_30_day_threshold_crossing_active: false, - active_rolling_30_day_threshold_event_id: null, - active_rolling_30_day_threshold_episode_id: null, - rolling_30_day_threshold_crossing_started_at: null, - active_rolling_30_day_threshold_snapshot: null, - rolling_30_day_threshold_reviewed_at: null, - rolling_30_day_threshold_recovered_at: recoveredAt, - updated_at: sql`now()`, - }; - } - return { - threshold_crossing_active: false, - active_threshold_event_id: null, - active_threshold_episode_id: null, - threshold_crossing_started_at: null, - active_threshold_snapshot: null, - threshold_reviewed_at: null, - threshold_recovered_at: recoveredAt, - updated_at: sql`now()`, - }; - })(); - await database - .update(cost_insight_owner_states) - .set(values) - .where(eq(cost_insight_owner_states.id, state.id)); -} - -async function acknowledgeCostInsightAlertInTransaction( - database: CostInsightDatabase, - params: { - owner: CostInsightSpendOwner; - alertKind: CostInsightAlertKind; - eventId: string; - actorUserId: string; - }, - writeEvent: CostInsightEventWriter -): Promise { - const state = await getOrCreateCostInsightOwnerState(database, params.owner); - const now = sql`now()`; - const reviewValues = - params.alertKind === 'anomaly' - ? { active_anomaly_reviewed_at: now, updated_at: now } - : params.alertKind === 'threshold_7d' - ? { rolling_7_day_threshold_reviewed_at: now, updated_at: now } - : params.alertKind === 'threshold_30d' - ? { rolling_30_day_threshold_reviewed_at: now, updated_at: now } - : { threshold_reviewed_at: now, updated_at: now }; - const activeEpisode = - params.alertKind === 'anomaly' - ? and( - sql`COALESCE(${cost_insight_owner_states.active_anomaly_episode_id}, ${cost_insight_owner_states.active_anomaly_event_id}) = ${params.eventId}`, - isNull(cost_insight_owner_states.active_anomaly_reviewed_at) - ) - : params.alertKind === 'threshold_7d' - ? and( - sql`COALESCE(${cost_insight_owner_states.active_rolling_7_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_7_day_threshold_event_id}) = ${params.eventId}`, - isNull(cost_insight_owner_states.rolling_7_day_threshold_reviewed_at) - ) - : params.alertKind === 'threshold_30d' - ? and( - sql`COALESCE(${cost_insight_owner_states.active_rolling_30_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_30_day_threshold_event_id}) = ${params.eventId}`, - isNull(cost_insight_owner_states.rolling_30_day_threshold_reviewed_at) - ) - : and( - sql`COALESCE(${cost_insight_owner_states.active_threshold_episode_id}, ${cost_insight_owner_states.active_threshold_event_id}) = ${params.eventId}`, - isNull(cost_insight_owner_states.threshold_reviewed_at) - ); - const [acknowledged] = await database - .update(cost_insight_owner_states) - .set(reviewValues) - .where(and(eq(cost_insight_owner_states.id, state.id), activeEpisode)) - .returning({ id: cost_insight_owner_states.id }); - - if (!acknowledged) return false; - - await writeEvent(database, { - owner: params.owner, - eventType: 'alert_reviewed', - alertKind: params.alertKind, - actorUserId: params.actorUserId, - title: - params.alertKind === 'anomaly' - ? 'Spend Anomaly Alert reviewed' - : params.alertKind === 'threshold_7d' - ? '7-day Spend Threshold Alert reviewed' - : params.alertKind === 'threshold_30d' - ? '30-day Spend Threshold Alert reviewed' - : '24-hour Spend Threshold Alert reviewed', - description: 'Alert acknowledgment recorded for the current episode.', - dedupeKey: `alert-reviewed:${params.eventId}`, - }); - return true; -} - -export async function acknowledgeCostInsightAlert( - database: CostInsightRootDatabase, - params: { - owner: CostInsightSpendOwner; - alertKind: CostInsightAlertKind; - eventId: string; - actorUserId: string; - } -): Promise { - return await database.transaction(async transaction => - acknowledgeCostInsightAlertInTransaction(transaction, params, createCostInsightEvent) - ); -} - -export async function upsertCostInsightActiveSuggestion( - database: CostInsightDatabase, - input: CostInsightSuggestionInput -): Promise<{ id: string; created: boolean }> { - const [suggestion] = await database - .insert(cost_insight_active_suggestions) - .values({ - ...costInsightOwnerInsertValues(input.owner), - suggestion_kind: input.suggestionKind, - suggestion_key: input.suggestionKey, - title: input.title, - description: input.description, - cta_label: input.ctaLabel, - cta_href: input.ctaHref, - evidence_window_start: input.evidenceWindowStart, - evidence_window_end: input.evidenceWindowEnd, - observed_microdollars: input.observedMicrodollars, - benefit_label: input.benefitLabel, - benefit_detail: input.benefitDetail, - }) - .onConflictDoNothing() - .returning({ id: cost_insight_active_suggestions.id }); - - if (suggestion) return { id: suggestion.id, created: true }; - - const [existing] = await database - .select({ id: cost_insight_active_suggestions.id }) - .from(cost_insight_active_suggestions) - .where( - and( - costInsightOwnerWhere(input.owner, cost_insight_active_suggestions), - eq(cost_insight_active_suggestions.suggestion_key, input.suggestionKey) - ) - ) - .limit(1); - if (!existing) throw new Error('Cost Insights suggestion upsert returned no row.'); - return { id: existing.id, created: false }; -} - -export async function listActiveCostInsightSuggestions( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -) { - return await database - .select() - .from(cost_insight_active_suggestions) - .where( - and( - costInsightOwnerWhere(owner, cost_insight_active_suggestions), - isNull(cost_insight_active_suggestions.dismissed_at) - ) - ) - .orderBy( - desc(cost_insight_active_suggestions.created_at), - desc(cost_insight_active_suggestions.id) - ); -} - -async function dismissCostInsightSuggestionInTransaction( - database: CostInsightDatabase, - params: { owner: CostInsightSpendOwner; suggestionId: string; actorUserId: string }, - writeEvent: CostInsightEventWriter -): Promise { - const [suggestion] = await database - .update(cost_insight_active_suggestions) - .set({ - dismissed_at: sql`now()`, - dismissed_by_user_id: params.actorUserId, - updated_at: sql`now()`, - }) - .where( - and( - eq(cost_insight_active_suggestions.id, params.suggestionId), - costInsightOwnerWhere(params.owner, cost_insight_active_suggestions), - isNull(cost_insight_active_suggestions.dismissed_at) - ) - ) - .returning(); - - if (!suggestion) return null; - await writeEvent(database, { - owner: params.owner, - eventType: 'suggestion_dismissed', - suggestionKind: suggestion.suggestion_kind, - activeSuggestionId: suggestion.id, - actorUserId: params.actorUserId, - title: 'Cost Suggestion dismissed', - description: suggestion.title, - snapshot: { - suggestion: { - suggestionKey: suggestion.suggestion_key, - evidenceWindowStart: suggestion.evidence_window_start, - evidenceWindowEnd: suggestion.evidence_window_end, - observedMicrodollars: suggestion.observed_microdollars, - ctaHref: suggestion.cta_href, - }, - }, - dedupeKey: `suggestion-dismissed:${suggestion.id}`, - }); - return suggestion.suggestion_kind; -} - -export async function dismissCostInsightSuggestion( - database: CostInsightRootDatabase, - params: { owner: CostInsightSpendOwner; suggestionId: string; actorUserId: string } -): Promise { - return await database.transaction(async transaction => - dismissCostInsightSuggestionInTransaction(transaction, params, createCostInsightEvent) - ); -} - -export async function deleteExpiredCostInsightEvents( - database: CostInsightDatabase, - retentionCutoff: string -): Promise { - const rows = await database - .delete(cost_insight_events) - .where(lt(cost_insight_events.occurred_at, retentionCutoff)) - .returning({ id: cost_insight_events.id }); - return rows.length; -} - -export async function ownerHasUnreviewedCostInsightAlert( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const [row] = await database - .select({ id: cost_insight_owner_states.id }) - .from(cost_insight_owner_states) - .where( - and( - costInsightOwnerWhere(owner, cost_insight_owner_states), - or( - and( - isNull(cost_insight_owner_states.active_anomaly_reviewed_at), - sql`COALESCE(${cost_insight_owner_states.active_anomaly_episode_id}, ${cost_insight_owner_states.active_anomaly_event_id}) IS NOT NULL` - ), - and( - isNull(cost_insight_owner_states.threshold_reviewed_at), - sql`COALESCE(${cost_insight_owner_states.active_threshold_episode_id}, ${cost_insight_owner_states.active_threshold_event_id}) IS NOT NULL` - ), - and( - isNull(cost_insight_owner_states.rolling_7_day_threshold_reviewed_at), - sql`COALESCE(${cost_insight_owner_states.active_rolling_7_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_7_day_threshold_event_id}) IS NOT NULL` - ), - and( - isNull(cost_insight_owner_states.rolling_30_day_threshold_reviewed_at), - sql`COALESCE(${cost_insight_owner_states.active_rolling_30_day_threshold_episode_id}, ${cost_insight_owner_states.active_rolling_30_day_threshold_event_id}) IS NOT NULL` - ) - ) - ) - ) - .limit(1); - return Boolean(row); -} - -export async function countUnreviewedCostInsightAlerts( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const [state] = await database - .select({ - activeAnomalyEpisodeId: cost_insight_owner_states.active_anomaly_episode_id, - activeAnomalyEventId: cost_insight_owner_states.active_anomaly_event_id, - activeAnomalyReviewedAt: cost_insight_owner_states.active_anomaly_reviewed_at, - activeThresholdEpisodeId: cost_insight_owner_states.active_threshold_episode_id, - activeThresholdEventId: cost_insight_owner_states.active_threshold_event_id, - thresholdReviewedAt: cost_insight_owner_states.threshold_reviewed_at, - active7DayThresholdEpisodeId: - cost_insight_owner_states.active_rolling_7_day_threshold_episode_id, - active7DayThresholdEventId: cost_insight_owner_states.active_rolling_7_day_threshold_event_id, - threshold7DayReviewedAt: cost_insight_owner_states.rolling_7_day_threshold_reviewed_at, - active30DayThresholdEpisodeId: - cost_insight_owner_states.active_rolling_30_day_threshold_episode_id, - active30DayThresholdEventId: - cost_insight_owner_states.active_rolling_30_day_threshold_event_id, - threshold30DayReviewedAt: cost_insight_owner_states.rolling_30_day_threshold_reviewed_at, - }) - .from(cost_insight_owner_states) - .where(costInsightOwnerWhere(owner, cost_insight_owner_states)) - .limit(1); - - return ( - ((state?.activeAnomalyEpisodeId ?? state?.activeAnomalyEventId) && - !state.activeAnomalyReviewedAt - ? 1 - : 0) + - ((state?.activeThresholdEpisodeId ?? state?.activeThresholdEventId) && - !state.thresholdReviewedAt - ? 1 - : 0) + - ((state?.active7DayThresholdEpisodeId ?? state?.active7DayThresholdEventId) && - !state.threshold7DayReviewedAt - ? 1 - : 0) + - ((state?.active30DayThresholdEpisodeId ?? state?.active30DayThresholdEventId) && - !state.threshold30DayReviewedAt - ? 1 - : 0) - ); -} - -export async function countOpenCostInsightReviewItems( - database: CostInsightDatabase, - owner: CostInsightSpendOwner -): Promise { - const alertCount = await countUnreviewedCostInsightAlerts(database, owner); - const [config] = await database - .select({ costSuggestionsEnabled: cost_insight_owner_configs.cost_suggestions_enabled }) - .from(cost_insight_owner_configs) - .where(costInsightOwnerWhere(owner, cost_insight_owner_configs)) - .limit(1); - - if (config?.costSuggestionsEnabled === false) return alertCount; - - const [suggestions] = await database - .select({ value: count() }) - .from(cost_insight_active_suggestions) - .where( - and( - costInsightOwnerWhere(owner, cost_insight_active_suggestions), - isNull(cost_insight_active_suggestions.dismissed_at) - ) - ); - - return alertCount + (suggestions?.value ?? 0); -} - -export const costInsightRepositoryInternals = { - acknowledgeCostInsightAlertInTransaction, - dismissCostInsightSuggestionInTransaction, -}; diff --git a/apps/web/src/lib/cost-insights/rollup-maintenance.integration.test.ts b/apps/web/src/lib/cost-insights/rollup-maintenance.integration.test.ts deleted file mode 100644 index edc929c8d76..00000000000 --- a/apps/web/src/lib/cost-insights/rollup-maintenance.integration.test.ts +++ /dev/null @@ -1,530 +0,0 @@ -import { afterEach, describe, expect, test } from '@jest/globals'; -import { and, eq, sql } from 'drizzle-orm'; - -import { db } from '@/lib/drizzle'; -import { - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, - cost_insight_evaluation_dirty_owners, - cost_insight_rollup_coverage, - cost_insight_rollup_degraded_intervals, - cost_insight_rollup_repairs, - kilocode_users, - microdollar_usage, -} from '@kilocode/db/schema'; -import { captureCostInsightSpend } from '@kilocode/db/cost-insights-rollups'; - -import { - backfillCostInsightHour, - backfillCostInsightRollupsNewestFirst, - initializeCostInsightRollupCoverage, - recordCostInsightDegradedInterval, - reconcileCostInsightRollups, - repairOwnerSpendRollups, - resolveCostInsightDegradedInterval, -} from './rollup-maintenance'; -import { - claimPendingCostInsightRollupRepairs, - completeCostInsightRollupRepair, - enqueueCostInsightRollupRepair, - failCostInsightRollupRepair, - processPendingCostInsightRollupRepairs, -} from './rollup-repairs'; - -const userIds = new Set(); - -async function createUser(): Promise { - const id = `cost-insights-maintenance-${crypto.randomUUID()}`; - userIds.add(id); - await db.insert(kilocode_users).values({ - id, - google_user_email: `${id}@example.com`, - google_user_name: 'Cost Insights Maintenance Test', - google_user_image_url: 'https://example.com/avatar.png', - stripe_customer_id: `cus_${crypto.randomUUID()}`, - }); - return id; -} - -function rawUsage(userId: string, cost: number, createdAt: string) { - return { - kilo_user_id: userId, - cost, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: createdAt, - provider: 'provider', - model: 'model', - }; -} - -afterEach(async () => { - await db - .delete(cost_insight_rollup_degraded_intervals) - .where(eq(cost_insight_rollup_degraded_intervals.reason, 'reconciliation_mismatch')); - await db - .delete(cost_insight_rollup_coverage) - .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); - for (const userId of userIds) { - await db - .delete(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.owned_by_user_id, userId)); - await db - .delete(cost_insight_evaluation_dirty_owners) - .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)); - await db - .delete(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); - await db - .delete(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - await db.delete(microdollar_usage).where(eq(microdollar_usage.kilo_user_id, userId)); - await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); - } - userIds.clear(); -}); - -describe('Cost Insights rollup maintenance integration', () => { - test('initializes coverage and rebuilds a canonical hour idempotently', async () => { - const userId = await createUser(); - await db.insert(microdollar_usage).values(rawUsage(userId, 11, '2026-06-01T01:30:00.000Z')); - await initializeCostInsightRollupCoverage(db, '2026-06-01T02:00:00.000Z'); - - const first = await backfillCostInsightHour(db, '2026-06-01T01:00:00.000Z'); - const second = await backfillCostInsightHour(db, '2026-06-01T01:00:00.000Z'); - - expect(first).toMatchObject({ - canonicalMicrodollars: 11, - canonicalSpendRecordCount: 1, - coverageAdvanced: true, - }); - expect(second).toMatchObject({ - canonicalMicrodollars: 11, - canonicalSpendRecordCount: 1, - coverageAdvanced: false, - }); - const [coverage] = await db - .select() - .from(cost_insight_rollup_coverage) - .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); - expect(new Date(coverage.coverage_start_hour ?? '').toISOString()).toBe( - '2026-06-01T01:00:00.000Z' - ); - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - const drivers = await db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); - expect(totals).toEqual([ - expect.objectContaining({ total_microdollars: 11, spend_record_count: 1 }), - ]); - expect(drivers).toEqual([ - expect.objectContaining({ total_microdollars: 11, spend_record_count: 1 }), - ]); - }); - - test('stages multi-hour canonical sources once and persists newest-first results', async () => { - const userId = await createUser(); - await db - .insert(microdollar_usage) - .values([ - rawUsage(userId, 5, '2026-06-01T00:30:00.000Z'), - rawUsage(userId, 7, '2026-06-01T01:30:00.000Z'), - ]); - await initializeCostInsightRollupCoverage(db, '2026-06-01T02:00:00.000Z'); - - const results = await backfillCostInsightRollupsNewestFirst(db, { - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - maxHours: 2, - }); - - expect(results.map(result => result.hourStart)).toEqual([ - '2026-06-01T01:00:00.000Z', - '2026-06-01T00:00:00.000Z', - ]); - const totals = await db - .select({ - hourStart: cost_insight_owner_hour_totals.hour_start, - amount: cost_insight_owner_hour_totals.total_microdollars, - }) - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)) - .orderBy(cost_insight_owner_hour_totals.hour_start); - expect( - totals.map(row => ({ hourStart: new Date(row.hourStart).toISOString(), amount: row.amount })) - ).toEqual([ - { hourStart: '2026-06-01T00:00:00.000Z', amount: 5 }, - { hourStart: '2026-06-01T01:00:00.000Z', amount: 7 }, - ]); - }); - - test('rejects bulk replacement at or after live-capture cutover', async () => { - const userId = await createUser(); - await db.insert(microdollar_usage).values(rawUsage(userId, 13, '2026-06-01T02:30:00.000Z')); - await initializeCostInsightRollupCoverage(db, '2026-06-01T02:00:00.000Z'); - - await expect(backfillCostInsightHour(db, '2026-06-01T02:00:00.000Z')).rejects.toThrow( - 'restricted to pre-cutover hours' - ); - - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - expect(totals).toHaveLength(0); - }); - - test('targeted repair deletes stale rows when canonical owner-hour spend is zero', async () => { - const userId = await createUser(); - await db.insert(cost_insight_owner_hour_totals).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'variable', - total_microdollars: 99, - spend_record_count: 1, - }); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'variable', - driver_key: 'a'.repeat(64), - source: 'ai_gateway', - product_key: 'other', - feature_key: 'other', - model_or_plan_key: 'model', - provider_key: 'provider', - actor_user_id: userId, - total_microdollars: 99, - spend_record_count: 1, - }); - - await repairOwnerSpendRollups(db, { - owner: { type: 'user', id: userId }, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T01:00:00.000Z', - maxHours: 1, - }); - - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - const drivers = await db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); - expect(totals).toHaveLength(0); - expect(drivers).toHaveLength(0); - }); - - test('targeted repair reconstructs a failed best-effort capture from canonical usage', async () => { - const userId = await createUser(); - await db.insert(microdollar_usage).values(rawUsage(userId, 17, '2026-06-01T00:30:00.000Z')); - - await repairOwnerSpendRollups(db, { - owner: { type: 'user', id: userId }, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T01:00:00.000Z', - maxHours: 1, - }); - - const [total] = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - const [driver] = await db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); - expect(total).toMatchObject({ total_microdollars: 17, spend_record_count: 1 }); - expect(driver).toMatchObject({ total_microdollars: 17, spend_record_count: 1 }); - }); - - test('deferred owner-hour repair reconstructs canonical usage and schedules evaluation', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const occurredAt = '2026-06-01T00:30:00.000Z'; - const [usage] = await db - .insert(microdollar_usage) - .values(rawUsage(userId, 19, occurredAt)) - .returning({ id: microdollar_usage.id }); - - await enqueueCostInsightRollupRepair(db, { usageId: usage.id, owner, occurredAt }); - await db - .update(cost_insight_rollup_repairs) - .set({ next_attempt_at: '2026-06-01T01:01:00.000Z' }) - .where(eq(cost_insight_rollup_repairs.owned_by_user_id, userId)); - - const summary = await processPendingCostInsightRollupRepairs(db, { limit: 1 }); - expect(summary).toMatchObject({ claimed: 1, repaired: 1, failed: [] }); - - const [total] = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - expect(total).toMatchObject({ total_microdollars: 19, spend_record_count: 1 }); - expect( - await db - .select() - .from(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.owned_by_user_id, userId)) - ).toHaveLength(0); - expect( - await db - .select() - .from(cost_insight_evaluation_dirty_owners) - .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId)) - ).toHaveLength(1); - }); - - test('coalesces repeated owner-hour repair requests without losing newer work', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const occurredAt = '2026-06-01T00:30:00.000Z'; - const [firstUsage, secondUsage] = await db - .insert(microdollar_usage) - .values([rawUsage(userId, 1, occurredAt), rawUsage(userId, 2, occurredAt)]) - .returning({ id: microdollar_usage.id }); - - await enqueueCostInsightRollupRepair(db, { usageId: firstUsage.id, owner, occurredAt }); - await enqueueCostInsightRollupRepair(db, { usageId: secondUsage.id, owner, occurredAt }); - - const repairs = await db - .select() - .from(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.owned_by_user_id, userId)); - expect(repairs).toHaveLength(2); - expect(repairs).toEqual([ - expect.objectContaining({ generation: 1, attempt_count: 0 }), - expect.objectContaining({ generation: 1, attempt_count: 0 }), - ]); - }); - - test('preserves a newer repair request enqueued while an earlier claim is blocked', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const occurredAt = '2026-06-01T00:30:00.000Z'; - const [usage] = await db - .insert(microdollar_usage) - .values(rawUsage(userId, 3, occurredAt)) - .returning({ id: microdollar_usage.id }); - await enqueueCostInsightRollupRepair(db, { usageId: usage.id, owner, occurredAt }); - await db - .update(cost_insight_rollup_repairs) - .set({ next_attempt_at: '2026-06-01T01:01:00.000Z' }) - .where(eq(cost_insight_rollup_repairs.owned_by_user_id, userId)); - - const [claim] = await claimPendingCostInsightRollupRepairs(db, 1); - await enqueueCostInsightRollupRepair(db, { usageId: usage.id, owner, occurredAt }); - - await expect(completeCostInsightRollupRepair(db, claim, owner)).resolves.toBe(false); - const [remaining] = await db - .select() - .from(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.owned_by_user_id, userId)); - expect(remaining).toMatchObject({ generation: 2, claim_token: null }); - }); - - test('retains a failed claimed repair with bounded retry and safe error classification', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const occurredAt = '2026-06-01T00:30:00.000Z'; - const [usage] = await db - .insert(microdollar_usage) - .values(rawUsage(userId, 5, occurredAt)) - .returning({ id: microdollar_usage.id }); - await enqueueCostInsightRollupRepair(db, { usageId: usage.id, owner, occurredAt }); - await db - .update(cost_insight_rollup_repairs) - .set({ next_attempt_at: '2026-06-01T01:01:00.000Z' }) - .where(eq(cost_insight_rollup_repairs.usage_id, usage.id)); - - const [claim] = await claimPendingCostInsightRollupRepairs(db, 1); - const failedAt = Date.now(); - await failCostInsightRollupRepair(db, claim, 'secret=value database failed with 55P03'); - - const [repair] = await db - .select() - .from(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.usage_id, usage.id)); - expect(repair).toMatchObject({ - attempt_count: 1, - claimed_at: null, - claim_token: null, - last_error_redacted: 'postgres:55P03', - }); - expect(Date.parse(repair.next_attempt_at)).toBeGreaterThan(failedAt); - }); - - test('processor reports canonical repair failure and leaves durable retry state', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const occurredAt = '2026-06-01T00:30:00.000Z'; - const result = await db.execute<{ id: string }>(sql` - INSERT INTO microdollar_usage ( - kilo_user_id, - cost, - input_tokens, - output_tokens, - cache_write_tokens, - cache_hit_tokens, - created_at, - provider, - model - ) VALUES ( - ${userId}, - 9007199254740992, - 0, - 0, - 0, - 0, - ${occurredAt}, - 'provider', - 'model' - ) - RETURNING id::text - `); - const usageId = result.rows[0]?.id; - if (!usageId) throw new Error('Failed repair fixture did not return usage ID.'); - await enqueueCostInsightRollupRepair(db, { usageId, owner, occurredAt }); - await db - .update(cost_insight_rollup_repairs) - .set({ next_attempt_at: '2026-06-01T01:01:00.000Z' }) - .where(eq(cost_insight_rollup_repairs.usage_id, usageId)); - - const summary = await processPendingCostInsightRollupRepairs(db, { limit: 1 }); - expect(summary.failed).toEqual([ - expect.objectContaining({ - owner, - hourStart: '2026-06-01T00:00:00.000Z', - error: 'cost_insight_rollup_repair_failed', - }), - ]); - const [repair] = await db - .select() - .from(cost_insight_rollup_repairs) - .where(eq(cost_insight_rollup_repairs.usage_id, usageId)); - expect(repair).toMatchObject({ - attempt_count: 1, - claimed_at: null, - claim_token: null, - last_error_redacted: 'cost_insight_rollup_repair_failed', - }); - }); - - test('reconciles multi-hour ranges in bounded repeatable-read chunks', async () => { - await db.insert(cost_insight_rollup_coverage).values({ - rollup_version: 1, - live_capture_start_hour: '2026-06-01T02:00:00.000Z', - coverage_start_hour: '2026-06-01T00:00:00.000Z', - }); - let transactionCount = 0; - const countingDatabase = new Proxy(db, { - get(target, property, receiver) { - if (property !== 'transaction') return Reflect.get(target, property, receiver); - return (...args: unknown[]) => { - transactionCount++; - return Reflect.apply(target.transaction, target, args); - }; - }, - }); - - const report = await reconcileCostInsightRollups(countingDatabase, { - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - maxHours: 2, - chunkHours: 1, - }); - - expect(transactionCount).toBe(2); - expect(report).toMatchObject({ - checkedHourCount: 2, - mismatchCount: 0, - detailsTruncated: false, - }); - }); - - test('records degraded intervals idempotently and resolves them explicitly', async () => { - const params = { - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - reason: 'reconciliation_mismatch' as const, - }; - const firstId = await recordCostInsightDegradedInterval(db, params); - const secondId = await recordCostInsightDegradedInterval(db, params); - const overlappingId = await recordCostInsightDegradedInterval(db, { - ...params, - startHour: '2026-06-01T01:00:00.000Z', - endHourExclusive: '2026-06-01T03:00:00.000Z', - }); - expect(secondId).toBe(firstId); - expect(overlappingId).toBe(firstId); - - const unresolved = await db.select().from(cost_insight_rollup_degraded_intervals); - expect(unresolved).toEqual([ - expect.objectContaining({ - id: firstId, - start_hour: expect.any(String), - end_hour_exclusive: expect.any(String), - }), - ]); - expect(new Date(unresolved[0].start_hour).toISOString()).toBe('2026-06-01T00:00:00.000Z'); - expect(new Date(unresolved[0].end_hour_exclusive).toISOString()).toBe( - '2026-06-01T03:00:00.000Z' - ); - - await resolveCostInsightDegradedInterval(db, firstId); - const [resolved] = await db - .select() - .from(cost_insight_rollup_degraded_intervals) - .where(eq(cost_insight_rollup_degraded_intervals.id, firstId)); - expect(resolved.resolved_at).not.toBeNull(); - }); - - test('serializes concurrent live capture and owner repair without losing spend', async () => { - const userId = await createUser(); - const owner = { type: 'user', id: userId } as const; - const occurredAt = '2026-06-01T00:30:00.000Z'; - - await Promise.all([ - repairOwnerSpendRollups(db, { - owner, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T01:00:00.000Z', - maxHours: 1, - }), - db.transaction(async transaction => { - await transaction.insert(microdollar_usage).values(rawUsage(userId, 5, occurredAt)); - await captureCostInsightSpend(transaction, { - owner, - actorUserId: userId, - occurredAt, - amountMicrodollars: 5, - category: 'variable', - source: 'ai_gateway', - productKey: 'other', - featureKey: 'other', - modelOrPlanKey: 'model', - providerKey: 'provider', - }); - }), - ]); - - const [total] = await db - .select() - .from(cost_insight_owner_hour_totals) - .where( - and( - eq(cost_insight_owner_hour_totals.owned_by_user_id, userId), - eq(cost_insight_owner_hour_totals.hour_start, '2026-06-01T00:00:00.000Z') - ) - ); - expect(total).toMatchObject({ total_microdollars: 5, spend_record_count: 1 }); - }); -}); diff --git a/apps/web/src/lib/cost-insights/rollup-maintenance.test.ts b/apps/web/src/lib/cost-insights/rollup-maintenance.test.ts deleted file mode 100644 index ed0c70142d7..00000000000 --- a/apps/web/src/lib/cost-insights/rollup-maintenance.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { CanonicalCostInsightOwnerTotal } from './canonical-sources'; -import { - assertCanonicalTotalsMatchDrivers, - compareCostInsightHourAggregates, -} from './rollup-maintenance'; - -const hourStart = '2026-06-01T00:00:00.000Z'; -const owner = { type: 'user', id: 'user-1' } as const; -const total: CanonicalCostInsightOwnerTotal = { - owner, - category: 'variable', - totalMicrodollars: 10, - spendRecordCount: 2, -}; - -function rollupValue(params: { amount: number; count: number }) { - return { - owner, - category: 'variable' as const, - totalMicrodollars: params.amount, - spendRecordCount: params.count, - }; -} - -describe('Cost Insights rollup maintenance', () => { - test('accepts canonical totals that equal combined driver sums', () => { - expect(() => - assertCanonicalTotalsMatchDrivers({ - totals: [total], - drivers: [ - { - owner, - category: 'variable', - source: 'other', - productKey: 'exa', - featureKey: 'search', - modelOrPlanKey: 'other', - providerKey: 'exa', - actorUserId: 'user-1', - totalMicrodollars: 4, - spendRecordCount: 1, - driverKey: 'a'.repeat(64), - }, - { - owner, - category: 'variable', - source: 'ai_gateway', - productKey: 'direct-gateway', - featureKey: 'responses', - modelOrPlanKey: 'model', - providerKey: 'provider', - actorUserId: 'user-1', - totalMicrodollars: 6, - spendRecordCount: 1, - driverKey: 'b'.repeat(64), - }, - ], - }) - ).not.toThrow(); - }); - - test('rejects canonical totals that differ from driver sums', () => { - expect(() => - assertCanonicalTotalsMatchDrivers({ - totals: [total], - drivers: [ - { - owner, - category: 'variable', - source: 'other', - productKey: 'exa', - featureKey: 'search', - modelOrPlanKey: 'other', - providerKey: 'exa', - actorUserId: 'user-1', - totalMicrodollars: 9, - spendRecordCount: 2, - driverKey: 'a'.repeat(64), - }, - ], - }) - ).toThrow('totals do not equal combined driver sums'); - }); - - test('reports missing total, driver sum, and unknown taxonomy mismatch classes', () => { - const mismatches = compareCostInsightHourAggregates({ - hourStart, - canonicalTotals: [total], - persistedTotals: new Map(), - persistedDriverSums: new Map(), - unknownTaxonomyValues: [ - { - sourceFamily: 'exa', - field: 'feature_key', - value: '/new-operation', - spendRecordCount: 1, - }, - ], - }); - - expect(mismatches.map(mismatch => mismatch.type)).toEqual([ - 'missing_total', - 'record_count_difference', - 'driver_sum_difference', - 'unknown_taxonomy_value', - ]); - }); - - test('reports amount, record-count, and driver-sum differences independently', () => { - const mismatches = compareCostInsightHourAggregates({ - hourStart, - canonicalTotals: [total], - persistedTotals: new Map([['user:user-1:variable', rollupValue({ amount: 8, count: 1 })]]), - persistedDriverSums: new Map([ - ['user:user-1:variable', rollupValue({ amount: 7, count: 1 })], - ]), - }); - - expect(mismatches).toEqual([ - expect.objectContaining({ - type: 'amount_difference', - expectedMicrodollars: 10, - actualMicrodollars: 8, - }), - expect.objectContaining({ - type: 'record_count_difference', - expectedRecordCount: 2, - actualRecordCount: 1, - }), - expect.objectContaining({ - type: 'driver_sum_difference', - expectedMicrodollars: 10, - actualMicrodollars: 7, - expectedRecordCount: 2, - actualRecordCount: 1, - }), - ]); - }); - - test('reports inflated orphan rollup rows against a zero canonical source sum', () => { - const mismatches = compareCostInsightHourAggregates({ - hourStart, - canonicalTotals: [], - persistedTotals: new Map([['user:user-1:variable', rollupValue({ amount: 10, count: 2 })]]), - persistedDriverSums: new Map([ - ['user:user-1:variable', rollupValue({ amount: 10, count: 2 })], - ]), - }); - - expect(mismatches.map(mismatch => mismatch.type)).toEqual([ - 'amount_difference', - 'record_count_difference', - 'driver_sum_difference', - ]); - }); -}); diff --git a/apps/web/src/lib/cost-insights/rollup-maintenance.ts b/apps/web/src/lib/cost-insights/rollup-maintenance.ts deleted file mode 100644 index 8d1e708005c..00000000000 --- a/apps/web/src/lib/cost-insights/rollup-maintenance.ts +++ /dev/null @@ -1,1162 +0,0 @@ -import { - acquireCostInsightOwnerHourLock, - type CostInsightSpendOwner, -} from '@kilocode/db/cost-insights-rollups'; -import { - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, - cost_insight_rollup_coverage, - cost_insight_rollup_degraded_intervals, -} from '@kilocode/db/schema'; -import type { CostInsightRollupDegradedReason } from '@kilocode/db/schema-types'; -import { and, asc, eq, gte, inArray, isNull, lte, or, sql, type SQL } from 'drizzle-orm'; - -import { type DrizzleTransaction, type db } from '@/lib/drizzle'; - -import { - COST_INSIGHT_ROLLUP_VERSION, - loadCanonicalCostInsightAggregation, - loadCanonicalCostInsightAggregationsByHour, - parseSafeDatabaseInteger, - requireUtcHour, - type CanonicalCostInsightAggregation, - type CanonicalCostInsightDriverAggregate, - type CanonicalCostInsightOwnerTotal, - type CostInsightSpendCategory, - type CostInsightSpendSource, - type CostInsightUnknownTaxonomyValue, -} from './canonical-sources'; -import { getCostInsightRollupCoverage } from './spend-repository'; - -const HOUR_MS = 60 * 60 * 1_000; -const INSERT_BATCH_SIZE = 500; -const BACKFILL_SOURCE_CHUNK_HOURS = 24; -const MAX_RECONCILIATION_MISMATCH_DETAILS = 1_000; -const DEFAULT_RECONCILIATION_CHUNK_HOURS = 24; -const MAX_RECONCILIATION_CHUNK_HOURS = 24; -const MAINTENANCE_STATEMENT_TIMEOUT_MS = 120_000; -const MAINTENANCE_LOCK_TIMEOUT_MS = 5_000; - -export type CostInsightMaintenanceDatabase = Pick; - -export type CostInsightHourReplacementResult = { - hourStart: string; - totalRowCount: number; - driverRowCount: number; - canonicalSpendRecordCount: number; - canonicalMicrodollars: number; - coverageAdvanced: boolean; - durationMs: number; -}; - -export type CostInsightReconciliationMismatch = - | { - type: 'missing_total'; - hourStart: string; - owner: CostInsightSpendOwner; - category: CostInsightSpendCategory; - expectedMicrodollars: number; - expectedRecordCount: number; - } - | { - type: 'amount_difference'; - hourStart: string; - owner: CostInsightSpendOwner; - category: CostInsightSpendCategory; - expectedMicrodollars: number; - actualMicrodollars: number; - } - | { - type: 'record_count_difference'; - hourStart: string; - owner: CostInsightSpendOwner; - category: CostInsightSpendCategory; - expectedRecordCount: number; - actualRecordCount: number; - } - | { - type: 'driver_sum_difference'; - hourStart: string; - owner: CostInsightSpendOwner; - category: CostInsightSpendCategory; - expectedMicrodollars: number; - actualMicrodollars: number; - expectedRecordCount: number; - actualRecordCount: number; - } - | { - type: 'unknown_taxonomy_value'; - hourStart: string; - value: CostInsightUnknownTaxonomyValue; - } - | { - type: 'coverage_hole'; - hourStart: string; - }; - -export type CostInsightReconciliationReport = { - startHour: string; - endHourExclusive: string; - checkedHourCount: number; - mismatchCount: number; - mismatchCounts: Record; - mismatches: CostInsightReconciliationMismatch[]; - detailsTruncated: boolean; -}; - -type RollupAggregateRow = { - hour_start: string | Date; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - spend_category: CostInsightSpendCategory; - total_microdollars: string | number | bigint; - spend_record_count: string | number | bigint; -}; - -type VerificationRow = { - mismatch_count: string | number | bigint; -}; - -function normalizeHourRange(params: { - startHour: string; - endHourExclusive: string; - maxHours: number; -}): { startHour: string; endHourExclusive: string; hourCount: number } { - if (!Number.isSafeInteger(params.maxHours) || params.maxHours <= 0) { - throw new Error('Cost Insights maxHours must be an explicit positive safe integer.'); - } - const startHour = requireUtcHour(params.startHour, 'startHour'); - const endHourExclusive = requireUtcHour(params.endHourExclusive, 'endHourExclusive'); - const hourCount = (Date.parse(endHourExclusive) - Date.parse(startHour)) / HOUR_MS; - if (!Number.isInteger(hourCount) || hourCount <= 0 || hourCount > params.maxHours) { - throw new Error(`Cost Insights maintenance range must contain 1-${params.maxHours} UTC hours.`); - } - return { startHour, endHourExclusive, hourCount }; -} - -async function setCostInsightMaintenanceTimeouts( - transaction: Pick -): Promise { - await transaction.execute( - sql.raw(`SET LOCAL statement_timeout = '${MAINTENANCE_STATEMENT_TIMEOUT_MS}'`) - ); - await transaction.execute(sql.raw(`SET LOCAL lock_timeout = '${MAINTENANCE_LOCK_TIMEOUT_MS}'`)); -} - -function nextHour(hourStart: string): string { - return new Date(Date.parse(hourStart) + HOUR_MS).toISOString(); -} - -function ownerIdentity(owner: CostInsightSpendOwner): string { - return `${owner.type}:${owner.id}`; -} - -function aggregateIdentity( - owner: CostInsightSpendOwner, - category: CostInsightSpendCategory -): string { - return `${ownerIdentity(owner)}:${category}`; -} - -function ownerFromColumns( - ownedByUserId: string | null, - ownedByOrganizationId: string | null -): CostInsightSpendOwner { - if (ownedByOrganizationId && !ownedByUserId) { - return { type: 'organization', id: ownedByOrganizationId }; - } - if (ownedByUserId && !ownedByOrganizationId) { - return { type: 'user', id: ownedByUserId }; - } - throw new Error('Cost Insights rollup row must have exactly one Spend owner.'); -} - -function ownerColumns(owner: CostInsightSpendOwner): { - owned_by_user_id: string | null; - owned_by_organization_id: string | null; -} { - return owner.type === 'organization' - ? { owned_by_user_id: null, owned_by_organization_id: owner.id } - : { owned_by_user_id: owner.id, owned_by_organization_id: null }; -} - -function ownerSqlPredicate( - owner: CostInsightSpendOwner, - ownedByUserColumn: SQL, - ownedByOrganizationColumn: SQL -): SQL { - return owner.type === 'organization' - ? sql`${ownedByUserColumn} IS NULL AND ${ownedByOrganizationColumn} = ${owner.id}` - : sql`${ownedByOrganizationColumn} IS NULL AND ${ownedByUserColumn} = ${owner.id}`; -} - -function ownerTotalsWhere(owner: CostInsightSpendOwner, hourStart: string): SQL { - return sql`${cost_insight_owner_hour_totals.hour_start} = ${hourStart} - AND ${ownerSqlPredicate( - owner, - sql`${cost_insight_owner_hour_totals.owned_by_user_id}`, - sql`${cost_insight_owner_hour_totals.owned_by_organization_id}` - )}`; -} - -function ownerDriversWhere(owner: CostInsightSpendOwner, hourStart: string): SQL { - return sql`${cost_insight_owner_hour_driver_buckets.hour_start} = ${hourStart} - AND ${ownerSqlPredicate( - owner, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` - )}`; -} - -function sumSafe(values: number[], fieldName: string): number { - return values.reduce((total, value) => { - const result = total + value; - if (!Number.isSafeInteger(result)) { - throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); - } - return result; - }, 0); -} - -function chunkRows(rows: T[]): T[][] { - const chunks: T[][] = []; - for (let index = 0; index < rows.length; index += INSERT_BATCH_SIZE) { - chunks.push(rows.slice(index, index + INSERT_BATCH_SIZE)); - } - return chunks; -} - -export function assertCanonicalTotalsMatchDrivers( - aggregation: Pick -): void { - const driverSums = new Map(); - for (const driver of aggregation.drivers) { - const key = aggregateIdentity(driver.owner, driver.category); - const prior = driverSums.get(key) ?? { amount: 0, count: 0 }; - driverSums.set(key, { - amount: sumSafe( - [prior.amount, driver.totalMicrodollars], - 'canonical driver sum microdollars' - ), - count: sumSafe([prior.count, driver.spendRecordCount], 'canonical driver sum record count'), - }); - } - for (const total of aggregation.totals) { - const driverSum = driverSums.get(aggregateIdentity(total.owner, total.category)); - if ( - !driverSum || - driverSum.amount !== total.totalMicrodollars || - driverSum.count !== total.spendRecordCount - ) { - throw new Error('Canonical Cost Insights totals do not equal combined driver sums.'); - } - driverSums.delete(aggregateIdentity(total.owner, total.category)); - } - if (driverSums.size > 0) { - throw new Error('Canonical Cost Insights drivers exist without a matching owner total.'); - } -} - -async function insertTotals( - transaction: DrizzleTransaction, - hourStart: string, - totals: CanonicalCostInsightOwnerTotal[] -): Promise { - for (const chunk of chunkRows(totals)) { - await transaction.insert(cost_insight_owner_hour_totals).values( - chunk.map(total => ({ - ...ownerColumns(total.owner), - hour_start: hourStart, - spend_category: total.category, - total_microdollars: total.totalMicrodollars, - spend_record_count: total.spendRecordCount, - })) - ); - } -} - -async function insertDrivers( - transaction: DrizzleTransaction, - hourStart: string, - drivers: CanonicalCostInsightDriverAggregate[] -): Promise { - for (const chunk of chunkRows(drivers)) { - await transaction.insert(cost_insight_owner_hour_driver_buckets).values( - chunk.map(driver => ({ - ...ownerColumns(driver.owner), - hour_start: hourStart, - spend_category: driver.category, - driver_key: driver.driverKey, - source: driver.source, - product_key: driver.productKey, - feature_key: driver.featureKey, - model_or_plan_key: driver.modelOrPlanKey, - provider_key: driver.providerKey, - actor_user_id: driver.actorUserId, - total_microdollars: driver.totalMicrodollars, - spend_record_count: driver.spendRecordCount, - })) - ); - } -} - -async function verifyPersistedTotalsMatchDrivers( - transaction: DrizzleTransaction, - params: { hourStart: string; owner?: CostInsightSpendOwner } -): Promise { - const totalOwnerPredicate = params.owner - ? ownerSqlPredicate(params.owner, sql`t.owned_by_user_id`, sql`t.owned_by_organization_id`) - : sql`TRUE`; - const driverOwnerPredicate = params.owner - ? ownerSqlPredicate(params.owner, sql`d.owned_by_user_id`, sql`d.owned_by_organization_id`) - : sql`TRUE`; - const result = await transaction.execute(sql` - WITH totals AS ( - SELECT - owned_by_user_id, - owned_by_organization_id, - spend_category, - total_microdollars, - spend_record_count - FROM ${cost_insight_owner_hour_totals} t - WHERE t.hour_start = ${params.hourStart} - AND ${totalOwnerPredicate} - ), driver_sums AS ( - SELECT - owned_by_user_id, - owned_by_organization_id, - spend_category, - SUM(total_microdollars) AS total_microdollars, - SUM(spend_record_count) AS spend_record_count - FROM ${cost_insight_owner_hour_driver_buckets} d - WHERE d.hour_start = ${params.hourStart} - AND ${driverOwnerPredicate} - GROUP BY 1, 2, 3 - ) - SELECT COUNT(*)::text AS mismatch_count - FROM totals - FULL OUTER JOIN driver_sums - ON totals.owned_by_user_id IS NOT DISTINCT FROM driver_sums.owned_by_user_id - AND totals.owned_by_organization_id IS NOT DISTINCT FROM driver_sums.owned_by_organization_id - AND totals.spend_category = driver_sums.spend_category - WHERE totals.total_microdollars IS DISTINCT FROM driver_sums.total_microdollars - OR totals.spend_record_count IS DISTINCT FROM driver_sums.spend_record_count - `); - const mismatchCount = parseSafeDatabaseInteger( - result.rows[0]?.mismatch_count ?? 0, - 'persisted total/driver mismatch count' - ); - if (mismatchCount !== 0) { - throw new Error('Persisted Cost Insights totals do not equal combined driver sums.'); - } -} - -async function replaceAllRollupsForHour( - transaction: DrizzleTransaction, - hourStart: string, - aggregation: CanonicalCostInsightAggregation -): Promise { - assertCanonicalTotalsMatchDrivers(aggregation); - await transaction - .delete(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.hour_start, hourStart)); - await transaction - .delete(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.hour_start, hourStart)); - await insertTotals(transaction, hourStart, aggregation.totals); - await insertDrivers(transaction, hourStart, aggregation.drivers); - await verifyPersistedTotalsMatchDrivers(transaction, { hourStart }); -} - -async function replaceOwnerRollupsForHour( - transaction: DrizzleTransaction, - owner: CostInsightSpendOwner, - hourStart: string, - aggregation: CanonicalCostInsightAggregation -): Promise { - assertCanonicalTotalsMatchDrivers(aggregation); - await transaction - .delete(cost_insight_owner_hour_driver_buckets) - .where(ownerDriversWhere(owner, hourStart)); - await transaction - .delete(cost_insight_owner_hour_totals) - .where(ownerTotalsWhere(owner, hourStart)); - await insertTotals(transaction, hourStart, aggregation.totals); - await insertDrivers(transaction, hourStart, aggregation.drivers); - await verifyPersistedTotalsMatchDrivers(transaction, { hourStart, owner }); -} - -async function advanceCoverageIfContiguous( - transaction: DrizzleTransaction, - hourStart: string, - endHourExclusive: string -): Promise { - const updated = await transaction - .update(cost_insight_rollup_coverage) - .set({ coverage_start_hour: hourStart, updated_at: sql`now()` }) - .where( - and( - eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION), - or( - eq(cost_insight_rollup_coverage.coverage_start_hour, endHourExclusive), - and( - isNull(cost_insight_rollup_coverage.coverage_start_hour), - eq(cost_insight_rollup_coverage.live_capture_start_hour, endHourExclusive) - ) - ) - ) - ); - return (updated.rowCount ?? 0) > 0; -} - -function summarizeReplacement( - hourStart: string, - aggregation: CanonicalCostInsightAggregation, - coverageAdvanced: boolean, - startedAt: number -): CostInsightHourReplacementResult { - return { - hourStart, - totalRowCount: aggregation.totals.length, - driverRowCount: aggregation.drivers.length, - canonicalSpendRecordCount: sumSafe( - aggregation.totals.map(total => total.spendRecordCount), - 'canonical hour record count' - ), - canonicalMicrodollars: sumSafe( - aggregation.totals.map(total => total.totalMicrodollars), - 'canonical hour microdollars' - ), - coverageAdvanced, - durationMs: performance.now() - startedAt, - }; -} - -export async function initializeCostInsightRollupCoverage( - database: CostInsightMaintenanceDatabase, - liveCaptureStartHourInput: string -): Promise { - const liveCaptureStartHour = requireUtcHour(liveCaptureStartHourInput, 'liveCaptureStartHour'); - - await database.transaction( - async transaction => { - const [inserted] = await transaction - .insert(cost_insight_rollup_coverage) - .values({ - rollup_version: COST_INSIGHT_ROLLUP_VERSION, - live_capture_start_hour: liveCaptureStartHour, - coverage_start_hour: liveCaptureStartHour, - }) - .onConflictDoNothing() - .returning({ rollupVersion: cost_insight_rollup_coverage.rollup_version }); - if (inserted) return; - - const [existing] = await transaction - .select({ - liveCaptureStartHour: cost_insight_rollup_coverage.live_capture_start_hour, - coverageStartHour: cost_insight_rollup_coverage.coverage_start_hour, - }) - .from(cost_insight_rollup_coverage) - .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)) - .limit(1) - .for('update'); - if (!existing) { - throw new Error('Cost Insights coverage initialization lost its version row.'); - } - if ( - existing.liveCaptureStartHour !== null && - new Date(existing.liveCaptureStartHour).toISOString() === liveCaptureStartHour - ) { - return; - } - if (existing.liveCaptureStartHour === null && existing.coverageStartHour === null) { - await transaction - .update(cost_insight_rollup_coverage) - .set({ - live_capture_start_hour: liveCaptureStartHour, - coverage_start_hour: liveCaptureStartHour, - updated_at: sql`now()`, - }) - .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)); - return; - } - throw new Error( - `Cost Insights live capture start is already initialized as ${String(existing.liveCaptureStartHour)}.` - ); - }, - { isolationLevel: 'read committed' } - ); -} - -export async function recordCostInsightDegradedInterval( - database: CostInsightMaintenanceDatabase, - params: { - startHour: string; - endHourExclusive: string; - source?: CostInsightSpendSource; - reason: CostInsightRollupDegradedReason; - } -): Promise { - const range = normalizeHourRange({ ...params, maxHours: Number.MAX_SAFE_INTEGER }); - return database.transaction( - async transaction => { - await setCostInsightMaintenanceTimeouts(transaction); - await transaction.execute( - sql`SELECT pg_catalog.pg_advisory_xact_lock( - pg_catalog.hashtextextended('cost-insight-degraded-intervals:v1', 0::bigint) - )` - ); - const overlapping = await transaction - .select({ - id: cost_insight_rollup_degraded_intervals.id, - startHour: cost_insight_rollup_degraded_intervals.start_hour, - endHourExclusive: cost_insight_rollup_degraded_intervals.end_hour_exclusive, - }) - .from(cost_insight_rollup_degraded_intervals) - .where( - and( - isNull(cost_insight_rollup_degraded_intervals.resolved_at), - eq(cost_insight_rollup_degraded_intervals.reason, params.reason), - lte(cost_insight_rollup_degraded_intervals.start_hour, range.endHourExclusive), - gte(cost_insight_rollup_degraded_intervals.end_hour_exclusive, range.startHour), - sql`${cost_insight_rollup_degraded_intervals.source} IS NOT DISTINCT FROM ${params.source ?? null}` - ) - ) - .orderBy( - asc(cost_insight_rollup_degraded_intervals.start_hour), - asc(cost_insight_rollup_degraded_intervals.id) - ) - .for('update'); - - const [keeper, ...duplicates] = overlapping; - if (keeper) { - const mergedStartHour = [range.startHour, ...overlapping.map(row => row.startHour)] - .map(value => new Date(value).toISOString()) - .sort()[0]; - const mergedEndHourExclusive = [ - range.endHourExclusive, - ...overlapping.map(row => row.endHourExclusive), - ] - .map(value => new Date(value).toISOString()) - .sort() - .at(-1); - if (!mergedStartHour || !mergedEndHourExclusive) { - throw new Error('Cost Insights degraded interval merge produced an empty range.'); - } - await transaction - .update(cost_insight_rollup_degraded_intervals) - .set({ - start_hour: mergedStartHour, - end_hour_exclusive: mergedEndHourExclusive, - updated_at: sql`now()`, - }) - .where(eq(cost_insight_rollup_degraded_intervals.id, keeper.id)); - if (duplicates.length > 0) { - await transaction.delete(cost_insight_rollup_degraded_intervals).where( - inArray( - cost_insight_rollup_degraded_intervals.id, - duplicates.map(interval => interval.id) - ) - ); - } - return keeper.id; - } - - const [inserted] = await transaction - .insert(cost_insight_rollup_degraded_intervals) - .values({ - start_hour: range.startHour, - end_hour_exclusive: range.endHourExclusive, - source: params.source, - reason: params.reason, - }) - .returning({ id: cost_insight_rollup_degraded_intervals.id }); - if (!inserted) throw new Error('Cost Insights degraded interval insert returned no row.'); - return inserted.id; - }, - { isolationLevel: 'read committed' } - ); -} - -export async function resolveCostInsightDegradedInterval( - database: CostInsightMaintenanceDatabase, - intervalId: string -): Promise { - if (!intervalId) throw new Error('Cost Insights degraded interval ID is required.'); - await database.transaction( - async transaction => { - const updated = await transaction - .update(cost_insight_rollup_degraded_intervals) - .set({ resolved_at: sql`now()`, updated_at: sql`now()` }) - .where( - and( - eq(cost_insight_rollup_degraded_intervals.id, intervalId), - isNull(cost_insight_rollup_degraded_intervals.resolved_at) - ) - ); - if ((updated.rowCount ?? 0) !== 1) { - throw new Error('Cost Insights degraded interval is missing or already resolved.'); - } - }, - { isolationLevel: 'read committed' } - ); -} - -async function assertBulkBackfillPrecedesLiveCapture( - transaction: DrizzleTransaction, - endHourExclusive: string -): Promise { - const [coverage] = await transaction - .select({ liveCaptureStartHour: cost_insight_rollup_coverage.live_capture_start_hour }) - .from(cost_insight_rollup_coverage) - .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)) - .limit(1); - if (!coverage?.liveCaptureStartHour) { - throw new Error('Cost Insights live capture start must be initialized before bulk backfill.'); - } - if (Date.parse(endHourExclusive) > Date.parse(coverage.liveCaptureStartHour)) { - throw new Error( - 'Cost Insights bulk backfill is restricted to pre-cutover hours; use owner repair for live-capture intervals.' - ); - } -} - -export async function backfillCostInsightHour( - database: CostInsightMaintenanceDatabase, - hourStartInput: string -): Promise { - const hourStart = requireUtcHour(hourStartInput, 'hourStart'); - const endHourExclusive = nextHour(hourStart); - const startedAt = performance.now(); - return database.transaction( - async transaction => { - await setCostInsightMaintenanceTimeouts(transaction); - await assertBulkBackfillPrecedesLiveCapture(transaction, endHourExclusive); - const aggregation = await loadCanonicalCostInsightAggregation(transaction, { - startInclusive: hourStart, - endExclusive: endHourExclusive, - }); - await replaceAllRollupsForHour(transaction, hourStart, aggregation); - const coverageAdvanced = await advanceCoverageIfContiguous( - transaction, - hourStart, - endHourExclusive - ); - return summarizeReplacement(hourStart, aggregation, coverageAdvanced, startedAt); - }, - { isolationLevel: 'repeatable read' } - ); -} - -async function persistBackfilledCostInsightHour( - database: CostInsightMaintenanceDatabase, - hourStart: string, - aggregation: CanonicalCostInsightAggregation, - startedAt: number -): Promise { - const endHourExclusive = nextHour(hourStart); - return database.transaction( - async transaction => { - await setCostInsightMaintenanceTimeouts(transaction); - await assertBulkBackfillPrecedesLiveCapture(transaction, endHourExclusive); - await replaceAllRollupsForHour(transaction, hourStart, aggregation); - const coverageAdvanced = await advanceCoverageIfContiguous( - transaction, - hourStart, - endHourExclusive - ); - return summarizeReplacement(hourStart, aggregation, coverageAdvanced, startedAt); - }, - { isolationLevel: 'read committed' } - ); -} - -export async function backfillCostInsightRollupsNewestFirst( - database: CostInsightMaintenanceDatabase, - params: { - startHour: string; - endHourExclusive: string; - maxHours: number; - sleepMs?: number; - onHourComplete?: (result: CostInsightHourReplacementResult) => void | Promise; - } -): Promise { - const range = normalizeHourRange(params); - const sleepMs = params.sleepMs ?? 0; - if (!Number.isSafeInteger(sleepMs) || sleepMs < 0) { - throw new Error('Cost Insights sleepMs must be a non-negative safe integer.'); - } - const results: CostInsightHourReplacementResult[] = []; - let chunkEndTimestamp = Date.parse(range.endHourExclusive); - while (chunkEndTimestamp > Date.parse(range.startHour)) { - const chunkStartTimestamp = Math.max( - Date.parse(range.startHour), - chunkEndTimestamp - BACKFILL_SOURCE_CHUNK_HOURS * HOUR_MS - ); - const chunkStart = new Date(chunkStartTimestamp).toISOString(); - const chunkEnd = new Date(chunkEndTimestamp).toISOString(); - const hourly = await database.transaction( - async transaction => { - await setCostInsightMaintenanceTimeouts(transaction); - await assertBulkBackfillPrecedesLiveCapture(transaction, chunkEnd); - return loadCanonicalCostInsightAggregationsByHour(transaction, { - startInclusive: chunkStart, - endExclusive: chunkEnd, - }); - }, - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ); - const canonicalByHour = new Map( - hourly.map(aggregation => [aggregation.hourStart, aggregation]) - ); - - for ( - let hourTimestamp = chunkEndTimestamp - HOUR_MS; - hourTimestamp >= chunkStartTimestamp; - hourTimestamp -= HOUR_MS - ) { - const hourStart = new Date(hourTimestamp).toISOString(); - const startedAt = performance.now(); - const aggregation = canonicalByHour.get(hourStart) ?? { - totals: [], - drivers: [], - unknownTaxonomyValues: [], - }; - const result = await persistBackfilledCostInsightHour( - database, - hourStart, - aggregation, - startedAt - ); - results.push(result); - await params.onHourComplete?.(result); - if (sleepMs > 0 && hourTimestamp > Date.parse(range.startHour)) { - await new Promise(resolve => setTimeout(resolve, sleepMs)); - } - } - chunkEndTimestamp = chunkStartTimestamp; - } - return results; -} - -export async function repairOwnerSpendRollups( - database: CostInsightMaintenanceDatabase, - params: { - owner: CostInsightSpendOwner; - startHour: string; - endHourExclusive: string; - maxHours: number; - } -): Promise { - const range = normalizeHourRange(params); - const { owner } = params; - const ownerRepairPredicate = - owner.type === 'user' - ? sql`owned_by_user_id = ${owner.id} AND owned_by_organization_id IS NULL` - : sql`owned_by_organization_id = ${owner.id} AND owned_by_user_id IS NULL`; - const dirtyConflictTarget = - owner.type === 'user' - ? sql.raw('(owned_by_user_id) WHERE owned_by_organization_id IS NULL') - : sql.raw('(owned_by_organization_id) WHERE owned_by_user_id IS NULL'); - const ownerColumns = - owner.type === 'user' - ? { ownedByUserId: owner.id, ownedByOrganizationId: null } - : { ownedByUserId: null, ownedByOrganizationId: owner.id }; - const results: CostInsightHourReplacementResult[] = []; - for ( - let hourTimestamp = Date.parse(range.startHour); - hourTimestamp < Date.parse(range.endHourExclusive); - hourTimestamp += HOUR_MS - ) { - const hourStart = new Date(hourTimestamp).toISOString(); - const endHourExclusive = nextHour(hourStart); - const startedAt = performance.now(); - const result = await database.transaction( - async transaction => { - await setCostInsightMaintenanceTimeouts(transaction); - await acquireCostInsightOwnerHourLock(transaction, owner, hourStart); - const aggregation = await loadCanonicalCostInsightAggregation(transaction, { - owner, - startInclusive: hourStart, - endExclusive: endHourExclusive, - }); - await replaceOwnerRollupsForHour(transaction, owner, hourStart, aggregation); - await transaction.execute(sql` - DELETE FROM cost_insight_rollup_repairs - WHERE ${ownerRepairPredicate} - AND hour_start = ${hourStart} - `); - await transaction.execute(sql` - INSERT INTO cost_insight_evaluation_dirty_owners AS dirty_owner ( - owned_by_user_id, - owned_by_organization_id, - dirty_at, - next_attempt_at - ) VALUES ( - ${ownerColumns.ownedByUserId}, - ${ownerColumns.ownedByOrganizationId}, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ) - ON CONFLICT ${dirtyConflictTarget} - DO UPDATE SET - generation = dirty_owner.generation + 1, - dirty_at = CURRENT_TIMESTAMP, - next_attempt_at = CURRENT_TIMESTAMP, - claimed_at = NULL, - claim_token = NULL, - updated_at = CURRENT_TIMESTAMP - `); - return summarizeReplacement(hourStart, aggregation, false, startedAt); - }, - { isolationLevel: 'read committed' } - ); - results.push(result); - } - return results; -} - -function aggregateRowMap(rows: RollupAggregateRow[]): Map< - string, - { - owner: CostInsightSpendOwner; - category: CostInsightSpendCategory; - totalMicrodollars: number; - spendRecordCount: number; - } -> { - return new Map( - rows.map(row => { - const owner = ownerFromColumns(row.owned_by_user_id, row.owned_by_organization_id); - const value = { - owner, - category: row.spend_category, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'reconciliation total_microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'reconciliation spend_record_count' - ), - }; - return [aggregateIdentity(owner, row.spend_category), value]; - }) - ); -} - -type PersistedHourAggregates = { - totals: ReturnType; - driverSums: ReturnType; -}; - -function groupRollupRowsByHour(rows: RollupAggregateRow[]): Map { - const grouped = new Map(); - for (const row of rows) { - const hourStart = new Date(row.hour_start).toISOString(); - const existing = grouped.get(hourStart); - if (existing) { - existing.push(row); - } else { - grouped.set(hourStart, [row]); - } - } - return grouped; -} - -async function loadPersistedHourlyAggregates( - transaction: DrizzleTransaction, - range: { startHour: string; endHourExclusive: string } -): Promise> { - const totalsResult = await transaction.execute(sql` - SELECT - hour_start, - owned_by_user_id, - owned_by_organization_id, - spend_category, - total_microdollars::text AS total_microdollars, - spend_record_count::text AS spend_record_count - FROM ${cost_insight_owner_hour_totals} - WHERE hour_start >= ${range.startHour} - AND hour_start < ${range.endHourExclusive} - `); - const driverResult = await transaction.execute(sql` - SELECT - hour_start, - owned_by_user_id, - owned_by_organization_id, - spend_category, - SUM(total_microdollars)::text AS total_microdollars, - SUM(spend_record_count)::text AS spend_record_count - FROM ${cost_insight_owner_hour_driver_buckets} - WHERE hour_start >= ${range.startHour} - AND hour_start < ${range.endHourExclusive} - GROUP BY 1, 2, 3, 4 - `); - const totalsByHour = groupRollupRowsByHour(totalsResult.rows); - const driversByHour = groupRollupRowsByHour(driverResult.rows); - const hourly = new Map(); - for (const hourStart of new Set([...totalsByHour.keys(), ...driversByHour.keys()])) { - hourly.set(hourStart, { - totals: aggregateRowMap(totalsByHour.get(hourStart) ?? []), - driverSums: aggregateRowMap(driversByHour.get(hourStart) ?? []), - }); - } - return hourly; -} - -export function compareCostInsightHourAggregates(params: { - hourStart: string; - canonicalTotals: CanonicalCostInsightOwnerTotal[]; - persistedTotals: ReturnType; - persistedDriverSums: ReturnType; - unknownTaxonomyValues?: CostInsightUnknownTaxonomyValue[]; -}): CostInsightReconciliationMismatch[] { - const mismatches: CostInsightReconciliationMismatch[] = []; - const canonical = new Map( - params.canonicalTotals.map(total => [aggregateIdentity(total.owner, total.category), total]) - ); - const keys = new Set([ - ...canonical.keys(), - ...params.persistedTotals.keys(), - ...params.persistedDriverSums.keys(), - ]); - - for (const key of [...keys].sort()) { - const expected = canonical.get(key); - const actual = params.persistedTotals.get(key); - const driverSum = params.persistedDriverSums.get(key); - const owner = expected?.owner ?? actual?.owner ?? driverSum?.owner; - const category = expected?.category ?? actual?.category ?? driverSum?.category; - if (!owner || !category) { - throw new Error('Reconciliation aggregate identity has no owner/category payload.'); - } - if (expected && !actual) { - mismatches.push({ - type: 'missing_total', - hourStart: params.hourStart, - owner, - category, - expectedMicrodollars: expected.totalMicrodollars, - expectedRecordCount: expected.spendRecordCount, - }); - } else if ((expected?.totalMicrodollars ?? 0) !== (actual?.totalMicrodollars ?? 0)) { - mismatches.push({ - type: 'amount_difference', - hourStart: params.hourStart, - owner, - category, - expectedMicrodollars: expected?.totalMicrodollars ?? 0, - actualMicrodollars: actual?.totalMicrodollars ?? 0, - }); - } - if ((expected?.spendRecordCount ?? 0) !== (actual?.spendRecordCount ?? 0)) { - mismatches.push({ - type: 'record_count_difference', - hourStart: params.hourStart, - owner, - category, - expectedRecordCount: expected?.spendRecordCount ?? 0, - actualRecordCount: actual?.spendRecordCount ?? 0, - }); - } - if ( - (expected?.totalMicrodollars ?? 0) !== (driverSum?.totalMicrodollars ?? 0) || - (expected?.spendRecordCount ?? 0) !== (driverSum?.spendRecordCount ?? 0) - ) { - mismatches.push({ - type: 'driver_sum_difference', - hourStart: params.hourStart, - owner, - category, - expectedMicrodollars: expected?.totalMicrodollars ?? 0, - actualMicrodollars: driverSum?.totalMicrodollars ?? 0, - expectedRecordCount: expected?.spendRecordCount ?? 0, - actualRecordCount: driverSum?.spendRecordCount ?? 0, - }); - } - } - for (const value of params.unknownTaxonomyValues ?? []) { - mismatches.push({ type: 'unknown_taxonomy_value', hourStart: params.hourStart, value }); - } - return mismatches; -} - -function hourIsCovered( - hourStart: string, - coverage: Awaited> -): boolean { - const effectiveStart = coverage.coverageStartHour ?? coverage.liveCaptureStartHour; - if (!effectiveStart || Date.parse(hourStart) < Date.parse(effectiveStart)) { - return false; - } - const hourEnd = Date.parse(hourStart) + HOUR_MS; - return !coverage.degradedIntervals.some( - interval => - Date.parse(interval.startHour) < hourEnd && - Date.parse(interval.endHourExclusive) > Date.parse(hourStart) - ); -} - -export async function recordCostInsightReconciliationSuccess( - database: CostInsightMaintenanceDatabase -): Promise { - await database.transaction( - async transaction => { - const updated = await transaction - .update(cost_insight_rollup_coverage) - .set({ last_reconciled_at: sql`now()`, updated_at: sql`now()` }) - .where(eq(cost_insight_rollup_coverage.rollup_version, COST_INSIGHT_ROLLUP_VERSION)); - if ((updated.rowCount ?? 0) !== 1) { - throw new Error('Cost Insights coverage row is not initialized for reconciliation.'); - } - }, - { isolationLevel: 'read committed' } - ); -} - -function emptyMismatchCounts(): CostInsightReconciliationReport['mismatchCounts'] { - return { - missing_total: 0, - amount_difference: 0, - record_count_difference: 0, - driver_sum_difference: 0, - unknown_taxonomy_value: 0, - coverage_hole: 0, - }; -} - -async function reconcileCostInsightRollupChunk( - transaction: DrizzleTransaction, - params: { - startHour: string; - endHourExclusive: string; - maxMismatchDetails: number; - } -): Promise { - await setCostInsightMaintenanceTimeouts(transaction); - const coverage = await getCostInsightRollupCoverage(transaction, params); - const canonicalByHour = new Map( - ( - await loadCanonicalCostInsightAggregationsByHour(transaction, { - startInclusive: params.startHour, - endExclusive: params.endHourExclusive, - }) - ).map(aggregation => [aggregation.hourStart, aggregation]) - ); - const persistedByHour = await loadPersistedHourlyAggregates(transaction, params); - const mismatchCounts = emptyMismatchCounts(); - const mismatches: CostInsightReconciliationMismatch[] = []; - let mismatchCount = 0; - const appendMismatch = (mismatch: CostInsightReconciliationMismatch) => { - mismatchCount++; - mismatchCounts[mismatch.type]++; - if (mismatches.length < params.maxMismatchDetails) { - mismatches.push(mismatch); - } - }; - - for ( - let hourTimestamp = Date.parse(params.startHour); - hourTimestamp < Date.parse(params.endHourExclusive); - hourTimestamp += HOUR_MS - ) { - const hourStart = new Date(hourTimestamp).toISOString(); - const canonical = canonicalByHour.get(hourStart); - const persisted = persistedByHour.get(hourStart) ?? { - totals: aggregateRowMap([]), - driverSums: aggregateRowMap([]), - }; - for (const mismatch of compareCostInsightHourAggregates({ - hourStart, - canonicalTotals: canonical?.totals ?? [], - persistedTotals: persisted.totals, - persistedDriverSums: persisted.driverSums, - unknownTaxonomyValues: canonical?.unknownTaxonomyValues, - })) { - appendMismatch(mismatch); - } - if (!hourIsCovered(hourStart, coverage)) { - appendMismatch({ type: 'coverage_hole', hourStart }); - } - } - - return { - startHour: params.startHour, - endHourExclusive: params.endHourExclusive, - checkedHourCount: - (Date.parse(params.endHourExclusive) - Date.parse(params.startHour)) / HOUR_MS, - mismatchCount, - mismatchCounts, - mismatches, - detailsTruncated: mismatchCount > mismatches.length, - }; -} - -export async function reconcileCostInsightRollups( - database: CostInsightMaintenanceDatabase, - params: { - startHour: string; - endHourExclusive: string; - maxHours: number; - maxMismatchDetails?: number; - chunkHours?: number; - } -): Promise { - const range = normalizeHourRange(params); - const maxMismatchDetails = params.maxMismatchDetails ?? MAX_RECONCILIATION_MISMATCH_DETAILS; - if (!Number.isSafeInteger(maxMismatchDetails) || maxMismatchDetails < 0) { - throw new Error('maxMismatchDetails must be a non-negative safe integer.'); - } - const chunkHours = params.chunkHours ?? DEFAULT_RECONCILIATION_CHUNK_HOURS; - if ( - !Number.isSafeInteger(chunkHours) || - chunkHours <= 0 || - chunkHours > MAX_RECONCILIATION_CHUNK_HOURS - ) { - throw new Error( - `chunkHours must be a positive safe integer no greater than ${MAX_RECONCILIATION_CHUNK_HOURS}.` - ); - } - - const mismatchCounts = emptyMismatchCounts(); - const mismatches: CostInsightReconciliationMismatch[] = []; - let mismatchCount = 0; - for ( - let chunkStartTimestamp = Date.parse(range.startHour); - chunkStartTimestamp < Date.parse(range.endHourExclusive); - chunkStartTimestamp += chunkHours * HOUR_MS - ) { - const chunkStart = new Date(chunkStartTimestamp).toISOString(); - const chunkEnd = new Date( - Math.min(chunkStartTimestamp + chunkHours * HOUR_MS, Date.parse(range.endHourExclusive)) - ).toISOString(); - const chunk = await database.transaction( - transaction => - reconcileCostInsightRollupChunk(transaction, { - startHour: chunkStart, - endHourExclusive: chunkEnd, - maxMismatchDetails: Math.max(0, maxMismatchDetails - mismatches.length), - }), - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ); - mismatchCount = sumSafe([mismatchCount, chunk.mismatchCount], 'reconciliation mismatch count'); - for (const type of Object.keys(mismatchCounts) as Array) { - mismatchCounts[type] = sumSafe( - [mismatchCounts[type], chunk.mismatchCounts[type]], - `reconciliation ${type} mismatch count` - ); - } - mismatches.push(...chunk.mismatches); - } - - return { - startHour: range.startHour, - endHourExclusive: range.endHourExclusive, - checkedHourCount: range.hourCount, - mismatchCount, - mismatchCounts, - mismatches, - detailsTruncated: mismatchCount > mismatches.length, - }; -} diff --git a/apps/web/src/lib/cost-insights/rollup-repairs.ts b/apps/web/src/lib/cost-insights/rollup-repairs.ts deleted file mode 100644 index 91e2094e528..00000000000 --- a/apps/web/src/lib/cost-insights/rollup-repairs.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { - getCostInsightUtcHourStart, - type CostInsightSpendOwner, -} from '@kilocode/db/cost-insights-rollups'; -import { sql } from 'drizzle-orm'; -import pLimit from 'p-limit'; - -import type { CostInsightDatabase, CostInsightRootDatabase } from './repository'; -import { repairOwnerSpendRollups } from './rollup-maintenance'; - -const HOUR_MS = 60 * 60 * 1_000; -const REPAIR_CLAIM_LEASE_MINUTES = 5; - -export type ClaimedRollupRepairRow = { - id: string; - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - hour_start: string | Date; - generation: string | number | bigint; - claim_token: string; -}; - -export type CostInsightRollupRepairFailure = { - owner: CostInsightSpendOwner; - hourStart: string; - error: string; -}; - -export type CostInsightRollupRepairSummary = { - claimed: number; - repaired: number; - failed: CostInsightRollupRepairFailure[]; -}; - -function ownerKey(owner: CostInsightSpendOwner): string { - return `${owner.type}:${owner.id}`; -} - -function ownerFromRow(row: ClaimedRollupRepairRow): CostInsightSpendOwner { - if (row.owned_by_user_id && !row.owned_by_organization_id) { - return { type: 'user', id: row.owned_by_user_id }; - } - if (row.owned_by_organization_id && !row.owned_by_user_id) { - return { type: 'organization', id: row.owned_by_organization_id }; - } - throw new Error('Cost Insights rollup repair row has no valid Spend owner.'); -} - -function normalizeTimestamp(value: string | Date): string { - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); -} - -export async function enqueueCostInsightRollupRepair( - database: CostInsightDatabase, - params: { usageId: string; owner: CostInsightSpendOwner; occurredAt: string } -): Promise { - const hourStart = getCostInsightUtcHourStart(params.occurredAt); - const ownerColumns = - params.owner.type === 'user' - ? { ownedByUserId: params.owner.id, ownedByOrganizationId: null } - : { ownedByUserId: null, ownedByOrganizationId: params.owner.id }; - const ownerExists = - params.owner.type === 'user' - ? sql`EXISTS (SELECT 1 FROM kilocode_users WHERE id = ${params.owner.id})` - : sql`EXISTS (SELECT 1 FROM organizations WHERE id = ${params.owner.id})`; - - const result = await database.execute<{ id: string }>(sql` - INSERT INTO cost_insight_rollup_repairs AS repair ( - owned_by_user_id, - owned_by_organization_id, - usage_id, - hour_start, - next_attempt_at - ) SELECT - ${ownerColumns.ownedByUserId}, - ${ownerColumns.ownedByOrganizationId}, - ${params.usageId}, - ${hourStart}, - GREATEST( - CURRENT_TIMESTAMP + INTERVAL '1 minute', - ${hourStart}::timestamptz + INTERVAL '1 hour 1 minute' - ) - WHERE ${ownerExists} - ON CONFLICT (usage_id) - DO UPDATE SET - generation = repair.generation + 1, - next_attempt_at = GREATEST(repair.next_attempt_at, excluded.next_attempt_at), - claimed_at = NULL, - claim_token = NULL, - attempt_count = 0, - last_error_redacted = NULL, - updated_at = CURRENT_TIMESTAMP - RETURNING id - `); - if (result.rows.length > 1) - throw new Error('Cost Insights rollup repair enqueue was not unique.'); -} - -export async function acknowledgeCostInsightRollupCapture( - database: CostInsightDatabase, - usageId: string -): Promise { - const result = await database.execute<{ usage_id: string }>(sql` - DELETE FROM cost_insight_rollup_repairs - WHERE usage_id = ${usageId} - RETURNING usage_id - `); - return result.rows.length === 1; -} - -export async function claimPendingCostInsightRollupRepairs( - database: CostInsightRootDatabase, - limit: number -): Promise { - const claimToken = crypto.randomUUID(); - const result = await database.execute(sql` - WITH claimed AS ( - SELECT repair.id - FROM cost_insight_rollup_repairs repair - WHERE repair.next_attempt_at <= CURRENT_TIMESTAMP - AND ( - repair.claimed_at IS NULL - OR repair.claimed_at <= CURRENT_TIMESTAMP - make_interval( - mins => ${REPAIR_CLAIM_LEASE_MINUTES} - ) - ) - ORDER BY repair.attempt_count ASC, repair.next_attempt_at ASC, repair.hour_start ASC, repair.id ASC - LIMIT ${limit} - FOR UPDATE SKIP LOCKED - ) - UPDATE cost_insight_rollup_repairs repair - SET - claimed_at = CURRENT_TIMESTAMP, - claim_token = ${claimToken}, - attempt_count = repair.attempt_count + 1, - last_error_redacted = NULL, - updated_at = CURRENT_TIMESTAMP - FROM claimed - WHERE repair.id = claimed.id - RETURNING - repair.id, - repair.owned_by_user_id, - repair.owned_by_organization_id, - repair.hour_start, - repair.generation, - repair.claim_token - `); - return result.rows; -} - -export async function completeCostInsightRollupRepair( - database: CostInsightRootDatabase, - row: ClaimedRollupRepairRow, - owner: CostInsightSpendOwner -): Promise { - const ownerColumns = - owner.type === 'user' - ? { ownedByUserId: owner.id, ownedByOrganizationId: null } - : { ownedByUserId: null, ownedByOrganizationId: owner.id }; - const conflictTarget = - owner.type === 'user' - ? sql.raw('(owned_by_user_id) WHERE owned_by_organization_id IS NULL') - : sql.raw('(owned_by_organization_id) WHERE owned_by_user_id IS NULL'); - return database.transaction(async transaction => { - const result = await transaction.execute<{ id: string }>(sql` - WITH removed AS ( - DELETE FROM cost_insight_rollup_repairs - WHERE id = ${row.id} - AND generation = ${row.generation} - AND claim_token = ${row.claim_token} - RETURNING id - ), dirtied AS ( - INSERT INTO cost_insight_evaluation_dirty_owners AS dirty_owner ( - owned_by_user_id, - owned_by_organization_id, - dirty_at, - next_attempt_at - ) - SELECT - ${ownerColumns.ownedByUserId}, - ${ownerColumns.ownedByOrganizationId}, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - FROM removed - ON CONFLICT ${conflictTarget} - DO UPDATE SET - generation = dirty_owner.generation + 1, - dirty_at = CURRENT_TIMESTAMP, - next_attempt_at = CURRENT_TIMESTAMP, - claimed_at = NULL, - claim_token = NULL, - updated_at = CURRENT_TIMESTAMP - RETURNING 1 - ) - SELECT id FROM removed CROSS JOIN dirtied - `); - return result.rows.length === 1; - }); -} - -export async function failCostInsightRollupRepair( - database: CostInsightRootDatabase, - row: ClaimedRollupRepairRow, - error: string -): Promise { - await database.execute(sql` - UPDATE cost_insight_rollup_repairs - SET - claimed_at = NULL, - claim_token = NULL, - next_attempt_at = CURRENT_TIMESTAMP + make_interval( - mins => LEAST(60, 5 * attempt_count) - ), - last_error_redacted = ${safeRepairErrorCode(error)}, - updated_at = CURRENT_TIMESTAMP - WHERE id = ${row.id} - AND claim_token = ${row.claim_token} - `); -} - -function safeRepairErrorCode(error: string): string { - const postgresCode = error.match(/\b[0-9A-Z]{5}\b/)?.[0]; - return postgresCode ? `postgres:${postgresCode}` : 'cost_insight_rollup_repair_failed'; -} - -export async function listPendingCostInsightRepairOwnerKeys( - database: CostInsightRootDatabase, - owners: CostInsightSpendOwner[] -): Promise> { - if (owners.length === 0) return new Set(); - const predicates = owners.map(owner => - owner.type === 'user' - ? sql`(owned_by_user_id = ${owner.id} AND owned_by_organization_id IS NULL)` - : sql`(owned_by_organization_id = ${owner.id} AND owned_by_user_id IS NULL)` - ); - const result = await database.execute<{ - owned_by_user_id: string | null; - owned_by_organization_id: string | null; - }>(sql` - SELECT DISTINCT owned_by_user_id, owned_by_organization_id - FROM cost_insight_rollup_repairs - WHERE ${sql.join(predicates, sql` OR `)} - `); - return new Set( - result.rows.map(row => { - if (row.owned_by_user_id && !row.owned_by_organization_id) { - return ownerKey({ type: 'user', id: row.owned_by_user_id }); - } - if (row.owned_by_organization_id && !row.owned_by_user_id) { - return ownerKey({ type: 'organization', id: row.owned_by_organization_id }); - } - throw new Error('Cost Insights pending rollup repair has no valid Spend owner.'); - }) - ); -} - -export async function processPendingCostInsightRollupRepairs( - database: CostInsightRootDatabase, - options: { limit?: number; concurrency?: number } = {} -): Promise { - const limit = options.limit ?? 2; - if (!Number.isSafeInteger(limit) || limit <= 0) { - throw new Error('Cost Insights rollup repair limit must be a positive safe integer.'); - } - const concurrency = Math.max(1, Math.min(options.concurrency ?? 2, limit)); - const rows = await claimPendingCostInsightRollupRepairs(database, limit); - const summary: CostInsightRollupRepairSummary = { - claimed: rows.length, - repaired: 0, - failed: [], - }; - const limitRepair = pLimit(concurrency); - - await Promise.all( - rows.map(row => - limitRepair(async () => { - const owner = ownerFromRow(row); - const hourStart = normalizeTimestamp(row.hour_start); - try { - await repairOwnerSpendRollups(database, { - owner, - startHour: hourStart, - endHourExclusive: new Date(Date.parse(hourStart) + HOUR_MS).toISOString(), - maxHours: 1, - }); - summary.repaired += 1; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - const safeError = safeRepairErrorCode(message); - await failCostInsightRollupRepair(database, row, message); - summary.failed.push({ owner, hourStart, error: safeError }); - } - }) - ) - ); - - return summary; -} diff --git a/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts b/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts deleted file mode 100644 index c532ccc0a65..00000000000 --- a/apps/web/src/lib/cost-insights/spend-repository.integration.test.ts +++ /dev/null @@ -1,1119 +0,0 @@ -import { afterEach, describe, expect, test } from '@jest/globals'; -import { buildCostInsightDriver } from '@kilocode/db/cost-insights-rollups'; -import { eq } from 'drizzle-orm'; - -import { db } from '@/lib/drizzle'; -import { - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, - cost_insight_rollup_coverage, - cost_insight_rollup_degraded_intervals, - kilocode_users, - microdollar_usage, -} from '@kilocode/db/schema'; - -import { - getOwnerHourlySpend, - getOwnerHourDriverEvidence, - getOwnerRolling24HourDriverEvidenceExact, - getOwnerRolling24HourSpendExact, - getOwnerTopSpendDrivers, - getOwnerTopSpendDriversByRange, - loadOwnerDashboardHourlySpend, -} from './spend-repository'; - -const testUserIds = new Set(); - -async function createUser(): Promise { - const id = `cost-insights-read-${crypto.randomUUID()}`; - testUserIds.add(id); - await db.insert(kilocode_users).values({ - id, - google_user_email: `${id}@example.com`, - google_user_name: 'Cost Insights Read Test', - google_user_image_url: 'https://example.com/avatar.png', - stripe_customer_id: `cus_${crypto.randomUUID()}`, - }); - return id; -} - -async function initializeCoverage(): Promise { - await db.insert(cost_insight_rollup_coverage).values({ - rollup_version: 1, - live_capture_start_hour: '2026-06-01T00:00:00.000Z', - coverage_start_hour: '2026-05-01T00:00:00.000Z', - }); -} - -async function aiGatewayDriver(modelOrPlanKey: string, actorUserId: string) { - return await buildCostInsightDriver({ - source: 'ai_gateway', - productKey: 'other', - featureKey: 'other', - modelOrPlanKey, - providerKey: 'provider', - actorUserId, - }); -} - -afterEach(async () => { - await db - .delete(cost_insight_rollup_degraded_intervals) - .where(eq(cost_insight_rollup_degraded_intervals.reason, 'capture_bypass')); - await db - .delete(cost_insight_rollup_coverage) - .where(eq(cost_insight_rollup_coverage.rollup_version, 1)); - for (const userId of testUserIds) { - await db - .delete(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId)); - await db - .delete(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, userId)); - await db.delete(microdollar_usage).where(eq(microdollar_usage.kilo_user_id, userId)); - await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); - } - testUserIds.clear(); -}); - -describe('Cost Insights spend repository integration', () => { - test('shows driver-bucket spend before total-rollup coverage as incomplete evidence', async () => { - const userId = await createUser(); - const driver = await aiGatewayDriver('historical-model', userId); - await db.insert(cost_insight_rollup_coverage).values({ - rollup_version: 1, - live_capture_start_hour: '2026-06-01T02:00:00.000Z', - coverage_start_hour: '2026-06-01T02:00:00.000Z', - }); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'variable', - driver_key: driver.driverKey, - source: driver.source, - product_key: driver.productKey, - feature_key: driver.featureKey, - model_or_plan_key: driver.modelOrPlanKey, - provider_key: driver.providerKey, - actor_user_id: driver.actorUserId, - total_microdollars: 25, - spend_record_count: 2, - }); - await db.insert(cost_insight_owner_hour_totals).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T02:00:00.000Z', - spend_category: 'variable', - total_microdollars: 10, - spend_record_count: 1, - }); - - await expect( - loadOwnerDashboardHourlySpend(db, { - owner: { type: 'user', id: userId }, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T03:00:00.000Z', - }) - ).resolves.toEqual([ - expect.objectContaining({ - hourStart: '2026-06-01T00:00:00.000Z', - variableMicrodollars: 25, - totalMicrodollars: 25, - isCovered: false, - }), - expect.objectContaining({ - hourStart: '2026-06-01T01:00:00.000Z', - totalMicrodollars: null, - isCovered: false, - }), - expect.objectContaining({ - hourStart: '2026-06-01T02:00:00.000Z', - variableMicrodollars: 10, - totalMicrodollars: 10, - isCovered: true, - }), - ]); - }); - - test('replaces separated degraded dashboard hours with canonical spend', async () => { - const userId = await createUser(); - await initializeCoverage(); - await db.insert(cost_insight_owner_hour_totals).values([ - { - owned_by_user_id: userId, - hour_start: '2026-06-01T13:00:00.000Z', - spend_category: 'variable', - total_microdollars: 999, - spend_record_count: 1, - }, - { - owned_by_user_id: userId, - hour_start: '2026-06-01T14:00:00.000Z', - spend_category: 'variable', - total_microdollars: 100, - spend_record_count: 1, - }, - { - owned_by_user_id: userId, - hour_start: '2026-06-01T15:00:00.000Z', - spend_category: 'variable', - total_microdollars: 999, - spend_record_count: 1, - }, - ]); - await db.insert(cost_insight_rollup_degraded_intervals).values([ - { - start_hour: '2026-06-01T13:00:00.000Z', - end_hour_exclusive: '2026-06-01T14:00:00.000Z', - reason: 'capture_bypass', - }, - { - start_hour: '2026-06-01T15:00:00.000Z', - end_hour_exclusive: '2026-06-01T16:00:00.000Z', - reason: 'capture_bypass', - }, - ]); - await db.insert(microdollar_usage).values([ - { - kilo_user_id: userId, - cost: 11, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T13:15:00.000Z', - }, - { - kilo_user_id: userId, - cost: 50, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T14:15:00.000Z', - }, - { - kilo_user_id: userId, - cost: 13, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T15:15:00.000Z', - }, - ]); - - await expect( - loadOwnerDashboardHourlySpend(db, { - owner: { type: 'user', id: userId }, - startHour: '2026-06-01T13:00:00.000Z', - endHourExclusive: '2026-06-01T16:00:00.000Z', - }) - ).resolves.toEqual([ - expect.objectContaining({ - hourStart: '2026-06-01T13:00:00.000Z', - variableMicrodollars: 11, - totalMicrodollars: 11, - isCovered: true, - }), - expect.objectContaining({ - hourStart: '2026-06-01T14:00:00.000Z', - variableMicrodollars: 100, - totalMicrodollars: 100, - isCovered: true, - }), - expect.objectContaining({ - hourStart: '2026-06-01T15:00:00.000Z', - variableMicrodollars: 13, - totalMicrodollars: 13, - isCovered: true, - }), - ]); - }); - - test('ranks each nested dashboard range independently from one 90-day bucket scan', async () => { - const userId = await createUser(); - const recentDriver = await aiGatewayDriver('recent-winner', userId); - const dayDriver = await aiGatewayDriver('day-winner', userId); - await db.insert(cost_insight_owner_hour_driver_buckets).values([ - { - owned_by_user_id: userId, - hour_start: '2026-06-01T01:00:00.000Z', - spend_category: 'variable', - driver_key: recentDriver.driverKey, - source: recentDriver.source, - product_key: recentDriver.productKey, - feature_key: recentDriver.featureKey, - model_or_plan_key: recentDriver.modelOrPlanKey, - provider_key: recentDriver.providerKey, - actor_user_id: recentDriver.actorUserId, - total_microdollars: 30, - spend_record_count: 3, - }, - { - owned_by_user_id: userId, - hour_start: '2026-05-31T02:00:00.000Z', - spend_category: 'variable', - driver_key: dayDriver.driverKey, - source: dayDriver.source, - product_key: dayDriver.productKey, - feature_key: dayDriver.featureKey, - model_or_plan_key: dayDriver.modelOrPlanKey, - provider_key: dayDriver.providerKey, - actor_user_id: dayDriver.actorUserId, - total_microdollars: 100, - spend_record_count: 1, - }, - ]); - - const driversByRange = await getOwnerTopSpendDriversByRange(db, { - owner: { type: 'user', id: userId }, - ranges: [ - { key: '1h', startHour: '2026-06-01T01:00:00.000Z' }, - { key: '24h', startHour: '2026-05-31T02:00:00.000Z' }, - ], - endHourExclusive: '2026-06-01T02:00:00.000Z', - limit: 1, - }); - - expect(driversByRange.get('1h')).toEqual([ - expect.objectContaining({ modelOrPlanKey: 'recent-winner', totalMicrodollars: 30 }), - ]); - expect(driversByRange.get('24h')).toEqual([ - expect.objectContaining({ modelOrPlanKey: 'day-winner', totalMicrodollars: 100 }), - ]); - }); - - test('zero-fills covered sparse hours, isolates owners, and suppresses degraded hours', async () => { - const userId = await createUser(); - const otherUserId = await createUser(); - await initializeCoverage(); - await db.insert(cost_insight_owner_hour_totals).values([ - { - owned_by_user_id: userId, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'variable', - total_microdollars: 10, - spend_record_count: 2, - }, - { - owned_by_user_id: otherUserId, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'scheduled', - total_microdollars: 999, - spend_record_count: 1, - }, - ]); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'variable', - driver_key: 'a'.repeat(64), - source: 'ai_gateway', - product_key: 'direct-gateway', - feature_key: 'chat_completions', - model_or_plan_key: 'model', - provider_key: 'provider', - actor_user_id: userId, - total_microdollars: 10, - spend_record_count: 2, - }); - - const owner = { type: 'user', id: userId } as const; - await expect( - getOwnerHourlySpend(db, { - owner, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - }) - ).resolves.toEqual([ - expect.objectContaining({ - hourStart: '2026-06-01T00:00:00.000Z', - variableMicrodollars: 10, - scheduledMicrodollars: 0, - totalMicrodollars: 10, - isCovered: true, - }), - expect.objectContaining({ - hourStart: '2026-06-01T01:00:00.000Z', - variableMicrodollars: 0, - scheduledMicrodollars: 0, - totalMicrodollars: 0, - isCovered: true, - }), - ]); - await expect( - getOwnerTopSpendDrivers(db, { - owner, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - }) - ).resolves.toEqual([ - expect.objectContaining({ - actorUserId: userId, - totalMicrodollars: 10, - spendRecordCount: 2, - }), - ]); - - await db.insert(cost_insight_rollup_degraded_intervals).values({ - start_hour: '2026-06-01T01:00:00.000Z', - end_hour_exclusive: '2026-06-01T02:00:00.000Z', - reason: 'capture_bypass', - }); - const degraded = await getOwnerHourlySpend(db, { - owner, - startHour: '2026-06-01T01:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - }); - expect(degraded[0]).toMatchObject({ - isCovered: false, - variableMicrodollars: null, - scheduledMicrodollars: null, - totalMicrodollars: null, - }); - }); - - test('returns exact rolling 24-hour canonical driver evidence', async () => { - const userId = await createUser(); - await db.insert(microdollar_usage).values([ - { - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 10_000_000, - input_tokens: 100, - output_tokens: 50, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T11:29:59.999Z', - provider: 'anthropic', - model: 'claude-sonnet-4', - requested_model: 'claude-sonnet-4', - inference_provider: 'anthropic', - has_error: false, - abuse_classification: 0, - }, - { - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 20_000_000, - input_tokens: 200, - output_tokens: 100, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T11:30:00.000Z', - provider: 'openai', - model: 'gpt-4.1', - requested_model: 'gpt-4.1', - inference_provider: 'openai', - has_error: false, - abuse_classification: 0, - }, - { - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 30_000_000, - input_tokens: 300, - output_tokens: 150, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-02T11:29:59.999Z', - provider: 'google', - model: 'gemini-2.5-pro', - requested_model: 'gemini-2.5-pro', - inference_provider: 'google', - has_error: false, - abuse_classification: 0, - }, - { - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 40_000_000, - input_tokens: 400, - output_tokens: 200, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-02T11:30:00.000Z', - provider: 'openai', - model: 'gpt-5', - requested_model: 'gpt-5', - inference_provider: 'openai', - has_error: false, - abuse_classification: 0, - }, - ]); - - await expect( - getOwnerRolling24HourDriverEvidenceExact(db, { - owner: { type: 'user', id: userId }, - asOf: '2026-06-02T11:30:00.000Z', - }) - ).resolves.toEqual({ - asOf: '2026-06-02T11:30:00.000Z', - windowStart: '2026-06-01T11:30:00.000Z', - variableMicrodollars: 50_000_000, - scheduledMicrodollars: 0, - totalMicrodollars: 50_000_000, - topDrivers: [ - expect.objectContaining({ - category: 'variable', - totalMicrodollars: 30_000_000, - spendRecordCount: 1, - }), - expect.objectContaining({ - category: 'variable', - totalMicrodollars: 20_000_000, - spendRecordCount: 1, - }), - ], - }); - }); - - test('uses canonical current-hour driver evidence for partial-hour windows', async () => { - const userId = await createUser(); - await initializeCoverage(); - const owner = { type: 'user', id: userId } as const; - const driver = await aiGatewayDriver('rollup-model', userId); - await db.insert(cost_insight_owner_hour_totals).values({ - owned_by_user_id: userId, - hour_start: '2026-06-02T12:00:00.000Z', - spend_category: 'variable', - total_microdollars: 42, - spend_record_count: 2, - }); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-02T12:00:00.000Z', - spend_category: 'variable', - driver_key: driver.driverKey, - source: 'ai_gateway', - product_key: 'other', - feature_key: 'other', - model_or_plan_key: 'rollup-model', - provider_key: 'provider', - actor_user_id: userId, - total_microdollars: 42, - spend_record_count: 2, - }); - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 7, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-02T12:15:00.000Z', - provider: 'provider', - model: 'canonical-model', - requested_model: 'canonical-model', - inference_provider: 'provider', - has_error: false, - abuse_classification: 0, - }); - - await expect( - getOwnerHourDriverEvidence(db, { - owner, - hourStart: '2026-06-02T12:00:00.000Z', - intervalEnd: '2026-06-02T12:30:00.000Z', - category: 'variable', - }) - ).resolves.toEqual({ - startInclusive: '2026-06-02T12:00:00.000Z', - endExclusive: '2026-06-02T12:30:00.000Z', - variableMicrodollars: 7, - scheduledMicrodollars: 0, - totalMicrodollars: 7, - topDrivers: [ - expect.objectContaining({ - modelOrPlanKey: 'canonical-model', - totalMicrodollars: 7, - spendRecordCount: 1, - }), - ], - usedCanonicalFallback: true, - degradedIntervalCount: 0, - }); - }); - - test('uses rollup-backed completed-hour driver evidence when coverage is complete', async () => { - const userId = await createUser(); - await initializeCoverage(); - const owner = { type: 'user', id: userId } as const; - const driver = await aiGatewayDriver('rollup-model', userId); - await db.insert(cost_insight_owner_hour_totals).values({ - owned_by_user_id: userId, - hour_start: '2026-06-02T12:00:00.000Z', - spend_category: 'variable', - total_microdollars: 42, - spend_record_count: 2, - }); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-02T12:00:00.000Z', - spend_category: 'variable', - driver_key: driver.driverKey, - source: 'ai_gateway', - product_key: 'other', - feature_key: 'other', - model_or_plan_key: 'rollup-model', - provider_key: 'provider', - actor_user_id: userId, - total_microdollars: 42, - spend_record_count: 2, - }); - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 7, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-02T12:15:00.000Z', - provider: 'provider', - model: 'canonical-model', - requested_model: 'canonical-model', - inference_provider: 'provider', - has_error: false, - abuse_classification: 0, - }); - - await expect( - getOwnerHourDriverEvidence(db, { - owner, - hourStart: '2026-06-02T12:00:00.000Z', - intervalEnd: '2026-06-02T13:00:00.000Z', - category: 'variable', - }) - ).resolves.toEqual({ - startInclusive: '2026-06-02T12:00:00.000Z', - endExclusive: '2026-06-02T13:00:00.000Z', - variableMicrodollars: 42, - scheduledMicrodollars: 0, - totalMicrodollars: 42, - topDrivers: [ - expect.objectContaining({ - modelOrPlanKey: 'rollup-model', - totalMicrodollars: 42, - spendRecordCount: 2, - }), - ], - usedCanonicalFallback: false, - degradedIntervalCount: 0, - }); - }); - - test('falls back to canonical current-hour driver evidence when coverage is degraded', async () => { - const userId = await createUser(); - await initializeCoverage(); - const owner = { type: 'user', id: userId } as const; - const driver = await aiGatewayDriver('degraded-rollup-model', userId); - await db.insert(cost_insight_owner_hour_totals).values({ - owned_by_user_id: userId, - hour_start: '2026-06-02T12:00:00.000Z', - spend_category: 'variable', - total_microdollars: 999, - spend_record_count: 1, - }); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-02T12:00:00.000Z', - spend_category: 'variable', - driver_key: driver.driverKey, - source: 'ai_gateway', - product_key: 'other', - feature_key: 'other', - model_or_plan_key: 'degraded-rollup-model', - provider_key: 'provider', - actor_user_id: userId, - total_microdollars: 999, - spend_record_count: 1, - }); - await db.insert(cost_insight_rollup_degraded_intervals).values({ - start_hour: '2026-06-02T12:00:00.000Z', - end_hour_exclusive: '2026-06-02T13:00:00.000Z', - reason: 'capture_bypass', - }); - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 11, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-02T12:15:00.000Z', - provider: 'provider', - model: 'canonical-model', - requested_model: 'canonical-model', - inference_provider: 'provider', - has_error: false, - abuse_classification: 0, - }); - - await expect( - getOwnerHourDriverEvidence(db, { - owner, - hourStart: '2026-06-02T12:00:00.000Z', - intervalEnd: '2026-06-02T12:30:00.000Z', - category: 'variable', - }) - ).resolves.toEqual({ - startInclusive: '2026-06-02T12:00:00.000Z', - endExclusive: '2026-06-02T12:30:00.000Z', - variableMicrodollars: 11, - scheduledMicrodollars: 0, - totalMicrodollars: 11, - topDrivers: [ - expect.objectContaining({ - modelOrPlanKey: 'canonical-model', - totalMicrodollars: 11, - spendRecordCount: 1, - }), - ], - usedCanonicalFallback: true, - degradedIntervalCount: 1, - }); - }); - - test('merges complete rollup interior with exact canonical boundaries before ranking', async () => { - const userId = await createUser(); - await initializeCoverage(); - const boundaryDrivers = [ - { model: 'model-1', cost: 60 }, - { model: 'model-2', cost: 60 }, - { model: 'model-3', cost: 40 }, - { model: 'model-4', cost: 30 }, - { model: 'model-5', cost: 20 }, - { model: 'model-6', cost: 10 }, - ]; - const model6Driver = await aiGatewayDriver('model-6', userId); - const scheduledDriver = await buildCostInsightDriver({ - source: 'coding_plan', - productKey: 'coding-plan', - featureKey: 'renewal', - modelOrPlanKey: 'plan-1', - providerKey: 'provider-1', - actorUserId: userId, - }); - await db.insert(cost_insight_owner_hour_driver_buckets).values([ - { - owned_by_user_id: userId, - hour_start: '2026-06-01T13:00:00.000Z', - spend_category: 'variable', - driver_key: model6Driver.driverKey, - source: 'ai_gateway', - product_key: 'other', - feature_key: 'other', - model_or_plan_key: 'model-6', - provider_key: 'provider', - actor_user_id: userId, - total_microdollars: 100, - spend_record_count: 2, - }, - { - owned_by_user_id: userId, - hour_start: '2026-06-01T14:00:00.000Z', - spend_category: 'scheduled', - driver_key: scheduledDriver.driverKey, - source: 'coding_plan', - product_key: 'coding-plan', - feature_key: 'renewal', - model_or_plan_key: 'plan-1', - provider_key: 'provider-1', - actor_user_id: userId, - total_microdollars: 7, - spend_record_count: 1, - }, - ]); - await db.insert(microdollar_usage).values([ - ...boundaryDrivers.map(({ model, cost }, index) => ({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: `2026-06-01T12:45:0${index}.000Z`, - provider: 'provider', - model, - requested_model: model, - inference_provider: 'provider', - has_error: false, - })), - { - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 5, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-02T12:15:00.000Z', - provider: 'provider', - model: 'model-5', - requested_model: 'model-5', - inference_provider: 'provider', - has_error: false, - }, - ]); - - await expect( - getOwnerRolling24HourDriverEvidenceExact(db, { - owner: { type: 'user', id: userId }, - asOf: '2026-06-02T12:30:00.000Z', - }) - ).resolves.toEqual({ - asOf: '2026-06-02T12:30:00.000Z', - windowStart: '2026-06-01T12:30:00.000Z', - variableMicrodollars: 325, - scheduledMicrodollars: 7, - totalMicrodollars: 332, - topDrivers: [ - expect.objectContaining({ - modelOrPlanKey: 'model-6', - totalMicrodollars: 110, - spendRecordCount: 3, - }), - expect.objectContaining({ modelOrPlanKey: 'model-1', totalMicrodollars: 60 }), - expect.objectContaining({ modelOrPlanKey: 'model-2', totalMicrodollars: 60 }), - expect.objectContaining({ modelOrPlanKey: 'model-3', totalMicrodollars: 40 }), - expect.objectContaining({ modelOrPlanKey: 'model-4', totalMicrodollars: 30 }), - ], - }); - }); - - test('combines covered rollup drivers with canonical degraded-hour and boundary drivers', async () => { - const userId = await createUser(); - await initializeCoverage(); - const driver = await aiGatewayDriver('fallback-model', userId); - const coveredDriver = await aiGatewayDriver('covered-model', userId); - await db.insert(cost_insight_owner_hour_driver_buckets).values([ - { - owned_by_user_id: userId, - hour_start: '2026-06-01T13:00:00.000Z', - spend_category: 'variable', - driver_key: driver.driverKey, - source: driver.source, - product_key: driver.productKey, - feature_key: driver.featureKey, - model_or_plan_key: driver.modelOrPlanKey, - provider_key: driver.providerKey, - actor_user_id: driver.actorUserId, - total_microdollars: 999, - spend_record_count: 1, - }, - { - owned_by_user_id: userId, - hour_start: '2026-06-01T14:00:00.000Z', - spend_category: 'variable', - driver_key: coveredDriver.driverKey, - source: coveredDriver.source, - product_key: coveredDriver.productKey, - feature_key: coveredDriver.featureKey, - model_or_plan_key: coveredDriver.modelOrPlanKey, - provider_key: coveredDriver.providerKey, - actor_user_id: coveredDriver.actorUserId, - total_microdollars: 100, - spend_record_count: 1, - }, - ]); - await db.insert(cost_insight_rollup_degraded_intervals).values({ - start_hour: '2026-06-01T13:00:00.000Z', - end_hour_exclusive: '2026-06-01T14:00:00.000Z', - reason: 'capture_bypass', - }); - await db.insert(microdollar_usage).values( - [ - { createdAt: '2026-06-01T12:45:00.000Z', cost: 3 }, - { createdAt: '2026-06-01T13:15:00.000Z', cost: 11 }, - { createdAt: '2026-06-01T14:15:00.000Z', cost: 50 }, - { createdAt: '2026-06-02T12:15:00.000Z', cost: 4 }, - ].map(({ createdAt, cost }) => ({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: createdAt, - provider: 'provider', - model: 'fallback-model', - requested_model: 'fallback-model', - inference_provider: 'provider', - has_error: false, - })) - ); - - await expect( - getOwnerRolling24HourDriverEvidenceExact(db, { - owner: { type: 'user', id: userId }, - asOf: '2026-06-02T12:30:00.000Z', - }) - ).resolves.toEqual({ - asOf: '2026-06-02T12:30:00.000Z', - windowStart: '2026-06-01T12:30:00.000Z', - variableMicrodollars: 118, - scheduledMicrodollars: 0, - totalMicrodollars: 118, - topDrivers: [ - expect.objectContaining({ - modelOrPlanKey: 'covered-model', - totalMicrodollars: 100, - spendRecordCount: 1, - }), - expect.objectContaining({ - modelOrPlanKey: 'fallback-model', - totalMicrodollars: 18, - spendRecordCount: 3, - }), - ], - }); - }); - - test('combines covered rollup totals with canonical degraded-hour and boundary totals', async () => { - const userId = await createUser(); - await initializeCoverage(); - await db.insert(cost_insight_owner_hour_totals).values([ - { - owned_by_user_id: userId, - hour_start: '2026-06-01T13:00:00.000Z', - spend_category: 'variable', - total_microdollars: 999, - spend_record_count: 1, - }, - { - owned_by_user_id: userId, - hour_start: '2026-06-01T14:00:00.000Z', - spend_category: 'variable', - total_microdollars: 100, - spend_record_count: 1, - }, - ]); - await db.insert(cost_insight_rollup_degraded_intervals).values({ - start_hour: '2026-06-01T13:00:00.000Z', - end_hour_exclusive: '2026-06-01T14:00:00.000Z', - reason: 'capture_bypass', - }); - await db.insert(microdollar_usage).values( - [ - { createdAt: '2026-06-01T12:45:00.000Z', cost: 3 }, - { createdAt: '2026-06-01T13:15:00.000Z', cost: 11 }, - { createdAt: '2026-06-01T14:15:00.000Z', cost: 50 }, - { createdAt: '2026-06-02T12:15:00.000Z', cost: 4 }, - ].map(({ createdAt, cost }) => ({ - kilo_user_id: userId, - cost, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: createdAt, - })) - ); - - await expect( - getOwnerRolling24HourSpendExact(db, { - owner: { type: 'user', id: userId }, - asOf: '2026-06-02T12:30:00.000Z', - fallbackToCanonical: true, - }) - ).resolves.toEqual({ - asOf: '2026-06-02T12:30:00.000Z', - windowStart: '2026-06-01T12:30:00.000Z', - variableMicrodollars: 118, - scheduledMicrodollars: 0, - totalMicrodollars: 118, - isComplete: true, - }); - }); - - test('rejects matching category and driver keys with mismatched dimensions', async () => { - const userId = await createUser(); - await initializeCoverage(); - const driver = await aiGatewayDriver('collision-model', userId); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T13:00:00.000Z', - spend_category: 'variable', - driver_key: driver.driverKey, - source: 'ai_gateway', - product_key: 'other', - feature_key: 'other', - model_or_plan_key: 'collision-model', - provider_key: 'mismatched-provider', - actor_user_id: userId, - total_microdollars: 10, - spend_record_count: 1, - }); - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 1, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T12:45:00.000Z', - provider: 'provider', - model: 'collision-model', - requested_model: 'collision-model', - inference_provider: 'provider', - has_error: false, - abuse_classification: 0, - }); - - await expect( - getOwnerRolling24HourDriverEvidenceExact(db, { - owner: { type: 'user', id: userId }, - asOf: '2026-06-02T12:30:00.000Z', - }) - ).rejects.toThrow('mismatched dimensions across exact evidence fragments'); - }); - - test('rejects unsafe sums while merging exact driver fragments', async () => { - const userId = await createUser(); - await initializeCoverage(); - const driver = await aiGatewayDriver('overflow-model', userId); - await db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T13:00:00.000Z', - spend_category: 'variable', - driver_key: driver.driverKey, - source: 'ai_gateway', - product_key: 'other', - feature_key: 'other', - model_or_plan_key: 'overflow-model', - provider_key: 'provider', - actor_user_id: userId, - total_microdollars: Number.MAX_SAFE_INTEGER, - spend_record_count: 1, - }); - await db.insert(microdollar_usage).values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - cost: 1, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T12:45:00.000Z', - provider: 'provider', - model: 'overflow-model', - requested_model: 'overflow-model', - inference_provider: 'provider', - has_error: false, - abuse_classification: 0, - }); - - await expect( - getOwnerRolling24HourDriverEvidenceExact(db, { - owner: { type: 'user', id: userId }, - asOf: '2026-06-02T12:30:00.000Z', - }) - ).rejects.toThrow( - 'exact driver total_microdollars is outside the JavaScript safe-integer range' - ); - }); - - test('filters top drivers to the requested hour and spend category', async () => { - const userId = await createUser(); - const baseDriver = { - owned_by_user_id: userId, - source: 'ai_gateway' as const, - product_key: 'direct-gateway', - feature_key: 'chat_completions', - model_or_plan_key: 'model', - provider_key: 'provider', - actor_user_id: userId, - spend_record_count: 1, - }; - await db.insert(cost_insight_owner_hour_driver_buckets).values([ - { - ...baseDriver, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'variable', - driver_key: 'b'.repeat(64), - total_microdollars: 30, - }, - { - ...baseDriver, - hour_start: '2026-06-01T00:00:00.000Z', - spend_category: 'scheduled', - driver_key: 'c'.repeat(64), - total_microdollars: 90, - }, - { - ...baseDriver, - hour_start: '2026-05-31T23:00:00.000Z', - spend_category: 'variable', - driver_key: 'd'.repeat(64), - total_microdollars: 120, - }, - ]); - - await expect( - getOwnerTopSpendDrivers(db, { - owner: { type: 'user', id: userId }, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T01:00:00.000Z', - category: 'variable', - }) - ).resolves.toEqual([ - expect.objectContaining({ - category: 'variable', - totalMicrodollars: 30, - spendRecordCount: 1, - }), - ]); - }); - - test('combines rollup interior with canonical raw boundary fragments exactly once', async () => { - const userId = await createUser(); - await initializeCoverage(); - await db.insert(cost_insight_owner_hour_totals).values({ - owned_by_user_id: userId, - hour_start: '2026-06-01T13:00:00.000Z', - spend_category: 'variable', - total_microdollars: 100, - spend_record_count: 1, - }); - await db.insert(microdollar_usage).values([ - { - kilo_user_id: userId, - cost: 3, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-01T12:45:00.000Z', - }, - { - kilo_user_id: userId, - cost: 4, - input_tokens: 0, - output_tokens: 0, - cache_write_tokens: 0, - cache_hit_tokens: 0, - created_at: '2026-06-02T12:15:00.000Z', - }, - ]); - - await expect( - getOwnerRolling24HourSpendExact(db, { - owner: { type: 'user', id: userId }, - asOf: '2026-06-02T12:30:00.000Z', - }) - ).resolves.toEqual({ - asOf: '2026-06-02T12:30:00.000Z', - windowStart: '2026-06-01T12:30:00.000Z', - variableMicrodollars: 107, - scheduledMicrodollars: 0, - totalMicrodollars: 107, - isComplete: true, - }); - }); -}); diff --git a/apps/web/src/lib/cost-insights/spend-repository.test.ts b/apps/web/src/lib/cost-insights/spend-repository.test.ts deleted file mode 100644 index 67688f90d2b..00000000000 --- a/apps/web/src/lib/cost-insights/spend-repository.test.ts +++ /dev/null @@ -1,394 +0,0 @@ -import type { CostInsightQueryExecutor } from './canonical-sources'; -import { PgDialect } from 'drizzle-orm/pg-core'; -import { - getCostInsightRollupCoverage, - getOwnerCurrentHourSpend, - getOwnerHourlySpend, - getOwnerTopSpendDriversByRange, - getRolling24HourFragments, - getRollingWindowFragments, - groupContiguousHourlyIntervals, - loadOwnerDashboardHourlySpend, -} from './spend-repository'; - -const owner = { type: 'user', id: 'user-1' } as const; - -function executorReturning(...rowsByCall: unknown[][]): CostInsightQueryExecutor { - const execute = jest.fn(); - for (const rows of rowsByCall) { - execute.mockResolvedValueOnce({ rows }); - } - return { execute } as unknown as CostInsightQueryExecutor; -} - -describe('Cost Insights spend repository', () => { - test('splits exact rolling 24 hours into raw boundaries and rollup interior', () => { - expect(getRolling24HourFragments('2026-06-02T12:30:00.000Z')).toEqual({ - asOf: '2026-06-02T12:30:00.000Z', - windowStart: '2026-06-01T12:30:00.000Z', - oldestBoundaryEnd: '2026-06-01T13:00:00.000Z', - interiorStart: '2026-06-01T13:00:00.000Z', - interiorEnd: '2026-06-02T12:00:00.000Z', - currentBoundaryStart: '2026-06-02T12:00:00.000Z', - }); - }); - - test('splits an exact rolling 30-day window into raw boundaries and rollup interior', () => { - expect(getRollingWindowFragments('2026-06-02T12:30:00.000Z', 30 * 24)).toEqual({ - asOf: '2026-06-02T12:30:00.000Z', - windowStart: '2026-05-03T12:30:00.000Z', - oldestBoundaryEnd: '2026-05-03T13:00:00.000Z', - interiorStart: '2026-05-03T13:00:00.000Z', - interiorEnd: '2026-06-02T12:00:00.000Z', - currentBoundaryStart: '2026-06-02T12:00:00.000Z', - }); - }); - - test('splits an exact rolling 7-day window into raw boundaries and rollup interior', () => { - expect(getRollingWindowFragments('2026-06-02T12:30:00.000Z', 7 * 24)).toEqual({ - asOf: '2026-06-02T12:30:00.000Z', - windowStart: '2026-05-26T12:30:00.000Z', - oldestBoundaryEnd: '2026-05-26T13:00:00.000Z', - interiorStart: '2026-05-26T13:00:00.000Z', - interiorEnd: '2026-06-02T12:00:00.000Z', - currentBoundaryStart: '2026-06-02T12:00:00.000Z', - }); - }); - - test('skips both raw fragments on exact UTC-hour boundaries', () => { - const fragments = getRolling24HourFragments('2026-06-02T12:00:00.000Z'); - expect(fragments.windowStart).toBe(fragments.oldestBoundaryEnd); - expect(fragments.currentBoundaryStart).toBe(fragments.asOf); - expect(fragments.interiorStart).toBe('2026-06-01T12:00:00.000Z'); - expect(fragments.interiorEnd).toBe('2026-06-02T12:00:00.000Z'); - }); - - test('groups adjacent hours with the same coverage state into intervals', () => { - const points = [ - { hourStart: '2026-06-01T00:00:00.000Z', isCovered: false }, - { hourStart: '2026-06-01T01:00:00.000Z', isCovered: false }, - { hourStart: '2026-06-01T02:00:00.000Z', isCovered: true }, - { hourStart: '2026-06-01T03:00:00.000Z', isCovered: false }, - ]; - - expect(groupContiguousHourlyIntervals(points, false)).toEqual([ - { - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - }, - { - startHour: '2026-06-01T03:00:00.000Z', - endHourExclusive: '2026-06-01T04:00:00.000Z', - }, - ]); - expect(groupContiguousHourlyIntervals(points, true)).toEqual([ - { - startHour: '2026-06-01T02:00:00.000Z', - endHourExclusive: '2026-06-01T03:00:00.000Z', - }, - ]); - }); - - test('returns covered sparse hours as zero and uncovered hours as null', async () => { - const executor = executorReturning([ - { - hour_start: '2026-06-01 02:00:00+02', - variable_microdollars: '0', - scheduled_microdollars: '0', - variable_record_count: '0', - scheduled_record_count: '0', - is_covered: true, - }, - { - hour_start: '2026-06-01 01:00:00+00', - variable_microdollars: null, - scheduled_microdollars: null, - variable_record_count: null, - scheduled_record_count: null, - is_covered: false, - }, - ]); - - await expect( - getOwnerHourlySpend(executor, { - owner, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - }) - ).resolves.toEqual([ - { - hourStart: '2026-06-01T00:00:00.000Z', - variableMicrodollars: 0, - scheduledMicrodollars: 0, - totalMicrodollars: 0, - variableRecordCount: 0, - scheduledRecordCount: 0, - isCovered: true, - }, - { - hourStart: '2026-06-01T01:00:00.000Z', - variableMicrodollars: null, - scheduledMicrodollars: null, - totalMicrodollars: null, - variableRecordCount: null, - scheduledRecordCount: null, - isCovered: false, - }, - ]); - }); - - test('leaves pre-coverage dashboard hours unavailable while replacing degraded covered hours', async () => { - const execute = jest - .fn() - .mockResolvedValueOnce({ - rows: [ - { - hour_start: '2026-06-01 00:00:00+00', - variable_microdollars: null, - scheduled_microdollars: null, - variable_record_count: null, - scheduled_record_count: null, - is_covered: false, - }, - { - hour_start: '2026-06-01 01:00:00+00', - variable_microdollars: null, - scheduled_microdollars: null, - variable_record_count: null, - scheduled_record_count: null, - is_covered: false, - }, - { - hour_start: '2026-06-01 02:00:00+00', - variable_microdollars: '7', - scheduled_microdollars: '0', - variable_record_count: '1', - scheduled_record_count: '0', - is_covered: true, - }, - ], - }) - .mockResolvedValueOnce({ - rows: [ - { - rollup_version: '1', - live_capture_start_hour: '2026-06-01 01:00:00+00', - coverage_start_hour: null, - last_reconciled_at: null, - database_now: '2026-06-01 03:00:00+00', - }, - ], - }) - .mockResolvedValueOnce({ - rows: [ - { - id: 'degraded-1', - start_hour: '2026-06-01 01:00:00+00', - end_hour_exclusive: '2026-06-01 02:00:00+00', - source: null, - reason: 'capture_bypass', - detected_at: '2026-06-01 02:00:00+00', - }, - ], - }) - .mockResolvedValueOnce({ - rows: [ - { - hour_start: '2026-06-01 01:00:00+00', - owned_by_user_id: 'user-1', - owned_by_organization_id: null, - actor_user_id: 'user-1', - raw_product_key: null, - raw_feature_key: null, - requested_model: 'model', - resolved_model: 'model', - inference_provider: 'provider', - gateway_provider: 'provider', - total_microdollars: '11', - spend_record_count: '1', - }, - ], - }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ rows: [] }) - .mockResolvedValueOnce({ - rows: [ - { - hour_start: '2026-06-01 00:00:00+00', - variable_microdollars: '5', - scheduled_microdollars: '3', - variable_record_count: '1', - scheduled_record_count: '1', - }, - ], - }); - const database = { - transaction: async (callback: (transaction: CostInsightQueryExecutor) => Promise) => - await callback({ execute } as unknown as CostInsightQueryExecutor), - }; - - await expect( - loadOwnerDashboardHourlySpend(database as never, { - owner, - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T03:00:00.000Z', - }) - ).resolves.toEqual([ - expect.objectContaining({ - hourStart: '2026-06-01T00:00:00.000Z', - variableMicrodollars: 5, - scheduledMicrodollars: 3, - totalMicrodollars: 8, - isCovered: false, - }), - expect.objectContaining({ - hourStart: '2026-06-01T01:00:00.000Z', - totalMicrodollars: 11, - isCovered: true, - }), - expect.objectContaining({ - hourStart: '2026-06-01T02:00:00.000Z', - totalMicrodollars: 7, - isCovered: true, - }), - ]); - - expect(execute).toHaveBeenCalledTimes(8); - const canonicalQueries = execute.mock.calls - .slice(3, 7) - .map(([query]) => new PgDialect().sqlToQuery(query).params); - expect(canonicalQueries).toEqual( - expect.arrayContaining([ - expect.arrayContaining(['2026-06-01T01:00:00.000Z', '2026-06-01T02:00:00.000Z']), - ]) - ); - for (const queryParameters of canonicalQueries) { - expect(queryParameters).not.toContain('2026-06-01T00:00:00.000Z'); - } - const driverQueryParameters = new PgDialect().sqlToQuery(execute.mock.calls[7]?.[0]).params; - expect(driverQueryParameters).toEqual( - expect.arrayContaining(['2026-06-01T00:00:00.000Z', '2026-06-01T01:00:00.000Z']) - ); - }); - - test('adds current-hour categories using safe integer conversion', async () => { - const executor = executorReturning([ - { - variable_microdollars: '13', - scheduled_microdollars: '21', - variable_record_count: '2', - scheduled_record_count: '1', - }, - ]); - - await expect(getOwnerCurrentHourSpend(executor, owner)).resolves.toEqual({ - variableMicrodollars: 13, - scheduledMicrodollars: 21, - totalMicrodollars: 34, - variableRecordCount: 2, - scheduledRecordCount: 1, - }); - }); - - test('returns independently ranked drivers for all ranges in one query', async () => { - const executor = executorReturning([ - { - range_key: '1h', - spend_category: 'variable', - source: 'ai_gateway', - product_key: 'recent-winner', - feature_key: 'other', - model_or_plan_key: 'other', - provider_key: 'other', - actor_user_id: 'user-1', - total_microdollars: '30', - spend_record_count: '3', - }, - { - range_key: '24h', - spend_category: 'scheduled', - source: 'kiloclaw', - product_key: 'day-winner', - feature_key: 'other', - model_or_plan_key: 'other', - provider_key: 'other', - actor_user_id: 'user-1', - total_microdollars: '100', - spend_record_count: '1', - }, - ]); - - const driversByRange = await getOwnerTopSpendDriversByRange(executor, { - owner, - ranges: [ - { key: '1h', startHour: '2026-06-01T01:00:00.000Z' }, - { key: '24h', startHour: '2026-05-31T02:00:00.000Z' }, - ], - endHourExclusive: '2026-06-01T02:00:00.000Z', - }); - - expect(executor.execute).toHaveBeenCalledTimes(1); - expect(driversByRange.get('1h')).toEqual([ - expect.objectContaining({ productKey: 'recent-winner', totalMicrodollars: 30 }), - ]); - expect(driversByRange.get('24h')).toEqual([ - expect.objectContaining({ productKey: 'day-winner', totalMicrodollars: 100 }), - ]); - }); - - test('marks range incomplete when unresolved degraded interval overlaps it', async () => { - const executor = executorReturning( - [ - { - rollup_version: '1', - live_capture_start_hour: '2026-06-01 00:00:00+00', - coverage_start_hour: '2026-05-01 00:00:00+00', - last_reconciled_at: '2026-06-02 00:00:00+00', - database_now: '2026-06-03 00:30:00+00', - }, - ], - [ - { - id: 'degraded-1', - start_hour: '2026-06-01 01:00:00+00', - end_hour_exclusive: '2026-06-01 02:00:00+00', - source: 'other', - reason: 'capture_bypass', - detected_at: '2026-06-01 02:00:00+00', - }, - ] - ); - - const coverage = await getCostInsightRollupCoverage(executor, { - startHour: '2026-06-01T00:00:00.000Z', - endHourExclusive: '2026-06-01T03:00:00.000Z', - }); - - expect(coverage).toMatchObject({ - rollupVersion: 1, - liveCaptureStartHour: '2026-06-01T00:00:00.000Z', - coverageStartHour: '2026-05-01T00:00:00.000Z', - lastReconciledAt: '2026-06-02T00:00:00.000Z', - isFullyCovered: false, - }); - expect(coverage.degradedIntervals).toEqual([ - expect.objectContaining({ - id: 'degraded-1', - startHour: '2026-06-01T01:00:00.000Z', - endHourExclusive: '2026-06-01T02:00:00.000Z', - }), - ]); - }); - - test('rejects more than 90 days of hourly buckets', async () => { - const executor = executorReturning([]); - await expect( - getOwnerHourlySpend(executor, { - owner, - startHour: '2026-01-01T00:00:00.000Z', - endHourExclusive: '2026-04-02T00:00:00.000Z', - }) - ).rejects.toThrow('between 1 and 2160 UTC hours'); - }); -}); diff --git a/apps/web/src/lib/cost-insights/spend-repository.ts b/apps/web/src/lib/cost-insights/spend-repository.ts deleted file mode 100644 index 361c0bb6d41..00000000000 --- a/apps/web/src/lib/cost-insights/spend-repository.ts +++ /dev/null @@ -1,1729 +0,0 @@ -import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; -import { - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, - cost_insight_rollup_coverage, - cost_insight_rollup_degraded_intervals, -} from '@kilocode/db/schema'; -import { startSpan } from '@sentry/nextjs'; -import { sql, type SQL } from 'drizzle-orm'; - -import type { db } from '@/lib/drizzle'; - -import { - COST_INSIGHT_ROLLUP_VERSION, - loadCanonicalCostInsightAggregation, - loadCanonicalCostInsightAggregationsByIntervals, - parseSafeDatabaseInteger, - requireUtcHour, - requireUtcTimestamp, - type CostInsightQueryExecutor, - type CostInsightSpendCategory, - type CostInsightSpendSource, - type loadCanonicalCostInsightAggregationsByHour, -} from './canonical-sources'; - -const HOUR_MS = 60 * 60 * 1_000; -export const COST_INSIGHT_MAX_HOURLY_BUCKETS = 2_160; -export const COST_INSIGHT_MAX_TOP_DRIVERS = 5; - -export type OwnerHourlySpend = { - hourStart: string; - variableMicrodollars: number | null; - scheduledMicrodollars: number | null; - totalMicrodollars: number | null; - variableRecordCount: number | null; - scheduledRecordCount: number | null; - isCovered: boolean; -}; - -export type OwnerTopSpendDriver = { - category: CostInsightSpendCategory; - source: CostInsightSpendSource; - productKey: string; - featureKey: string; - modelOrPlanKey: string; - providerKey: string; - actorUserId: string; - totalMicrodollars: number; - spendRecordCount: number; -}; - -export type CostInsightDegradedInterval = { - id: string; - startHour: string; - endHourExclusive: string; - source: CostInsightSpendSource | null; - reason: string; - detectedAt: string; -}; - -export type CostInsightRollupCoverage = { - rollupVersion: number; - liveCaptureStartHour: string | null; - coverageStartHour: string | null; - lastReconciledAt: string | null; - degradedIntervals: CostInsightDegradedInterval[]; - isFullyCovered: boolean; -}; - -export type OwnerRollingSpendExact = { - asOf: string; - windowStart: string; - variableMicrodollars: number | null; - scheduledMicrodollars: number | null; - totalMicrodollars: number | null; - isComplete: boolean; -}; - -export type OwnerSpendDriverEvidenceExact = { - startInclusive: string; - endExclusive: string; - variableMicrodollars: number; - scheduledMicrodollars: number; - totalMicrodollars: number; - topDrivers: OwnerTopSpendDriver[]; - usedCanonicalFallback?: boolean; - degradedIntervalCount?: number; -}; - -export type OwnerRollingDriverEvidenceExact = { - asOf: string; - windowStart: string; - variableMicrodollars: number; - scheduledMicrodollars: number; - totalMicrodollars: number; - topDrivers: OwnerTopSpendDriver[]; -}; - -type OwnerHourDriverEvidence = OwnerSpendDriverEvidenceExact & { - usedCanonicalFallback: boolean; - degradedIntervalCount: number; -}; - -export type RollingWindowFragments = { - asOf: string; - windowStart: string; - oldestBoundaryEnd: string; - interiorStart: string; - interiorEnd: string; - currentBoundaryStart: string; -}; - -export type OwnerRolling24HourSpendExact = OwnerRollingSpendExact; -export type OwnerRolling24HourDriverEvidenceExact = OwnerRollingDriverEvidenceExact; -export type Rolling24HourFragments = RollingWindowFragments; - -export type HourInterval = { - startHour: string; - endHourExclusive: string; -}; - -type DenseHourlySpendRow = { - hour_start: string | Date; - variable_microdollars: string | number | bigint | null; - scheduled_microdollars: string | number | bigint | null; - variable_record_count: string | number | bigint | null; - scheduled_record_count: string | number | bigint | null; - is_covered: boolean; -}; - -type HourlyDriverSpendRow = { - hour_start: string | Date; - variable_microdollars: string | number | bigint; - scheduled_microdollars: string | number | bigint; - variable_record_count: string | number | bigint; - scheduled_record_count: string | number | bigint; -}; - -type TopDriverRow = { - spend_category: CostInsightSpendCategory; - source: CostInsightSpendSource; - product_key: string; - feature_key: string; - model_or_plan_key: string; - provider_key: string; - actor_user_id: string; - total_microdollars: string | number | bigint; - spend_record_count: string | number | bigint; -}; - -type TopDriverByRangeRow = TopDriverRow & { - range_key: string; -}; - -type CurrentHourRow = { - variable_microdollars: string | number | bigint; - scheduled_microdollars: string | number | bigint; - variable_record_count: string | number | bigint; - scheduled_record_count: string | number | bigint; -}; - -type CoverageRow = { - rollup_version: string | number | bigint; - live_capture_start_hour: string | Date | null; - coverage_start_hour: string | Date | null; - last_reconciled_at: string | Date | null; - database_now: string | Date; -}; - -type DegradedIntervalRow = { - id: string; - start_hour: string | Date; - end_hour_exclusive: string | Date; - source: CostInsightSpendSource | null; - reason: string; - detected_at: string | Date; -}; - -type InteriorDriverRow = TopDriverRow & { - driver_key: string; -}; - -type MergeableSpendDriver = OwnerTopSpendDriver & { - driverKey: string; -}; - -type DatabaseTimestampRow = { - value: string | Date; -}; - -type ExactRollingDatabase = Pick; - -function ownerPredicate( - owner: CostInsightSpendOwner, - ownedByUserId: SQL, - ownedByOrganizationId: SQL -): SQL { - return owner.type === 'organization' - ? sql`${ownedByUserId} IS NULL AND ${ownedByOrganizationId} = ${owner.id}` - : sql`${ownedByOrganizationId} IS NULL AND ${ownedByUserId} = ${owner.id}`; -} - -function normalizeDatabaseTimestamp(value: string | Date, fieldName: string): string { - const timestamp = new Date(value); - if (!Number.isFinite(timestamp.getTime())) { - throw new Error(`${fieldName} is not a valid timestamp.`); - } - return timestamp.toISOString(); -} - -function normalizeNullableDatabaseTimestamp( - value: string | Date | null, - fieldName: string -): string | null { - return value === null ? null : normalizeDatabaseTimestamp(value, fieldName); -} - -function requireHourlyRange(params: { - startHour: string; - endHourExclusive: string; - maxBuckets?: number; -}): { startHour: string; endHourExclusive: string; bucketCount: number } { - const startHour = requireUtcHour(params.startHour, 'startHour'); - const endHourExclusive = requireUtcHour(params.endHourExclusive, 'endHourExclusive'); - const bucketCount = (Date.parse(endHourExclusive) - Date.parse(startHour)) / HOUR_MS; - const maxBuckets = params.maxBuckets ?? COST_INSIGHT_MAX_HOURLY_BUCKETS; - if (!Number.isInteger(bucketCount) || bucketCount <= 0 || bucketCount > maxBuckets) { - throw new Error(`Cost Insights range must contain between 1 and ${maxBuckets} UTC hours.`); - } - return { startHour, endHourExclusive, bucketCount }; -} - -function sumSafe(left: number, right: number, fieldName: string): number { - const total = left + right; - if (!Number.isSafeInteger(total)) { - throw new Error(`${fieldName} is outside the JavaScript safe-integer range.`); - } - return total; -} - -export function groupContiguousHourlyIntervals( - points: readonly Pick[], - isCovered: boolean -): HourInterval[] { - const intervals: HourInterval[] = []; - for (const point of points) { - if (point.isCovered !== isCovered) continue; - const previous = intervals.at(-1); - if (previous && previous.endHourExclusive === point.hourStart) { - previous.endHourExclusive = addHours(point.hourStart, 1); - } else { - intervals.push({ - startHour: point.hourStart, - endHourExclusive: addHours(point.hourStart, 1), - }); - } - } - return intervals; -} - -function addHours(hourStart: string, hours: number): string { - return new Date(Date.parse(hourStart) + hours * HOUR_MS).toISOString(); -} - -function hasIntervals(intervals: readonly T[]): intervals is readonly [T, ...T[]] { - return intervals.length > 0; -} - -function toCanonicalIntervals( - intervals: readonly [HourInterval, ...HourInterval[]] -): readonly [ - { startInclusive: string; endExclusive: string }, - ...{ startInclusive: string; endExclusive: string }[], -] { - return intervals.map(interval => ({ - startInclusive: interval.startHour, - endExclusive: interval.endHourExclusive, - })) as [ - { startInclusive: string; endExclusive: string }, - ...{ startInclusive: string; endExclusive: string }[], - ]; -} - -function summarizeCanonicalHourlySpend( - hourly: Awaited> -): { variableMicrodollars: number; scheduledMicrodollars: number } { - let variableMicrodollars = 0; - let scheduledMicrodollars = 0; - for (const aggregation of hourly) { - for (const total of aggregation.totals) { - if (total.category === 'variable') { - variableMicrodollars = sumSafe( - variableMicrodollars, - total.totalMicrodollars, - 'canonical hourly variable microdollars' - ); - } else { - scheduledMicrodollars = sumSafe( - scheduledMicrodollars, - total.totalMicrodollars, - 'canonical hourly scheduled microdollars' - ); - } - } - } - return { variableMicrodollars, scheduledMicrodollars }; -} - -function canonicalHourlyDrivers( - hourly: Awaited>, - owner: CostInsightSpendOwner -): MergeableSpendDriver[] { - return hourly.flatMap(aggregation => - aggregation.drivers.map(driver => { - if (driver.owner.type !== owner.type || driver.owner.id !== owner.id) { - throw new Error('Canonical Cost Insights driver resolved to the wrong Spend owner.'); - } - return { - category: driver.category, - driverKey: driver.driverKey, - source: driver.source, - productKey: driver.productKey, - featureKey: driver.featureKey, - modelOrPlanKey: driver.modelOrPlanKey, - providerKey: driver.providerKey, - actorUserId: driver.actorUserId, - totalMicrodollars: driver.totalMicrodollars, - spendRecordCount: driver.spendRecordCount, - }; - }) - ); -} - -async function withCanonicalFallbackTelemetry( - operation: 'dashboard_evidence_90d' | 'rolling_spend' | 'rolling_driver_evidence', - intervals: readonly HourInterval[], - callback: () => Promise -): Promise { - if (intervals.length === 0) return await callback(); - const intervalHours = intervals.reduce( - (total, interval) => - sumSafe( - total, - (Date.parse(interval.endHourExclusive) - Date.parse(interval.startHour)) / HOUR_MS, - 'canonical fallback interval hours' - ), - 0 - ); - return await startSpan( - { name: `cost_insights.${operation}`, op: 'cost_insights.canonical_fallback' }, - async span => { - const startedAt = performance.now(); - span.setAttribute('cost_insights.operation', operation); - span.setAttribute('cost_insights.occurrence', 1); - span.setAttribute('cost_insights.interval_count', intervals.length); - span.setAttribute('cost_insights.interval_hours', intervalHours); - try { - return await callback(); - } finally { - span.setAttribute( - 'cost_insights.fallback_duration_ms', - Math.round(performance.now() - startedAt) - ); - } - } - ); -} - -function floorUtcHour(timestamp: number): number { - return Math.floor(timestamp / HOUR_MS) * HOUR_MS; -} - -function ceilUtcHour(timestamp: number): number { - return Math.ceil(timestamp / HOUR_MS) * HOUR_MS; -} - -export function getRollingWindowFragments( - asOfInput: string, - windowHours: number -): RollingWindowFragments { - if (!Number.isSafeInteger(windowHours) || windowHours <= 0 || windowHours > 24 * 90) { - throw new Error('Cost Insights rolling window must contain between 1 and 2160 hours.'); - } - const asOf = requireUtcTimestamp(asOfInput, 'asOf'); - const asOfTimestamp = Date.parse(asOf); - const windowStartTimestamp = asOfTimestamp - windowHours * HOUR_MS; - const oldestBoundaryEndTimestamp = ceilUtcHour(windowStartTimestamp); - const currentBoundaryStartTimestamp = floorUtcHour(asOfTimestamp); - return { - asOf, - windowStart: new Date(windowStartTimestamp).toISOString(), - oldestBoundaryEnd: new Date(oldestBoundaryEndTimestamp).toISOString(), - interiorStart: new Date(oldestBoundaryEndTimestamp).toISOString(), - interiorEnd: new Date(currentBoundaryStartTimestamp).toISOString(), - currentBoundaryStart: new Date(currentBoundaryStartTimestamp).toISOString(), - }; -} - -export function getRolling24HourFragments(asOfInput: string): Rolling24HourFragments { - return getRollingWindowFragments(asOfInput, 24); -} - -export async function getOwnerHourlySpend( - executor: CostInsightQueryExecutor, - params: { - owner: CostInsightSpendOwner; - startHour: string; - endHourExclusive: string; - } -): Promise { - const range = requireHourlyRange(params); - const owner = params.owner; - const result = await executor.execute(sql` - WITH hours AS ( - SELECT generate_series( - ${range.startHour}::timestamptz, - ${range.endHourExclusive}::timestamptz - INTERVAL '1 hour', - INTERVAL '1 hour' - ) AS hour_start - ), coverage_status AS ( - SELECT - hours.hour_start, - COALESCE( - hours.hour_start >= COALESCE( - ${cost_insight_rollup_coverage.coverage_start_hour}, - ${cost_insight_rollup_coverage.live_capture_start_hour} - ) - AND hours.hour_start <= date_trunc('hour', CURRENT_TIMESTAMP, 'UTC') - AND NOT EXISTS ( - SELECT 1 - FROM ${cost_insight_rollup_degraded_intervals} degraded - WHERE degraded.resolved_at IS NULL - AND degraded.start_hour < hours.hour_start + INTERVAL '1 hour' - AND degraded.end_hour_exclusive > hours.hour_start - ), - FALSE - ) AS is_covered - FROM hours - LEFT JOIN ${cost_insight_rollup_coverage} - ON ${cost_insight_rollup_coverage.rollup_version} = ${COST_INSIGHT_ROLLUP_VERSION} - ) - SELECT - coverage_status.hour_start, - CASE WHEN coverage_status.is_covered - THEN COALESCE(variable_total.total_microdollars, 0)::text ELSE NULL END - AS variable_microdollars, - CASE WHEN coverage_status.is_covered - THEN COALESCE(scheduled_total.total_microdollars, 0)::text ELSE NULL END - AS scheduled_microdollars, - CASE WHEN coverage_status.is_covered - THEN COALESCE(variable_total.spend_record_count, 0)::text ELSE NULL END - AS variable_record_count, - CASE WHEN coverage_status.is_covered - THEN COALESCE(scheduled_total.spend_record_count, 0)::text ELSE NULL END - AS scheduled_record_count, - coverage_status.is_covered - FROM coverage_status - LEFT JOIN ${cost_insight_owner_hour_totals} variable_total - ON variable_total.hour_start = coverage_status.hour_start - AND variable_total.spend_category = 'variable' - AND ${ownerPredicate( - owner, - sql`variable_total.owned_by_user_id`, - sql`variable_total.owned_by_organization_id` - )} - LEFT JOIN ${cost_insight_owner_hour_totals} scheduled_total - ON scheduled_total.hour_start = coverage_status.hour_start - AND scheduled_total.spend_category = 'scheduled' - AND ${ownerPredicate( - owner, - sql`scheduled_total.owned_by_user_id`, - sql`scheduled_total.owned_by_organization_id` - )} - ORDER BY coverage_status.hour_start ASC - `); - - return result.rows.map(row => { - const hourStart = normalizeDatabaseTimestamp(row.hour_start, 'hour_start'); - if (!row.is_covered) { - return { - hourStart, - variableMicrodollars: null, - scheduledMicrodollars: null, - totalMicrodollars: null, - variableRecordCount: null, - scheduledRecordCount: null, - isCovered: false, - }; - } - if ( - row.variable_microdollars === null || - row.scheduled_microdollars === null || - row.variable_record_count === null || - row.scheduled_record_count === null - ) { - throw new Error('Covered Cost Insights hour returned incomplete aggregate values.'); - } - const variableMicrodollars = parseSafeDatabaseInteger( - row.variable_microdollars, - 'variable_microdollars' - ); - const scheduledMicrodollars = parseSafeDatabaseInteger( - row.scheduled_microdollars, - 'scheduled_microdollars' - ); - return { - hourStart, - variableMicrodollars, - scheduledMicrodollars, - totalMicrodollars: sumSafe( - variableMicrodollars, - scheduledMicrodollars, - 'hourly total microdollars' - ), - variableRecordCount: parseSafeDatabaseInteger( - row.variable_record_count, - 'variable_record_count' - ), - scheduledRecordCount: parseSafeDatabaseInteger( - row.scheduled_record_count, - 'scheduled_record_count' - ), - isCovered: true, - }; - }); -} - -function canonicalHourlySpend( - hourly: Awaited>, - intervals: readonly HourInterval[] -): OwnerHourlySpend[] { - const byHour = new Map(hourly.map(hour => [hour.hourStart, hour])); - const points: OwnerHourlySpend[] = []; - for (const interval of intervals) { - for ( - let hourStart = interval.startHour; - hourStart < interval.endHourExclusive; - hourStart = addHours(hourStart, 1) - ) { - const aggregation = byHour.get(hourStart); - const variable = aggregation?.totals.find(total => total.category === 'variable'); - const scheduled = aggregation?.totals.find(total => total.category === 'scheduled'); - const variableMicrodollars = variable?.totalMicrodollars ?? 0; - const scheduledMicrodollars = scheduled?.totalMicrodollars ?? 0; - points.push({ - hourStart, - variableMicrodollars, - scheduledMicrodollars, - totalMicrodollars: sumSafe( - variableMicrodollars, - scheduledMicrodollars, - 'canonical hourly total microdollars' - ), - variableRecordCount: variable?.spendRecordCount ?? 0, - scheduledRecordCount: scheduled?.spendRecordCount ?? 0, - isCovered: true, - }); - } - } - return points; -} - -async function getOwnerHourlyDriverSpend( - executor: CostInsightQueryExecutor, - params: { - owner: CostInsightSpendOwner; - startHour: string; - endHourExclusive: string; - } -): Promise>> { - const range = requireHourlyRange(params); - const owner = params.owner; - const result = await executor.execute(sql` - SELECT - ${cost_insight_owner_hour_driver_buckets.hour_start} AS hour_start, - COALESCE(SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars}) - FILTER (WHERE ${cost_insight_owner_hour_driver_buckets.spend_category} = 'variable'), 0)::text - AS variable_microdollars, - COALESCE(SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars}) - FILTER (WHERE ${cost_insight_owner_hour_driver_buckets.spend_category} = 'scheduled'), 0)::text - AS scheduled_microdollars, - COALESCE(SUM(${cost_insight_owner_hour_driver_buckets.spend_record_count}) - FILTER (WHERE ${cost_insight_owner_hour_driver_buckets.spend_category} = 'variable'), 0)::text - AS variable_record_count, - COALESCE(SUM(${cost_insight_owner_hour_driver_buckets.spend_record_count}) - FILTER (WHERE ${cost_insight_owner_hour_driver_buckets.spend_category} = 'scheduled'), 0)::text - AS scheduled_record_count - FROM ${cost_insight_owner_hour_driver_buckets} - WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= ${range.startHour} - AND ${cost_insight_owner_hour_driver_buckets.hour_start} < ${range.endHourExclusive} - AND ${ownerPredicate( - owner, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` - )} - GROUP BY 1 - ORDER BY 1 ASC - `); - - return new Map( - result.rows.map(row => { - const variableMicrodollars = parseSafeDatabaseInteger( - row.variable_microdollars, - 'driver variable_microdollars' - ); - const scheduledMicrodollars = parseSafeDatabaseInteger( - row.scheduled_microdollars, - 'driver scheduled_microdollars' - ); - return [ - normalizeDatabaseTimestamp(row.hour_start, 'driver hour_start'), - { - variableMicrodollars, - scheduledMicrodollars, - totalMicrodollars: sumSafe( - variableMicrodollars, - scheduledMicrodollars, - 'driver hourly total microdollars' - ), - variableRecordCount: parseSafeDatabaseInteger( - row.variable_record_count, - 'driver variable_record_count' - ), - scheduledRecordCount: parseSafeDatabaseInteger( - row.scheduled_record_count, - 'driver scheduled_record_count' - ), - }, - ]; - }) - ); -} - -export async function loadOwnerDashboardHourlySpend( - primaryDatabase: ExactRollingDatabase, - params: { - owner: CostInsightSpendOwner; - startHour: string; - endHourExclusive: string; - } -): Promise { - const range = requireHourlyRange(params); - return await primaryDatabase.transaction( - async transaction => { - const rollupPoints = await getOwnerHourlySpend(transaction, { - owner: params.owner, - startHour: range.startHour, - endHourExclusive: range.endHourExclusive, - }); - const coverage = await getCostInsightRollupCoverage(transaction, range); - const effectiveCoverageStart = coverage.coverageStartHour ?? coverage.liveCaptureStartHour; - const uncoveredIntervals = groupContiguousHourlyIntervals( - rollupPoints.filter( - point => - !point.isCovered && - effectiveCoverageStart !== null && - point.hourStart >= effectiveCoverageStart - ), - false - ); - const canonicalByHour = hasIntervals(uncoveredIntervals) - ? new Map( - ( - await withCanonicalFallbackTelemetry( - 'dashboard_evidence_90d', - uncoveredIntervals, - async () => - canonicalHourlySpend( - await loadCanonicalCostInsightAggregationsByIntervals(transaction, { - owner: params.owner, - intervals: toCanonicalIntervals(uncoveredIntervals), - }), - uncoveredIntervals - ) - ) - ).map(point => [point.hourStart, point]) - ) - : new Map(); - const pointsWithCanonicalFallback = rollupPoints.map(point => - point.isCovered ? point : (canonicalByHour.get(point.hourStart) ?? point) - ); - - const preCoverageEnd = effectiveCoverageStart - ? new Date( - Math.min(Date.parse(effectiveCoverageStart), Date.parse(range.endHourExclusive)) - ).toISOString() - : range.endHourExclusive; - if (preCoverageEnd <= range.startHour) return pointsWithCanonicalFallback; - - const driverSpendByHour = await getOwnerHourlyDriverSpend(transaction, { - owner: params.owner, - startHour: range.startHour, - endHourExclusive: preCoverageEnd, - }); - return pointsWithCanonicalFallback.map(point => { - if (point.isCovered || point.hourStart >= preCoverageEnd) return point; - const driverSpend = driverSpendByHour.get(point.hourStart); - return driverSpend ? { ...point, ...driverSpend } : point; - }); - }, - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ); -} - -export async function getOwnerTopSpendDrivers( - executor: CostInsightQueryExecutor, - params: { - owner: CostInsightSpendOwner; - startHour: string; - endHourExclusive: string; - category?: CostInsightSpendCategory; - limit?: number; - } -): Promise { - const range = requireHourlyRange(params); - const requestedLimit = params.limit ?? COST_INSIGHT_MAX_TOP_DRIVERS; - if (!Number.isSafeInteger(requestedLimit) || requestedLimit <= 0) { - throw new Error('Cost Insights top-driver limit must be a positive safe integer.'); - } - const limit = Math.min(requestedLimit, COST_INSIGHT_MAX_TOP_DRIVERS); - const categoryPredicate = params.category - ? sql`${cost_insight_owner_hour_driver_buckets.spend_category} = ${params.category}` - : sql`TRUE`; - const owner = params.owner; - const result = await executor.execute(sql` - SELECT - ${cost_insight_owner_hour_driver_buckets.spend_category} AS spend_category, - ${cost_insight_owner_hour_driver_buckets.source} AS source, - ${cost_insight_owner_hour_driver_buckets.product_key} AS product_key, - ${cost_insight_owner_hour_driver_buckets.feature_key} AS feature_key, - ${cost_insight_owner_hour_driver_buckets.model_or_plan_key} AS model_or_plan_key, - ${cost_insight_owner_hour_driver_buckets.provider_key} AS provider_key, - ${cost_insight_owner_hour_driver_buckets.actor_user_id} AS actor_user_id, - SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars})::text - AS total_microdollars, - SUM(${cost_insight_owner_hour_driver_buckets.spend_record_count})::text - AS spend_record_count - FROM ${cost_insight_owner_hour_driver_buckets} - WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= ${range.startHour} - AND ${cost_insight_owner_hour_driver_buckets.hour_start} < ${range.endHourExclusive} - AND ${ownerPredicate( - owner, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` - )} - AND ${categoryPredicate} - GROUP BY 1, 2, 3, 4, 5, 6, 7 - ORDER BY - SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars}) DESC, - ${cost_insight_owner_hour_driver_buckets.spend_category} ASC, - ${cost_insight_owner_hour_driver_buckets.source} ASC, - ${cost_insight_owner_hour_driver_buckets.product_key} ASC, - ${cost_insight_owner_hour_driver_buckets.feature_key} ASC, - ${cost_insight_owner_hour_driver_buckets.model_or_plan_key} ASC, - ${cost_insight_owner_hour_driver_buckets.provider_key} ASC, - ${cost_insight_owner_hour_driver_buckets.actor_user_id} ASC - LIMIT ${limit} - `); - return result.rows.map(row => ({ - category: row.spend_category, - source: row.source, - productKey: row.product_key, - featureKey: row.feature_key, - modelOrPlanKey: row.model_or_plan_key, - providerKey: row.provider_key, - actorUserId: row.actor_user_id, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'top-driver total_microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'top-driver spend_record_count' - ), - })); -} - -export async function getOwnerTopSpendDriversByRange( - executor: CostInsightQueryExecutor, - params: { - owner: CostInsightSpendOwner; - ranges: readonly { key: string; startHour: string }[]; - endHourExclusive: string; - limit?: number; - } -): Promise> { - const endHourExclusive = requireUtcHour(params.endHourExclusive, 'endHourExclusive'); - if (params.ranges.length === 0) { - throw new Error('Cost Insights top-driver ranges must not be empty.'); - } - - const rangeKeys = new Set(); - const ranges = params.ranges.map(range => { - if (!range.key) { - throw new Error('Cost Insights top-driver range keys must not be empty.'); - } - if (rangeKeys.has(range.key)) { - throw new Error(`Cost Insights top-driver range key ${range.key} is duplicated.`); - } - rangeKeys.add(range.key); - return { - key: range.key, - ...requireHourlyRange({ startHour: range.startHour, endHourExclusive }), - }; - }); - - const requestedLimit = params.limit ?? COST_INSIGHT_MAX_TOP_DRIVERS; - if (!Number.isSafeInteger(requestedLimit) || requestedLimit <= 0) { - throw new Error('Cost Insights top-driver limit must be a positive safe integer.'); - } - const limit = Math.min(requestedLimit, COST_INSIGHT_MAX_TOP_DRIVERS); - const owner = params.owner; - const rangeValues = sql.join( - ranges.map(range => sql`(${range.key}, ${range.startHour}::timestamptz)`), - sql`, ` - ); - const result = await executor.execute(sql` - WITH ranges(range_key, start_hour) AS ( - VALUES ${rangeValues} - ), owner_buckets AS MATERIALIZED ( - SELECT - ${cost_insight_owner_hour_driver_buckets.hour_start} AS hour_start, - ${cost_insight_owner_hour_driver_buckets.spend_category} AS spend_category, - ${cost_insight_owner_hour_driver_buckets.source} AS source, - ${cost_insight_owner_hour_driver_buckets.product_key} AS product_key, - ${cost_insight_owner_hour_driver_buckets.feature_key} AS feature_key, - ${cost_insight_owner_hour_driver_buckets.model_or_plan_key} AS model_or_plan_key, - ${cost_insight_owner_hour_driver_buckets.provider_key} AS provider_key, - ${cost_insight_owner_hour_driver_buckets.actor_user_id} AS actor_user_id, - ${cost_insight_owner_hour_driver_buckets.total_microdollars} AS total_microdollars, - ${cost_insight_owner_hour_driver_buckets.spend_record_count} AS spend_record_count - FROM ${cost_insight_owner_hour_driver_buckets} - WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= ( - SELECT MIN(start_hour) FROM ranges - ) - AND ${cost_insight_owner_hour_driver_buckets.hour_start} < ${endHourExclusive} - AND ${ownerPredicate( - owner, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` - )} - ), driver_totals AS ( - SELECT - ranges.range_key, - owner_buckets.spend_category AS spend_category, - owner_buckets.source AS source, - owner_buckets.product_key AS product_key, - owner_buckets.feature_key AS feature_key, - owner_buckets.model_or_plan_key AS model_or_plan_key, - owner_buckets.provider_key AS provider_key, - owner_buckets.actor_user_id AS actor_user_id, - SUM(owner_buckets.total_microdollars)::text - AS total_microdollars, - SUM(owner_buckets.spend_record_count)::text - AS spend_record_count - FROM owner_buckets - CROSS JOIN ranges - WHERE owner_buckets.hour_start >= ranges.start_hour - GROUP BY 1, 2, 3, 4, 5, 6, 7, 8 - ), ranked_drivers AS ( - SELECT - *, - ROW_NUMBER() OVER ( - PARTITION BY range_key - ORDER BY - total_microdollars::numeric DESC, - spend_category ASC, - source ASC, - product_key ASC, - feature_key ASC, - model_or_plan_key ASC, - provider_key ASC, - actor_user_id ASC - ) AS rank - FROM driver_totals - ) - SELECT - range_key, - spend_category, - source, - product_key, - feature_key, - model_or_plan_key, - provider_key, - actor_user_id, - total_microdollars, - spend_record_count - FROM ranked_drivers - WHERE rank <= ${limit} - ORDER BY range_key ASC, rank ASC - `); - - const driversByRange = new Map(); - for (const range of ranges) { - driversByRange.set(range.key, []); - } - for (const row of result.rows) { - const drivers = driversByRange.get(row.range_key); - if (!drivers) { - throw new Error(`Cost Insights top-driver query returned unknown range ${row.range_key}.`); - } - drivers.push({ - category: row.spend_category, - source: row.source, - productKey: row.product_key, - featureKey: row.feature_key, - modelOrPlanKey: row.model_or_plan_key, - providerKey: row.provider_key, - actorUserId: row.actor_user_id, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'top-driver total_microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'top-driver spend_record_count' - ), - }); - } - return driversByRange; -} - -export async function getOwnerCurrentHourSpend( - primaryExecutor: CostInsightQueryExecutor, - owner: CostInsightSpendOwner -): Promise<{ - variableMicrodollars: number; - scheduledMicrodollars: number; - totalMicrodollars: number; - variableRecordCount: number; - scheduledRecordCount: number; -}> { - const result = await primaryExecutor.execute(sql` - SELECT - COALESCE(SUM(total_microdollars) FILTER (WHERE spend_category = 'variable'), 0)::text - AS variable_microdollars, - COALESCE(SUM(total_microdollars) FILTER (WHERE spend_category = 'scheduled'), 0)::text - AS scheduled_microdollars, - COALESCE(SUM(spend_record_count) FILTER (WHERE spend_category = 'variable'), 0)::text - AS variable_record_count, - COALESCE(SUM(spend_record_count) FILTER (WHERE spend_category = 'scheduled'), 0)::text - AS scheduled_record_count - FROM ${cost_insight_owner_hour_totals} - WHERE ${cost_insight_owner_hour_totals.hour_start} = date_trunc( - 'hour', CURRENT_TIMESTAMP, 'UTC' - ) - AND ${ownerPredicate( - owner, - sql`${cost_insight_owner_hour_totals.owned_by_user_id}`, - sql`${cost_insight_owner_hour_totals.owned_by_organization_id}` - )} - `); - const row = result.rows[0]; - if (!row) { - throw new Error('Cost Insights current-hour query returned no aggregate row.'); - } - const variableMicrodollars = parseSafeDatabaseInteger( - row.variable_microdollars, - 'current-hour variable_microdollars' - ); - const scheduledMicrodollars = parseSafeDatabaseInteger( - row.scheduled_microdollars, - 'current-hour scheduled_microdollars' - ); - return { - variableMicrodollars, - scheduledMicrodollars, - totalMicrodollars: sumSafe( - variableMicrodollars, - scheduledMicrodollars, - 'current-hour total microdollars' - ), - variableRecordCount: parseSafeDatabaseInteger( - row.variable_record_count, - 'current-hour variable_record_count' - ), - scheduledRecordCount: parseSafeDatabaseInteger( - row.scheduled_record_count, - 'current-hour scheduled_record_count' - ), - }; -} - -export async function getCostInsightRollupCoverage( - executor: CostInsightQueryExecutor, - params: { startHour: string; endHourExclusive: string } -): Promise { - const range = requireHourlyRange(params); - const coverageResult = await executor.execute(sql` - SELECT - ${cost_insight_rollup_coverage.rollup_version} AS rollup_version, - ${cost_insight_rollup_coverage.live_capture_start_hour} AS live_capture_start_hour, - ${cost_insight_rollup_coverage.coverage_start_hour} AS coverage_start_hour, - ${cost_insight_rollup_coverage.last_reconciled_at} AS last_reconciled_at, - CURRENT_TIMESTAMP AS database_now - FROM ${cost_insight_rollup_coverage} - WHERE ${cost_insight_rollup_coverage.rollup_version} = ${COST_INSIGHT_ROLLUP_VERSION} - LIMIT 1 - `); - const degradedResult = await executor.execute(sql` - SELECT - ${cost_insight_rollup_degraded_intervals.id} AS id, - ${cost_insight_rollup_degraded_intervals.start_hour} AS start_hour, - ${cost_insight_rollup_degraded_intervals.end_hour_exclusive} AS end_hour_exclusive, - ${cost_insight_rollup_degraded_intervals.source} AS source, - ${cost_insight_rollup_degraded_intervals.reason} AS reason, - ${cost_insight_rollup_degraded_intervals.detected_at} AS detected_at - FROM ${cost_insight_rollup_degraded_intervals} - WHERE ${cost_insight_rollup_degraded_intervals.resolved_at} IS NULL - AND ${cost_insight_rollup_degraded_intervals.start_hour} < ${range.endHourExclusive} - AND ${cost_insight_rollup_degraded_intervals.end_hour_exclusive} > ${range.startHour} - ORDER BY - ${cost_insight_rollup_degraded_intervals.start_hour} ASC, - ${cost_insight_rollup_degraded_intervals.id} ASC - `); - - const coverageRow = coverageResult.rows[0]; - const liveCaptureStartHour = coverageRow - ? normalizeNullableDatabaseTimestamp( - coverageRow.live_capture_start_hour, - 'live_capture_start_hour' - ) - : null; - const coverageStartHour = coverageRow - ? normalizeNullableDatabaseTimestamp(coverageRow.coverage_start_hour, 'coverage_start_hour') - : null; - const effectiveCoverageStart = coverageStartHour ?? liveCaptureStartHour; - const databaseNow = coverageRow - ? Date.parse(normalizeDatabaseTimestamp(coverageRow.database_now, 'database_now')) - : Number.NEGATIVE_INFINITY; - const latestCoveredEnd = ceilUtcHour(databaseNow); - const degradedIntervals = degradedResult.rows.map(row => ({ - id: row.id, - startHour: normalizeDatabaseTimestamp(row.start_hour, 'degraded start_hour'), - endHourExclusive: normalizeDatabaseTimestamp( - row.end_hour_exclusive, - 'degraded end_hour_exclusive' - ), - source: row.source, - reason: row.reason, - detectedAt: normalizeDatabaseTimestamp(row.detected_at, 'degraded detected_at'), - })); - - return { - rollupVersion: coverageRow - ? parseSafeDatabaseInteger(coverageRow.rollup_version, 'rollup_version') - : COST_INSIGHT_ROLLUP_VERSION, - liveCaptureStartHour, - coverageStartHour, - lastReconciledAt: coverageRow - ? normalizeNullableDatabaseTimestamp(coverageRow.last_reconciled_at, 'last_reconciled_at') - : null, - degradedIntervals, - isFullyCovered: - effectiveCoverageStart !== null && - Date.parse(range.startHour) >= Date.parse(effectiveCoverageStart) && - Date.parse(range.endHourExclusive) <= latestCoveredEnd && - degradedIntervals.length === 0, - }; -} - -function compareTopSpendDrivers(left: OwnerTopSpendDriver, right: OwnerTopSpendDriver): number { - if (left.totalMicrodollars !== right.totalMicrodollars) { - return left.totalMicrodollars > right.totalMicrodollars ? -1 : 1; - } - const leftKey = [ - left.category, - left.source, - left.productKey, - left.featureKey, - left.modelOrPlanKey, - left.providerKey, - left.actorUserId, - ].join('\u0000'); - const rightKey = [ - right.category, - right.source, - right.productKey, - right.featureKey, - right.modelOrPlanKey, - right.providerKey, - right.actorUserId, - ].join('\u0000'); - return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; -} - -function compareMergeableSpendDrivers( - left: MergeableSpendDriver, - right: MergeableSpendDriver -): number { - const dimensionOrder = compareTopSpendDrivers(left, right); - if (dimensionOrder !== 0) return dimensionOrder; - return left.driverKey < right.driverKey ? -1 : left.driverKey > right.driverKey ? 1 : 0; -} - -function haveMatchingDriverDimensions( - left: MergeableSpendDriver, - right: MergeableSpendDriver -): boolean { - return ( - left.source === right.source && - left.productKey === right.productKey && - left.featureKey === right.featureKey && - left.modelOrPlanKey === right.modelOrPlanKey && - left.providerKey === right.providerKey && - left.actorUserId === right.actorUserId - ); -} - -function mergeSpendDrivers(driverGroups: MergeableSpendDriver[][]): MergeableSpendDriver[] { - const merged = new Map(); - for (const drivers of driverGroups) { - for (const driver of drivers) { - const identity = JSON.stringify([driver.category, driver.driverKey]); - const existing = merged.get(identity); - if (!existing) { - merged.set(identity, { - ...driver, - totalMicrodollars: sumSafe( - 0, - driver.totalMicrodollars, - 'exact driver total_microdollars' - ), - spendRecordCount: sumSafe(0, driver.spendRecordCount, 'exact driver spend_record_count'), - }); - continue; - } - if (!haveMatchingDriverDimensions(existing, driver)) { - throw new Error( - `Cost Insights driver ${identity} has mismatched dimensions across exact evidence fragments.` - ); - } - existing.totalMicrodollars = sumSafe( - existing.totalMicrodollars, - driver.totalMicrodollars, - 'exact driver total_microdollars' - ); - existing.spendRecordCount = sumSafe( - existing.spendRecordCount, - driver.spendRecordCount, - 'exact driver spend_record_count' - ); - } - } - return [...merged.values()]; -} - -function summarizeSpendDrivers(drivers: MergeableSpendDriver[]): { - variableMicrodollars: number; - scheduledMicrodollars: number; - totalMicrodollars: number; - topDrivers: OwnerTopSpendDriver[]; -} { - let variableMicrodollars = 0; - let scheduledMicrodollars = 0; - for (const driver of drivers) { - if (driver.category === 'variable') { - variableMicrodollars = sumSafe( - variableMicrodollars, - driver.totalMicrodollars, - 'exact driver variable total' - ); - } else { - scheduledMicrodollars = sumSafe( - scheduledMicrodollars, - driver.totalMicrodollars, - 'exact driver scheduled total' - ); - } - } - const topDrivers = [...drivers] - .sort(compareMergeableSpendDrivers) - .slice(0, COST_INSIGHT_MAX_TOP_DRIVERS) - .map(driver => ({ - category: driver.category, - source: driver.source, - productKey: driver.productKey, - featureKey: driver.featureKey, - modelOrPlanKey: driver.modelOrPlanKey, - providerKey: driver.providerKey, - actorUserId: driver.actorUserId, - totalMicrodollars: driver.totalMicrodollars, - spendRecordCount: driver.spendRecordCount, - })); - return { - variableMicrodollars, - scheduledMicrodollars, - totalMicrodollars: sumSafe( - variableMicrodollars, - scheduledMicrodollars, - 'exact driver total microdollars' - ), - topDrivers, - }; -} - -function toMergeableSpendDrivers(drivers: OwnerTopSpendDriver[]): MergeableSpendDriver[] { - return drivers.map(driver => ({ - ...driver, - driverKey: JSON.stringify([ - driver.category, - driver.source, - driver.productKey, - driver.featureKey, - driver.modelOrPlanKey, - driver.providerKey, - driver.actorUserId, - ]), - })); -} - -async function getInteriorRollupDriversByIntervals( - executor: CostInsightQueryExecutor, - owner: CostInsightSpendOwner, - intervals: readonly HourInterval[] -): Promise { - if (!hasIntervals(intervals)) return []; - const first = intervals[0]; - const last = intervals.at(-1); - if (!last) return []; - const intervalValues = sql.join( - intervals.map( - interval => - sql`(${interval.startHour}::timestamptz, ${interval.endHourExclusive}::timestamptz)` - ), - sql`, ` - ); - const result = await executor.execute(sql` - SELECT - ${cost_insight_owner_hour_driver_buckets.spend_category} AS spend_category, - ${cost_insight_owner_hour_driver_buckets.driver_key} AS driver_key, - ${cost_insight_owner_hour_driver_buckets.source} AS source, - ${cost_insight_owner_hour_driver_buckets.product_key} AS product_key, - ${cost_insight_owner_hour_driver_buckets.feature_key} AS feature_key, - ${cost_insight_owner_hour_driver_buckets.model_or_plan_key} AS model_or_plan_key, - ${cost_insight_owner_hour_driver_buckets.provider_key} AS provider_key, - ${cost_insight_owner_hour_driver_buckets.actor_user_id} AS actor_user_id, - SUM(${cost_insight_owner_hour_driver_buckets.total_microdollars})::text - AS total_microdollars, - SUM(${cost_insight_owner_hour_driver_buckets.spend_record_count})::text - AS spend_record_count - FROM ${cost_insight_owner_hour_driver_buckets} - WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= ${first.startHour} - AND ${cost_insight_owner_hour_driver_buckets.hour_start} < ${last.endHourExclusive} - AND EXISTS ( - SELECT 1 - FROM (VALUES ${intervalValues}) AS covered_intervals(start_inclusive, end_exclusive) - WHERE ${cost_insight_owner_hour_driver_buckets.hour_start} >= covered_intervals.start_inclusive - AND ${cost_insight_owner_hour_driver_buckets.hour_start} < covered_intervals.end_exclusive - ) - AND ${ownerPredicate( - owner, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_user_id}`, - sql`${cost_insight_owner_hour_driver_buckets.owned_by_organization_id}` - )} - GROUP BY 1, 2, 3, 4, 5, 6, 7, 8 - ORDER BY 1, 2, 3, 4, 5, 6, 7, 8 - `); - return result.rows.map(row => ({ - category: row.spend_category, - driverKey: row.driver_key, - source: row.source, - productKey: row.product_key, - featureKey: row.feature_key, - modelOrPlanKey: row.model_or_plan_key, - providerKey: row.provider_key, - actorUserId: row.actor_user_id, - totalMicrodollars: parseSafeDatabaseInteger( - row.total_microdollars, - 'interior driver total_microdollars' - ), - spendRecordCount: parseSafeDatabaseInteger( - row.spend_record_count, - 'interior driver spend_record_count' - ), - })); -} - -async function getCanonicalDrivers( - executor: CostInsightQueryExecutor, - params: { - owner: CostInsightSpendOwner; - startInclusive: string; - endExclusive: string; - } -): Promise { - if (params.startInclusive === params.endExclusive) return []; - const aggregation = await loadCanonicalCostInsightAggregation(executor, params); - return aggregation.drivers.map(driver => { - if (driver.owner.type !== params.owner.type || driver.owner.id !== params.owner.id) { - throw new Error('Canonical Cost Insights driver resolved to the wrong Spend owner.'); - } - return { - category: driver.category, - driverKey: driver.driverKey, - source: driver.source, - productKey: driver.productKey, - featureKey: driver.featureKey, - modelOrPlanKey: driver.modelOrPlanKey, - providerKey: driver.providerKey, - actorUserId: driver.actorUserId, - totalMicrodollars: driver.totalMicrodollars, - spendRecordCount: driver.spendRecordCount, - }; - }); -} - -export async function getOwnerSpendDriverEvidenceExact( - primaryDatabase: ExactRollingDatabase, - params: { - owner: CostInsightSpendOwner; - startInclusive: string; - endExclusive: string; - category?: CostInsightSpendCategory; - } -): Promise { - const startInclusive = requireUtcTimestamp(params.startInclusive, 'startInclusive'); - const endExclusive = requireUtcTimestamp(params.endExclusive, 'endExclusive'); - const intervalMilliseconds = Date.parse(endExclusive) - Date.parse(startInclusive); - if ( - intervalMilliseconds <= 0 || - intervalMilliseconds > COST_INSIGHT_MAX_HOURLY_BUCKETS * HOUR_MS - ) { - throw new Error( - 'Cost Insights exact driver interval must be greater than zero and at most 90 days.' - ); - } - - return primaryDatabase.transaction( - async transaction => { - const aggregation = await loadCanonicalCostInsightAggregation(transaction, { - owner: params.owner, - startInclusive, - endExclusive, - }); - const totals = params.category - ? aggregation.totals.filter(total => total.category === params.category) - : aggregation.totals; - const drivers = params.category - ? aggregation.drivers.filter(driver => driver.category === params.category) - : aggregation.drivers; - const variableMicrodollars = totals - .filter(total => total.category === 'variable') - .reduce( - (sum, total) => sumSafe(sum, total.totalMicrodollars, 'exact driver variable total'), - 0 - ); - const scheduledMicrodollars = totals - .filter(total => total.category === 'scheduled') - .reduce( - (sum, total) => sumSafe(sum, total.totalMicrodollars, 'exact driver scheduled total'), - 0 - ); - const topDrivers = drivers - .map(driver => ({ - category: driver.category, - source: driver.source, - productKey: driver.productKey, - featureKey: driver.featureKey, - modelOrPlanKey: driver.modelOrPlanKey, - providerKey: driver.providerKey, - actorUserId: driver.actorUserId, - totalMicrodollars: driver.totalMicrodollars, - spendRecordCount: driver.spendRecordCount, - })) - .sort(compareTopSpendDrivers) - .slice(0, COST_INSIGHT_MAX_TOP_DRIVERS); - - return { - startInclusive, - endExclusive, - variableMicrodollars, - scheduledMicrodollars, - totalMicrodollars: sumSafe( - variableMicrodollars, - scheduledMicrodollars, - 'exact driver total microdollars' - ), - topDrivers, - }; - }, - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ); -} - -export async function getOwnerHourDriverEvidence( - primaryDatabase: ExactRollingDatabase, - params: { - owner: CostInsightSpendOwner; - hourStart: string; - intervalEnd: string; - category?: CostInsightSpendCategory; - } -): Promise { - const hourStart = requireUtcHour(params.hourStart, 'hourStart'); - const intervalEnd = requireUtcTimestamp(params.intervalEnd, 'intervalEnd'); - const hourEndExclusive = new Date(Date.parse(hourStart) + HOUR_MS).toISOString(); - if (Date.parse(intervalEnd) <= Date.parse(hourStart)) { - throw new Error('Cost Insights hour evidence interval must end after hourStart.'); - } - if (Date.parse(intervalEnd) > Date.parse(hourEndExclusive)) { - throw new Error('Cost Insights hour evidence interval cannot exceed one UTC hour.'); - } - const isCompleteHour = intervalEnd === hourEndExclusive; - - return primaryDatabase.transaction( - async transaction => { - const coverage = await getCostInsightRollupCoverage(transaction, { - startHour: hourStart, - endHourExclusive: hourEndExclusive, - }); - - if (!coverage.isFullyCovered) { - const canonicalDrivers = ( - await getCanonicalDrivers(transaction, { - owner: params.owner, - startInclusive: hourStart, - endExclusive: intervalEnd, - }) - ).filter(driver => !params.category || driver.category === params.category); - const canonicalEvidence = summarizeSpendDrivers(canonicalDrivers); - return { - startInclusive: hourStart, - endExclusive: intervalEnd, - ...canonicalEvidence, - usedCanonicalFallback: true, - degradedIntervalCount: coverage.degradedIntervals.length, - }; - } - - if (!isCompleteHour) { - const canonicalDrivers = ( - await getCanonicalDrivers(transaction, { - owner: params.owner, - startInclusive: hourStart, - endExclusive: intervalEnd, - }) - ).filter(driver => !params.category || driver.category === params.category); - const canonicalEvidence = summarizeSpendDrivers(canonicalDrivers); - return { - startInclusive: hourStart, - endExclusive: intervalEnd, - ...canonicalEvidence, - usedCanonicalFallback: true, - degradedIntervalCount: 0, - }; - } - - const hourly = await getOwnerHourlySpend(transaction, { - owner: params.owner, - startHour: hourStart, - endHourExclusive: hourEndExclusive, - }); - const hour = hourly[0]; - if (!hour?.isCovered) { - const canonicalDrivers = ( - await getCanonicalDrivers(transaction, { - owner: params.owner, - startInclusive: hourStart, - endExclusive: intervalEnd, - }) - ).filter(driver => !params.category || driver.category === params.category); - const canonicalEvidence = summarizeSpendDrivers(canonicalDrivers); - return { - startInclusive: hourStart, - endExclusive: intervalEnd, - ...canonicalEvidence, - usedCanonicalFallback: true, - degradedIntervalCount: coverage.degradedIntervals.length, - }; - } - - const drivers = await getOwnerTopSpendDrivers(transaction, { - owner: params.owner, - startHour: hourStart, - endHourExclusive: hourEndExclusive, - category: params.category, - }); - const evidence = summarizeSpendDrivers(toMergeableSpendDrivers(drivers)); - const categoryFiltered = params.category - ? { - variableMicrodollars: - params.category === 'variable' ? (hour.variableMicrodollars ?? 0) : 0, - scheduledMicrodollars: - params.category === 'scheduled' ? (hour.scheduledMicrodollars ?? 0) : 0, - } - : { - variableMicrodollars: hour.variableMicrodollars ?? 0, - scheduledMicrodollars: hour.scheduledMicrodollars ?? 0, - }; - return { - startInclusive: hourStart, - endExclusive: intervalEnd, - variableMicrodollars: categoryFiltered.variableMicrodollars, - scheduledMicrodollars: categoryFiltered.scheduledMicrodollars, - totalMicrodollars: sumSafe( - categoryFiltered.variableMicrodollars, - categoryFiltered.scheduledMicrodollars, - 'hour driver total microdollars' - ), - topDrivers: evidence.topDrivers, - usedCanonicalFallback: false, - degradedIntervalCount: 0, - }; - }, - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ); -} - -export async function getOwnerRollingDriverEvidenceExact( - primaryDatabase: ExactRollingDatabase, - params: { owner: CostInsightSpendOwner; windowHours: number; asOf?: string } -): Promise { - const requestedAsOf = - params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); - - return primaryDatabase.transaction( - async transaction => { - const asOfResult = await transaction.execute(sql` - SELECT COALESCE(${requestedAsOf ?? null}::timestamptz, CURRENT_TIMESTAMP) AS value - `); - const asOfRow = asOfResult.rows[0]; - if (!asOfRow) { - throw new Error('Cost Insights exact driver query could not establish an as-of value.'); - } - const fragments = getRollingWindowFragments( - normalizeDatabaseTimestamp(asOfRow.value, 'as_of'), - params.windowHours - ); - const interiorPoints = - fragments.interiorStart === fragments.interiorEnd - ? [] - : await getOwnerHourlySpend(transaction, { - owner: params.owner, - startHour: fragments.interiorStart, - endHourExclusive: fragments.interiorEnd, - }); - const coveredIntervals = groupContiguousHourlyIntervals(interiorPoints, true); - const uncoveredIntervals = groupContiguousHourlyIntervals(interiorPoints, false); - const canonicalIntervals: HourInterval[] = [ - ...(fragments.windowStart === fragments.oldestBoundaryEnd - ? [] - : [ - { - startHour: fragments.windowStart, - endHourExclusive: fragments.oldestBoundaryEnd, - }, - ]), - ...uncoveredIntervals, - ...(fragments.currentBoundaryStart === fragments.asOf - ? [] - : [ - { - startHour: fragments.currentBoundaryStart, - endHourExclusive: fragments.asOf, - }, - ]), - ]; - const canonicalDrivers = await withCanonicalFallbackTelemetry( - 'rolling_driver_evidence', - uncoveredIntervals, - async () => - hasIntervals(canonicalIntervals) - ? canonicalHourlyDrivers( - await loadCanonicalCostInsightAggregationsByIntervals(transaction, { - owner: params.owner, - intervals: toCanonicalIntervals(canonicalIntervals), - }), - params.owner - ) - : [] - ); - const mergedDrivers = mergeSpendDrivers([ - await getInteriorRollupDriversByIntervals(transaction, params.owner, coveredIntervals), - canonicalDrivers, - ]); - const evidence = summarizeSpendDrivers(mergedDrivers); - return { - asOf: fragments.asOf, - windowStart: fragments.windowStart, - ...evidence, - }; - }, - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ); -} - -export async function getOwnerRolling24HourDriverEvidenceExact( - primaryDatabase: ExactRollingDatabase, - params: { owner: CostInsightSpendOwner; asOf?: string } -): Promise { - return await getOwnerRollingDriverEvidenceExact(primaryDatabase, { - ...params, - windowHours: 24, - }); -} - -export async function getOwnerRollingSpendExact( - primaryDatabase: ExactRollingDatabase, - params: { - owner: CostInsightSpendOwner; - windowHours: number; - asOf?: string; - fallbackToCanonical?: boolean; - } -): Promise { - const requestedAsOf = - params.asOf === undefined ? undefined : requireUtcTimestamp(params.asOf, 'asOf'); - - return primaryDatabase.transaction( - async transaction => { - const asOfResult = await transaction.execute(sql` - SELECT COALESCE(${requestedAsOf ?? null}::timestamptz, CURRENT_TIMESTAMP) AS value - `); - const asOfRow = asOfResult.rows[0]; - if (!asOfRow) { - throw new Error('Cost Insights exact rolling query could not establish an as-of value.'); - } - const fragments = getRollingWindowFragments( - normalizeDatabaseTimestamp(asOfRow.value, 'as_of'), - params.windowHours - ); - const { - asOf, - windowStart, - oldestBoundaryEnd, - interiorStart, - interiorEnd, - currentBoundaryStart, - } = fragments; - - const interiorPoints = - interiorStart === interiorEnd - ? [] - : await getOwnerHourlySpend(transaction, { - owner: params.owner, - startHour: interiorStart, - endHourExclusive: interiorEnd, - }); - const uncoveredIntervals = groupContiguousHourlyIntervals(interiorPoints, false); - if (uncoveredIntervals.length > 0 && !params.fallbackToCanonical) { - return { - asOf, - windowStart, - variableMicrodollars: null, - scheduledMicrodollars: null, - totalMicrodollars: null, - isComplete: false, - }; - } - - let interior = { variableMicrodollars: 0, scheduledMicrodollars: 0 }; - for (const point of interiorPoints) { - if (!point.isCovered) continue; - interior = { - variableMicrodollars: sumSafe( - interior.variableMicrodollars, - point.variableMicrodollars ?? 0, - 'rolling interior variable microdollars' - ), - scheduledMicrodollars: sumSafe( - interior.scheduledMicrodollars, - point.scheduledMicrodollars ?? 0, - 'rolling interior scheduled microdollars' - ), - }; - } - const canonicalIntervals: HourInterval[] = [ - ...(windowStart === oldestBoundaryEnd - ? [] - : [{ startHour: windowStart, endHourExclusive: oldestBoundaryEnd }]), - ...uncoveredIntervals, - ...(currentBoundaryStart === asOf - ? [] - : [{ startHour: currentBoundaryStart, endHourExclusive: asOf }]), - ]; - const canonical = await withCanonicalFallbackTelemetry( - 'rolling_spend', - uncoveredIntervals, - async () => - hasIntervals(canonicalIntervals) - ? summarizeCanonicalHourlySpend( - await loadCanonicalCostInsightAggregationsByIntervals(transaction, { - owner: params.owner, - intervals: toCanonicalIntervals(canonicalIntervals), - }) - ) - : { variableMicrodollars: 0, scheduledMicrodollars: 0 } - ); - interior = { - variableMicrodollars: sumSafe( - interior.variableMicrodollars, - canonical.variableMicrodollars, - 'rolling interior variable microdollars' - ), - scheduledMicrodollars: sumSafe( - interior.scheduledMicrodollars, - canonical.scheduledMicrodollars, - 'rolling interior scheduled microdollars' - ), - }; - const variableMicrodollars = interior.variableMicrodollars; - const scheduledMicrodollars = interior.scheduledMicrodollars; - return { - asOf, - windowStart, - variableMicrodollars, - scheduledMicrodollars, - totalMicrodollars: sumSafe( - variableMicrodollars, - scheduledMicrodollars, - 'rolling total microdollars' - ), - isComplete: true, - }; - }, - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ); -} - -export async function getOwnerRolling24HourSpendExact( - primaryDatabase: ExactRollingDatabase, - params: { owner: CostInsightSpendOwner; asOf?: string; fallbackToCanonical?: boolean } -): Promise { - return await getOwnerRollingSpendExact(primaryDatabase, { - ...params, - windowHours: 24, - }); -} diff --git a/apps/web/src/lib/cost-insights/suggestion-eligibility.test.ts b/apps/web/src/lib/cost-insights/suggestion-eligibility.test.ts deleted file mode 100644 index bca2e90de23..00000000000 --- a/apps/web/src/lib/cost-insights/suggestion-eligibility.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MINIMAX_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/minimax'; -import type { OwnerTopSpendDriver } from './spend-repository'; -import { isCodingPlanSuggestionEligible } from './suggestion-eligibility'; - -const minimumSpendMicrodollars = 50_000_000; -const coveredDriver = { - category: 'variable', - source: 'ai_gateway', - productKey: 'cli', - featureKey: 'messages', - modelOrPlanKey: MINIMAX_CURRENT_MODEL_ID, - providerKey: 'minimax', - actorUserId: 'user-1', - totalMicrodollars: minimumSpendMicrodollars, - spendRecordCount: 1, -} satisfies OwnerTopSpendDriver; - -describe('Coding Plan suggestion eligibility', () => { - test('allows personal usage of a model covered by a Coding Plan', () => { - expect( - isCodingPlanSuggestionEligible( - { type: 'user', id: 'user-1' }, - coveredDriver, - minimumSpendMicrodollars - ) - ).toBe(true); - }); - - test('rejects organization-owned usage', () => { - expect( - isCodingPlanSuggestionEligible( - { type: 'organization', id: '00000000-0000-0000-0000-000000000001' }, - coveredDriver, - minimumSpendMicrodollars - ) - ).toBe(false); - }); - - test.each([ - ['anthropic', 'anthropic/claude-opus-4.8'], - ['openai', 'openai/gpt-5.4'], - ['minimax', 'minimax/minimax-unsupported'], - ])('rejects unsupported provider/model %s %s', (providerKey, modelOrPlanKey) => { - expect( - isCodingPlanSuggestionEligible( - { type: 'user', id: 'user-1' }, - { ...coveredDriver, providerKey, modelOrPlanKey }, - minimumSpendMicrodollars - ) - ).toBe(false); - }); - - test('rejects scheduled spend', () => { - expect( - isCodingPlanSuggestionEligible( - { type: 'user', id: 'user-1' }, - { ...coveredDriver, category: 'scheduled' }, - minimumSpendMicrodollars - ) - ).toBe(false); - }); -}); diff --git a/apps/web/src/lib/cost-insights/suggestion-eligibility.ts b/apps/web/src/lib/cost-insights/suggestion-eligibility.ts deleted file mode 100644 index 706e0d23897..00000000000 --- a/apps/web/src/lib/cost-insights/suggestion-eligibility.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { CostInsightSpendOwner } from '@kilocode/db/cost-insights-rollups'; - -import { hasCodingPlanForModel } from '@/lib/coding-plans/pricing'; -import type { OwnerTopSpendDriver } from './spend-repository'; - -export function isCodingPlanSuggestionEligible( - owner: CostInsightSpendOwner, - driver: OwnerTopSpendDriver | undefined, - minimumSpendMicrodollars: number -): boolean { - return ( - owner.type === 'user' && - driver?.category === 'variable' && - hasCodingPlanForModel(driver.providerKey, driver.modelOrPlanKey) && - driver.totalMicrodollars >= minimumSpendMicrodollars - ); -} diff --git a/apps/web/src/lib/exa-paths.ts b/apps/web/src/lib/exa-paths.ts index 5b2347e57d7..8264d374bcf 100644 --- a/apps/web/src/lib/exa-paths.ts +++ b/apps/web/src/lib/exa-paths.ts @@ -13,15 +13,3 @@ const exaAllowedPathSet: ReadonlySet = new Set(EXA_ALLOWED_PATHS); export function isExaAllowedPath(path: string): path is ExaAllowedPath { return exaAllowedPathSet.has(path); } - -const exaCostInsightFeatureKeyByPath: Record = { - '/search': 'search', - '/contents': 'contents', - '/findSimilar': 'findSimilar', - '/answer': 'answer', - '/context': 'context', -}; - -export function getExaCostInsightFeatureKey(path: string): string { - return isExaAllowedPath(path) ? exaCostInsightFeatureKeyByPath[path] : 'other'; -} diff --git a/apps/web/src/lib/exa-usage.test.ts b/apps/web/src/lib/exa-usage.test.ts index de7cfcc46e5..07480cc3d9b 100644 --- a/apps/web/src/lib/exa-usage.test.ts +++ b/apps/web/src/lib/exa-usage.test.ts @@ -1,8 +1,6 @@ import { describe, test, expect, afterEach } from '@jest/globals'; import { db } from '@/lib/drizzle'; import { - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, exa_monthly_usage, exa_usage_log, kilocode_users, @@ -106,12 +104,6 @@ describe('Exa Usage Tracking', () => { expect(rows[0].total_charged_microdollars).toBe(0); expect(rows[0].request_count).toBe(1); expect(rows[0].free_allowance_microdollars).toBe(10_000_000); - - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); - expect(totals).toHaveLength(0); }); test('increments existing counter on subsequent requests', async () => { @@ -230,43 +222,9 @@ describe('Exa Usage Tracking', () => { .where(eq(kilocode_users.id, user.id)); expect(updated.microdollars_used).toBe(7000); - - const [source] = await db - .select() - .from(exa_usage_log) - .where(eq(exa_usage_log.kilo_user_id, user.id)); - const [total] = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, user.id)); - expect(total).toBeDefined(); - const sourceHour = new Date(source.created_at); - sourceHour.setUTCMinutes(0, 0, 0); - expect(new Date(total.hour_start).toISOString()).toBe(sourceHour.toISOString()); - expect(total).toMatchObject({ - owned_by_organization_id: null, - spend_category: 'variable', - total_microdollars: 7000, - spend_record_count: 1, - }); - - const [driver] = await db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, user.id)); - expect(driver).toMatchObject({ - source: 'other', - product_key: 'exa', - feature_key: 'search', - model_or_plan_key: 'other', - provider_key: 'exa', - actor_user_id: user.id, - total_microdollars: 7000, - spend_record_count: 1, - }); }); - test('charges organization, tracks member usage, and captures spend atomically', async () => { + test('charges organization and tracks member usage', async () => { const user = await insertTestUser(); const organization = await createTestOrganization('Exa usage organization', user.id, 50_000); @@ -293,55 +251,9 @@ describe('Exa Usage Tracking', () => { kilo_user_id: user.id, microdollar_usage: 9000, }); - - const [total] = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_organization_id, organization.id)); - expect(total).toMatchObject({ - owned_by_user_id: null, - spend_category: 'variable', - total_microdollars: 9000, - spend_record_count: 1, - }); - - const [driver] = await db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where( - eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, organization.id) - ); - expect(driver).toMatchObject({ - source: 'other', - product_key: 'exa', - feature_key: 'contents', - model_or_plan_key: 'other', - provider_key: 'exa', - actor_user_id: user.id, - total_microdollars: 9000, - }); }); - test('maps unsupported Exa paths to the controlled other feature', async () => { - const user = await insertTestUser(); - - await recordExaUsage({ - userId: user.id, - organizationId: undefined, - path: '/unsupported-client-path', - costMicrodollars: 1000, - chargedToBalance: true, - freeAllowanceMicrodollars: 10_000_000, - }); - - const [driver] = await db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, user.id)); - expect(driver.feature_key).toBe('other'); - }); - - test('rolls back Exa source and monthly rows when mandatory capture fails', async () => { + test('rolls back charged Exa source and monthly rows when the user row is missing', async () => { const missingUserId = `missing-exa-user-${crypto.randomUUID()}`; await expect( @@ -353,7 +265,7 @@ describe('Exa Usage Tracking', () => { chargedToBalance: true, freeAllowanceMicrodollars: 10_000_000, }) - ).rejects.toThrow(); + ).rejects.toThrow('user disappeared during Exa consumption'); const sourceRows = await db .select() @@ -363,13 +275,8 @@ describe('Exa Usage Tracking', () => { .select() .from(exa_monthly_usage) .where(eq(exa_monthly_usage.kilo_user_id, missingUserId)); - const totals = await db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, missingUserId)); expect(sourceRows).toHaveLength(0); expect(monthlyRows).toHaveLength(0); - expect(totals).toHaveLength(0); }); test('does not deduct from balance when chargedToBalance is false', async () => { diff --git a/apps/web/src/lib/exa-usage.ts b/apps/web/src/lib/exa-usage.ts index fa8e919ea4e..47b0ba6b447 100644 --- a/apps/web/src/lib/exa-usage.ts +++ b/apps/web/src/lib/exa-usage.ts @@ -5,13 +5,6 @@ import { eq, sql } from 'drizzle-orm'; import { scheduleOrganizationLowBalanceAlert } from '@/lib/organizations/organization-usage'; import type { OrganizationUsageMutationResult } from '@/lib/organizations/organization-usage'; import { EXA_MONTHLY_ALLOWANCE_MICRODOLLARS } from '@/lib/constants'; -import { - captureCostInsightSpend, - COST_INSIGHT_DRIVER_FALLBACK, - COST_INSIGHT_EXA_PRODUCT_KEY, -} from '@kilocode/db/cost-insights-rollups'; -import { scheduleCostInsightEvaluationAfterSpend } from '@/lib/cost-insights/evaluation'; -import { getExaCostInsightFeatureKey } from '@/lib/exa-paths'; import { recordOrganizationConsumption } from '@/lib/kilo-pass-org/consumption'; export type ExaMonthlyUsageResult = { @@ -64,8 +57,8 @@ export async function getExaMonthlyUsage( } /** - * Records the source row, monthly counter, charged owner mutation, and Cost Insights - * rollup atomically. Low-balance notification scheduling happens only after commit. + * Records the source row, monthly counter, and charged owner mutation atomically. + * Low-balance notification scheduling happens only after commit. */ export async function recordExaUsage(params: { userId: string; @@ -123,32 +116,12 @@ export async function recordExaUsage(params: { sourceId, }); - await captureCostInsightSpend(tx, { - owner: organizationId - ? { type: 'organization', id: organizationId } - : { type: 'user', id: userId }, - actorUserId: userId, - occurredAt, - amountMicrodollars: costMicrodollars, - category: 'variable', - source: 'other', - productKey: COST_INSIGHT_EXA_PRODUCT_KEY, - featureKey: getExaCostInsightFeatureKey(path), - modelOrPlanKey: COST_INSIGHT_DRIVER_FALLBACK, - providerKey: COST_INSIGHT_EXA_PRODUCT_KEY, - }); - return result; }); if (organizationId && organizationUsage) { scheduleOrganizationLowBalanceAlert(organizationId, organizationUsage); } - if (chargedToBalance && costMicrodollars > 0) { - scheduleCostInsightEvaluationAfterSpend( - organizationId ? { type: 'organization', id: organizationId } : { type: 'user', id: userId } - ); - } } /** @@ -234,11 +207,17 @@ async function deductFromBalance( return result.organizationUsage; } - await tx + const updated = await tx .update(kilocode_users) .set({ microdollars_used: sql`${kilocode_users.microdollars_used} + ${costMicrodollars}`, }) .where(eq(kilocode_users.id, userId)); + // Charged usage must never commit without its balance debit. The organization + // branch already fails when its row disappears; this keeps the personal branch + // atomic now that it no longer depends on a mandatory Cost Insights capture. + if ((updated.rowCount ?? 0) === 0) { + throw new Error('user disappeared during Exa consumption'); + } return null; } diff --git a/apps/web/src/lib/kiloclaw/credit-billing.ts b/apps/web/src/lib/kiloclaw/credit-billing.ts index 479afe0d69c..10de967bc19 100644 --- a/apps/web/src/lib/kiloclaw/credit-billing.ts +++ b/apps/web/src/lib/kiloclaw/credit-billing.ts @@ -14,11 +14,6 @@ import { type KiloClawSubscriptionChangeAction, type KiloClawSubscriptionChangeActor, } from '@kilocode/db'; -import { - captureCostInsightSpend, - COST_INSIGHT_DRIVER_FALLBACK, - COST_INSIGHT_KILOCLAW_PRODUCT_KEY, -} from '@kilocode/db/cost-insights-rollups'; import { credit_transactions, kilocode_users, @@ -34,7 +29,6 @@ import { autoResumeIfSuspended, clearTrialInactivityStopAfterTrialTransition, } from '@/lib/kiloclaw/instance-lifecycle'; -import { scheduleCostInsightEvaluationAfterSpend } from '@/lib/cost-insights/evaluation'; import { buildAffiliateEventDedupeKey, enqueueAffiliateEventForUser, @@ -1542,19 +1536,6 @@ export async function enrollWithCredits(params: { return; } - await captureCostInsightSpend(tx, { - owner: { type: 'user', id: userId }, - actorUserId: userId, - occurredAt: periodStartIso, - amountMicrodollars: costMicrodollars, - category: 'scheduled', - source: 'kiloclaw', - productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, - featureKey: 'enrollment', - modelOrPlanKey: plan, - providerKey: COST_INSIGHT_DRIVER_FALLBACK, - }); - if ( currentSubscription?.status === 'canceled' && currentSubscription.kiloclaw_price_version !== kiloclawPriceVersion @@ -1701,8 +1682,6 @@ export async function enrollWithCredits(params: { error: error instanceof Error ? error.message : String(error), }); } - scheduleCostInsightEvaluationAfterSpend({ type: 'user', id: userId }); - // Step 5: Auto-resume if suspended (spec rule 7) if (transitionedFromTrial) { try { diff --git a/apps/web/src/routers/kiloclaw-billing-router.test.ts b/apps/web/src/routers/kiloclaw-billing-router.test.ts index ed2b029ffe0..82363fe12ee 100644 --- a/apps/web/src/routers/kiloclaw-billing-router.test.ts +++ b/apps/web/src/routers/kiloclaw-billing-router.test.ts @@ -45,8 +45,6 @@ import { } from '@/lib/kilo-pass/enums'; import { differenceInCalendarMonths } from 'date-fns'; import { CURRENT_KILOCLAW_PRICE_VERSION, LEGACY_KILOCLAW_PRICE_VERSION } from '@kilocode/db'; -import type { CaptureCostInsightSpendInput } from '@kilocode/db/cost-insights-rollups'; - (kiloclaw_subscriptions.kiloclaw_price_version as { defaultFn: () => string }).defaultFn = () => LEGACY_KILOCLAW_PRICE_VERSION; @@ -63,18 +61,6 @@ jest.setTimeout(15_000); // ── Mocks ────────────────────────────────────────────────────────────────── -jest.mock('@kilocode/db/cost-insights-rollups', () => ({ - captureCostInsightSpend: jest.fn(async () => undefined), - COST_INSIGHT_DRIVER_FALLBACK: 'other', - COST_INSIGHT_KILOCLAW_PRODUCT_KEY: 'kiloclaw-hosting', -})); - -const captureCostInsightSpendMock = jest.requireMock<{ - captureCostInsightSpend: jest.Mock< - (tx: unknown, input: CaptureCostInsightSpendInput) => Promise - >; -}>('@kilocode/db/cost-insights-rollups').captureCostInsightSpend; - jest.mock('@/lib/stripe-client', () => { // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const { errors } = require('stripe').default ?? require('stripe'); @@ -305,7 +291,6 @@ beforeEach(async () => { kiloclawInternalClientMock.__destroyMock.mockResolvedValue(undefined); kiloclawInternalClientMock.__startAsyncMock.mockReset(); kiloclawInternalClientMock.__startAsyncMock.mockResolvedValue(undefined); - captureCostInsightSpendMock.mockClear(); posthogCaptureMock.mockReset(); // Default mock returns for live-fetch calls @@ -7058,22 +7043,6 @@ describe('enrollWithCredits', () => { expect(deduction).toBeDefined(); expect(deduction!.amount_microdollars).toBe(-4_000_000); expect(deduction!.credit_category).toContain('kiloclaw-subscription:'); - expect(captureCostInsightSpendMock).toHaveBeenCalledWith(expect.anything(), { - owner: { type: 'user', id: user.id }, - actorUserId: user.id, - occurredAt: expect.any(String), - amountMicrodollars: 4_000_000, - category: 'scheduled', - source: 'kiloclaw', - productKey: 'kiloclaw-hosting', - featureKey: 'enrollment', - modelOrPlanKey: 'standard', - providerKey: 'other', - }); - const captureInput = captureCostInsightSpendMock.mock.calls[0]?.[1] as - | { occurredAt: string } - | undefined; - expect(new Date(deduction!.created_at).toISOString()).toBe(captureInput?.occurredAt); // Verify credit spend recorded at intro amount const [updatedUser] = await db @@ -7379,36 +7348,6 @@ describe('enrollWithCredits', () => { ); }); - it('rolls back credit enrollment when scheduled-spend capture fails', async () => { - await createCreditEnrollmentAnchor(user.id); - await giveUserCredits(user.id, 50_000_000); - captureCostInsightSpendMock.mockImplementationOnce(async () => { - throw new Error('rollup unavailable'); - }); - - const caller = await createCallerForUser(user.id); - await expect(caller.kiloclaw.enrollWithCredits({ plan: 'standard' })).rejects.toThrow( - 'rollup unavailable' - ); - - const [subscription] = await db - .select() - .from(kiloclaw_subscriptions) - .where(eq(kiloclaw_subscriptions.user_id, user.id)); - const [updatedUser] = await db - .select({ used: kilocode_users.microdollars_used }) - .from(kilocode_users) - .where(eq(kilocode_users.id, user.id)); - const transactions = await db - .select() - .from(credit_transactions) - .where(eq(credit_transactions.kilo_user_id, user.id)); - - expect(subscription.status).toBe('trialing'); - expect(updatedUser.used).toBe(0); - expect(transactions).toHaveLength(0); - }); - it('rejects enrollment when subscription is active', async () => { const instance = await createInstance(user.id); await giveUserCredits(user.id, 50_000_000); @@ -8254,7 +8193,6 @@ describe('enrollWithCredits', () => { await expect(caller.kiloclaw.enrollWithCredits({ plan: 'standard' })).rejects.toThrow( 'Enrollment already processed for this billing period' ); - expect(captureCostInsightSpendMock).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/web/src/scripts/db/cost-insights-rollups.ts b/apps/web/src/scripts/db/cost-insights-rollups.ts deleted file mode 100644 index 449e1c776e5..00000000000 --- a/apps/web/src/scripts/db/cost-insights-rollups.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { db } from '@/lib/drizzle'; -import { - backfillCostInsightRollupsNewestFirst, - initializeCostInsightRollupCoverage, - reconcileCostInsightRollups, - recordCostInsightDegradedInterval, - recordCostInsightReconciliationSuccess, -} from '@/lib/cost-insights/rollup-maintenance'; -import { requireUtcHour } from '@/lib/cost-insights/canonical-sources'; - -const HOUR_MS = 60 * 60 * 1_000; - -export type CostInsightRollupScriptArgs = { - execute: boolean; - startHour: string; - endHourExclusive: string; - maxHours: number; - sleepMs: number; - liveCaptureStartHour?: string; -}; - -function usage(): string { - return [ - 'Usage:', - ' pnpm --filter web script:run db cost-insights-rollups --start-hour --end-hour --max-hours [--sleep-ms ] [--execute] [--live-capture-start-hour ]', - '', - 'Defaults to dry-run reconciliation. --execute performs absolute newest-first hourly replacement.', - 'First execution must set --live-capture-start-hour to the first full UTC hour after all writers were deployed.', - 'Both bounds must be exact UTC hours; --end-hour is exclusive and cannot exceed current completed-hour boundary.', - ].join('\n'); -} - -function parseNonNegativeInteger(value: string | undefined, flag: string): number { - if (!value || !/^\d+$/.test(value)) { - throw new Error(`${flag} must be a non-negative integer.\n${usage()}`); - } - const parsed = Number(value); - if (!Number.isSafeInteger(parsed)) { - throw new Error(`${flag} exceeds the JavaScript safe-integer range.\n${usage()}`); - } - return parsed; -} - -export function parseCostInsightRollupScriptArgs(args: string[]): CostInsightRollupScriptArgs { - let execute = false; - let startHour: string | undefined; - let endHourExclusive: string | undefined; - let maxHours: number | undefined; - let sleepMs = 0; - let liveCaptureStartHour: string | undefined; - const seen = new Set(); - - for (let index = 0; index < args.length; index++) { - const flag = args[index]; - if (seen.has(flag)) { - throw new Error(`Duplicate flag: ${flag}.\n${usage()}`); - } - seen.add(flag); - if (flag === '--execute') { - execute = true; - continue; - } - const value = args[index + 1]; - if (!value || value.startsWith('--')) { - throw new Error(`Missing value for ${flag}.\n${usage()}`); - } - index++; - if (flag === '--start-hour') { - startHour = requireUtcHour(value, '--start-hour'); - } else if (flag === '--end-hour') { - endHourExclusive = requireUtcHour(value, '--end-hour'); - } else if (flag === '--max-hours') { - maxHours = parseNonNegativeInteger(value, '--max-hours'); - } else if (flag === '--sleep-ms') { - sleepMs = parseNonNegativeInteger(value, '--sleep-ms'); - } else if (flag === '--live-capture-start-hour') { - liveCaptureStartHour = requireUtcHour(value, '--live-capture-start-hour'); - } else { - throw new Error(`Unknown flag: ${flag}.\n${usage()}`); - } - } - - if (!startHour || !endHourExclusive || maxHours === undefined || maxHours === 0) { - throw new Error(usage()); - } - const hourCount = (Date.parse(endHourExclusive) - Date.parse(startHour)) / HOUR_MS; - if (!Number.isInteger(hourCount) || hourCount <= 0 || hourCount > maxHours) { - throw new Error(`Requested range must contain 1-${maxHours} UTC hours.\n${usage()}`); - } - const currentCompletedHourBoundary = Math.floor(Date.now() / HOUR_MS) * HOUR_MS; - if (Date.parse(endHourExclusive) > currentCompletedHourBoundary) { - throw new Error(`--end-hour must not include current or future UTC hours.\n${usage()}`); - } - if (liveCaptureStartHour && !execute) { - throw new Error(`--live-capture-start-hour requires --execute.\n${usage()}`); - } - - return { - execute, - startHour, - endHourExclusive, - maxHours, - sleepMs, - ...(liveCaptureStartHour ? { liveCaptureStartHour } : {}), - }; -} - -function printReconciliation( - report: Awaited>, - mode: 'dry-run' | 'post-execute-reconciliation' -): void { - console.log( - JSON.stringify( - { - mode, - startHour: report.startHour, - endHourExclusive: report.endHourExclusive, - checkedHourCount: report.checkedHourCount, - mismatchCount: report.mismatchCount, - mismatchCounts: report.mismatchCounts, - detailsTruncated: report.detailsTruncated, - mismatches: report.mismatches, - }, - null, - 2 - ) - ); -} - -export async function run(...args: string[]): Promise { - const parsed = parseCostInsightRollupScriptArgs(args); - if (!parsed.execute) { - const report = await reconcileCostInsightRollups(db, { - startHour: parsed.startHour, - endHourExclusive: parsed.endHourExclusive, - maxHours: parsed.maxHours, - }); - printReconciliation(report, 'dry-run'); - return; - } - - if (parsed.liveCaptureStartHour) { - await initializeCostInsightRollupCoverage(db, parsed.liveCaptureStartHour); - } - console.log( - JSON.stringify({ - mode: 'execute', - order: 'newest-first', - startHour: parsed.startHour, - endHourExclusive: parsed.endHourExclusive, - maxHours: parsed.maxHours, - sleepMs: parsed.sleepMs, - liveCaptureStartHour: parsed.liveCaptureStartHour ?? null, - }) - ); - await backfillCostInsightRollupsNewestFirst(db, { - startHour: parsed.startHour, - endHourExclusive: parsed.endHourExclusive, - maxHours: parsed.maxHours, - sleepMs: parsed.sleepMs, - onHourComplete: result => { - console.log(JSON.stringify({ mode: 'execute-hour', ...result })); - }, - }); - - const report = await reconcileCostInsightRollups(db, { - startHour: parsed.startHour, - endHourExclusive: parsed.endHourExclusive, - maxHours: parsed.maxHours, - }); - printReconciliation(report, 'post-execute-reconciliation'); - if (report.mismatchCount > 0) { - const degradedIntervalId = await recordCostInsightDegradedInterval(db, { - startHour: parsed.startHour, - endHourExclusive: parsed.endHourExclusive, - reason: 'reconciliation_mismatch', - }); - throw new Error( - `Cost Insights reconciliation found mismatches; degraded interval ${degradedIntervalId} remains unresolved.` - ); - } - await recordCostInsightReconciliationSuccess(db); -} diff --git a/apps/web/src/scripts/db/cost-insights-source-indexes.ts b/apps/web/src/scripts/db/cost-insights-source-indexes.ts deleted file mode 100644 index 369ed75f60d..00000000000 --- a/apps/web/src/scripts/db/cost-insights-source-indexes.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { sql } from 'drizzle-orm'; -import type { SQL } from 'drizzle-orm'; - -export type CostInsightsSourceIndexScriptArgs = { - execute: boolean; -}; - -type CostInsightsSourceIndexDb = { - execute(query: SQL): PromiseLike<{ rows: Record[] }>; -}; - -type CostInsightsSourceIndexCatalogRow = { - schema_name: string; - index_name: string; - table_schema_name: string; - table_name: string; - is_valid: boolean; - is_ready: boolean; -}; - -const costInsightsSourceIndexDefinitions = [ - { - name: 'IDX_coding_plan_terms_credit_transaction', - tableName: 'coding_plan_terms', - statement: - 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_coding_plan_terms_credit_transaction" ON "coding_plan_terms" ("credit_transaction_id")', - }, - { - name: 'idx_microdollar_usage_org_created_at', - tableName: 'microdollar_usage', - statement: - 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_microdollar_usage_org_created_at" ON "microdollar_usage" ("organization_id", "created_at") WHERE "organization_id" IS NOT NULL', - }, -] as const; - -function usage(): string { - return [ - 'Usage:', - ' pnpm --filter web script:run db cost-insights-source-indexes [--execute]', - '', - 'Defaults to dry-run. --execute creates Cost Insights source fallback indexes concurrently.', - ].join('\n'); -} - -export function parseCostInsightsSourceIndexScriptArgs( - args: string[] -): CostInsightsSourceIndexScriptArgs { - if (args.length === 0) return { execute: false }; - if (args.length === 1 && args[0] === '--execute') return { execute: true }; - throw new Error(`Unknown arguments: ${args.join(' ')}.\n${usage()}`); -} - -async function getCostInsightsSourceIndexState( - fromDb: CostInsightsSourceIndexDb, - indexName: string -): Promise { - const result = await fromDb.execute(sql` - SELECT - index_namespace.nspname AS schema_name, - index_class.relname AS index_name, - table_namespace.nspname AS table_schema_name, - table_class.relname AS table_name, - index_catalog.indisvalid AS is_valid, - index_catalog.indisready AS is_ready - FROM pg_catalog.pg_index AS index_catalog - INNER JOIN pg_catalog.pg_class AS index_class - ON index_class.oid = index_catalog.indexrelid - INNER JOIN pg_catalog.pg_namespace AS index_namespace - ON index_namespace.oid = index_class.relnamespace - INNER JOIN pg_catalog.pg_class AS table_class - ON table_class.oid = index_catalog.indrelid - INNER JOIN pg_catalog.pg_namespace AS table_namespace - ON table_namespace.oid = table_class.relnamespace - WHERE index_namespace.nspname = 'public' - AND index_class.relname = ${indexName} - `); - - const [indexState] = result.rows as CostInsightsSourceIndexCatalogRow[]; - return indexState ?? null; -} - -function assertIndexTargetsTable( - state: CostInsightsSourceIndexCatalogRow, - tableName: string -): void { - if ( - state.schema_name !== 'public' || - state.table_schema_name !== 'public' || - state.table_name !== tableName - ) { - throw new Error( - `Index ${state.schema_name}.${state.index_name} targets ${state.table_schema_name}.${state.table_name}, expected public.${tableName}.` - ); - } -} - -export async function provisionCostInsightsSourceIndexes( - fromDb: CostInsightsSourceIndexDb, - options: CostInsightsSourceIndexScriptArgs -): Promise { - console.log( - JSON.stringify({ - mode: options.execute ? 'execute' : 'dry-run', - indexes: costInsightsSourceIndexDefinitions.map(index => index.name), - }) - ); - - for (const index of costInsightsSourceIndexDefinitions) { - const initialState = await getCostInsightsSourceIndexState(fromDb, index.name); - if (initialState) assertIndexTargetsTable(initialState, index.tableName); - - const needsRebuild = - initialState !== null && (!initialState.is_valid || !initialState.is_ready); - const needsCreate = initialState === null || needsRebuild; - console.log( - JSON.stringify({ - mode: options.execute ? 'execute-index' : 'dry-run-index', - indexName: index.name, - tableName: index.tableName, - initialState: initialState - ? { valid: initialState.is_valid, ready: initialState.is_ready } - : null, - action: needsRebuild ? 'rebuild' : needsCreate ? 'create' : 'verify', - }) - ); - - if (!options.execute) continue; - - if (needsRebuild) { - await fromDb.execute(sql.raw(`DROP INDEX CONCURRENTLY IF EXISTS "public"."${index.name}"`)); - } - if (needsCreate) { - await fromDb.execute(sql.raw(index.statement)); - } - - const finalState = await getCostInsightsSourceIndexState(fromDb, index.name); - if (!finalState) - throw new Error(`Index public.${index.name} was not found after provisioning.`); - assertIndexTargetsTable(finalState, index.tableName); - if (!finalState.is_valid || !finalState.is_ready) { - throw new Error( - `Index public.${index.name} is not valid and ready after provisioning (indisvalid=${finalState.is_valid}, indisready=${finalState.is_ready}).` - ); - } - } -} - -export async function run(...args: string[]): Promise { - const { db } = await import('../../lib/drizzle'); - await provisionCostInsightsSourceIndexes(db, parseCostInsightsSourceIndexScriptArgs(args)); -} diff --git a/apps/web/src/scripts/db/credit-transaction-indexes.ts b/apps/web/src/scripts/db/credit-transaction-indexes.ts index 1bc3252d8b8..688b1d4a9b2 100644 --- a/apps/web/src/scripts/db/credit-transaction-indexes.ts +++ b/apps/web/src/scripts/db/credit-transaction-indexes.ts @@ -34,7 +34,7 @@ function usage(): string { 'Usage:', ' pnpm --filter web script:run db credit-transaction-indexes [--execute]', '', - 'Defaults to dry-run. --execute creates Cost Insights credit transaction indexes concurrently.', + 'Defaults to dry-run. --execute creates the credit transaction indexes concurrently.', ].join('\n'); } diff --git a/apps/web/vercel.json b/apps/web/vercel.json index 0d3277e453f..301aa54f1dd 100644 --- a/apps/web/vercel.json +++ b/apps/web/vercel.json @@ -61,13 +61,9 @@ "schedule": "0 12 * * *" }, { - "path": "/api/cron/cost-insights-hourly", + "path": "/api/cron/usage-daily-rollup-repairs", "schedule": "5 * * * *" }, - { - "path": "/api/cron/cost-insights-retention", - "schedule": "15 3 * * *" - }, { "path": "/api/cron/db-pool-metrics", "schedule": "* * * * *" diff --git a/packages/db/package.json b/packages/db/package.json index b557ebb6a9b..d33ea3f6793 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -11,7 +11,6 @@ ".": "./src/index.ts", "./schema": "./src/schema.ts", "./schema-types": "./src/schema-types.ts", - "./cost-insights-rollups": "./src/cost-insights-rollups.ts", "./kiloclaw-pricing-catalog": "./src/kiloclaw-pricing-catalog.ts", "./kiloclaw-commit-retirement": "./src/kiloclaw-commit-retirement.ts", "./kiloclaw-organization-trial-expiry-candidates": "./src/kiloclaw-organization-trial-expiry-candidates.ts", diff --git a/packages/db/src/cost-insights-rollups.test.ts b/packages/db/src/cost-insights-rollups.test.ts deleted file mode 100644 index c3015a16ce0..00000000000 --- a/packages/db/src/cost-insights-rollups.test.ts +++ /dev/null @@ -1,733 +0,0 @@ -import { afterAll, describe, expect, it } from '@jest/globals'; -import { eq, or, sql } from 'drizzle-orm'; - -import { createDrizzleClient } from './client'; -import { - COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH, - COST_INSIGHT_EVALUATION_DEBOUNCE_MS, - buildCostInsightDriver, - captureCostInsightSpend, - getCostInsightUtcHourStart, - normalizeCostInsightDriverDimension, - type CaptureCostInsightSpendInput, - type CostInsightRollupTransactionWriter, -} from './cost-insights-rollups'; -import { computeDatabaseUrl } from './database-url'; -import { - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, - cost_insight_evaluation_dirty_owners, - cost_insight_rollup_coverage, - cost_insight_rollup_degraded_intervals, - cost_insight_rollup_repairs, - kilocode_users, - organizations, -} from './schema'; -import { - CostInsightRollupDegradedReason, - CostInsightSpendCategory, - CostInsightSpendSource, -} from './schema-types'; - -const testDatabase = createDrizzleClient({ - connectionString: computeDatabaseUrl(), - poolConfig: { application_name: 'cost-insights-rollups-test', max: 12 }, -}); - -type CostInsightTestFixture = { - userId: string; - organizationId: string; -}; - -async function withCostInsightFixture( - testFn: (fixture: CostInsightTestFixture) => Promise -): Promise { - const uniqueId = crypto.randomUUID(); - const userId = `cost-insights-${uniqueId}`; - const organizationId = crypto.randomUUID(); - - await testDatabase.db.insert(kilocode_users).values({ - id: userId, - google_user_email: `${userId}@example.com`, - google_user_name: 'Cost Insights Test User', - google_user_image_url: 'https://example.com/avatar.png', - stripe_customer_id: `cus_${uniqueId}`, - }); - await testDatabase.db.insert(organizations).values({ - id: organizationId, - name: `Cost Insights ${uniqueId}`, - }); - - try { - await testFn({ userId, organizationId }); - } finally { - await testDatabase.db - .delete(cost_insight_rollup_repairs) - .where( - or( - eq(cost_insight_rollup_repairs.owned_by_user_id, userId), - eq(cost_insight_rollup_repairs.owned_by_organization_id, organizationId) - ) - ); - await testDatabase.db - .delete(cost_insight_evaluation_dirty_owners) - .where( - or( - eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, userId), - eq(cost_insight_evaluation_dirty_owners.owned_by_organization_id, organizationId) - ) - ); - await testDatabase.db - .delete(cost_insight_owner_hour_driver_buckets) - .where( - or( - eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, userId), - eq(cost_insight_owner_hour_driver_buckets.owned_by_organization_id, organizationId), - eq(cost_insight_owner_hour_driver_buckets.actor_user_id, userId) - ) - ); - await testDatabase.db - .delete(cost_insight_owner_hour_totals) - .where( - or( - eq(cost_insight_owner_hour_totals.owned_by_user_id, userId), - eq(cost_insight_owner_hour_totals.owned_by_organization_id, organizationId) - ) - ); - await testDatabase.db.delete(organizations).where(eq(organizations.id, organizationId)); - await testDatabase.db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); - } -} - -function costInsightOwnerHourLockKey( - owner: CaptureCostInsightSpendInput['owner'], - hourStart: string -): string { - return [ - 'cost-insight-owner-hour:v1', - `${owner.type.length}:${owner.type}`, - `${owner.id.length}:${owner.id}`, - `${hourStart.length}:${hourStart}`, - ].join('|'); -} - -function createDeferred(): { promise: Promise; resolve: () => void } { - let resolvePromise: (() => void) | undefined; - const promise = new Promise(resolve => { - resolvePromise = resolve; - }); - return { - promise, - resolve: () => { - if (!resolvePromise) throw new Error('Deferred promise is not initialized.'); - resolvePromise(); - }, - }; -} - -function captureInput( - fixture: CostInsightTestFixture, - overrides: Partial = {} -): CaptureCostInsightSpendInput { - return { - owner: { type: 'user', id: fixture.userId }, - actorUserId: fixture.userId, - occurredAt: '2026-11-01T01:30:00-04:00', - amountMicrodollars: 125, - category: CostInsightSpendCategory.Variable, - source: CostInsightSpendSource.AiGateway, - productKey: 'direct-gateway', - featureKey: 'chat_completions', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - ...overrides, - }; -} - -async function expectConstraintViolation( - insertPromise: Promise, - constraint: string -): Promise { - await expect(insertPromise).rejects.toMatchObject({ - cause: { constraint }, - }); -} - -afterAll(async () => { - await testDatabase.pool.end(); -}); - -describe('Cost Insights rollup capture', () => { - it('uses explicit UTC hour buckets across DST offset changes', () => { - expect(getCostInsightUtcHourStart('2026-11-01T01:30:00-04:00')).toBe( - '2026-11-01T05:00:00.000Z' - ); - expect(getCostInsightUtcHourStart('2026-11-01T01:30:00-05:00')).toBe( - '2026-11-01T06:00:00.000Z' - ); - expect(getCostInsightUtcHourStart('2026-04-29 01:16:12.945+00')).toBe( - '2026-04-29T01:00:00.000Z' - ); - }); - - it('normalizes bounded identifiers and keeps the v1 driver digest stable', async () => { - expect(normalizeCostInsightDriverDimension('')).toBe('other'); - expect(normalizeCostInsightDriverDimension('request label with spaces')).toBe('other'); - expect( - normalizeCostInsightDriverDimension('x'.repeat(COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH + 5)) - ).toHaveLength(COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH); - - await expect( - buildCostInsightDriver({ - source: CostInsightSpendSource.AiGateway, - productKey: ' direct-gateway ', - featureKey: 'chat_completions', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - actorUserId: 'user-123', - }) - ).resolves.toEqual({ - source: CostInsightSpendSource.AiGateway, - productKey: 'direct-gateway', - featureKey: 'chat_completions', - modelOrPlanKey: 'anthropic/claude-sonnet-4', - providerKey: 'anthropic', - actorUserId: 'user-123', - driverKey: '03dcdfd758bed98d48c43ec1ebf68e101ba2de835422bce541ccb1e2e56a3783', - }); - }); - - it.each([ - { - description: 'missing owner ID', - overrides: { owner: { type: 'user' as const, id: '' } }, - error: 'cost_insight_invalid_owner_id', - }, - { - description: 'timestamp without an explicit timezone', - overrides: { occurredAt: '2026-06-25T12:30:00' }, - error: 'cost_insight_invalid_occurred_at', - }, - { - description: 'invalid calendar timestamp', - overrides: { occurredAt: '2026-02-30T12:30:00Z' }, - error: 'cost_insight_invalid_occurred_at', - }, - { - description: 'unsafe amount', - overrides: { amountMicrodollars: Number.MAX_SAFE_INTEGER + 1 }, - error: 'cost_insight_invalid_amount_microdollars', - }, - { - description: 'non-positive count', - overrides: { spendRecordCount: 0 }, - error: 'cost_insight_invalid_spend_record_count', - }, - { - description: 'uncontrolled category', - overrides: { category: 'refund' as CostInsightSpendCategory }, - error: 'cost_insight_invalid_category', - }, - { - description: 'uncontrolled source', - overrides: { source: 'exa' as CostInsightSpendSource }, - error: 'cost_insight_invalid_source', - }, - ])('rejects $description before writing', async ({ overrides, error }) => { - await withCostInsightFixture(async fixture => { - await expect( - testDatabase.db.transaction(tx => - captureCostInsightSpend(tx, captureInput(fixture, overrides)) - ) - ).rejects.toThrow(error); - }); - }); - - it('writes lock, total, and driver through one database round trip', async () => { - const execute = jest.fn(async () => ({ rows: [{ outcome: 'ok' }] })); - const transaction = { execute } as unknown as CostInsightRollupTransactionWriter; - - await captureCostInsightSpend( - transaction, - captureInput({ userId: 'user-1', organizationId: crypto.randomUUID() }) - ); - - expect(execute).toHaveBeenCalledTimes(1); - }); - - it('allows live capture while another transaction holds the shared owner-hour lock', async () => { - await withCostInsightFixture(async fixture => { - const input = captureInput(fixture); - const hourStart = getCostInsightUtcHourStart(input.occurredAt); - const lockKey = costInsightOwnerHourLockKey(input.owner, hourStart); - const lockAcquired = createDeferred(); - const releaseLock = createDeferred(); - const lockHolder = testDatabase.db.transaction(async tx => { - await tx.execute( - sql`SELECT pg_catalog.pg_advisory_xact_lock_shared( - pg_catalog.hashtextextended(${lockKey}, 0::bigint) - )` - ); - lockAcquired.resolve(); - await releaseLock.promise; - }); - - await lockAcquired.promise; - try { - await testDatabase.db.transaction(async tx => { - await tx.execute(sql`SET LOCAL lock_timeout = '500ms'`); - await captureCostInsightSpend(tx, input); - }); - } finally { - releaseLock.resolve(); - await lockHolder; - } - - const [total] = await testDatabase.db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); - expect(total).toMatchObject({ total_microdollars: 125, spend_record_count: 1 }); - }); - }); - - it('rolls back within the local lock timeout when repair holds the exclusive owner-hour lock', async () => { - await withCostInsightFixture(async fixture => { - const input = captureInput(fixture); - const hourStart = getCostInsightUtcHourStart(input.occurredAt); - const lockKey = costInsightOwnerHourLockKey(input.owner, hourStart); - const lockAcquired = createDeferred(); - const releaseLock = createDeferred(); - const lockHolder = testDatabase.db.transaction(async tx => { - await tx.execute( - sql`SELECT pg_catalog.pg_advisory_xact_lock( - pg_catalog.hashtextextended(${lockKey}, 0::bigint) - )` - ); - lockAcquired.resolve(); - await releaseLock.promise; - }); - - await lockAcquired.promise; - const startedAt = performance.now(); - try { - await expect( - testDatabase.db.transaction(async tx => { - await tx.execute(sql`SET LOCAL lock_timeout = '100ms'`); - await captureCostInsightSpend(tx, input); - }) - ).rejects.toMatchObject({ cause: { code: '55P03' } }); - expect(performance.now() - startedAt).toBeLessThan(2_000); - } finally { - releaseLock.resolve(); - await lockHolder; - } - - const totals = await testDatabase.db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); - expect(totals).toHaveLength(0); - }); - }); - - it('adds totals before matching driver buckets under a non-UTC database timezone', async () => { - await withCostInsightFixture(async fixture => { - await testDatabase.db.transaction(async tx => { - await tx.execute(sql`SET LOCAL TIME ZONE 'America/Los_Angeles'`); - await captureCostInsightSpend(tx, captureInput(fixture)); - await captureCostInsightSpend( - tx, - captureInput(fixture, { - occurredAt: '2026-11-01T05:55:00.000Z', - amountMicrodollars: 75, - spendRecordCount: 3, - }) - ); - }); - - const totals = await testDatabase.db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); - const drivers = await testDatabase.db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId)); - - expect(totals).toHaveLength(1); - expect(totals[0]).toMatchObject({ - spend_category: CostInsightSpendCategory.Variable, - total_microdollars: 200, - spend_record_count: 4, - }); - expect(new Date(totals[0]?.hour_start ?? '').toISOString()).toBe('2026-11-01T05:00:00.000Z'); - expect(drivers).toHaveLength(1); - expect(drivers[0]).toMatchObject({ - total_microdollars: 200, - spend_record_count: 4, - }); - expect(new Date(drivers[0]?.hour_start ?? '').toISOString()).toBe('2026-11-01T05:00:00.000Z'); - }); - }); - - it('debounces dirty-owner evaluation while coalescing generations', async () => { - await withCostInsightFixture(async fixture => { - await testDatabase.db.transaction(async tx => { - await captureCostInsightSpend(tx, captureInput(fixture)); - await captureCostInsightSpend( - tx, - captureInput(fixture, { amountMicrodollars: 75, spendRecordCount: 2 }) - ); - }); - - const databaseNowResult = await testDatabase.db.execute<{ - database_now: string | Date; - }>(sql`SELECT CURRENT_TIMESTAMP AS database_now`); - const databaseNow = databaseNowResult.rows[0]?.database_now; - const [dirtyOwner] = await testDatabase.db - .select() - .from(cost_insight_evaluation_dirty_owners) - .where(eq(cost_insight_evaluation_dirty_owners.owned_by_user_id, fixture.userId)); - const nextAttemptDelayMs = - Date.parse(String(dirtyOwner?.next_attempt_at)) - Date.parse(String(databaseNow)); - - expect(dirtyOwner?.generation).toBe(2); - expect(nextAttemptDelayMs).toBeGreaterThan(COST_INSIGHT_EVALUATION_DEBOUNCE_MS - 10_000); - expect(nextAttemptDelayMs).toBeLessThan(COST_INSIGHT_EVALUATION_DEBOUNCE_MS + 10_000); - }); - }); - - it('keeps personal and organization owner-hour rows isolated', async () => { - await withCostInsightFixture(async fixture => { - await testDatabase.db.transaction(async tx => { - await captureCostInsightSpend(tx, captureInput(fixture)); - await captureCostInsightSpend( - tx, - captureInput(fixture, { - owner: { type: 'organization', id: fixture.organizationId }, - }) - ); - }); - - const totals = await testDatabase.db - .select() - .from(cost_insight_owner_hour_totals) - .where( - or( - eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId), - eq(cost_insight_owner_hour_totals.owned_by_organization_id, fixture.organizationId) - ) - ); - const drivers = await testDatabase.db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where( - or( - eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId), - eq( - cost_insight_owner_hour_driver_buckets.owned_by_organization_id, - fixture.organizationId - ) - ) - ); - - expect(totals).toHaveLength(2); - expect(drivers).toHaveLength(2); - }); - }); - - it('serializes a high-concurrency owner-hour burst and preserves exact sums', async () => { - await withCostInsightFixture(async fixture => { - await Promise.all( - Array.from({ length: 32 }, () => - testDatabase.db.transaction(tx => - captureCostInsightSpend( - tx, - captureInput(fixture, { amountMicrodollars: 7, spendRecordCount: 2 }) - ) - ) - ) - ); - - const [total] = await testDatabase.db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); - const [driver] = await testDatabase.db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId)); - - expect(total).toMatchObject({ total_microdollars: 224, spend_record_count: 64 }); - expect(driver).toMatchObject({ total_microdollars: 224, spend_record_count: 64 }); - }); - }); - - it('rejects a digest collision and rolls back the preceding total upsert', async () => { - await withCostInsightFixture(async fixture => { - const input = captureInput(fixture); - const driver = await buildCostInsightDriver(input); - const hourStart = getCostInsightUtcHourStart(input.occurredAt); - - await testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - owned_by_user_id: fixture.userId, - owned_by_organization_id: null, - hour_start: hourStart, - spend_category: input.category, - driver_key: driver.driverKey, - source: driver.source, - product_key: 'different-product', - feature_key: driver.featureKey, - model_or_plan_key: driver.modelOrPlanKey, - provider_key: driver.providerKey, - actor_user_id: driver.actorUserId, - total_microdollars: 1, - spend_record_count: 1, - }); - - await expect( - testDatabase.db.transaction(tx => captureCostInsightSpend(tx, input)) - ).rejects.toThrow('cost_insight_driver_digest_collision'); - - const totals = await testDatabase.db - .select() - .from(cost_insight_owner_hour_totals) - .where(eq(cost_insight_owner_hour_totals.owned_by_user_id, fixture.userId)); - const [storedDriver] = await testDatabase.db - .select() - .from(cost_insight_owner_hour_driver_buckets) - .where(eq(cost_insight_owner_hour_driver_buckets.owned_by_user_id, fixture.userId)); - - expect(totals).toHaveLength(0); - expect(storedDriver).toMatchObject({ - product_key: 'different-product', - total_microdollars: 1, - spend_record_count: 1, - }); - }); - }); -}); - -describe('Cost Insights rollup constraints', () => { - it('enforces total owner, hour, category, positive, and safe-integer contracts', async () => { - await withCostInsightFixture(async fixture => { - const valid = { - owned_by_user_id: fixture.userId, - owned_by_organization_id: null, - hour_start: '2026-06-25T12:00:00.000Z', - spend_category: CostInsightSpendCategory.Variable, - total_microdollars: 1, - spend_record_count: 1, - }; - - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_totals).values({ - ...valid, - owned_by_user_id: null, - }), - 'cost_insight_owner_hour_totals_owner_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_totals).values({ - ...valid, - hour_start: '2026-06-25T12:00:01.000Z', - }), - 'cost_insight_owner_hour_totals_hour_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_totals).values({ - ...valid, - spend_category: 'refund' as CostInsightSpendCategory, - }), - 'cost_insight_owner_hour_totals_category_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_totals).values({ - ...valid, - total_microdollars: 0, - }), - 'cost_insight_owner_hour_totals_amount_positive_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_totals).values({ - ...valid, - total_microdollars: Number.MAX_SAFE_INTEGER + 1, - }), - 'cost_insight_owner_hour_totals_amount_safe_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_totals).values({ - ...valid, - spend_record_count: 0, - }), - 'cost_insight_owner_hour_totals_count_positive_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_totals).values({ - ...valid, - spend_record_count: Number.MAX_SAFE_INTEGER + 1, - }), - 'cost_insight_owner_hour_totals_count_safe_check' - ); - }); - }); - - it('enforces driver digest, dimension, source, and owner contracts', async () => { - await withCostInsightFixture(async fixture => { - const valid = { - owned_by_user_id: fixture.userId, - owned_by_organization_id: null, - hour_start: '2026-06-25T12:00:00.000Z', - spend_category: CostInsightSpendCategory.Variable, - driver_key: 'a'.repeat(64), - source: CostInsightSpendSource.AiGateway, - product_key: 'direct-gateway', - feature_key: 'chat_completions', - model_or_plan_key: 'anthropic/claude-sonnet-4', - provider_key: 'anthropic', - actor_user_id: fixture.userId, - total_microdollars: 1, - spend_record_count: 1, - }; - - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - owned_by_user_id: null, - }), - 'cost_insight_driver_buckets_owner_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - driver_key: 'not-a-digest', - }), - 'cost_insight_driver_buckets_driver_key_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - source: 'exa' as CostInsightSpendSource, - }), - 'cost_insight_driver_buckets_source_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - product_key: 'x'.repeat(COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH + 1), - }), - 'cost_insight_driver_buckets_product_key_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - total_microdollars: 0, - }), - 'cost_insight_driver_buckets_amount_positive_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - total_microdollars: Number.MAX_SAFE_INTEGER + 1, - }), - 'cost_insight_driver_buckets_amount_safe_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - spend_record_count: 0, - }), - 'cost_insight_driver_buckets_count_positive_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_owner_hour_driver_buckets).values({ - ...valid, - spend_record_count: Number.MAX_SAFE_INTEGER + 1, - }), - 'cost_insight_driver_buckets_count_safe_check' - ); - }); - }); - - it('enforces coverage and degraded interval hour/range taxonomies', async () => { - const version = 31_999; - const intervalIds: string[] = []; - - try { - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_rollup_coverage).values({ - rollup_version: version, - live_capture_start_hour: '2026-06-25T12:30:00.000Z', - }), - 'cost_insight_rollup_coverage_live_hour_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_rollup_coverage).values({ - rollup_version: version, - coverage_start_hour: '2026-06-25T12:00:00.000Z', - }), - 'cost_insight_rollup_coverage_range_check' - ); - - const degradedBase = { - start_hour: '2026-06-25T12:00:00.000Z', - end_hour_exclusive: '2026-06-25T13:00:00.000Z', - source: CostInsightSpendSource.AiGateway, - reason: CostInsightRollupDegradedReason.ReconciliationMismatch, - }; - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ - ...degradedBase, - start_hour: '2026-06-25T12:00:01.000Z', - }), - 'cost_insight_degraded_intervals_start_hour_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ - ...degradedBase, - end_hour_exclusive: degradedBase.start_hour, - }), - 'cost_insight_degraded_intervals_range_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ - ...degradedBase, - detected_at: '2026-06-25T12:30:00.000Z', - resolved_at: '2026-06-25T12:29:59.999Z', - }), - 'cost_insight_degraded_intervals_resolution_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ - ...degradedBase, - reason: 'freeform-error' as CostInsightRollupDegradedReason, - }), - 'cost_insight_degraded_intervals_reason_check' - ); - await expectConstraintViolation( - testDatabase.db.insert(cost_insight_rollup_degraded_intervals).values({ - ...degradedBase, - source: 'exa' as CostInsightSpendSource, - }), - 'cost_insight_degraded_intervals_source_check' - ); - - const [interval] = await testDatabase.db - .insert(cost_insight_rollup_degraded_intervals) - .values(degradedBase) - .returning({ id: cost_insight_rollup_degraded_intervals.id }); - if (!interval) throw new Error('failed_to_insert_cost_insight_degraded_interval'); - intervalIds.push(interval.id); - } finally { - for (const id of intervalIds) { - await testDatabase.db - .delete(cost_insight_rollup_degraded_intervals) - .where(eq(cost_insight_rollup_degraded_intervals.id, id)); - } - await testDatabase.db - .delete(cost_insight_rollup_coverage) - .where(eq(cost_insight_rollup_coverage.rollup_version, version)); - } - }); -}); diff --git a/packages/db/src/cost-insights-rollups.ts b/packages/db/src/cost-insights-rollups.ts deleted file mode 100644 index da83521fba7..00000000000 --- a/packages/db/src/cost-insights-rollups.ts +++ /dev/null @@ -1,491 +0,0 @@ -import { createHash } from 'node:crypto'; - -import { sql, type SQL } from 'drizzle-orm'; - -import type { WorkerDb } from './client'; -import { - cost_insight_evaluation_dirty_owners, - cost_insight_owner_hour_driver_buckets, - cost_insight_owner_hour_totals, -} from './schema'; -import { - CostInsightSpendCategory, - CostInsightSpendSource, - type CostInsightSpendCategory as CostInsightSpendCategoryType, - type CostInsightSpendSource as CostInsightSpendSourceType, -} from './schema-types'; - -export const COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH = 128; -export const COST_INSIGHT_DRIVER_FALLBACK = 'other'; -export const COST_INSIGHT_CODING_PLAN_PRODUCT_KEY = 'coding-plan'; -export const COST_INSIGHT_EXA_PRODUCT_KEY = 'exa'; -export const COST_INSIGHT_KILOCLAW_PRODUCT_KEY = 'kiloclaw-hosting'; -export const COST_INSIGHT_EVALUATION_DEBOUNCE_MS = 60_000; - -const DRIVER_KEY_SERIALIZATION_VERSION = 'cost-insight-driver-key:v1'; -const DRIVER_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/; -const TIMESTAMP_WITH_TIMEZONE_PATTERN = - /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}(?::?\d{2})?)$/i; -const spendCategories = new Set(Object.values(CostInsightSpendCategory)); -const spendSources = new Set(Object.values(CostInsightSpendSource)); - -export type CostInsightSpendOwner = - | { type: 'user'; id: string } - | { type: 'organization'; id: string }; - -export type CostInsightDriverInput = { - source: CostInsightSpendSourceType; - productKey: string; - featureKey: string; - modelOrPlanKey: string; - providerKey: string; - actorUserId: string; -}; - -export type CostInsightDriver = { - source: CostInsightSpendSourceType; - productKey: string; - featureKey: string; - modelOrPlanKey: string; - providerKey: string; - actorUserId: string; - driverKey: string; -}; - -export type CaptureCostInsightSpendInput = CostInsightDriverInput & { - owner: CostInsightSpendOwner; - occurredAt: string; - amountMicrodollars: number; - spendRecordCount?: number; - category: CostInsightSpendCategoryType; -}; - -export type CostInsightRollupTransactionWriter = Pick; - -function assertIdentifier(value: string, errorCode: string): void { - if (value.length === 0 || value.trim() !== value) { - throw new Error(errorCode); - } -} - -function assertOwner(owner: unknown): asserts owner is CostInsightSpendOwner { - if ( - typeof owner !== 'object' || - owner === null || - !('type' in owner) || - (owner.type !== 'user' && owner.type !== 'organization') - ) { - throw new Error('cost_insight_invalid_owner_type'); - } - if (!('id' in owner) || typeof owner.id !== 'string') { - throw new Error('cost_insight_invalid_owner_id'); - } - assertIdentifier(owner.id, 'cost_insight_invalid_owner_id'); -} - -function assertPositiveSafeInteger(value: number, errorCode: string): void { - if (!Number.isSafeInteger(value) || value <= 0) { - throw new Error(errorCode); - } -} - -function hasValidTimestampFields(value: string): boolean { - const match = TIMESTAMP_WITH_TIMEZONE_PATTERN.exec(value); - if (!match) return false; - - const fields = match.slice(1, 7).map(Number); - const [year, month, day, hour, minute, second] = fields; - const timezone = match[7]; - if ( - year === undefined || - month === undefined || - day === undefined || - hour === undefined || - minute === undefined || - second === undefined || - timezone === undefined - ) { - return false; - } - - const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); - const daysPerMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - const daysInMonth = daysPerMonth[month - 1]; - if ( - daysInMonth === undefined || - day < 1 || - day > daysInMonth || - hour > 23 || - minute > 59 || - second > 59 - ) { - return false; - } - - if (timezone.toUpperCase() === 'Z') return true; - - const offset = timezone.slice(1).replace(':', ''); - const offsetHour = Number(offset.slice(0, 2)); - const offsetMinute = offset.length === 2 ? 0 : Number(offset.slice(2)); - return offsetHour <= 23 && offsetMinute <= 59; -} - -function parseTimestamp(value: string): Date { - if ( - typeof value !== 'string' || - value.length === 0 || - value.trim() !== value || - !hasValidTimestampFields(value) - ) { - throw new Error('cost_insight_invalid_occurred_at'); - } - - const parsed = new Date(value); - if (!Number.isFinite(parsed.getTime())) { - throw new Error('cost_insight_invalid_occurred_at'); - } - return parsed; -} - -export function getCostInsightUtcHourStart(occurredAt: string): string { - const parsed = parseTimestamp(occurredAt); - parsed.setUTCMinutes(0, 0, 0); - return parsed.toISOString(); -} - -export function normalizeCostInsightDriverDimension(value: unknown): string { - if (typeof value !== 'string') return COST_INSIGHT_DRIVER_FALLBACK; - - const normalized = value.trim(); - if (normalized.length === 0 || !DRIVER_IDENTIFIER_PATTERN.test(normalized)) { - return COST_INSIGHT_DRIVER_FALLBACK; - } - - return normalized.slice(0, COST_INSIGHT_DRIVER_DIMENSION_MAX_LENGTH); -} - -function serializeLengthPrefixedUtf8(values: readonly string[]): Uint8Array { - const encoder = new TextEncoder(); - const encodedValues = values.map(value => encoder.encode(value)); - const totalLength = encodedValues.reduce((length, value) => length + 4 + value.byteLength, 0); - const serialized = new Uint8Array(totalLength); - let offset = 0; - - for (const value of encodedValues) { - const length = value.byteLength; - serialized[offset] = (length >>> 24) & 0xff; - serialized[offset + 1] = (length >>> 16) & 0xff; - serialized[offset + 2] = (length >>> 8) & 0xff; - serialized[offset + 3] = length & 0xff; - serialized.set(value, offset + 4); - offset += 4 + length; - } - - return serialized; -} - -function sha256Hex(value: Uint8Array): string { - return createHash('sha256').update(value).digest('hex'); -} - -export async function buildCostInsightDriver( - input: CostInsightDriverInput -): Promise { - if (!spendSources.has(input.source)) { - throw new Error('cost_insight_invalid_source'); - } - if (typeof input.actorUserId !== 'string') { - throw new Error('cost_insight_invalid_actor_user_id'); - } - assertIdentifier(input.actorUserId, 'cost_insight_invalid_actor_user_id'); - - const driver = { - source: input.source, - productKey: normalizeCostInsightDriverDimension(input.productKey), - featureKey: normalizeCostInsightDriverDimension(input.featureKey), - modelOrPlanKey: normalizeCostInsightDriverDimension(input.modelOrPlanKey), - providerKey: normalizeCostInsightDriverDimension(input.providerKey), - actorUserId: input.actorUserId, - }; - const serialized = serializeLengthPrefixedUtf8([ - DRIVER_KEY_SERIALIZATION_VERSION, - driver.source, - driver.productKey, - driver.featureKey, - driver.modelOrPlanKey, - driver.providerKey, - driver.actorUserId, - ]); - - return { - ...driver, - driverKey: sha256Hex(serialized), - }; -} - -function ownerColumns(owner: CostInsightSpendOwner): { - owned_by_user_id: string | null; - owned_by_organization_id: string | null; -} { - return owner.type === 'user' - ? { owned_by_user_id: owner.id, owned_by_organization_id: null } - : { owned_by_user_id: null, owned_by_organization_id: owner.id }; -} - -function buildCostInsightOwnerHourLockKey(owner: CostInsightSpendOwner, hourStart: string): string { - return [ - 'cost-insight-owner-hour:v1', - `${owner.type.length}:${owner.type}`, - `${owner.id.length}:${owner.id}`, - `${hourStart.length}:${hourStart}`, - ].join('|'); -} - -export async function acquireCostInsightOwnerHourLock( - tx: CostInsightRollupTransactionWriter, - owner: CostInsightSpendOwner, - hourStart: string -): Promise { - assertOwner(owner); - const normalizedHourStart = getCostInsightUtcHourStart(hourStart); - const lockKey = buildCostInsightOwnerHourLockKey(owner, normalizedHourStart); - - await tx.execute( - sql`SELECT pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(${lockKey}, 0::bigint))` - ); -} - -export async function acquireCostInsightOwnerHourSharedLock( - tx: CostInsightRollupTransactionWriter, - owner: CostInsightSpendOwner, - hourStart: string -): Promise { - assertOwner(owner); - const normalizedHourStart = getCostInsightUtcHourStart(hourStart); - const lockKey = buildCostInsightOwnerHourLockKey(owner, normalizedHourStart); - - await tx.execute( - sql`SELECT pg_catalog.pg_advisory_xact_lock_shared(pg_catalog.hashtextextended(${lockKey}, 0::bigint))` - ); -} - -type CostInsightCaptureOutcome = { - outcome: 'ok' | 'cost_insight_driver_digest_collision'; -}; - -function costInsightConflictTargets(owner: CostInsightSpendOwner): { - total: SQL; - driver: SQL; - dirtyOwner: SQL; -} { - return owner.type === 'user' - ? { - total: sql.raw(` - (owned_by_user_id, hour_start, spend_category) - WHERE owned_by_organization_id IS NULL - `), - driver: sql.raw(` - (owned_by_user_id, hour_start, spend_category, driver_key) - WHERE owned_by_organization_id IS NULL - `), - dirtyOwner: sql.raw(` - (owned_by_user_id) - WHERE owned_by_organization_id IS NULL - `), - } - : { - total: sql.raw(` - (owned_by_organization_id, hour_start, spend_category) - WHERE owned_by_user_id IS NULL - `), - driver: sql.raw(` - (owned_by_organization_id, hour_start, spend_category, driver_key) - WHERE owned_by_user_id IS NULL - `), - dirtyOwner: sql.raw(` - (owned_by_organization_id) - WHERE owned_by_user_id IS NULL - `), - }; -} - -const COST_INSIGHT_EVALUATION_DEBOUNCE_SECONDS = COST_INSIGHT_EVALUATION_DEBOUNCE_MS / 1_000; - -async function writeCostInsightSpend( - tx: CostInsightRollupTransactionWriter, - values: { - owner: CostInsightSpendOwner; - ownedByUserId: string | null; - ownedByOrganizationId: string | null; - hourStart: string; - category: CostInsightSpendCategoryType; - driver: CostInsightDriver; - amountMicrodollars: number; - spendRecordCount: number; - } -): Promise { - const conflictTargets = costInsightConflictTargets(values.owner); - const lockKey = buildCostInsightOwnerHourLockKey(values.owner, values.hourStart); - const result = await tx.execute(sql` - WITH capture_input AS MATERIALIZED ( - SELECT - ${values.ownedByUserId}::text AS owned_by_user_id, - ${values.ownedByOrganizationId}::uuid AS owned_by_organization_id, - ${values.hourStart}::timestamptz AS hour_start, - ${values.category}::text AS spend_category, - ${values.driver.driverKey}::text AS driver_key, - ${values.driver.source}::text AS source, - ${values.driver.productKey}::text AS product_key, - ${values.driver.featureKey}::text AS feature_key, - ${values.driver.modelOrPlanKey}::text AS model_or_plan_key, - ${values.driver.providerKey}::text AS provider_key, - ${values.driver.actorUserId}::text AS actor_user_id, - ${values.amountMicrodollars}::bigint AS amount_microdollars, - ${values.spendRecordCount}::bigint AS spend_record_count, - ${lockKey}::text AS lock_key - ), owner_hour_lock AS MATERIALIZED ( - SELECT pg_catalog.pg_advisory_xact_lock_shared( - pg_catalog.hashtextextended(capture_input.lock_key, 0::bigint) - ) AS acquired - FROM capture_input - ), owner_total_upsert AS ( - INSERT INTO ${cost_insight_owner_hour_totals} AS current_total ( - owned_by_user_id, - owned_by_organization_id, - hour_start, - spend_category, - total_microdollars, - spend_record_count - ) - SELECT - capture_input.owned_by_user_id, - capture_input.owned_by_organization_id, - capture_input.hour_start, - capture_input.spend_category, - capture_input.amount_microdollars, - capture_input.spend_record_count - FROM capture_input - CROSS JOIN owner_hour_lock - WHERE TRUE - ON CONFLICT ${conflictTargets.total} - DO UPDATE SET - total_microdollars = current_total.total_microdollars + excluded.total_microdollars, - spend_record_count = current_total.spend_record_count + excluded.spend_record_count, - updated_at = pg_catalog.now() - RETURNING 1 AS upserted - ), driver_upsert AS ( - INSERT INTO ${cost_insight_owner_hour_driver_buckets} AS current_driver ( - owned_by_user_id, - owned_by_organization_id, - hour_start, - spend_category, - driver_key, - source, - product_key, - feature_key, - model_or_plan_key, - provider_key, - actor_user_id, - total_microdollars, - spend_record_count - ) - SELECT - capture_input.owned_by_user_id, - capture_input.owned_by_organization_id, - capture_input.hour_start, - capture_input.spend_category, - capture_input.driver_key, - capture_input.source, - capture_input.product_key, - capture_input.feature_key, - capture_input.model_or_plan_key, - capture_input.provider_key, - capture_input.actor_user_id, - capture_input.amount_microdollars, - capture_input.spend_record_count - FROM capture_input - CROSS JOIN owner_total_upsert - WHERE TRUE - ON CONFLICT ${conflictTargets.driver} - DO UPDATE SET - total_microdollars = current_driver.total_microdollars + excluded.total_microdollars, - spend_record_count = current_driver.spend_record_count + excluded.spend_record_count, - updated_at = pg_catalog.now() - WHERE current_driver.source = excluded.source - AND current_driver.product_key = excluded.product_key - AND current_driver.feature_key = excluded.feature_key - AND current_driver.model_or_plan_key = excluded.model_or_plan_key - AND current_driver.provider_key = excluded.provider_key - AND current_driver.actor_user_id = excluded.actor_user_id - RETURNING 'ok'::text AS outcome - ), evaluation_dirty_upsert AS ( - INSERT INTO ${cost_insight_evaluation_dirty_owners} AS dirty_owner ( - owned_by_user_id, - owned_by_organization_id, - dirty_at, - next_attempt_at - ) - SELECT - capture_input.owned_by_user_id, - capture_input.owned_by_organization_id, - pg_catalog.clock_timestamp(), - pg_catalog.clock_timestamp() + make_interval( - secs => ${COST_INSIGHT_EVALUATION_DEBOUNCE_SECONDS} - ) - FROM capture_input - CROSS JOIN driver_upsert - WHERE TRUE - ON CONFLICT ${conflictTargets.dirtyOwner} - DO UPDATE SET - generation = dirty_owner.generation + 1, - dirty_at = pg_catalog.clock_timestamp(), - next_attempt_at = pg_catalog.clock_timestamp() + make_interval( - secs => ${COST_INSIGHT_EVALUATION_DEBOUNCE_SECONDS} - ), - updated_at = pg_catalog.clock_timestamp() - RETURNING 'ok'::text AS outcome - ) - SELECT COALESCE( - (SELECT outcome FROM evaluation_dirty_upsert), - 'cost_insight_driver_digest_collision' - ) AS outcome - `); - const outcome = result.rows[0]?.outcome; - if (outcome === 'cost_insight_driver_digest_collision') { - throw new Error('cost_insight_driver_digest_collision'); - } - if (outcome !== 'ok') { - throw new Error('cost_insight_rollup_write_missing_outcome'); - } -} - -export async function captureCostInsightSpend( - tx: CostInsightRollupTransactionWriter, - input: CaptureCostInsightSpendInput -): Promise { - assertOwner(input.owner); - if (!spendCategories.has(input.category)) { - throw new Error('cost_insight_invalid_category'); - } - if (!spendSources.has(input.source)) { - throw new Error('cost_insight_invalid_source'); - } - assertPositiveSafeInteger(input.amountMicrodollars, 'cost_insight_invalid_amount_microdollars'); - const spendRecordCount = input.spendRecordCount === undefined ? 1 : input.spendRecordCount; - assertPositiveSafeInteger(spendRecordCount, 'cost_insight_invalid_spend_record_count'); - - const hourStart = getCostInsightUtcHourStart(input.occurredAt); - const driver = await buildCostInsightDriver(input); - const owner = ownerColumns(input.owner); - - await writeCostInsightSpend(tx, { - owner: input.owner, - ownedByUserId: owner.owned_by_user_id, - ownedByOrganizationId: owner.owned_by_organization_id, - hourStart, - category: input.category, - driver, - amountMicrodollars: input.amountMicrodollars, - spendRecordCount, - }); -} diff --git a/services/kiloclaw-billing/src/lifecycle.test.ts b/services/kiloclaw-billing/src/lifecycle.test.ts index 0cc6938f252..ed3dc3a627d 100644 --- a/services/kiloclaw-billing/src/lifecycle.test.ts +++ b/services/kiloclaw-billing/src/lifecycle.test.ts @@ -2,15 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type * as DbModule from '@kilocode/db'; const { - mockCaptureCostInsightSpend, mockFindLatestPreCutoffUserCommitSwitchQualification, mockGetWorkerDb, mockGetMissingSnowflakeConfig, mockQueryKiloclawActiveUserIds, } = vi.hoisted(() => ({ - mockCaptureCostInsightSpend: vi.fn<(tx: unknown, input: { occurredAt: string }) => Promise>( - async () => undefined - ), mockFindLatestPreCutoffUserCommitSwitchQualification: vi.fn< () => Promise >(async () => null), @@ -29,12 +25,6 @@ vi.mock('@kilocode/db', async importOriginal => { }; }); -vi.mock('@kilocode/db/cost-insights-rollups', () => ({ - captureCostInsightSpend: mockCaptureCostInsightSpend, - COST_INSIGHT_DRIVER_FALLBACK: 'other', - COST_INSIGHT_KILOCLAW_PRODUCT_KEY: 'kiloclaw-hosting', -})); - vi.mock('./snowflake.js', () => ({ getMissingSnowflakeConfig: mockGetMissingSnowflakeConfig, queryKiloclawActiveUserIds: mockQueryKiloclawActiveUserIds, @@ -1444,7 +1434,6 @@ describe('credit renewal fanout queue processing', () => { }) ); expect(duplicateResult.credit_renewals_skipped_duplicate).toBe(1); - expect(mockCaptureCostInsightSpend).not.toHaveBeenCalled(); expect(staleResult.credit_renewals).toBe(0); expect(staleResult.credit_renewals_past_due).toBe(0); expect(staleResult.errors).toBe(0); @@ -1583,28 +1572,6 @@ describe('credit renewal fanout queue processing', () => { expect(fetchImpl).not.toHaveBeenCalled(); }); - it('does not mutate balance or subscription when scheduled-spend capture fails', async () => { - const row = creditRenewalRow(); - const { db, txInserts, txUpdates } = createMockDb([[row], [row]]); - mockGetWorkerDb.mockReturnValue(db); - mockCaptureCostInsightSpend.mockRejectedValueOnce(new Error('rollup unavailable')); - - await expect( - processCreditRenewalItem( - createEnvWithQueueMocks(vi.fn()).env, - creditRenewalItemMessage({ renewalBoundary: '2026-06-01T00:00:00.000Z' }), - 1 - ) - ).rejects.toThrow('rollup unavailable'); - - expect(txInserts).toEqual([ - expect.objectContaining({ - credit_category: 'kiloclaw-subscription:22222222-2222-4222-8222-222222222222:2026-06', - }), - ]); - expect(txUpdates).toHaveLength(0); - }); - it('resolves a terminal failure when an operator retry finalizes an expected past-due outcome', async () => { const row = creditRenewalRow({ total_microdollars_acquired: 0, @@ -5987,23 +5954,8 @@ describe('credit renewal sweep affiliate tracking', () => { }), ]) ); - expect(mockCaptureCostInsightSpend).toHaveBeenCalledWith(expect.anything(), { - owner: { type: 'user', id: 'user-1' }, - actorUserId: 'user-1', - occurredAt: expect.any(String), - amountMicrodollars: 9_000_000, - category: 'scheduled', - source: 'kiloclaw', - productKey: 'kiloclaw-hosting', - featureKey: 'renewal', - modelOrPlanKey: 'standard', - providerKey: 'other', - }); - const captureInput = mockCaptureCostInsightSpend.mock.calls[0]?.[1] as - | { occurredAt: string } - | undefined; const deduction = txInserts.find(insert => insert.amount_microdollars === -9_000_000); - expect(deduction?.created_at).toBe(captureInput?.occurredAt); + expect(deduction).toBeDefined(); const saleCall = fetch.mock.calls .map( diff --git a/services/kiloclaw-billing/src/lifecycle.ts b/services/kiloclaw-billing/src/lifecycle.ts index 39291cd3ba7..58232dc0c53 100644 --- a/services/kiloclaw-billing/src/lifecycle.ts +++ b/services/kiloclaw-billing/src/lifecycle.ts @@ -24,11 +24,6 @@ import { type OrganizationSeatsPurchase, } from '@kilocode/db'; import { classifyOrganizationEntitlement } from '@kilocode/organization-entitlement'; -import { - captureCostInsightSpend, - COST_INSIGHT_DRIVER_FALLBACK, - COST_INSIGHT_KILOCLAW_PRODUCT_KEY, -} from '@kilocode/db/cost-insights-rollups'; import { listOrganizationTrialExpiryEnforcementCandidates, type OrganizationTrialExpiryCandidateRow, @@ -2106,19 +2101,6 @@ async function processCreditRenewalRow( } satisfies CreditRenewalTransactionOutcome; } - await captureCostInsightSpend(tx, { - owner: { type: 'user', id: userId }, - actorUserId: userId, - occurredAt, - amountMicrodollars: costMicrodollars, - category: 'scheduled', - source: 'kiloclaw', - productKey: COST_INSIGHT_KILOCLAW_PRODUCT_KEY, - featureKey: 'renewal', - modelOrPlanKey: effectivePlan, - providerKey: COST_INSIGHT_DRIVER_FALLBACK, - }); - await tx .update(kilocode_users) .set({