File size: 1,557 Bytes
6484585
 
0fd3320
 
 
 
 
 
 
6484585
 
0fd3320
 
 
6484585
 
 
0fd3320
 
 
 
 
 
 
 
6484585
 
 
 
0fd3320
6484585
 
0fd3320
6484585
0fd3320
6484585
0fd3320
 
6484585
 
0fd3320
 
6484585
0fd3320
6484585
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
    }
}