0026. Read WooCommerce consent from the connector plugin's user meta¶
Date: 2026-09-02
Status: Partially superseded by 0041 — reading consent from the connector plugin's user meta remains in effect, but three details below no longer hold. confirmed is reported as null (unknown), not false, because the checkbox cannot say whether an address was confirmed. Consent arrives as its own ConsentData record next to the customer, not inside CustomerData::$consents. There is no last write wins across writers, because a consent mirror has one writer; the stale-write guard stays per mirror, refusing only a consentUpdatedAt strictly before the mirror's, so a flag flipped without moving date_modified_gmt still writes. The timestamp also stays required for settling a two-way conflict between mirrors.
Context¶
CFX-179 built the consent machinery — ConsentData, ConsentRecorder, CanPullConsent,
HasConsentChannels — without a single shop actually producing consent. CFX-93 supplies the first
one: WooCommerce, inbound only (the write-back half is out of scope here).
The problem is that WooCommerce has no consent field. A WC customer carries name, email, billing
and shipping, order totals — nothing that says whether the person wants email from the shop. Unlike
PrestaShop, which at least has a newsletter column, there is nothing on the platform to map.
Three ways to get one:
- Derive it from behaviour. Treat "has placed an order" or "has an account" as consent. This is not consent in any legal sense and would manufacture opt-ins out of thin air. Rejected outright.
- Read a third-party marketing plugin's storage. Mailchimp for WooCommerce, Klaviyo and others each keep their own opt-in state, in their own tables or meta keys. Conflux would have to detect which one a store runs, follow each plugin's schema changes, and reconcile disagreements between two of them. That is an open-ended integration surface for a single boolean.
- Have our own plugin record it.
conflux-connectoris already installed on every connected store — it is how the connection is established and verified in the first place. It can render a newsletter checkbox and store the answer.
Option 3 is the only one that produces real consent from a source we control. That leaves the
question of how Conflux reads it: a dedicated plugin endpoint
(/wp-json/conflux-connector/v1/consent), or a WordPress user meta entry surfaced on the standard
WC customer payload.
Decision¶
conflux-connector records the newsletter opt-in as WordPress user meta under the key
conflux_newsletter, exposed on the WC REST customer object's meta_data. That key is the
contract between plugin and platform; Conflux reads no other consent source for WooCommerce.
WooCommerceTransformer::CONSENT_META_KEY holds it on the Conflux side, and
WooCommerceTransformer::mapConsent() is the single place that interprets it:
- Present → a
ConsentDataonConsentChannel::Email.subscribedis the flag's boolean value;confirmedis alwaysfalse, because a checkbox is a single opt-in and WooCommerce has no confirmation step to report.sourceisConsentSource::Integration— the flag itself does not record whether a customer ticked it or a shop admin set it, and inventingUserwould claim knowledge we do not have. - Absent →
null, andCustomerData::$consentsstays empty. A store without the plugin, or on a version predating the checkbox, says nothing about consent; it does not withdraw it. consentUpdatedAtcomes from the customer record'sdate_modified_gmt, falling back todate_created_gmt, parsed through the run'sSourceDateTimeslike every other WooCommerce datetime. A record with neither readable yields no consent at all:ConsentRecorderorders writes by that timestamp, so an invented one would decide a conflict it cannot see.
Reading it off meta_data rather than from a dedicated endpoint means consent arrives on the
existing bulk-export payload: SystemCapability::Customers already fetches
/wp-json/wc/v3/customers, so a full or delta sync carries consent at no extra request and with no
second pagination path to keep in step. CanPullConsent::pullConsent() reads the same field
from /wp-json/wc/v3/customers/{id} for a single contact, so both paths share one mapper and can
never disagree.
WooCommerceIntegration::supportedConsentChannels() returns [ConsentChannel::Email]. One checkbox
is one channel; the store cannot express SMS consent and a propagator must not try to push it there.
Consequences¶
The meta key is now a published interface, not an implementation detail. Renaming it silently breaks consent sync for every store running an older plugin build — and breaks it in the quiet direction, since a missing key reads as "no signal" rather than as an error. Any change to it has to ship as a plugin release first, with the old key still honoured until the fleet has caught up.
confirmed is hard-wired to false for WooCommerce. If a store ever runs double opt-in through the
connector, this ADR needs revisiting — the flag alone cannot carry that state, and reporting a
confirmed opt-in as unconfirmed is the safe direction to be wrong in.
The date_modified_gmt proxy inherits the caveat ADR-0024's
stale-write guard already documents: the timestamp belongs to the customer record, not to the
consent field. A plugin that flips the flag without touching the customer record leaves
date_modified_gmt unchanged, and the guard will then
discard the newer state as stale. The connector must therefore keep the customer record's
modification timestamp moving when it writes the flag — which update_user_meta() on the customer's
own user does through WooCommerce's own hooks — and the two-timestamp alternative (a companion
conflux_newsletter_updated_at meta) stays available if that ever proves insufficient.
Nothing calls pullConsent() yet. It is implemented here because the mapping and the credentials
are the same ones bulk export already uses, so implementing both halves together costs one method
and keeps a future caller from having to re-derive the field contract.