# Buy Local Lowveld — Mailchimp Automation Flows One section per trigger event. Each shows: - What event starts the flow - What gets written to Mailchimp - What tags end up on the subscriber - Which customer journey (if any) gets triggered --- ## Flow A — Newsletter signup **Trigger:** visitor submits email in the footer of any page. ``` footer input | | POST /newsletter-submit.php | { email } v validate email format | v mc_upsert_member( email, merge_fields: [], tags: ['Newsletter Signup'], status_if_new: 'subscribed' ) | v +----------------+ | Mailchimp | +----------------+ | 1 x PUT /lists/{id}/members/{hash} -- creates or updates | 1 x POST /lists/{id}/members/{hash}/tags -- applies tag +----------------+ | v redirect with ?newsletter=ok ``` **End state in Mailchimp:** - Subscribed member with only `Newsletter Signup` tag - No merge fields populated (just email) - No lifecycle or tier tag (newsletter subscribers aren't necessarily leads) - No customer journey triggered --- ## Flow B — Contact form submission **Trigger:** visitor submits `/contact.php`. ``` contact form | | POST /contact-submit.php | { first_name, last_name, email, phone, | topic, message, consent? } v validate required fields | v +----------------------+ | Log the message | | to | | contact-messages.log | +----------------------+ | v consent ticked? | yes +-----> status = 'subscribed' | no +-----> status = 'transactional' | v mc_upsert_member( email, merge_fields: [FNAME, LNAME, PHONE], tags: ['Contact Form'], status_if_new: status ) | v if consent: mc_set_exclusive_tag( email, MC_LIFECYCLE_TAGS, 'Lead' ) | v mc_trigger_journey( MC_JOURNEY_CONTACT_LEAD, -- skipped if journey_id == 0 email ) | v redirect to contact.php?success=1 ``` **End state in Mailchimp (with consent):** - Subscribed member - Merge fields: FNAME, LNAME, PHONE (if given) - Source tag: `Contact Form` - Lifecycle tag: `Lead` (all other lifecycle tags removed) - Customer journey: contact-lead journey started **End state in Mailchimp (no consent):** - Member exists but status = `transactional` (can't receive campaigns) - Source tag: `Contact Form` - No lifecycle tag change - No journey triggered --- ## Flow C — Become-a-member signup ★ most important **Trigger:** visitor submits the signup form on `/become-member.php`. ``` signup form | | POST /signup-submit.php | { first_name, last_name, email, phone, business, | industry, tier, notes, consent } v validate all required fields | v mc_upsert_member( email, merge_fields: [ FNAME, LNAME, BUSINESS, INDUSTRY, TIER, JOINDATE = today, RENEWAL = today + 1y, PHONE (optional) ], tags: [], -- tags applied in step 3-5 below status_if_new: 'subscribed' ) | v mc_set_exclusive_tag( -- ✱ mutually exclusive email, MC_TIER_TAGS, -- Bronze|Silver|Gold|Platinum|Diamond chosen_tier ) | v mc_set_exclusive_tag( -- ✱ mutually exclusive email, MC_LIFECYCLE_TAGS, -- New Member|Payment Received|... 'New Member' ) | v mc_upsert_member( email, merge_fields: [], tags: ['Become a Member'] -- source tag, additive ) | v mc_trigger_journey( MC_JOURNEY_NEW_MEMBER, -- 12-month onboarding (SOW 2.3.A) email ) | v log: "New member signup: ..." | v redirect to become-member.php?success=1 ``` **End state in Mailchimp:** - Subscribed member - 7 merge fields populated - Tags: one tier tag, `New Member` lifecycle tag, `Become a Member` source tag - Customer journey: 12-month new-member journey started **Key design decisions in this flow:** 1. **Mutually-exclusive tags.** Tier and lifecycle use `mc_set_exclusive_tag()`, which sends `inactive` for every tag in the family except the chosen one — prevents a member from ending up with both "Bronze" and "Gold" by accident when they upgrade. 2. **Failure is non-blocking.** If any Mailchimp call fails, the user still gets a success page. Failures are logged to `mailchimp.log` for admin review. The alternative (blocking error page) would lose the signup entirely. 3. **Journey is optional.** If `MC_JOURNEY_NEW_MEMBER['journey_id']` is still `0` (journey not yet built in the Mailchimp UI), the trigger silently skips. Once the journey exists, paste the IDs into `config.php` and every future signup enters it automatically. --- ## Flow D — Member updates their info ✅ live in Phase 2a **Trigger:** a logged-in member saves changes in `/member/edit-business.php`. ``` edit-business form (authenticated session + CSRF token) | | POST /member/edit-business.php v csrf_verify() | v validate required fields | v db_update(members, …new field values…) | v db_update(listings, …) or db_insert(listings, …) -- upsert | v mc_sync_member_from_db($fresh_member, 'profile_update') | | inside that function: | 1. PUT /lists/{id}/members/{hash} with all 7 merge fields | 2. mc_set_exclusive_tag() for the tier (in case it changed) | 3. POST /.../members/{hash}/tags to add the industry tag | 4. INSERT into mailchimp_sync_log (audit) v mc_trigger_journey(MC_JOURNEY_PROFILE_UPDATE, email) | v reload member + listing; re-render form with "Saved." alert ``` **Merge fields updated on every save:** `FNAME`, `LNAME`, `PHONE`, `BUSINESS`, `INDUSTRY`, `TIER`, and (if set on the member row) `JOINDATE` / `RENEWAL`. **Tags handled:** - Tier — mutually exclusive; a Silver→Gold upgrade removes Silver and adds Gold - Industry — additive; a member might grow into a second category without losing their first - Lifecycle — **not touched** by edit-business. It's controlled by payment / renewal / cancellation events elsewhere. **Audit trail:** every call adds a row to the `mailchimp_sync_log` DB table with `event = 'profile_update'` and `success = 0/1`. Admins can query this to confirm a given member's profile change reached Mailchimp. --- ## Flow E — Member buys items (payment confirmed) ✅ live in Phase 2b **Trigger:** PayFast ITN confirms a successful payment. ``` PayFast POST /payfast-itn.php | v verify signature + source IP + amount + server-to-server postback | all 4 pass v INSERT payments row (idempotent on pf_payment_id) UPDATE invoice status -> paid UPDATE order status -> paid (if any) INSERT transaction row (negative = credit) store PayFast subscription token (if recurring) promote member pending -> active (if first charge) | v mc_event_payment_received(member) | | inside that: | mc_set_exclusive_tag(email, MC_LIFECYCLE_TAGS, 'Payment Received') | mc_trigger_journey(MC_JOURNEY_PAYMENT_RECEIVED, email) | INSERT mailchimp_sync_log v UPDATE payments.processed = 1 ``` Fires for both flows — one-off cart payments AND the initial membership charge. Recurring membership charges in later years each fire the same event (since each is a distinct PayFast payment with a new `pf_payment_id`). --- ## Flow F — Renewal reminder, 30 days before ✅ live in Phase 2b **Trigger:** daily cron (`cron/renewal-reminder.php`). ``` cron/renewal-reminder.php (CLI or HTTP + ?secret=XXX) | v INSERT cron_runs (outcome='running') | v SELECT * FROM members WHERE status = 'active' AND renewal_date = DATE_ADD(CURDATE(), INTERVAL 30 DAY) | v for each member: mc_event_renewal_reminder(member) | sets 'Renewal Reminder' lifecycle tag (mutually exclusive) | triggers MC_JOURNEY_RENEWAL_REMINDER | INSERT mailchimp_sync_log v UPDATE cron_runs (outcome='ok', rows_processed=N) ``` The cron is idempotent on re-runs within the same day — the lifecycle tag just gets re-applied (no duplicate) and the journey trigger treats a repeated call as a no-op. If you run it twice across different days the tag stays but the journey re-triggers, which is usually fine. See also: `admin/cron-status.php` for a log of recent runs. --- ## Tag taxonomy reference The three families that drive all segmentation: | Family | Behaviour | Values | |--------|-----------|--------| | **Lifecycle** | Mutually exclusive | `New Member`, `Payment Received`, `Renewal Reminder`, `Payment Overdue`, `Cancelled Member`, `Lead` | | **Tier** | Mutually exclusive | `Bronze`, `Silver`, `Gold`, `Platinum`, `Diamond` | | **Source** | Additive (a person can have multiple) | `Newsletter Signup`, `Contact Form`, `Become a Member` | | **Industry** | Additive | The 33 directory categories | See `includes/config.php` for the canonical lists (`MC_LIFECYCLE_TAGS`, `MC_TIER_TAGS`, `MC_SOURCE_TAGS`). ## Customer journey placeholders In `config.php`: ```php define('MC_JOURNEY_NEW_MEMBER', ['journey_id' => 0, 'step_id' => 0]); define('MC_JOURNEY_CONTACT_LEAD', ['journey_id' => 0, 'step_id' => 0]); ``` Both start at `0`. Once the marketing team has built the journeys in the Mailchimp dashboard: 1. Open the journey 2. Click the "API trigger" starting step 3. Copy the Journey ID and Step ID from the right-hand panel 4. Paste into `config.php` Until you do that, `mc_trigger_journey()` silently skips. Everything else still works — a signup will still write the member, apply the tags, and log itself; only the automated email sequence is delayed. ## Flow G — Payment overdue ✅ live in Phase 2b **Trigger:** daily cron (`cron/overdue.php`). ``` cron/overdue.php | v UPDATE invoices SET status = 'overdue' WHERE status = 'unpaid' AND due_at < CURDATE() | v SELECT DISTINCT members with overdue invoices | v for each member: mc_event_payment_overdue(member) | 'Payment Overdue' lifecycle tag (mutually exclusive) | triggers MC_JOURNEY_PAYMENT_OVERDUE v UPDATE cron_runs ``` Important: this runs AFTER the renewal-reminder cron, not before. If a member hits both states on the same day, overdue wins (since it's the more urgent lifecycle label). ## Flow H — Member cancels ✅ live in Phase 2b **Trigger:** member POSTs from `member/cancel-membership.php`. ``` cancel confirmation form (auth + CSRF) | | POST + "confirm" checkbox ticked v UPDATE members SET status = 'cancelled' UPDATE payment_tokens SET status = 'cancelled' (our side) | v mc_event_cancelled(member) | 'Cancelled Member' lifecycle tag (mutually exclusive) | triggers MC_JOURNEY_CANCELLATION (retention / win-back) v show confirmation + logout link ``` Production note: also call PayFast's subscription-cancel API so they stop charging. The current code only marks the token cancelled locally because the public sandbox doesn't expose the API.