Phase 1: Financial audit logging foundation
- Add FinancialAuditLog model to track all financial changes - Log action (CREATE, UPDATE, DELETE) with before/after values - Capture who made changes (IP address) - Record timestamp and reason for change - Create /admin/financial-audit page to view audit trail - Add filtering by entity type, action, date range - Show summary stats (total changes, deletions, etc) - Integrate with purchases: log create and delete operations - Red-flag deletions for security awareness - Add "Financial audit trail" link to admin menu This provides foundation for Phase 2 (reporting UI) and Phase 3 (bank reconciliation). All financial changes are now traceable and auditable for compliance. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
e69759081c
commit
d56f942a88
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user