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.
This commit is contained in:
96
pledge-now-pay-later/src/lib/activity-log.ts
Normal file
96
pledge-now-pay-later/src/lib/activity-log.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user