edtech / apps /api /src /services /wallet.ts
CognxSafeTrack
feat(billing): implement full wallet/ledger system with hard-stop enforcement
0fd3320
import { WalletTransactionType } from '@repo/database';
import { prisma } from './prisma';
import { redis } from '../lib/redis';
import { logger } from '../logger';
const CACHE_KEY = (orgId: string) => `wallet:ok:${orgId}`;
export interface CreditWalletParams {
organizationId: string;
amount: number;
type: 'TOP_UP_MANUAL' | 'TOP_UP_PAYMENT' | 'ADJUSTMENT';
actorId?: string;
description?: string;
}
export async function creditWallet(params: CreditWalletParams): Promise<{ newBalance: number }> {
const { organizationId, amount, type, actorId, description } = params;
const updatedOrg = await prisma.$transaction(async tx => {
const org = await tx.organization.update({
where: { id: organizationId },
data: {
walletBalance: { increment: amount },
isHardStopped: false,
},
select: { walletBalance: true, name: true },
});
await tx.walletTransaction.create({
data: {
organizationId,
amount,
balanceAfter: org.walletBalance,
type: type as WalletTransactionType,
description,
actorId,
byok: false,
metadata: {},
},
});
await tx.auditLog.create({
data: {
action: 'WALLET_CREDIT',
actorId,
resourceId: organizationId,
details: { amount, type, description, newBalance: org.walletBalance },
},
});
return org;
});
// Invalidate cache so requireCredits picks up the new balance immediately
redis.del(CACHE_KEY(organizationId)).catch(() => {});
logger.info({ organizationId, amount, type, newBalance: updatedOrg.walletBalance }, '[WALLET-API] creditWallet success');
return { newBalance: updatedOrg.walletBalance };
}