feat(crm): newsletter campaigns behind a newsletters flag (#1264)
Part B of #1264. Flag off by default, so an install that never enables it gains no route, no nav entry and no way to mass-mail. A campaign is a body plus a recipient rule. Queueing one writes ordinary email_queue rows (email_type 'newsletter', origin 'campaign', new campaign_id), so retry, rendered_html, sent_at and error_message all come from the existing processor rather than a parallel sender. Throttling staggers scheduled_at; the processor loop is untouched. Two rules the service enforces: no raw HTML is ever stored (sanitized on write and again on render, idempotently), and opt-out is checked at queue time AND again at send time. Migration 199 adds email_campaigns, email_campaign_recipients, email_queue.campaign_id, customer_accounts.marketing_opt_out(_at), and the newsletters.view / newsletters.send permissions. Three rounds of external review are folded in, including several that would otherwise have shipped broken: - Campaign rows never came due on SQLite. queueEmail writes a Date, which the sqlite3 binding stores as epoch ms; ISO text in the same column compares as TEXT against an INTEGER, and SQLite orders every INTEGER below every TEXT. The feature silently sent nothing there. - The flag had no Settings card and no sidebar entry, so it could not be enabled through the UI at all. - Consent is per ADDRESS, not per row: two accounts sharing an inbox meant unsubscribing stopped one and not the other, at both queue and send time. - The unsubscribe GET mutated consent, so a mail-security scanner walking a campaign could have unsubscribed much of the list. GET now confirms, POST acts. - The rate ceiling is clamped to the queue's real throughput (10/min), so the composer's estimate stops being wrong by up to 12x. Closes #1264
This commit is contained in:
@@ -72,6 +72,10 @@ export const DEFAULT_FLAGS: FeatureFlags = {
|
||||
workflows: false,
|
||||
// #1074 — off by default is the whole "zero behaviour change" guarantee.
|
||||
faces: false,
|
||||
// Newsletter campaigns (migration 199, #1264). Off by default — an
|
||||
// install that never turns this on never gains a nav entry or a way to
|
||||
// mass-mail its customers.
|
||||
newsletters: false,
|
||||
};
|
||||
|
||||
export const FEATURE_FLAGS_QUERY_KEY = ['feature-flags'] as const;
|
||||
@@ -131,6 +135,9 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
|
||||
// NOTE: taxReport is intentionally NOT here anymore — the Tax export
|
||||
// moved permanently into the Accounting section (its own master).
|
||||
// future siblings: || out.messaging
|
||||
// #1264 — newsletters is a Clients child; without it here the staged
|
||||
// sidebar preview disagrees with the server until Save.
|
||||
|| out.newsletters
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user