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:
@@ -20,8 +20,14 @@ export interface PledgeData {
|
||||
donorName: string
|
||||
donorEmail: string
|
||||
donorPhone: string
|
||||
donorAddressLine1: string
|
||||
donorPostcode: string
|
||||
giftAid: boolean
|
||||
isZakat: boolean
|
||||
emailOptIn: boolean
|
||||
whatsappOptIn: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
consentMeta?: any
|
||||
// Scheduling
|
||||
scheduleMode: "now" | "date" | "installments"
|
||||
dueDate?: string
|
||||
@@ -64,8 +70,12 @@ export default function PledgePage() {
|
||||
donorName: "",
|
||||
donorEmail: "",
|
||||
donorPhone: "",
|
||||
donorAddressLine1: "",
|
||||
donorPostcode: "",
|
||||
giftAid: false,
|
||||
isZakat: false,
|
||||
emailOptIn: false,
|
||||
whatsappOptIn: false,
|
||||
scheduleMode: "now",
|
||||
})
|
||||
const [pledgeResult, setPledgeResult] = useState<{
|
||||
@@ -137,16 +147,24 @@ export default function PledgePage() {
|
||||
}
|
||||
|
||||
// Submit pledge (from identity step, or card/DD steps)
|
||||
const submitPledge = async (identity: { donorName: string; donorEmail: string; donorPhone: string; giftAid: boolean; isZakat?: boolean }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const submitPledge = async (identity: any) => {
|
||||
const finalData = { ...pledgeData, ...identity }
|
||||
setPledgeData(finalData)
|
||||
|
||||
// Inject IP + user agent into consent metadata for audit trail
|
||||
const consentMeta = finalData.consentMeta ? {
|
||||
...finalData.consentMeta,
|
||||
userAgent: navigator.userAgent,
|
||||
} : undefined
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/pledges", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...finalData,
|
||||
consentMeta,
|
||||
eventId: eventInfo?.id,
|
||||
qrSourceId: eventInfo?.qrSourceId,
|
||||
isZakat: finalData.isZakat || false,
|
||||
@@ -209,7 +227,7 @@ export default function PledgePage() {
|
||||
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />,
|
||||
1: <ScheduleStep amount={pledgeData.amountPence} onSelect={handleScheduleSelected} />,
|
||||
2: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} />,
|
||||
3: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} zakatEligible={eventInfo?.zakatEligible} />,
|
||||
3: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} zakatEligible={eventInfo?.zakatEligible} orgName={eventInfo?.organizationName} />,
|
||||
4: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} donorPhone={pledgeData.donorPhone} />,
|
||||
5: pledgeResult && (
|
||||
<ConfirmationStep
|
||||
|
||||
Reference in New Issue
Block a user