bulletproof consent: Gift Aid (HMRC), email opt-in, WhatsApp opt-in with full audit trail
GIFT AID (HMRC compliance):
- Exact HMRC model declaration text displayed and recorded
- Home address (line 1 + postcode) collected when Gift Aid is ticked
- giftAidAt timestamp recorded separately from the boolean
- Declaration text, donor name, timestamp stored in consentMeta JSON
EMAIL + WHATSAPP (GDPR/PECR compliance):
- Separate, granular opt-in checkboxes (not bundled, not pre-ticked)
- Each consent records: exact text shown, timestamp, consent version
- Consent checkboxes only appear when relevant contact info is provided
- Cron reminders gated on consent — no sends without opt-in
- Pledge creation WhatsApp receipt gated on whatsappOptIn
AUDIT TRAIL (consentMeta JSON on every pledge):
- giftAid: {declared, declarationText, declaredAt}
- email: {granted, consentText, grantedAt}
- whatsapp: {granted, consentText, grantedAt}
- IP address captured server-side from x-forwarded-for
- User agent captured client-side
- consentVersion field for tracking wording changes
EXPORTS:
- CRM CSV now includes: donor_address, donor_postcode, gift_aid_declared_at,
is_zakat, email_opt_in, whatsapp_opt_in
- Gift Aid export has full HMRC-required fields
Schema: 6 new columns on Pledge (donorAddressLine1, donorPostcode,
giftAidAt, emailOptIn, whatsappOptIn, consentMeta)
This commit is contained in:
@@ -105,7 +105,13 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const { amountPence, rail, donorName, donorEmail, donorPhone, giftAid, isZakat, eventId, qrSourceId, scheduleMode, dueDate, installmentCount, installmentDates } = parsed.data
|
||||
const { amountPence, rail, donorName, donorEmail, donorPhone, donorAddressLine1, donorPostcode, giftAid, isZakat, emailOptIn, whatsappOptIn, consentMeta, eventId, qrSourceId, scheduleMode, dueDate, installmentCount, installmentDates } = parsed.data
|
||||
|
||||
// Capture IP for consent audit trail
|
||||
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
|
||||
|| request.headers.get("x-real-ip")
|
||||
|| "unknown"
|
||||
const consentMetaWithIp = consentMeta ? { ...consentMeta, ip } : undefined
|
||||
|
||||
// Get event + org
|
||||
const event = await prisma.event.findUnique({
|
||||
@@ -160,8 +166,14 @@ export async function POST(request: NextRequest) {
|
||||
donorName: donorName || null,
|
||||
donorEmail: donorEmail || null,
|
||||
donorPhone: donorPhone || null,
|
||||
donorAddressLine1: donorAddressLine1 || null,
|
||||
donorPostcode: donorPostcode || null,
|
||||
giftAid,
|
||||
giftAidAt: giftAid ? new Date() : null,
|
||||
isZakat: isZakat || false,
|
||||
emailOptIn: emailOptIn || false,
|
||||
whatsappOptIn: whatsappOptIn || false,
|
||||
consentMeta: consentMetaWithIp || undefined,
|
||||
eventId,
|
||||
qrSourceId: qrSourceId || null,
|
||||
organizationId: org.id,
|
||||
@@ -189,8 +201,8 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
})
|
||||
|
||||
// WhatsApp receipt for the plan
|
||||
if (donorPhone) {
|
||||
// WhatsApp receipt for the plan (only if they consented)
|
||||
if (donorPhone && whatsappOptIn) {
|
||||
const name = donorName?.split(" ")[0] || "there"
|
||||
const { sendWhatsAppMessage } = await import("@/lib/whatsapp")
|
||||
sendWhatsAppMessage(donorPhone,
|
||||
@@ -230,8 +242,14 @@ export async function POST(request: NextRequest) {
|
||||
donorName: donorName || null,
|
||||
donorEmail: donorEmail || null,
|
||||
donorPhone: donorPhone || null,
|
||||
donorAddressLine1: donorAddressLine1 || null,
|
||||
donorPostcode: donorPostcode || null,
|
||||
giftAid,
|
||||
giftAidAt: giftAid ? new Date() : null,
|
||||
isZakat: isZakat || false,
|
||||
emailOptIn: emailOptIn || false,
|
||||
whatsappOptIn: whatsappOptIn || false,
|
||||
consentMeta: consentMetaWithIp || undefined,
|
||||
eventId,
|
||||
qrSourceId: qrSourceId || null,
|
||||
organizationId: org.id,
|
||||
@@ -311,8 +329,8 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Async: Send WhatsApp receipt to donor (non-blocking)
|
||||
if (donorPhone) {
|
||||
// Async: Send WhatsApp receipt to donor (only if they consented)
|
||||
if (donorPhone && whatsappOptIn) {
|
||||
sendPledgeReceipt(donorPhone, {
|
||||
donorName: donorName || undefined,
|
||||
amountPounds: (amountPence / 100).toFixed(0),
|
||||
|
||||
Reference in New Issue
Block a user