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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user