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 | |
| } | |
| } | |