import { prisma } from '@/lib/prisma' export interface SprintTokenRow { sprintId: string sprintCode: string sprintGoal: string totalTokens: number totalCostUsd: number jobCount: number } export interface DayTokenRow { day: string totalTokens: number totalCostUsd: number } export interface PbiTokenRow { pbiId: string pbiCode: string pbiTitle: string totalTokens: number totalCostUsd: number } type RawSprintRow = { sprint_id: string sprint_code: string sprint_goal: string total_tokens: bigint total_cost: number | null job_count: bigint } type RawDayRow = { day: Date total_tokens: bigint total_cost: number | null } type RawPbiRow = { pbi_id: string pbi_code: string pbi_title: string total_tokens: bigint total_cost: number | null } export async function getSprintTokenHistory( userId: string, productId?: string, limit = 8, ): Promise { const rows = productId ? await prisma.$queryRaw` SELECT sp.id AS sprint_id, sp.code AS sprint_code, sp.sprint_goal, COALESCE(SUM(cj.input_tokens + cj.output_tokens + cj.cache_read_tokens + cj.cache_write_tokens + COALESCE(cj.actual_thinking_tokens, 0)), 0) AS total_tokens, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) FILTER (WHERE cj.input_tokens IS NOT NULL) AS total_cost, COUNT(*) FILTER (WHERE cj.input_tokens IS NOT NULL) AS job_count FROM claude_jobs cj JOIN tasks t ON cj.task_id = t.id JOIN stories s ON t.story_id = s.id JOIN sprints sp ON s.sprint_id = sp.id LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.product_id = ${productId} GROUP BY sp.id, sp.code, sp.sprint_goal ORDER BY sp.created_at DESC LIMIT ${limit} ` : await prisma.$queryRaw` SELECT sp.id AS sprint_id, sp.code AS sprint_code, sp.sprint_goal, COALESCE(SUM(cj.input_tokens + cj.output_tokens + cj.cache_read_tokens + cj.cache_write_tokens + COALESCE(cj.actual_thinking_tokens, 0)), 0) AS total_tokens, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) FILTER (WHERE cj.input_tokens IS NOT NULL) AS total_cost, COUNT(*) FILTER (WHERE cj.input_tokens IS NOT NULL) AS job_count FROM claude_jobs cj JOIN tasks t ON cj.task_id = t.id JOIN stories s ON t.story_id = s.id JOIN sprints sp ON s.sprint_id = sp.id LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' GROUP BY sp.id, sp.code, sp.sprint_goal ORDER BY sp.created_at DESC LIMIT ${limit} ` return rows.map(r => ({ sprintId: r.sprint_id, sprintCode: r.sprint_code, sprintGoal: r.sprint_goal, totalTokens: Number(r.total_tokens), totalCostUsd: Number(r.total_cost ?? 0), jobCount: Number(r.job_count), })) } export async function getDayTokenData(userId: string, sprintId: string): Promise { if (!sprintId) return [] const rows = await prisma.$queryRaw` SELECT DATE(cj.finished_at) AS day, COALESCE(SUM(cj.input_tokens + cj.output_tokens + cj.cache_read_tokens + cj.cache_write_tokens + COALESCE(cj.actual_thinking_tokens, 0)), 0) AS total_tokens, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) FILTER (WHERE cj.input_tokens IS NOT NULL) AS total_cost FROM claude_jobs cj JOIN tasks t ON cj.task_id = t.id JOIN stories s ON t.story_id = s.id LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND s.sprint_id = ${sprintId} AND cj.status = 'DONE' AND cj.finished_at IS NOT NULL GROUP BY DATE(cj.finished_at) ORDER BY day ASC ` return rows.map(r => ({ day: r.day.toISOString().slice(0, 10), totalTokens: Number(r.total_tokens), totalCostUsd: Number(r.total_cost ?? 0), })) } export async function getPbiTokenAggregates(userId: string, sprintId: string): Promise { if (!sprintId) return [] const rows = await prisma.$queryRaw` SELECT p.id AS pbi_id, p.code AS pbi_code, p.title AS pbi_title, COALESCE(SUM(cj.input_tokens + cj.output_tokens + cj.cache_read_tokens + cj.cache_write_tokens + COALESCE(cj.actual_thinking_tokens, 0)), 0) AS total_tokens, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) FILTER (WHERE cj.input_tokens IS NOT NULL) AS total_cost FROM claude_jobs cj JOIN tasks t ON cj.task_id = t.id JOIN stories s ON t.story_id = s.id JOIN pbis p ON s.pbi_id = p.id LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND s.sprint_id = ${sprintId} AND cj.status = 'DONE' GROUP BY p.id, p.code, p.title ORDER BY total_cost DESC ` return rows.map(r => ({ pbiId: r.pbi_id, pbiCode: r.pbi_code, pbiTitle: r.pbi_title, totalTokens: Number(r.total_tokens), totalCostUsd: Number(r.total_cost ?? 0), })) }