import { randomUUID } from "node:crypto"; import type { FastifyReply, FastifyRequest } from "fastify"; import { and, eq, gte, sql } from "drizzle-orm"; import { serviceEntitlements, serviceUsageEvents } from "@fedeo/push-db"; import { db } from "../db/client.js"; export type CentralServiceName = "ai" | "banking"; export async function requireCentralService( request: FastifyRequest, reply: FastifyReply, service: CentralServiceName, ) { const instance = request.pushInstance!; const [entitlement] = await db .select() .from(serviceEntitlements) .where(and(eq(serviceEntitlements.instanceId, instance.id), eq(serviceEntitlements.service, service))) .limit(1); if (!entitlement?.enabled) { await reply.code(403).send({ error: "service_not_enabled", message: `Der zentrale Dienst '${service}' ist für diese Instanz nicht freigeschaltet.`, }); return null; } if (entitlement.monthlyLimit !== null) { const monthStart = new Date(); monthStart.setUTCDate(1); monthStart.setUTCHours(0, 0, 0, 0); const [usage] = await db .select({ units: sql`coalesce(sum(${serviceUsageEvents.units}), 0)::bigint` }) .from(serviceUsageEvents) .where(and( eq(serviceUsageEvents.instanceId, instance.id), eq(serviceUsageEvents.service, service), eq(serviceUsageEvents.status, "succeeded"), gte(serviceUsageEvents.createdAt, monthStart), )); if (Number(usage?.units || 0) >= entitlement.monthlyLimit) { await reply.code(429).send({ error: "service_monthly_limit_reached", message: `Das Monatslimit für den zentralen Dienst '${service}' ist erreicht.`, limit: entitlement.monthlyLimit, used: Number(usage?.units || 0), }); return null; } } return entitlement; } export async function recordServiceUsage(input: { request: FastifyRequest; service: CentralServiceName; operation: string; units?: number; status: "succeeded" | "failed"; unitPriceMicros: number; provider?: string; providerRequestId?: string | null; errorCode?: string; metadata?: Record; }) { const requestId = String(input.request.headers["x-fedeo-request-id"] || randomUUID()); const units = Math.max(0, Math.trunc(input.units ?? 1)); await db.insert(serviceUsageEvents).values({ requestId, instanceId: input.request.pushInstance!.id, service: input.service, operation: input.operation, units, costMicros: input.status === "succeeded" ? units * input.unitPriceMicros : 0, status: input.status, provider: input.provider, providerRequestId: input.providerRequestId || null, errorCode: input.errorCode, metadata: input.metadata || {}, }); return requestId; }