import { FastifyRequest, FastifyReply } from 'fastify'; import { prisma } from '../services/prisma'; import { redis } from '../lib/redis'; const CACHE_TTL = 30; function cacheKey(organizationId: string) { return `wallet:ok:${organizationId}`; } /** * Fastify preHandler — blocks with 402 if the org wallet is exhausted or hard-stopped. * Mirrors the worker's checkWalletBalance() using the same Redis cache key. * Fails open on DB/Redis error to avoid blocking legitimate requests. */ export async function requireCredits(request: FastifyRequest, reply: FastifyReply): Promise { const organizationId = request.organizationId ?? (request.headers['x-organization-id'] as string); if (!organizationId) return; try { const cached = await redis.get(cacheKey(organizationId)); if (cached === 'ok') return; } catch { // Redis unavailable — proceed to DB check } try { const org = await prisma.organization.findUnique({ where: { id: organizationId }, select: { walletBalance: true, isHardStopped: true }, }); if (!org) return; // unknown org — fail open if (org.isHardStopped || org.walletBalance <= 0) { return reply.code(402).send({ error: 'wallet_exhausted', message: 'Wallet balance exhausted. Please top up to continue.', }); } redis.setex(cacheKey(organizationId), CACHE_TTL, 'ok').catch(() => {}); } catch { // DB error — fail open } }