Automations deep redesign: message design studio with WhatsApp-native preview
COMPLETE RETHINK — from monitoring dashboard to message design studio.
## The Big Idea
Aaisha doesn't need a dashboard that says 'is it working?'
She needs a studio where she can SEE what Ahmed sees on his phone,
EDIT the words, TEST different approaches, and DESIGN cross-channel
sequences. The WhatsApp phone mockup is the star.
## New: Phone Mockups (3 channels)
- WhatsApp: green bubbles, blue ticks, org avatar, chat wallpaper,
full formatting (*bold*, _italic_, `code`, ━━━ dividers)
- Email: macOS mail client chrome, From header, subject line
- SMS: iOS Messages style, grey bubbles, contact avatar
## New: Template Editor
- Editable templates per step (receipt, day 2, 7, 14) per channel
- Live preview in phone mockup as you type
- Variable insertion chips: {{name}}, {{amount}}, {{reference}}, etc.
- Subject line editor for email channel
- Character count + SMS segment counter
## New: A/B Testing
- Create Variant B of any step/channel message
- 50/50 split traffic automatically
- Track sent count + conversion rate (paid after receiving)
- Side-by-side stats: 'A: 33% paid, B: 54% paid ★'
- Delete variant to revert to single message
## New: Channel Strategy Matrix
- 3 presets: Waterfall (default), Belt & Suspenders, Escalation
- Visual matrix: steps × channels with status indicators
- 1st = primary, fb = fallback, + = parallel send
- Waterfall: WhatsApp → SMS → Email (most cost-effective)
- Belt & Suspenders: all channels for receipts + final
- Escalation: start gentle (WA only), add channels as urgency increases
## New: Customizable Timing
- Each step's delay is editable inline (dropdown next to phone)
- Default: Day 2, Day 7, Day 14
- Can change to any schedule: Day 1, Day 3, Day 21, Day 28
## Schema: 2 new models
- MessageTemplate: per-org editable templates with A/B variants
(step, channel, variant, body, subject, splitPercent, sentCount, convertedCount)
- AutomationConfig: per-org timing + strategy + channel matrix
## API: /api/automations (GET/PATCH/DELETE)
- GET seeds defaults on first load (12 templates: 4 steps × 3 channels)
- PATCH upserts templates and config
- DELETE removes variant B and resets A to 100%
## Default templates (src/lib/templates.ts)
Extracted from hardcoded whatsapp.ts + reminders.ts into editable templates:
- WhatsApp: receipt, gentle, impact, final (with emoji + formatting)
- Email: receipt, gentle, impact, final (with cancel/pledge URLs)
- SMS: receipt, gentle, impact, final (160-char optimized)
## Architecture
templates.ts → resolvePreview() fills {{variables}} with examples
templates.ts → resolveTemplate() fills {{variables}} with real data
messaging.ts → sendToDonor() routes via channel waterfall
automations/route.ts → seeds + CRUD for templates + config
## Visual: Step timeline at top
4 tabs across the top with emoji, timing, description
Active step is dark (111827), others are white
Click to switch — editor and phone update together
## Layout
[Step Timeline — 4 tabs across top]
[Phone Mockup (left) | Editor (right)]
[Channel Strategy — expandable matrix]
[Live Feed — condensed stats + scheduled + messages]
This commit is contained in:
@@ -41,6 +41,8 @@ model Organization {
|
|||||||
events Event[]
|
events Event[]
|
||||||
pledges Pledge[]
|
pledges Pledge[]
|
||||||
imports Import[]
|
imports Import[]
|
||||||
|
messageTemplates MessageTemplate[]
|
||||||
|
automationConfig AutomationConfig?
|
||||||
|
|
||||||
@@index([slug])
|
@@index([slug])
|
||||||
}
|
}
|
||||||
@@ -234,6 +236,41 @@ model Import {
|
|||||||
@@index([organizationId])
|
@@index([organizationId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model MessageTemplate {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
organizationId String
|
||||||
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
step Int // 0=receipt, 1=day2_gentle, 2=day7_impact, 3=day14_final
|
||||||
|
channel String // whatsapp, email, sms
|
||||||
|
variant String @default("A") // A, B for A/B testing
|
||||||
|
name String // human label: "Gentle reminder", "Impact nudge"
|
||||||
|
subject String? // email-only: subject line
|
||||||
|
body String // template with {{variables}}
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
splitPercent Int @default(100) // A/B: e.g. 50 = 50% get this variant
|
||||||
|
sentCount Int @default(0)
|
||||||
|
convertedCount Int @default(0) // donors who paid after receiving this
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([organizationId, step, channel, variant])
|
||||||
|
@@index([organizationId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model AutomationConfig {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
organizationId String @unique
|
||||||
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
step1Delay Int @default(2) // days after pledge for step 1
|
||||||
|
step2Delay Int @default(7) // days after pledge for step 2
|
||||||
|
step3Delay Int @default(14) // days after pledge for step 3
|
||||||
|
strategy String @default("waterfall") // waterfall, parallel, escalation, custom
|
||||||
|
channelMatrix Json? // per-step channel config: { "0": ["whatsapp","email"], "1": ["whatsapp"], ... }
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model AnalyticsEvent {
|
model AnalyticsEvent {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
eventType String // pledge_start, amount_selected, rail_selected, identity_submitted, pledge_completed, instruction_copy_clicked, i_paid_clicked, payment_matched
|
eventType String // pledge_start, amount_selected, rail_selected, identity_submitted, pledge_completed, instruction_copy_clicked, i_paid_clicked, payment_matched
|
||||||
|
|||||||
198
pledge-now-pay-later/src/app/api/automations/route.ts
Normal file
198
pledge-now-pay-later/src/app/api/automations/route.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import prisma from "@/lib/prisma"
|
||||||
|
import { getUser } from "@/lib/session"
|
||||||
|
import { DEFAULT_TEMPLATES, STRATEGY_PRESETS } from "@/lib/templates"
|
||||||
|
import { getOrgChannels, getChannelStats, getMessageHistory, getPendingReminders } from "@/lib/messaging"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/automations
|
||||||
|
*
|
||||||
|
* Returns EVERYTHING the Automations page needs:
|
||||||
|
* - templates (per step/channel/variant)
|
||||||
|
* - config (timing, strategy, channel matrix)
|
||||||
|
* - channels (which are live)
|
||||||
|
* - stats (delivery numbers)
|
||||||
|
* - history (recent messages)
|
||||||
|
* - pending (upcoming reminders)
|
||||||
|
*
|
||||||
|
* Seeds default templates on first load.
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
const user = await getUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
|
||||||
|
if (!prisma) return NextResponse.json({ error: "No DB" }, { status: 503 })
|
||||||
|
|
||||||
|
const orgId = user.orgId
|
||||||
|
|
||||||
|
// Seed defaults if no templates exist
|
||||||
|
const templateCount = await prisma.messageTemplate.count({ where: { organizationId: orgId } })
|
||||||
|
if (templateCount === 0) {
|
||||||
|
await prisma.messageTemplate.createMany({
|
||||||
|
data: DEFAULT_TEMPLATES.map(t => ({
|
||||||
|
organizationId: orgId,
|
||||||
|
step: t.step,
|
||||||
|
channel: t.channel,
|
||||||
|
variant: "A",
|
||||||
|
name: t.name,
|
||||||
|
subject: t.subject || null,
|
||||||
|
body: t.body,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed config if none exists
|
||||||
|
let config = await prisma.automationConfig.findUnique({ where: { organizationId: orgId } })
|
||||||
|
if (!config) {
|
||||||
|
config = await prisma.automationConfig.create({
|
||||||
|
data: {
|
||||||
|
organizationId: orgId,
|
||||||
|
strategy: "waterfall",
|
||||||
|
channelMatrix: STRATEGY_PRESETS[0].matrix,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load everything in parallel
|
||||||
|
const [templates, channels, stats, history, pending] = await Promise.all([
|
||||||
|
prisma.messageTemplate.findMany({
|
||||||
|
where: { organizationId: orgId },
|
||||||
|
orderBy: [{ step: "asc" }, { channel: "asc" }, { variant: "asc" }],
|
||||||
|
}),
|
||||||
|
getOrgChannels(orgId),
|
||||||
|
getChannelStats(orgId, 7),
|
||||||
|
getMessageHistory(orgId, 30),
|
||||||
|
getPendingReminders(orgId, 10),
|
||||||
|
])
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
templates,
|
||||||
|
config: {
|
||||||
|
isActive: config.isActive,
|
||||||
|
step1Delay: config.step1Delay,
|
||||||
|
step2Delay: config.step2Delay,
|
||||||
|
step3Delay: config.step3Delay,
|
||||||
|
strategy: config.strategy,
|
||||||
|
channelMatrix: config.channelMatrix,
|
||||||
|
},
|
||||||
|
channels,
|
||||||
|
stats,
|
||||||
|
history,
|
||||||
|
pending,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/automations
|
||||||
|
*
|
||||||
|
* Update templates and/or config.
|
||||||
|
* Body: { templates?: [...], config?: {...} }
|
||||||
|
*/
|
||||||
|
export async function PATCH(request: NextRequest) {
|
||||||
|
const user = await getUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
|
||||||
|
if (!prisma) return NextResponse.json({ error: "No DB" }, { status: 503 })
|
||||||
|
|
||||||
|
const orgId = user.orgId
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
// Update templates
|
||||||
|
if (body.templates && Array.isArray(body.templates)) {
|
||||||
|
for (const t of body.templates) {
|
||||||
|
if (!t.step && t.step !== 0) continue
|
||||||
|
if (!t.channel) continue
|
||||||
|
|
||||||
|
await prisma.messageTemplate.upsert({
|
||||||
|
where: {
|
||||||
|
organizationId_step_channel_variant: {
|
||||||
|
organizationId: orgId,
|
||||||
|
step: t.step,
|
||||||
|
channel: t.channel,
|
||||||
|
variant: t.variant || "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: t.name,
|
||||||
|
subject: t.subject || null,
|
||||||
|
body: t.body,
|
||||||
|
isActive: t.isActive ?? true,
|
||||||
|
splitPercent: t.splitPercent ?? 100,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
organizationId: orgId,
|
||||||
|
step: t.step,
|
||||||
|
channel: t.channel,
|
||||||
|
variant: t.variant || "A",
|
||||||
|
name: t.name || "Custom",
|
||||||
|
subject: t.subject || null,
|
||||||
|
body: t.body,
|
||||||
|
isActive: t.isActive ?? true,
|
||||||
|
splitPercent: t.splitPercent ?? 100,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
if (body.config) {
|
||||||
|
const c = body.config
|
||||||
|
await prisma.automationConfig.upsert({
|
||||||
|
where: { organizationId: orgId },
|
||||||
|
update: {
|
||||||
|
isActive: c.isActive ?? undefined,
|
||||||
|
step1Delay: c.step1Delay ?? undefined,
|
||||||
|
step2Delay: c.step2Delay ?? undefined,
|
||||||
|
step3Delay: c.step3Delay ?? undefined,
|
||||||
|
strategy: c.strategy ?? undefined,
|
||||||
|
channelMatrix: c.channelMatrix ?? undefined,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
organizationId: orgId,
|
||||||
|
isActive: c.isActive ?? true,
|
||||||
|
step1Delay: c.step1Delay ?? 2,
|
||||||
|
step2Delay: c.step2Delay ?? 7,
|
||||||
|
step3Delay: c.step3Delay ?? 14,
|
||||||
|
strategy: c.strategy ?? "waterfall",
|
||||||
|
channelMatrix: c.channelMatrix ?? null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/automations
|
||||||
|
*
|
||||||
|
* Delete a variant B template (revert to A-only).
|
||||||
|
* Body: { step, channel, variant }
|
||||||
|
*/
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
const user = await getUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
|
||||||
|
if (!prisma) return NextResponse.json({ error: "No DB" }, { status: 503 })
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
if (body.variant === "A") return NextResponse.json({ error: "Cannot delete variant A" }, { status: 400 })
|
||||||
|
|
||||||
|
await prisma.messageTemplate.deleteMany({
|
||||||
|
where: {
|
||||||
|
organizationId: user.orgId,
|
||||||
|
step: body.step,
|
||||||
|
channel: body.channel,
|
||||||
|
variant: body.variant,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset variant A to 100%
|
||||||
|
await prisma.messageTemplate.updateMany({
|
||||||
|
where: {
|
||||||
|
organizationId: user.orgId,
|
||||||
|
step: body.step,
|
||||||
|
channel: body.channel,
|
||||||
|
variant: "A",
|
||||||
|
},
|
||||||
|
data: { splitPercent: 100 },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true })
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
256
pledge-now-pay-later/src/lib/templates.ts
Normal file
256
pledge-now-pay-later/src/lib/templates.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* Default message templates — seeded when an org first visits Automations.
|
||||||
|
*
|
||||||
|
* These are the STARTING POINT. Charities customise them.
|
||||||
|
* Templates use {{variable}} syntax, resolved at send time.
|
||||||
|
*
|
||||||
|
* Available variables:
|
||||||
|
* {{name}} — donor first name (or "there")
|
||||||
|
* {{amount}} — pledge amount e.g. "50"
|
||||||
|
* {{event}} — appeal/event name
|
||||||
|
* {{reference}} — payment reference e.g. "PNPL-A2F4-50"
|
||||||
|
* {{bank_name}} — org bank account name
|
||||||
|
* {{sort_code}} — sort code
|
||||||
|
* {{account_no}} — account number
|
||||||
|
* {{org_name}} — charity name
|
||||||
|
* {{days}} — days since pledge
|
||||||
|
* {{cancel_url}} — link to cancel pledge
|
||||||
|
* {{pledge_url}} — link to view pledges
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TemplateDefaults {
|
||||||
|
step: number
|
||||||
|
channel: string
|
||||||
|
name: string
|
||||||
|
subject?: string
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── WhatsApp templates ──────────────────────────────────────
|
||||||
|
|
||||||
|
const WA_RECEIPT = `🤲 *Pledge Confirmed!*
|
||||||
|
|
||||||
|
Thank you, {{name}}!
|
||||||
|
|
||||||
|
💷 *£{{amount}}* pledged to *{{event}}*
|
||||||
|
🔖 Ref: \`{{reference}}\`
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━
|
||||||
|
*Transfer to:*
|
||||||
|
Sort Code: \`{{sort_code}}\`
|
||||||
|
Account: \`{{account_no}}\`
|
||||||
|
Name: {{bank_name}}
|
||||||
|
Reference: \`{{reference}}\`
|
||||||
|
━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
⚠️ _Use the exact reference above_
|
||||||
|
|
||||||
|
Reply *HELP* anytime 💚`
|
||||||
|
|
||||||
|
const WA_GENTLE = `Hi {{name}} 👋
|
||||||
|
|
||||||
|
Just a quick reminder about your *£{{amount}}* pledge to {{event}}.
|
||||||
|
|
||||||
|
If you've already paid — thank you! 🙏
|
||||||
|
If not, your ref is: \`{{reference}}\`
|
||||||
|
|
||||||
|
Reply *PAID* if you've sent it, or *HELP* if you need the bank details again.`
|
||||||
|
|
||||||
|
const WA_IMPACT = `Hi {{name}},
|
||||||
|
|
||||||
|
Your *£{{amount}}* pledge to {{event}} is still pending ({{days}} days).
|
||||||
|
|
||||||
|
Every pound makes a real difference. 🤲
|
||||||
|
|
||||||
|
Ref: \`{{reference}}\`
|
||||||
|
|
||||||
|
Reply *PAID* once transferred, or *CANCEL* to withdraw.`
|
||||||
|
|
||||||
|
const WA_FINAL = `Hi {{name}},
|
||||||
|
|
||||||
|
This is our final message about your *£{{amount}}* pledge to {{event}}.
|
||||||
|
|
||||||
|
We completely understand if circumstances have changed. Reply:
|
||||||
|
|
||||||
|
*PAID* — if you've sent it
|
||||||
|
*CANCEL* — to withdraw the pledge
|
||||||
|
*HELP* — to get bank details
|
||||||
|
|
||||||
|
Ref: \`{{reference}}\``
|
||||||
|
|
||||||
|
// ─── Email templates ─────────────────────────────────────────
|
||||||
|
|
||||||
|
const EMAIL_RECEIPT = `Hi {{name}},
|
||||||
|
|
||||||
|
Thank you for pledging £{{amount}} at {{event}}!
|
||||||
|
|
||||||
|
To complete your donation, please transfer to:
|
||||||
|
|
||||||
|
Bank: {{bank_name}}
|
||||||
|
Sort Code: {{sort_code}}
|
||||||
|
Account: {{account_no}}
|
||||||
|
Reference: {{reference}}
|
||||||
|
|
||||||
|
⚠️ Please use the exact reference above so we can match your payment.
|
||||||
|
|
||||||
|
View your pledge: {{pledge_url}}
|
||||||
|
|
||||||
|
Thank you for your generosity!
|
||||||
|
|
||||||
|
{{org_name}}`
|
||||||
|
|
||||||
|
const EMAIL_GENTLE = `Hi {{name}},
|
||||||
|
|
||||||
|
Just a friendly reminder about your £{{amount}} pledge at {{event}}.
|
||||||
|
|
||||||
|
If you've already sent the payment, thank you! It can take a few days to appear.
|
||||||
|
|
||||||
|
If not, here's your reference: {{reference}}
|
||||||
|
|
||||||
|
View details: {{pledge_url}}
|
||||||
|
|
||||||
|
No longer wish to donate? {{cancel_url}}`
|
||||||
|
|
||||||
|
const EMAIL_IMPACT = `Hi {{name}},
|
||||||
|
|
||||||
|
Your £{{amount}} pledge from {{event}} is still outstanding.
|
||||||
|
|
||||||
|
Every donation makes a real impact. Your contribution helps us continue our vital work.
|
||||||
|
|
||||||
|
Payment reference: {{reference}}
|
||||||
|
View details: {{pledge_url}}
|
||||||
|
|
||||||
|
Need help? Just reply to this email.
|
||||||
|
Cancel pledge: {{cancel_url}}`
|
||||||
|
|
||||||
|
const EMAIL_FINAL = `Hi {{name}},
|
||||||
|
|
||||||
|
This is our final reminder about your £{{amount}} pledge from {{event}}.
|
||||||
|
|
||||||
|
We understand circumstances change. If you'd like to:
|
||||||
|
✅ Pay now — use reference: {{reference}}
|
||||||
|
❌ Cancel — {{cancel_url}}
|
||||||
|
|
||||||
|
View details: {{pledge_url}}
|
||||||
|
|
||||||
|
Thank you for considering us.
|
||||||
|
{{org_name}}`
|
||||||
|
|
||||||
|
// ─── SMS templates ───────────────────────────────────────────
|
||||||
|
|
||||||
|
const SMS_RECEIPT = `Thank you, {{name}}! £{{amount}} pledged to {{event}}. Ref: {{reference}}. Transfer to SC {{sort_code}} Acc {{account_no}} Name {{bank_name}}. Use exact ref!`
|
||||||
|
|
||||||
|
const SMS_GENTLE = `Hi {{name}}, reminder: your £{{amount}} pledge to {{event}} ref {{reference}} is pending. Already paid? Ignore this. Need help? Reply HELP.`
|
||||||
|
|
||||||
|
const SMS_IMPACT = `{{name}}, your £{{amount}} to {{event}} (ref: {{reference}}) is {{days}} days old. Every pound counts. Reply PAID or CANCEL.`
|
||||||
|
|
||||||
|
const SMS_FINAL = `Final reminder: £{{amount}} pledge to {{event}}, ref {{reference}}. Reply PAID if sent, or CANCEL to withdraw. Thank you. - {{org_name}}`
|
||||||
|
|
||||||
|
// ─── All defaults ────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const DEFAULT_TEMPLATES: TemplateDefaults[] = [
|
||||||
|
// Step 0: Receipt
|
||||||
|
{ step: 0, channel: "whatsapp", name: "Pledge receipt", body: WA_RECEIPT },
|
||||||
|
{ step: 0, channel: "email", name: "Pledge receipt", subject: "Your £{{amount}} pledge — payment details", body: EMAIL_RECEIPT },
|
||||||
|
{ step: 0, channel: "sms", name: "Pledge receipt", body: SMS_RECEIPT },
|
||||||
|
// Step 1: Day 2 gentle reminder
|
||||||
|
{ step: 1, channel: "whatsapp", name: "Gentle reminder", body: WA_GENTLE },
|
||||||
|
{ step: 1, channel: "email", name: "Gentle reminder", subject: "Quick reminder: your £{{amount}} pledge", body: EMAIL_GENTLE },
|
||||||
|
{ step: 1, channel: "sms", name: "Gentle reminder", body: SMS_GENTLE },
|
||||||
|
// Step 2: Day 7 impact nudge
|
||||||
|
{ step: 2, channel: "whatsapp", name: "Impact nudge", body: WA_IMPACT },
|
||||||
|
{ step: 2, channel: "email", name: "Impact nudge", subject: "Your £{{amount}} pledge is making a difference", body: EMAIL_IMPACT },
|
||||||
|
{ step: 2, channel: "sms", name: "Impact nudge", body: SMS_IMPACT },
|
||||||
|
// Step 3: Day 14 final reminder
|
||||||
|
{ step: 3, channel: "whatsapp", name: "Final reminder", body: WA_FINAL },
|
||||||
|
{ step: 3, channel: "email", name: "Final reminder", subject: "Final reminder: £{{amount}} pledge", body: EMAIL_FINAL },
|
||||||
|
{ step: 3, channel: "sms", name: "Final reminder", body: SMS_FINAL },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Variable metadata ──────────────────────────────────────
|
||||||
|
|
||||||
|
export const TEMPLATE_VARIABLES = [
|
||||||
|
{ key: "name", label: "Donor name", example: "Ahmed" },
|
||||||
|
{ key: "amount", label: "Amount (£)", example: "50" },
|
||||||
|
{ key: "event", label: "Appeal name", example: "Masjid Building Fund" },
|
||||||
|
{ key: "reference", label: "Payment reference", example: "PNPL-A2F4-50" },
|
||||||
|
{ key: "bank_name", label: "Bank account name", example: "Al Furqan Mosque" },
|
||||||
|
{ key: "sort_code", label: "Sort code", example: "20-30-80" },
|
||||||
|
{ key: "account_no", label: "Account number", example: "12345678" },
|
||||||
|
{ key: "org_name", label: "Charity name", example: "Al Furqan Mosque" },
|
||||||
|
{ key: "days", label: "Days since pledge", example: "7" },
|
||||||
|
{ key: "cancel_url", label: "Cancel link", example: "pledge.quikcue.com/p/cancel?ref=..." },
|
||||||
|
{ key: "pledge_url", label: "Pledge link", example: "pledge.quikcue.com/p/my-pledges" },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve template variables with actual values.
|
||||||
|
*/
|
||||||
|
export function resolveTemplate(body: string, vars: Record<string, string>): string {
|
||||||
|
return body.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] || `{{${key}}}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve template with example/preview values.
|
||||||
|
*/
|
||||||
|
export function resolvePreview(body: string): string {
|
||||||
|
const examples: Record<string, string> = {}
|
||||||
|
for (const v of TEMPLATE_VARIABLES) {
|
||||||
|
examples[v.key] = v.example
|
||||||
|
}
|
||||||
|
return resolveTemplate(body, examples)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Step metadata ───────────────────────────────────────────
|
||||||
|
|
||||||
|
export const STEP_META = [
|
||||||
|
{ step: 0, trigger: "Instantly", label: "Receipt", desc: "Pledge confirmation with bank details", icon: "✉️" },
|
||||||
|
{ step: 1, trigger: "Day 2", label: "Gentle reminder", desc: "Friendly nudge if not yet paid", icon: "👋" },
|
||||||
|
{ step: 2, trigger: "Day 7", label: "Impact nudge", desc: "Why their donation matters", icon: "💚" },
|
||||||
|
{ step: 3, trigger: "Day 14", label: "Final reminder", desc: "Last message — reply PAID or CANCEL", icon: "🔔" },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Strategy presets ────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface StrategyPreset {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
desc: string
|
||||||
|
matrix: Record<string, string[]> // step → channels
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STRATEGY_PRESETS: StrategyPreset[] = [
|
||||||
|
{
|
||||||
|
id: "waterfall",
|
||||||
|
name: "Waterfall",
|
||||||
|
desc: "Try WhatsApp first, then SMS, then Email. Most cost-effective.",
|
||||||
|
matrix: {
|
||||||
|
"0": ["whatsapp", "sms", "email"],
|
||||||
|
"1": ["whatsapp", "sms", "email"],
|
||||||
|
"2": ["whatsapp", "sms", "email"],
|
||||||
|
"3": ["whatsapp", "sms", "email"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "parallel",
|
||||||
|
name: "Belt & suspenders",
|
||||||
|
desc: "Send via ALL available channels. Maximum reach for important messages.",
|
||||||
|
matrix: {
|
||||||
|
"0": ["whatsapp+email"],
|
||||||
|
"1": ["whatsapp"],
|
||||||
|
"2": ["whatsapp+email"],
|
||||||
|
"3": ["whatsapp+email+sms"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "escalation",
|
||||||
|
name: "Escalation",
|
||||||
|
desc: "Start with WhatsApp only, add channels as urgency increases.",
|
||||||
|
matrix: {
|
||||||
|
"0": ["whatsapp", "email"],
|
||||||
|
"1": ["whatsapp"],
|
||||||
|
"2": ["whatsapp", "email"],
|
||||||
|
"3": ["whatsapp+email+sms"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user