07 — Email/SMS Request Delivery Pipeline (Blueprint §4)
Cursor-ready plan: order webhooks → campaign eligibility → per-product TestimonialRequest rows → scheduled send → reminders → request events.
07 — Email/SMS Request Delivery Pipeline
Section titled “07 — Email/SMS Request Delivery Pipeline”Source: 02-Implementation-Blueprint.md — §4) Email/SMS Workflow (Operational Detail) (steps 1–8).
Product alignment: 01-Post-Purchase-Video-Testimonial-Collector-Plan.md — §6 MVP (post-purchase campaigns, tokenized links, one request per delivered product line item, email templates, reminders).
This document is a build spec only. No code changes are implied until a task references this file.
Related plans (do not duplicate):
06-public-submission-page-screen-13.md— customer consumessubmissionTokenon/t/:token.05-storefront-widgets-and-read-api.md— publishing approved media to the storefront.
0) Goal (one sentence)
Section titled “0) Goal (one sentence)”When Shopify fires the right order webhook, the app evaluates active campaigns, creates one TestimonialRequest per eligible delivered line item (product), schedules the first outreach, a worker/cron sends email and/or SMS using templates, appends rows to TestimonialRequestEvent, and sends reminders until the customer submits or max reminders is reached.
1) Preconditions (data + config that must exist)
Section titled “1) Preconditions (data + config that must exist)”| Prerequisite | Blueprint ref |
|---|---|
TestimonialCampaign, TestimonialCampaignProduct (if not “all products”) | §5.2, §5.3 |
TestimonialRequest, TestimonialRequestEvent | §5.5, §5.6 |
TestimonialTemplate (or equivalent) for initial_request / reminder_1 / reminder_2 | §5.4 |
Shop row with testimonial app installed, accessToken valid | Standard |
| Product mirror optional but helpful for line-item titles in messages | Product / Admin API |
Public base URL for submissionUrl = ${APP_URL}/t/${token} (or app proxy path — must match 06 route) | §6 public routes |
You may stub template bodies in DB seed until Screen 4 (Templates) admin exists; this pipeline still needs readable template rows to render emails.
2) Scope
Section titled “2) Scope”In scope
Section titled “In scope”- Webhook entrypoint(s) for order events: at minimum support what
TestimonialCampaign.triggerEventallows (order_paid/order_fulfilledper §5.2). Register inshopify.app.tomland implementapp/routes/webhooks.orders.*.jsx(exact file names from §6). - Idempotent processing: the same webhook delivery must not create duplicate
TestimonialRequestrows for the same (shop, order, line item product, campaign) — use a unique business key or idempotency table (see §5). - Eligibility engine: campaign
status,allProductsvsTestimonialCampaignProductallowlist,delayDays, channel. - Line-item expansion: one job row per delivered product line item the blueprint requires (see §3.2 for “delivered” definition).
- Token + URL: generate high-entropy
submissionToken; setsubmissionUrl(and persistTestimonialRequest.submissionUrlper §5.5). - Schedule: set
scheduledFor = order_event_time + delayDays(use UTC; document merchant interpretation if you later add “business days”). - Sender worker: process due
TestimonialRequestrows wherestatusinscheduled/queued/failed(retry policy) andscheduledFor <= now. - Send email (reuse
app/lib/email.server.js+ Brevo env pattern from boilerplate) and/or SMS via a pluggable provider (see §6). - Events: insert
TestimonialRequestEventforqueued→sent→ (optional)delivered/opened/clicked/failedper Screen 10 status list and §5.6. - Reminders: if
reminderEnabledand no submission byreminderDelayDaysafter first send, up tomaxReminders, usingreminder_1/reminder_2templates. - Stop reminders when
TestimonialRequest.submittedAtis set (from 06 submit path) or status becomessubmitted.
Out of scope (other plans)
Section titled “Out of scope (other plans)”- Full admin UI for campaigns (Screens 2–3) and templates (Screen 4) — minimum seed or migration defaults suffice for pipeline testing.
- Screen 10 (Requests Log) UI — surface same DB rows; optional read-only page later.
- Deep delivered/open tracking requires provider webhooks or pixel links — v1 can record
sentreliably andclickedwhen customer hits/t/:token(loader updatesclickedAtper 06).
3) Webhook handling
Section titled “3) Webhook handling”3.1 Subscribed topics
Section titled “3.1 Subscribed topics”Align §5.2 triggerEvent with Shopify webhook topics:
order_paid→ typicallyorders/paidpayload (confirm naming with Shopify Admin API version in use).order_fulfilled→orders/fulfilledor fulfillment-centric payloads — pick one canonical mapping and document it in code comments.
Implementation: one route file per topic or shared handler with topic discriminator — either is fine if tests cover both.
3.2 Payload normalization
Section titled “3.2 Payload normalization”From each webhook payload, derive:
shopDomain(session lookup →Shop)shopifyOrderId(GID or numeric — normalize to stored format consistent withTestimonialRequest.shopifyOrderId)- Line items: only items representing physical/digital products you support; skip shipping-only lines.
- Delivered criterion: for MVP, either:
- Fulfillment webhook: consider line items fulfilled, or
- Paid webhook: consider paid items only
Choose explicitly in code + README fragment so QA knows expected behavior.
3.3 Authentication & verification
Section titled “3.3 Authentication & verification”Use Shopify webhook HMAC verification per Shopify docs / existing boilerplate pattern (authenticate.webhook or equivalent). Reject invalid signatures before DB writes.
3.4 Idempotency
Section titled “3.4 Idempotency”Shopify retries webhooks. Before creating requests:
- Compute
webhookDeliveryIdif available in headers, or - Hash
(shopId, shopifyOrderId, triggerEvent, lineItemsStableFingerprint)and store processed webhook ids in a smallWebhookReceipttable or rely on unique constraint on(shopId, shopifyOrderId, shopifyProductId, campaignId)onTestimonialRequest.
If duplicate, exit 200 without error after verifying rows exist.
4) Campaign eligibility (pure logic module)
Section titled “4) Campaign eligibility (pure logic module)”Extract evaluateCampaignsForOrder({ shopId, orderPayload }) into app/lib/testimonial-campaign-eligibility.server.js (name flexible).
Rules:
- Load campaigns where
shopIdmatches andstatus === 'active'. - For each campaign, check
triggerEventmatches this webhook’s semantic type. - Audience:
- If
allProducts === true→ all eligible line items pass. - Else → line item’s
shopifyProductIdmust exist inTestimonialCampaignProduct.
- If
- Produce candidates:
(campaignId, shopifyProductId, shopifyVariantId?, quantity handling?).
Quantity: blueprint says one testimonial request per delivered product line item. If quantity > 1, decide:
- Option A: one
TestimonialRequestper unit (multiple rows), or - Option B: one row per unique product per order (single row).
Option A matches stricter “one link per item shipped.” Document the choice in the module header.
5) Persist TestimonialRequest rows
Section titled “5) Persist TestimonialRequest rows”For each candidate from §4:
-
Generate
submissionToken(opaque, unguessable). -
Build
submissionUrl→ must open 06 page. -
Insert row with:
status: start asscheduled(orqueuedif you send immediately — align enum with Screen 10 list).scheduledFor: now +campaign.delayDaysor webhook timestamp + delay — match blueprint §5.5.- Copy customer email/phone from order into
customerEmail/customerPhonefor channel routing. campaignId,shopifyOrderId,shopifyProductId, etc.
-
Insert
TestimonialRequestEventrow:eventType = queued(orscheduled).
Unique constraint: enforce at DB level to prevent duplicate outreach for same logical unit.
6) Worker / cron — send due requests
Section titled “6) Worker / cron — send due requests”6.1 Trigger
Section titled “6.1 Trigger”Provide GET or POST internal route, e.g. app/routes/api.cron.testimonial-send-due.jsx, protected by:
CRON_SECRETheader or query param (same pattern as other cron routes in repo), or- Platform scheduler with private network access.
Run every 1–5 minutes in production.
6.2 Query
Section titled “6.2 Query”Select TestimonialRequest where:
shopIdactive,scheduledFor <= now(),statusin allowed set (scheduled,failedwith retry budget),- Not yet
sentfor initial send (track viasentAtorstatus).
6.3 Email send
Section titled “6.3 Email send”-
Resolve template:
TestimonialTemplatefor(shopId, campaignId or null, channel=email, templateType=initial_request)with fallback to shop default. -
Substitute variables per blueprint Screen 4 list:
customer_first_name,shop_name,product_name,submission_link,incentive_text. -
Product name: join
ProductbyshopifyProductIdor fetch from cached title on request row if you denormalize. -
Send via existing mailer; on success:
- Set
sentAt,status = sent(or keepsentseparate from reminder states — define enum clearly). - Insert event
sent.
- Set
On failure: lastError, event failed, increment retry or dead-letter per policy.
6.4 SMS send (if channel includes SMS)
Section titled “6.4 SMS send (if channel includes SMS)”- Pluggable adapter: Twilio/MessageBird/etc. Store minimal config on
Shopor campaign (future); v1 can no-op with feature flag if SMS provider env missing, but recordfailedwith clear reason.
7) Reminders
Section titled “7) Reminders”Schedule next send only if:
reminderEnabled,submittedAtis null,reminderCount < maxReminders,- Elapsed time since first
sentAt≥reminderDelayDays(use calendar days UTC unless product says otherwise).
Template selection:
reminder_1for first reminder,reminder_2for second (ifmaxRemindersallows).
Increment reminderCount after each reminder send. Insert matching TestimonialRequestEvent rows.
Stop when 06 marks submission complete (submittedAt / status submitted).
8) Click / open tracking (v1 realistic subset)
Section titled “8) Click / open tracking (v1 realistic subset)”- Clicked: when
/t/:tokenloader runs (06), setclickedAtonce → correlates with funnel (Screen 11). - Opened: depends on email provider open pixel — optional v2.
- Delivered: provider-dependent — optional v2.
Do not block MVP on open/delivered webhooks.
9) Environment variables (add to .env.sample in implementation task)
Section titled “9) Environment variables (add to .env.sample in implementation task)”| Variable | Purpose |
|---|---|
CRON_SECRET or reuse existing | Protect cron send route |
APP_PUBLIC_URL / existing SHOPIFY_APP_URL | Build submissionUrl correctly |
| Brevo keys | Already in boilerplate for email |
| SMS provider keys | Optional, namespaced |
10) Testing checklist (acceptance criteria)
Section titled “10) Testing checklist (acceptance criteria)”- Webhook with invalid HMAC → 401/403, no DB rows.
- Valid order → creates N
TestimonialRequestrows matching N eligible line items; no duplicates on replay. - Campaign paused → no new rows.
-
delayDaysrespected:scheduledForis correct in UTC. - Cron picks due rows, sends email, sets
sentAt+ event row. - After submit (06), reminder cron does not send further messages for that request.
- Failure path: provider error →
failedevent +lastErrorpopulated.
11) Implementation order (for Cursor)
Section titled “11) Implementation order (for Cursor)”- Prisma migrations for §5.5 / §5.6 (and campaign tables if missing).
- Webhook route(s) + HMAC + idempotency.
- Eligibility module + unit-style tests (pure functions).
- Insert
TestimonialRequest+ initial event. - Email template resolution + variable substitution.
- Cron send route + initial outreach.
- Reminder scheduler loop.
- Wire
clickedAtfrom 06 if not already (cross-plan coordination).
12) References
Section titled “12) References”02-Implementation-Blueprint.md— §4, §5.2–§5.6, §6 webhook list, §7 steps 1–4 & 6–8.01-Post-Purchase-Video-Testimonial-Collector-Plan.md— §6 Collection & Requests + Messaging.
13) Note on numbering
Section titled “13) Note on numbering”Existing plans in this folder: 05-storefront-widgets-and-read-api.md, 06-public-submission-page-screen-13.md. This file is 07-… to keep ordering consistent.