import { z } from "zod"; const creditResultSchema = z.object({ success: z.boolean(), credits: z.number().int().nullable(), error_code: z.string().nullable().optional(), }); const authorizationSchema = z.object({ success: z.boolean(), reservation_id: z.string().uuid().nullable(), source: z.enum(["subscription", "credits"]).nullable(), credits: z.number().int().nullable(), subscription_id: z.string().uuid().nullable(), reason: z.string().nullable(), retry_after_seconds: z.number().int().nullable(), }); const settlementSchema = z.object({ success: z.boolean(), reservation_id: z.string().uuid().nullable(), credits: z.number().int().nullable(), error_code: z.string().nullable(), }); type CreditRpcName = "begin_consultation_credit" | "complete_consultation_credit" | "cancel_consultation_credit"; type AccountingClient = { rpc(rpcName: string, args: Record): PromiseLike<{ data: unknown; error: { message: string } | null; }>; }; export type CreditResult = z.infer; export type UsageAuthorization = z.infer; export type UsageSettlement = z.infer; export type ActualUsage = { eventKey: string; actualModelId: string; modelConfigVersion?: number; inputTokens: number; outputTokens: number; costMicrousd: number; durationMs: number; }; export class CreditRpcError extends Error { readonly code: string; constructor(code: string) { super(`Credit operation failed: ${code}`); this.name = "CreditRpcError"; this.code = code; } } function first(value: unknown) { return Array.isArray(value) ? value[0] : value; } async function runRpc( accounting: AccountingClient, rpcName: string, args: Record, schema: z.ZodType, ): Promise { let lastError = "unknown_billing_error"; for (let attempt = 1; attempt <= 3; attempt += 1) { try { const { data, error } = await accounting.rpc(rpcName, args); const parsed = schema.safeParse(first(data)); if (!error && parsed.success) return parsed.data; lastError = error?.message || "invalid_billing_response"; } catch (error) { lastError = error instanceof Error ? error.message : "billing_request_failed"; } if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, attempt * 150)); } throw new CreditRpcError(lastError); } export async function runCreditRpc( accounting: AccountingClient, rpcName: CreditRpcName, userId: string, requestId: string, ): Promise { return runRpc(accounting, rpcName, { p_user_id: userId, p_request_id: requestId, }, creditResultSchema); } export async function authorizeUsage( accounting: AccountingClient, input: { userId: string; requestId: string; featureKey: "chat.standard" | "chat.premium" | "rectification" | "report.full" | "report.export"; requestedModelId: string; creditCost: number; }, ): Promise { return runRpc(accounting, "authorize_usage", { p_user_id: input.userId, p_feature_key: input.featureKey, p_requested_model_id: input.requestedModelId, p_request_id: input.requestId, p_credit_cost: input.creditCost, }, authorizationSchema); } export async function completeUsage( accounting: AccountingClient, userId: string, requestId: string, usage: ActualUsage, ): Promise { return runRpc(accounting, "complete_usage", { p_user_id: userId, p_request_id: requestId, p_actual_usage: { eventKey: usage.eventKey, actualModelId: usage.actualModelId, modelConfigVersion: usage.modelConfigVersion, inputTokens: Math.max(0, Math.trunc(usage.inputTokens)), outputTokens: Math.max(0, Math.trunc(usage.outputTokens)), costMicrousd: Math.max(0, Math.trunc(usage.costMicrousd)), durationMs: Math.max(0, Math.trunc(usage.durationMs)), }, }, settlementSchema); } export async function releaseUsage( accounting: AccountingClient, userId: string, requestId: string, reason: string, ): Promise { return runRpc(accounting, "release_usage", { p_user_id: userId, p_request_id: requestId, p_reason: reason, }, settlementSchema); }