85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
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<number>`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<string, unknown>;
|
|
}) {
|
|
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;
|
|
}
|