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