edtech / apps /api /src /middleware /requireCredits.ts
CognxSafeTrack
feat(billing): implement full wallet/ledger system with hard-stop enforcement
0fd3320
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<void> {
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
}
}