Skip to content

Mailchimp Newsletter Subscription

Flow ID: CF-38 | Module(s): ecommercen/eshop/, application/libraries/ | Complexity: Medium Last Updated: 2026-06-28

Business Context

This flow is the storefront-facing Mailchimp newsletter signup: a public endpoint (POST /api/mailchimp) that subscribes an email address to a configured Mailchimp audience. It is reached from two storefront surfaces — (a) the homepage/footer newsletter modal, and (b) a server-to-server call from the checkout/order-preview flow when the customer ticks the "subscribe me" checkbox. It is a guest-accessible endpoint (no login required).

Two adjacent concerns live elsewhere and are cross-linked, not re-documented here:

  • The Mailchimp Marketing API client (Adv_Mailchimp_Api), the MAILCHIMP/* registry keys, double-opt-in, and the two-list (site vs checkout) pattern are documented in IN-13 Newsletter Integrations (canonical home for newsletter-provider API details).
  • The admin configuration page (settings/mailchimp) is part of AD-13 Settings.

Mailchimp is a parallel newsletter provider alongside Manago/Moosend/Apifon (IN-13); the standalone Adv_mailchimp controller exists so a single subscribe endpoint can serve both the footer modal and the checkout (git d8bcb07ea3, 2024-10-30).

API Reference

REST Endpoints (Modern Layer)

None. /api/mailchimp is a legacy CI route, not part of the modern /rest/v{N}/ stack (no src/Rest controller, no rest_policies.php entry). The modern REST checkout instead fires a MarketingConsentCaptured event that currently has no Mailchimp listener (src/Domains/Checkout/PlaceOrderService.php:261-283, src/Domains/Order/Event/MarketingConsentCaptured.php) — so REST checkout does not subscribe anyone to Mailchimp today.

Legacy Routes

RouteMaps tofile:lineAuth
api/mailchimpeshop/mailchimp/mailchimpSubscribeapplication/config/routes.php:200none (public/guest)
(\w{2})/mailchimpSubscribemailchimp/mailchimpSubscriberoutes.php:230none (locale-prefixed twin)
settings/mailchimpsettings/mailchimproutes.php:199admin (ADVISABLE/ADMIN) — see AD-13

Request (POST /api/mailchimp, Content-Type: application/json): body { "email": string, "url": string }, read via raw php://input (ecommercen/eshop/controllers/Adv_mailchimp.php:28, getJsonInput() :80-88). The url field routes config: "preview_order" → checkout list/interest/tag; anything else → site list/interest/tag (:34).

Response: {"message":"Successfully subscribed to Mailchimp"} 200 (:46); {"error":"Invalid input: email is missing"} 400 (:31); {"error":"Invalid JSON input"} 400 (:85); {"error":"Subscription failed","details":<msg>} 500 (:49).

Code Flow

Storefront footer modal (newsletter_section_mailchimp.php, gated by MAILCHIMP/ENABLED)
  └─ footer_js.php:580-641  (validate email + GDPR consent; derive url from page; POST /api/mailchimp {email,url})

Checkout (CheckoutPage.vue:659-675 checkbox name="register_mailchimp_newsletter")
  └─ Adv_order.php previewOrder :352-357
        └─ if POST register_mailchimp_newsletter && validateMailchimpRegistryValues()
             └─ registerCustomerToMailchimp() :1846-1858  (server-to-server cURL POST to site_url('api/mailchimp'), url='preview_order')


Adv_mailchimp::mailchimpSubscribe()  ecommercen/eshop/controllers/Adv_mailchimp.php:26-51
  ├─ getJsonInput() :80-88                     (JSON_THROW_ON_ERROR)
  ├─ require email else 400                     :30-32
  ├─ formType = url==='preview_order' ? checkout : site   :34
  ├─ getMailchimpConfig(formType) :59-72        (select checkout/site registry keys)
  ├─ build payload {email_address,status,interests,tags}  :37-42
  └─ Mailchimp_Api->addMemberToList(listId, data)  -> Mailchimp Marketing API POST /lists/{id}/members

Data Model

This flow performs no domain-table reads or writes — all subscriber data is pushed to Mailchimp's remote API and never persisted locally (no subscribers/mailchimp_* table). The only DB touch is configuration in the generic registry key-value table (database/initial/initial.sql:1034-1045), under reggroup='MAILCHIMP'. The registry table and its caching are owned by SY-19 Config & Registry; the MAILCHIMP/* keys themselves are catalogued in IN-13 and AD-13.

Domain Layer

Modern Domain / REST (src/)

None for this flow. The only modern adjacency is the REST checkout's MarketingConsentCaptured event (src/Domains/Order/Event/MarketingConsentCaptured.php), dispatched from PlaceOrderService.php:272 for future CRM-sync listeners — it does not call Mailchimp (no listener wired; in-code note PlaceOrderService.php:266-268).

Legacy Layer (ecommercen/)

FileResponsibility
ecommercen/eshop/controllers/Adv_mailchimp.php:1-105Storefront subscribe controller — mailchimpSubscribe() :26-51, getMailchimpConfig() :59-72, getJsonInput() :80-88, jsonResponse() :98-104.
application/modules/eshop/controllers/Mailchimp.php:10Concrete HMVC controller (extends Adv_mailchimp) the routes resolve to.
application/libraries/Mailchimp_Api.php:3Client-overridable thin wrapper (extends Adv_Mailchimp_Api).
ecommercen/libraries/Adv_Mailchimp_Api.php:1-150Mailchimp Marketing API v3 client (mailchimp/marketing ^3.0). addMemberToList() :46-48. Canonical home: IN-13.
ecommercen/eshop/controllers/Adv_order.php:352-357, 1809-1815, 1846-1891, 1893-1899Checkout-side trigger: displayMailchimpNewsletterCheckbox(), registerCustomerToMailchimp(), sendPostRequestWithCurl(), validateMailchimpRegistryValues().

Storefront views/JS: footer modal markup application/views/main/components/homepage/newsletter/newsletter_section_mailchimp.php (gated by MAILCHIMP/ENABLED); footer handler application/views/main/components/footer/footer_js.php:580-641; checkout checkbox assets/main/vue/CheckoutPage.vue:659-675 (option :753-758), Vue model assets/vue/mixins/checkoutPage.js:20.

Configuration

All config is registry-based (reggroup='MAILCHIMP'); there are no .env keys. Keys are written by the admin page (ecommercen/settings/controllers/Adv_settings.php:2332-2376) and read by this flow. The full key catalogue (ENABLED, API_KEY, SERVER_PREFIX, SUBSCRIBE_AS_PENDING, SITE_*, CHECKOUT_*, CUSTOM_*) is documented in IN-13; the keys this flow reads directly:

KeyRead atEffect
MAILCHIMP/ENABLEDfooter_js.php:580, newsletter_section_mailchimp.php:1, Adv_order.php:1811Gates the footer modal + checkout checkbox
MAILCHIMP/SUBSCRIBE_AS_PENDINGAdv_mailchimp.php:65,70status='pending' (double opt-in) vs 'subscribed'
MAILCHIMP/SITE_LIST / SITE_INTEREST / SITE_TAG_NAMEAdv_mailchimp.php:67,68,69Site-form audience/interest/tag
MAILCHIMP/CHECKOUT_LIST / CHECKOUT_INTEREST / CHECKOUT_TAG_NAMEAdv_mailchimp.php:62,63,64Checkout audience/interest/tag

config['csrf_protection'] = false globally (application/config/config.php:131) — applies to /api/mailchimp.

Client Extension Points

application/libraries/Mailchimp_Api.php (extends Adv_Mailchimp_Api) is the client-overridable wrapper — a fork can override API behavior there. Helper methods on Adv_mailchimp were widened privateprotected (git 7ff5374ced) to support storefront-controller overrides.

Business Rules

  1. Email required, presence only — empty email → 400 (Adv_mailchimp.php:30-32). Format is not validated server-side (the regex check is client-only, footer_js.php:584).
  2. Form-type routingurl === 'preview_order' selects the checkout list/interest/tag; any other URL selects the site set (Adv_mailchimp.php:34, getMailchimpConfig :59-72).
  3. Double opt-in toggleSUBSCRIBE_AS_PENDING → Mailchimp status='pending' (confirmation email) vs 'subscribed' (immediate) (:39; git 91a82344c7).
  4. Interest tagging — if an interest id is configured, interests => [$interestId => true]; else (object)[] so Mailchimp receives {} not [] (:40).
  5. Tag by name — the payload uses the tag name (*_TAG_NAME), not the tag id (:41,64,69).
  6. Checkout gating — checkout subscribes only if the register_mailchimp_newsletter POST flag is truthy and all four keys (ENABLED, API_KEY, SERVER_PREFIX, CHECKOUT_LIST) are set (Adv_order.php:352-357,1809-1815,1893-1899).
  7. No local persistence / no state machine — subscription is fire-and-forward to Mailchimp.

Known Issues & Security Gaps

  1. API key stored in plaintextMAILCHIMP/API_KEY is written and read unencrypted (Adv_settings.php:2354, Adv_Mailchimp_Api.php:34); a full-access Mailchimp credential sits in registry.regval (TEXT). Should use the encrypted registry path.
  2. No CSRF / captcha / rate limiting on public /api/mailchimpcsrf_protection=false; the endpoint (Adv_mailchimp.php:26) has no captcha, throttle, or auth. Any client can subscribe arbitrary emails — an abuse vector (especially with SUBSCRIBE_AS_PENDING sending confirmation emails to arbitrary addresses). Contrast the REST auth endpoints' LoginThrottle.
  3. Server-side email format not validated — only presence is checked (:30); format validation is client-only (footer_js.php:584). A direct POST bypasses it.
  4. Unguarded $postData['url']:34 reads url without isset/??; a body omitting url emits a PHP 8.1 "Undefined array key" warning (routes to 'site'). The email key is guarded; url is not.
  5. Dead SSL-verification options in checkout cURLAdv_order.php:1879-1880 sets CURLOPT_SSL_VERIFYHOST/VERIFYPEER after curl_exec() (:1877), so they never apply; the self-call's SSL hardening is a no-op.
  6. echo mid-checkout on cURL errorAdv_order.php:1856,1885 echo raw error strings (incl. curl_error) during previewOrder, which can corrupt the checkout response and leak internal detail.
  7. Synchronous blocking self-HTTP call during checkoutregisterCustomerToMailchimp (Adv_order.php:1853) makes a blocking cURL round-trip to the app's own /api/mailchimp (which then calls Mailchimp), serializing two network hops into checkout with no timeout and no deferred-task offload.
  8. CUSTOM_SITE_TAG/CUSTOM_CHECKOUT_TAG read but never writtenAdv_settings.php:539,544 read these keys, but saveMailchimpConfiguration only writes *_TAG_NAME (:2363,2370) — orphaned reads that always return null.
  9. Custom-tag-name save bugAdv_settings.php:2363,2370 persist $site_tag_name/$checkout_tag_name (the selected tag's name) into CUSTOM_*_TAG_NAME, never the $custom_site_tag/$custom_checkout_tag admin-entered values, which are silently discarded.
  10. No footer-modal error feedbackfooter_js.php:627-639 handles only the success branch and throws on failure with no .catch(); a failed subscription shows the user nothing.
  11. No dedup/idempotency — repeated submits re-call addListMember; an already-subscribed member surfaces as a generic 500 with the raw exception in details (Adv_mailchimp.php:48-49).
  12. Dead view fileapplication/views/main/components/modals/newsletter/newsletter_container.php:1-51 is an entire legacy embed form commented out with /* */, never rendered.

Tests

None. No unit/integration/legacy test exercises Adv_mailchimp, Adv_Mailchimp_Api, registerCustomerToMailchimp, or the subscribe path (verified: find tests -iname '*mailchimp*' empty). The pending toggle, form-type routing, and the checkout cURL self-call are all untested.

  • IN-13 Newsletter Integrations — canonical home for Adv_Mailchimp_Api, the MAILCHIMP/* registry keys, double opt-in, and the two-list pattern. Mailchimp is one of several parallel providers (Manago/Moosend/Apifon).
  • AD-13 Settings — the admin settings/mailchimp configuration page (RBAC, save path).
  • CF-06 Order Preview — the checkout flow that fires the server-to-server subscribe.
  • SY-19 Config & Registry — the registry table and its caching behaviour.