Files
redefined-designs/backend/migrations/1787800000000_add-analytics-consent.js
synAdminandClaude Opus 5 955049eac9
Linting / lint (pull_request) Successful in 3m0s
SonarQube Analysis / sonarqube (pull_request) Failing after 26m24s
fix(privacy): separate analytics consent from email consent (#56)
The previous commit widened the marketing consent sentence to cover the Brevo tracker, so one checkbox carried both purposes. That is the specific pattern GDPR rejects: consent has to be granular, and current EDPB guidance treats bundling tracking consent with subscription consent as invalid because the customer cannot accept one purpose and refuse the other. Quebec's Law 25 s.8.1 is stricter again — profiling technology has to be off until the person switches it on, with no pre-ticked box and no consent inherited from agreeing to something else. Building to both standards was the decision, since the storefront is publicly reachable and anyone can register.

So the marketing sentence is restored to exactly what it was, which leaves every existing email consent valid and untouched, and analytics gets its own column, its own sentence, its own checkbox at registration, its own toggle in the account page and its own endpoint. A customer can now hold either, both, or neither, and withdrawing one does not disturb the other.

The migration defaults analytics_consent to false, which is both the honest answer — none of the existing customers was ever asked — and what Law 25 requires. Nothing about this change opts anybody in.

Two details that are compliance requirements rather than wording preferences. The sentence names Brevo instead of saying "our email provider", because informed consent means the customer can tell who receives their data and a description they cannot act on is not disclosure. And the account toggle is as prominent and as easy to switch off as it is to switch on, because withdrawal has to be as easy as consenting.

The analytics endpoint is separate from the marketing one rather than a second field on it, so that a single call cannot change an answer the customer did not touch — the bundling problem moved from the form into the API. The unit tests now assert the two consents stay apart in both directions, including that the marketing sentence still says nothing about tracking, because re-bundling them would otherwise pass silently and is the mistake this project already made once.

Verified: backend tsc clean, both lint suites 0 errors with no new warnings, 478 unit tests passing across 33 suites, frontend production build green. Not verified: the migration has not been run against a database, and integration and e2e need a Node this machine does not have active. None of this is legal advice and the wording is worth a lawyer's eye before it ships.

Refs #56

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:58:20 -05:00

40 lines
1.8 KiB
JavaScript

exports.up = (pgm) => {
pgm.sql(`
-- Consent to the Brevo tracker, separate from marketing_consent (#56).
--
-- Separate because GDPR requires consent to be granular: email marketing
-- and behavioural tracking are two purposes with two recipients, and
-- current EDPB guidance treats bundling tracking consent with subscription
-- consent as invalid. Quebec's Law 25 s.8.1 goes further and requires
-- profiling technology to be off until the person switches it on.
--
-- DEFAULT FALSE is the part that must not be changed. Every existing
-- customer arrives at false, which is both the honest answer — none of them
-- were ever asked — and what Law 25 requires. A default of true would
-- silently opt in the entire customer base to something nobody agreed to.
ALTER TABLE customers
ADD COLUMN IF NOT EXISTS analytics_consent BOOLEAN NOT NULL DEFAULT FALSE;
-- When they agreed, and to exactly what wording. Same shape and same
-- reasoning as the marketing_consent pair: the stored sentence is what
-- makes the record say what the customer actually saw, so re-wording the
-- consent later cannot retroactively broaden anyone's.
--
-- Both nullable: a customer who has never consented has no date and no
-- text, and inventing either would be a false record of consent.
ALTER TABLE customers
ADD COLUMN IF NOT EXISTS analytics_consent_at TIMESTAMPTZ;
ALTER TABLE customers
ADD COLUMN IF NOT EXISTS analytics_consent_text TEXT;
`);
};
exports.down = (pgm) => {
pgm.sql(`
ALTER TABLE customers DROP COLUMN IF EXISTS analytics_consent_text;
ALTER TABLE customers DROP COLUMN IF EXISTS analytics_consent_at;
ALTER TABLE customers DROP COLUMN IF EXISTS analytics_consent;
`);
};