import { prisma } from '@/lib/prisma'; import { getClientIp } from '@/lib/rateLimit'; import { headers } from 'next/headers'; export type AuditAction = 'CREATE' | 'UPDATE' | 'DELETE'; export type EntityType = 'ORDER' | 'MANUAL_SALE' | 'PURCHASE' | 'EXPENSE'; interface AuditChangeSet { [field: string]: { old: any; new: any; }; } /** * Log a financial transaction change to the audit trail * @param action CREATE, UPDATE, or DELETE * @param entityType Type of entity (ORDER, MANUAL_SALE, etc) * @param entityId ID of the entity * @param changes Object with {field: {old: X, new: Y}} * @param amount Optional amount in cents for quick filtering * @param reason Why was this changed */ export async function logFinancialChange( action: AuditAction, entityType: EntityType, entityId: string, changes: AuditChangeSet, amount?: number, reason?: string ) { try { const ip = getClientIp(headers()); await prisma.financialAuditLog.create({ data: { action, entityType, entityId, changes: JSON.stringify(changes), amount, changedBy: ip, reason, }, }); } catch (err) { console.error('Failed to log financial change:', err); // Don't throw - audit logging failure shouldn't break the main transaction } } /** * Get audit logs for a specific entity */ export async function getEntityAuditTrail(entityType: EntityType, entityId: string) { return prisma.financialAuditLog.findMany({ where: { entityType, entityId }, orderBy: { changedAt: 'desc' }, }); } /** * Get all audit logs with optional filtering */ export async function getAuditLogs(options: { entityType?: EntityType; action?: AuditAction; fromDate?: Date; toDate?: Date; minAmount?: number; limit?: number; }) { return prisma.financialAuditLog.findMany({ where: { ...(options.entityType && { entityType: options.entityType }), ...(options.action && { action: options.action }), ...(options.fromDate || options.toDate ? { changedAt: { ...(options.fromDate && { gte: options.fromDate }), ...(options.toDate && { lte: options.toDate }), }, } : {}), ...(options.minAmount && { amount: { gte: options.minAmount } }), }, orderBy: { changedAt: 'desc' }, take: options.limit || 100, }); } /** * Get summary of financial changes (useful for spotting issues) */ export async function getAuditSummary(days: number = 30) { const fromDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000); const logs = await prisma.financialAuditLog.findMany({ where: { changedAt: { gte: fromDate } }, }); const summary = { total: logs.length, byAction: { CREATE: logs.filter((l) => l.action === 'CREATE').length, UPDATE: logs.filter((l) => l.action === 'UPDATE').length, DELETE: logs.filter((l) => l.action === 'DELETE').length, }, byEntity: { ORDER: logs.filter((l) => l.entityType === 'ORDER').length, MANUAL_SALE: logs.filter((l) => l.entityType === 'MANUAL_SALE').length, PURCHASE: logs.filter((l) => l.entityType === 'PURCHASE').length, EXPENSE: logs.filter((l) => l.entityType === 'EXPENSE').length, }, deletions: logs.filter((l) => l.action === 'DELETE').map((l) => ({ entity: l.entityType, id: l.entityId, at: l.changedAt, reason: l.reason, })), }; return summary; }