Files
calvana/pledge-now-pay-later/src/lib/activity-log.ts
Omair Saleh fcfae1c1a4 Ship all P0/P1/P2 gaps + 11 AI features
P0 Critical (7):
- STOP/UNSUBSCRIBE keyword → CANCEL (PECR compliance)
- Rate limiting on pledge creation (10/IP/5min)
- Terms of Service + Privacy Policy pages
- WhatsApp onboarding gate (persistent dashboard banner)
- Demo account seeding (demo@pnpl.app)
- Footer legal links
- Basic accessibility (aria labels on donor flow)

P1 Within 2 Weeks (8):
- Pledge editing by staff (PATCH amount, name, email, phone, rail)
- Donor self-cancel page (/p/cancel) + API
- Donor 'My Pledges' lookup page (/p/my-pledges)
- Bulk QR code download (print-ready HTML)
- Public event progress bar (/e/[slug]/progress)
- Email-only donor handling (honest status + WhatsApp fallback)
- Email verification (format + disposable domain blocking)
- Organisations page rewrite (multi-campaign, not multi-org)

P2 Within First Month (10):
- Event cloning with QR sources
- Account deletion (GDPR Article 17)
- Daily digest cron via WhatsApp
- AI-6 Smart reminder timing (due date anchoring, cultural sensitivity)
- H1 Duplicate donor detection (email, phone, Jaro-Winkler name)
- H5 Bank CSV format presets (10 UK banks)
- H16 Partial payment matching (underpay, overpay, instalment)
- H10 Activity logging (audit trail for staff actions)
- AI nudge endpoint + AI column mapping + AI event setup wizard
- AI anomaly detection wired into daily digest

AI Features (11): smart reconciliation, social proof, auto column mapper,
daily digest, impact storyteller, smart timing, nudge composer, event wizard,
NLU concierge, anomaly detection, bank presets

22 new files, 15 modified files, 0 TypeScript errors, clean build.
2026-03-04 20:10:34 +08:00

97 lines
2.4 KiB
TypeScript

/**
* Activity log utility — records staff actions for audit trail (H10)
*
* Uses Prisma's AnalyticsEvent table as a lightweight activity store.
* Each entry records WHO did WHAT to WHICH entity and WHEN.
*/
import prisma from "@/lib/prisma"
import type { ActivityAction } from "@/lib/ai"
interface LogActivityInput {
action: ActivityAction
entityType: string
entityId?: string
orgId: string
userId?: string
userName?: string
metadata?: Record<string, unknown>
}
/**
* Log an activity entry. Non-blocking — errors are silently swallowed.
*/
export async function logActivity(input: LogActivityInput): Promise<void> {
if (!prisma) return
try {
await prisma.analyticsEvent.create({
data: {
eventType: `activity.${input.action}`,
eventId: input.entityType === "event" ? input.entityId : undefined,
pledgeId: input.entityType === "pledge" ? input.entityId : undefined,
metadata: {
action: input.action,
entityType: input.entityType,
entityId: input.entityId,
orgId: input.orgId,
userId: input.userId,
userName: input.userName,
...input.metadata,
},
},
})
} catch (err) {
// Activity logging should never break the main flow
console.error("[activity-log] Failed to write:", err)
}
}
/**
* Query activity log for an org. Returns most recent entries.
*/
export async function getActivityLog(
orgId: string,
options: { limit?: number; entityType?: string; entityId?: string } = {}
) {
if (!prisma) return []
const { limit = 50, entityType, entityId } = options
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const where: any = {
eventType: { startsWith: "activity." },
}
if (entityId) {
where.OR = [
{ eventId: entityId },
{ pledgeId: entityId },
]
}
if (entityType) {
where.eventType = { startsWith: `activity.${entityType}.` }
}
const entries = await prisma.analyticsEvent.findMany({
where,
orderBy: { createdAt: "desc" },
take: limit,
})
return entries.map(e => {
const meta = e.metadata as Record<string, unknown> || {}
return {
id: e.id,
action: meta.action as string,
entityType: meta.entityType as string,
entityId: meta.entityId as string,
userId: meta.userId as string,
userName: meta.userName as string,
metadata: meta,
timestamp: e.createdAt,
}
})
}