/** * Org / project usage rollups from AgentRun cost facts (ADR-0021). * Operational usage accounting, not payment collection. Organizations may use * either their own provider credentials or a distinct platform-managed * connection; commercial billing remains outside the pilot scope. */ import type { Prisma, PrismaClient } from "@prisma/client"; export interface ProjectUsageRow { readonly projectId: string; readonly projectName: string; readonly folderId: string | null; readonly runCount: number; readonly runsWithCost: number; readonly runsWithoutCost: number; readonly inputTokens: number; readonly outputTokens: number; readonly costUsd: number | null; } export interface UsageReport { readonly from: string | null; readonly to: string | null; readonly projects: readonly ProjectUsageRow[]; readonly totals: { readonly runCount: number; readonly runsWithCost: number; readonly runsWithoutCost: number; readonly inputTokens: number; readonly outputTokens: number; readonly costUsd: number | null; }; } export async function getOrgUsage( prisma: PrismaClient, input: { readonly organizationId: string; readonly from?: Date | undefined; readonly to?: Date | undefined; readonly folderId?: string | undefined; }, ): Promise { const projectWhere: Prisma.ProjectWhereInput = { organizationId: input.organizationId, ...(input.folderId !== undefined ? { folderId: input.folderId } : {}), }; const projects = await prisma.project.findMany({ where: projectWhere, select: { id: true, name: true, folderId: true }, orderBy: { name: "asc" }, }); if (projects.length === 0) { return emptyReport(input.from, input.to); } const runWhere: Prisma.AgentRunWhereInput = { projectId: { in: projects.map((p) => p.id) }, ...(input.from !== undefined || input.to !== undefined ? { startedAt: { ...(input.from !== undefined ? { gte: input.from } : {}), ...(input.to !== undefined ? { lte: input.to } : {}), }, } : {}), }; const runs = await prisma.agentRun.findMany({ where: runWhere, select: { projectId: true, inputTokens: true, outputTokens: true, costUsd: true, }, }); type MutableRow = { projectId: string; projectName: string; folderId: string | null; runCount: number; runsWithCost: number; runsWithoutCost: number; inputTokens: number; outputTokens: number; costUsd: number | null; }; const byProject = new Map(); for (const p of projects) { byProject.set(p.id, { projectId: p.id, projectName: p.name, folderId: p.folderId, runCount: 0, runsWithCost: 0, runsWithoutCost: 0, inputTokens: 0, outputTokens: 0, costUsd: null, }); } for (const run of runs) { const row = byProject.get(run.projectId); if (row === undefined) continue; row.runCount += 1; row.inputTokens += run.inputTokens ?? 0; row.outputTokens += run.outputTokens ?? 0; if (run.costUsd === null) { row.runsWithoutCost += 1; } else { row.runsWithCost += 1; const cost = Number(run.costUsd); row.costUsd = (row.costUsd ?? 0) + cost; } } const projectsOut: ProjectUsageRow[] = [...byProject.values()]; let runCount = 0; let runsWithCost = 0; let runsWithoutCost = 0; let inputTokens = 0; let outputTokens = 0; let costUsd: number | null = null; for (const row of projectsOut) { runCount += row.runCount; runsWithCost += row.runsWithCost; runsWithoutCost += row.runsWithoutCost; inputTokens += row.inputTokens; outputTokens += row.outputTokens; if (row.costUsd !== null) { costUsd = (costUsd ?? 0) + row.costUsd; } } return { from: input.from?.toISOString() ?? null, to: input.to?.toISOString() ?? null, projects: projectsOut, totals: { runCount, runsWithCost, runsWithoutCost, inputTokens, outputTokens, costUsd, }, }; } export async function getProjectUsage( prisma: PrismaClient, input: { readonly organizationId: string; readonly projectId: string; readonly from?: Date | undefined; readonly to?: Date | undefined; }, ): Promise { const project = await prisma.project.findFirst({ where: { id: input.projectId, organizationId: input.organizationId }, select: { id: true, name: true, folderId: true }, }); if (project === null) { throw new Error(`project not found: ${input.projectId}`); } const report = await getOrgUsage(prisma, { organizationId: input.organizationId, from: input.from, to: input.to, }); const row = report.projects.find((p) => p.projectId === project.id); return ( row ?? { projectId: project.id, projectName: project.name, folderId: project.folderId, runCount: 0, runsWithCost: 0, runsWithoutCost: 0, inputTokens: 0, outputTokens: 0, costUsd: null, } ); } function emptyReport(from?: Date, to?: Date): UsageReport { return { from: from?.toISOString() ?? null, to: to?.toISOString() ?? null, projects: [], totals: { runCount: 0, runsWithCost: 0, runsWithoutCost: 0, inputTokens: 0, outputTokens: 0, costUsd: null, }, }; }