feat: zentrale selfhost-dienste bereitstellen
This commit is contained in:
84
push-server/apps/api/src/services/central-services.ts
Normal file
84
push-server/apps/api/src/services/central-services.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user