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

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server"
import prisma from "@/lib/prisma"
import { updatePledgeStatusSchema } from "@/lib/validators"
import { logActivity } from "@/lib/activity-log"
export async function GET(
request: NextRequest,
@@ -55,10 +56,15 @@ export async function PATCH(
return NextResponse.json({ error: "Pledge not found" }, { status: 404 })
}
const updateData: Record<string, unknown> = {
status: parsed.data.status,
notes: parsed.data.notes,
}
// Build update data — only include fields that were provided
const updateData: Record<string, unknown> = {}
if (parsed.data.status !== undefined) updateData.status = parsed.data.status
if (parsed.data.notes !== undefined) updateData.notes = parsed.data.notes
if (parsed.data.amountPence !== undefined) updateData.amountPence = parsed.data.amountPence
if (parsed.data.donorName !== undefined) updateData.donorName = parsed.data.donorName
if (parsed.data.donorEmail !== undefined) updateData.donorEmail = parsed.data.donorEmail
if (parsed.data.donorPhone !== undefined) updateData.donorPhone = parsed.data.donorPhone
if (parsed.data.rail !== undefined) updateData.rail = parsed.data.rail
if (parsed.data.status === "paid") {
updateData.paidAt = new Date()
@@ -73,13 +79,25 @@ export async function PATCH(
})
// If paid or cancelled, skip remaining reminders
if (["paid", "cancelled"].includes(parsed.data.status)) {
if (parsed.data.status && ["paid", "cancelled"].includes(parsed.data.status)) {
await prisma.reminder.updateMany({
where: { pledgeId: id, status: "pending" },
data: { status: "skipped" },
})
}
// Log activity
const changes = Object.keys(updateData).filter(k => k !== "paidAt" && k !== "cancelledAt")
await logActivity({
action: parsed.data.status === "paid" ? "pledge.marked_paid"
: parsed.data.status === "cancelled" ? "pledge.cancelled"
: "pledge.updated",
entityType: "pledge",
entityId: id,
orgId: existing.organizationId,
metadata: { changes, previousStatus: existing.status, newStatus: parsed.data.status },
})
return NextResponse.json(pledge)
} catch (error) {
console.error("Pledge update error:", error)