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:
2026-03-04 20:10:34 +08:00
parent 59485579ec
commit fcfae1c1a4
36 changed files with 3405 additions and 46 deletions

View File

@@ -30,12 +30,28 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ ok: true })
}
const text = payload.body.trim().toUpperCase()
let text = payload.body.trim().toUpperCase()
const fromPhone = payload.from.replace("@c.us", "")
// Only handle known commands
// Alias STOP / UNSUBSCRIBE / OPT OUT → CANCEL (PECR compliance)
if (["STOP", "UNSUBSCRIBE", "OPT OUT", "OPTOUT"].includes(text)) {
text = "CANCEL"
}
// Only handle known commands — all others go to AI NLU (if enabled)
if (!["PAID", "HELP", "CANCEL", "STATUS"].includes(text)) {
return NextResponse.json({ ok: true })
// AI-10: Natural Language Understanding for non-keyword messages
try {
const { classifyDonorMessage } = await import("@/lib/ai")
const intent = await classifyDonorMessage(payload.body.trim(), fromPhone)
if (intent && intent.confidence >= 0.8 && ["PAID", "HELP", "CANCEL", "STATUS"].includes(intent.action)) {
text = intent.action
} else {
return NextResponse.json({ ok: true })
}
} catch {
return NextResponse.json({ ok: true })
}
}
if (!prisma) return NextResponse.json({ ok: true })
@@ -100,8 +116,18 @@ export async function POST(request: NextRequest) {
where: { id: pledge.id },
data: { status: "cancelled", cancelledAt: new Date() },
})
// Revoke WhatsApp consent for ALL pledges from this number (PECR compliance)
await prisma.pledge.updateMany({
where: { donorPhone: { in: phoneVariants }, whatsappOptIn: true },
data: { whatsappOptIn: false },
})
// Skip all pending reminders for cancelled pledge
await prisma.reminder.updateMany({
where: { pledgeId: pledge.id, status: "pending" },
data: { status: "skipped" },
})
await sendWhatsAppMessage(fromPhone,
`Your £${amount} pledge to ${pledge.event.name} has been cancelled. No worries — thank you for considering! 🙏`
`Your £${amount} pledge to ${pledge.event.name} has been cancelled. You won't receive any more messages from us. Thank you for considering! 🙏`
)
break
}