{"id":"6a3e9f38cabf74f3ccde454c","title":"2026 06 26 Issue 293","path":"how-to/committee/release-notes/2026-06-26-issue-293","contentMarkdown":"# 26-Jun-2026 — unrestricted recipient selection for delete/disable workflows, with GDPR-safe deletion... [#293](https://github.com/nbarrett/ngx-ramblers/issues/293)\n\n## [build 721](https://github.com/nbarrett/ngx-ramblers/actions/runs/28248078096) — [commit 6c9f14f](https://github.com/nbarrett/ngx-ramblers/commit/6c9f14f67cf1ced5ccdc9f3650bea41b9323fbd5)\n\n_____\n\n## Email composer member picker\n\nMembership-expiry and other removal workflows need to reach members regardless of deliverability or group membership, because the recipient is deleted or disabled afterwards.\n\n- **Undisable suppressed members for removal workflows.** `deliverabilityOptional` now also returns true when the notification config carries a `BULK_DELETE_GROUP_MEMBER` or `DISABLE_GROUP_MEMBER` action, so unsubscribed, blocked and no-email members are selectable rather than greyed out. They stay in their labelled categories (\"unsubscribed or blocked from email\", \"no email address\", etc.) so it is clear why they differ from the rest.\n- **Include non-group members in the candidate pool.** The composer now loads every member once (`allMembers`) and derives the group-member list from it. `candidateMembers()` uses the full pool for removal workflows, so members with `groupMember: false` (e.g. already-expired members) can be selected. The cache invalidates when the workflow type changes.\n- **Correct expiry tense.** The expiry qualifier reads \"expires <date>\" for future dates and \"expired <date>\" for past ones, instead of always past tense.\n- **No bogus blocking validation.** For a removal workflow, undeliverable members are includable on purpose, so the \"N chosen members cannot be emailed\n- remove them to continue\" step error no longer fires. Forcing their removal would have left expired-but-undeliverable members in the database forever, which is the gap this feature exists to close.\n\n## Removal workflows delete every selected recipient\n\n- **Skipped recipients are still deleted.** The send post-actions now apply to every selected member who exists, sent or skipped, not just those who were emailed or had no email address. Previously a member skipped because they were already unsubscribed or had no Head Office consent was never deleted, so the whole reason for selecting them was lost.\n- **Honest per-recipient notes.** A skipped recipient's note reads as a pre-existing state (\"Already unsubscribed from all mailing lists\") alongside the real outcome (\"Deleted from database\"), rather than implying the workflow unsubscribed them.\n- **Whole-list campaign audience fix.** `applyCampaignPostSendActions` resolves its audience from the same member list used for the recipient count, so whole-list campaign sends run their post-send delete against the members actually emailed.\n\n## The database is King: provider and Salesforce kept in step\n\n- **Deletion is GDPR-complete.** Every member-deletion path (the expiry/disable post-send actions, the manual bulk-delete route, and single/all deletes from member admin) now removes the member's Brevo contact and writes the full opt-out back to Salesforce at the moment of deletion, via `bulkDeleteMembersCascade`. A member removed from the database never leaves an orphaned contact behind.\n- **Sync repairs orphans.** \"Update Brevo Mailing Lists\" no longer short-circuits on local member signatures; it always reconciles Brevo against the database and deletes contacts with no matching eligible member, so anything that ever slipped through is cleaned up. Contact queries now page through every contact, not just the first 1000.\n\n## Marketing consent is always a real boolean\n\n- **Coerced on save.** `emailMarketingConsent` and the granular group/area/other consents are coerced to a real boolean (via `booleanOf`) when a member is saved, so a \"Yes\"/\"No\" or \"true\"/\"false\" string can never be stored as the value.\n- **Existing records migrated.** A migration converts any string consent value already in the database to its boolean equivalent. This fixes both the display (a string like \"No\" was truthy and showed as \"consent given\") and the send-exclusion (`emailMarketingConsent === false` never matched the string \"No\", so a non-consenting member could be emailed).\n\n## Member admin\n\n- Filter selection persists to the URL via a generic `filter=` query param (`StoredValue.FILTER`) and localStorage, so a chosen filter survives a reload and is shareable.\n- New \"Not Committee Member\" filter under Administrators.\n\n## Brevo mailing-list sync tolerates a switched Brevo account\n\nThe contact sync no longer fails when subscribed list IDs do not exist in the connected Brevo account, which happens when a site is pointed at a different Brevo account. The import and create paths fetch the account's actual lists once and skip any list ID that is not present, instead of 404-ing the whole batch. Where a contact ends up with no valid list, its attributes are still updated via the batch-update endpoint (which does not require a list) rather than the import endpoint (which does). The import-completion notification email Brevo sends on each sync is also suppressed.\n\n## Email-config editor and Brevo admin fixes\n\n- Email content editor disables editing with an \"automatically generated\" note only when a built-in process generates the body in code: that is, a workflow-linked config (forgot-password, walk, expense, contact-us, booking, member-sync) whose template is only the BODY_CONTENT placeholder. A user-configured email with a placeholder template stays editable\n- the body is either written here or composed in the Email Composer\n- rather than being wrongly blanked out.\n- \"Reset to default\" is now a dropdown to reset the body to any available branded template, backed by a new /templates/local-names endpoint.\n- Domain configuration no longer crashes when Brevo returns a null DNS record (null-guarded mapDnsRecord).\n\n## Email composer: config content block and step-skip persistence\n\n- The composer now shows the read-only template-content block whenever the chosen configuration has body content, not only when the template carries BODY_CONTENT_TOP/BOTTOM placeholders. A fixed-content config such as \"Your Login Details\" previously showed no content block on the Compose step.\n- The \"Skip the Compose step\" and \"Skip the Events step\" configuration flags (omitComposeStep, omitEventsStep) now persist. They were present on the client model and the editor but missing from the server notification-config schema, so Mongoose strict mode silently dropped them on every save.\n\n- A \"Compose email\" button beside the Email Configuration selector on the Email Configurations page opens the Email Composer directly with that configuration pre-selected (via the config-id query param), so you can go straight from editing a configuration to sending with it. The button is hidden for built-in process configurations (forgot-password, walk, expense, contact-us, booking, member-sync), which are system-triggered and cannot be started by hand from the composer.","contentHtml":"<h1>26-Jun-2026 — unrestricted recipient selection for delete/disable workflows, with GDPR-safe deletion... <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/293\">#293</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/28248078096\">build 721</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/6c9f14f67cf1ced5ccdc9f3650bea41b9323fbd5\">commit 6c9f14f</a></h2>\n<hr>\n<h2>Email composer member picker</h2>\n<p>Membership-expiry and other removal workflows need to reach members regardless of deliverability or group membership, because the recipient is deleted or disabled afterwards.</p>\n<ul>\n<li><strong>Undisable suppressed members for removal workflows.</strong> <code>deliverabilityOptional</code> now also returns true when the notification config carries a <code>BULK_DELETE_GROUP_MEMBER</code> or <code>DISABLE_GROUP_MEMBER</code> action, so unsubscribed, blocked and no-email members are selectable rather than greyed out. They stay in their labelled categories (&quot;unsubscribed or blocked from email&quot;, &quot;no email address&quot;, etc.) so it is clear why they differ from the rest.</li>\n<li><strong>Include non-group members in the candidate pool.</strong> The composer now loads every member once (<code>allMembers</code>) and derives the group-member list from it. <code>candidateMembers()</code> uses the full pool for removal workflows, so members with <code>groupMember: false</code> (e.g. already-expired members) can be selected. The cache invalidates when the workflow type changes.</li>\n<li><strong>Correct expiry tense.</strong> The expiry qualifier reads &quot;expires <date>&quot; for future dates and &quot;expired <date>&quot; for past ones, instead of always past tense.</li>\n<li><strong>No bogus blocking validation.</strong> For a removal workflow, undeliverable members are includable on purpose, so the &quot;N chosen members cannot be emailed</li>\n<li>remove them to continue&quot; step error no longer fires. Forcing their removal would have left expired-but-undeliverable members in the database forever, which is the gap this feature exists to close.</li>\n</ul>\n<h2>Removal workflows delete every selected recipient</h2>\n<ul>\n<li><strong>Skipped recipients are still deleted.</strong> The send post-actions now apply to every selected member who exists, sent or skipped, not just those who were emailed or had no email address. Previously a member skipped because they were already unsubscribed or had no Head Office consent was never deleted, so the whole reason for selecting them was lost.</li>\n<li><strong>Honest per-recipient notes.</strong> A skipped recipient&#39;s note reads as a pre-existing state (&quot;Already unsubscribed from all mailing lists&quot;) alongside the real outcome (&quot;Deleted from database&quot;), rather than implying the workflow unsubscribed them.</li>\n<li><strong>Whole-list campaign audience fix.</strong> <code>applyCampaignPostSendActions</code> resolves its audience from the same member list used for the recipient count, so whole-list campaign sends run their post-send delete against the members actually emailed.</li>\n</ul>\n<h2>The database is King: provider and Salesforce kept in step</h2>\n<ul>\n<li><strong>Deletion is GDPR-complete.</strong> Every member-deletion path (the expiry/disable post-send actions, the manual bulk-delete route, and single/all deletes from member admin) now removes the member&#39;s Brevo contact and writes the full opt-out back to Salesforce at the moment of deletion, via <code>bulkDeleteMembersCascade</code>. A member removed from the database never leaves an orphaned contact behind.</li>\n<li><strong>Sync repairs orphans.</strong> &quot;Update Brevo Mailing Lists&quot; no longer short-circuits on local member signatures; it always reconciles Brevo against the database and deletes contacts with no matching eligible member, so anything that ever slipped through is cleaned up. Contact queries now page through every contact, not just the first 1000.</li>\n</ul>\n<h2>Marketing consent is always a real boolean</h2>\n<ul>\n<li><strong>Coerced on save.</strong> <code>emailMarketingConsent</code> and the granular group/area/other consents are coerced to a real boolean (via <code>booleanOf</code>) when a member is saved, so a &quot;Yes&quot;/&quot;No&quot; or &quot;true&quot;/&quot;false&quot; string can never be stored as the value.</li>\n<li><strong>Existing records migrated.</strong> A migration converts any string consent value already in the database to its boolean equivalent. This fixes both the display (a string like &quot;No&quot; was truthy and showed as &quot;consent given&quot;) and the send-exclusion (<code>emailMarketingConsent === false</code> never matched the string &quot;No&quot;, so a non-consenting member could be emailed).</li>\n</ul>\n<h2>Member admin</h2>\n<ul>\n<li>Filter selection persists to the URL via a generic <code>filter=</code> query param (<code>StoredValue.FILTER</code>) and localStorage, so a chosen filter survives a reload and is shareable.</li>\n<li>New &quot;Not Committee Member&quot; filter under Administrators.</li>\n</ul>\n<h2>Brevo mailing-list sync tolerates a switched Brevo account</h2>\n<p>The contact sync no longer fails when subscribed list IDs do not exist in the connected Brevo account, which happens when a site is pointed at a different Brevo account. The import and create paths fetch the account&#39;s actual lists once and skip any list ID that is not present, instead of 404-ing the whole batch. Where a contact ends up with no valid list, its attributes are still updated via the batch-update endpoint (which does not require a list) rather than the import endpoint (which does). The import-completion notification email Brevo sends on each sync is also suppressed.</p>\n<h2>Email-config editor and Brevo admin fixes</h2>\n<ul>\n<li>Email content editor disables editing with an &quot;automatically generated&quot; note only when a built-in process generates the body in code: that is, a workflow-linked config (forgot-password, walk, expense, contact-us, booking, member-sync) whose template is only the BODY_CONTENT placeholder. A user-configured email with a placeholder template stays editable</li>\n<li>the body is either written here or composed in the Email Composer</li>\n<li>rather than being wrongly blanked out.</li>\n<li>&quot;Reset to default&quot; is now a dropdown to reset the body to any available branded template, backed by a new /templates/local-names endpoint.</li>\n<li>Domain configuration no longer crashes when Brevo returns a null DNS record (null-guarded mapDnsRecord).</li>\n</ul>\n<h2>Email composer: config content block and step-skip persistence</h2>\n<ul>\n<li><p>The composer now shows the read-only template-content block whenever the chosen configuration has body content, not only when the template carries BODY_CONTENT_TOP/BOTTOM placeholders. A fixed-content config such as &quot;Your Login Details&quot; previously showed no content block on the Compose step.</p>\n</li>\n<li><p>The &quot;Skip the Compose step&quot; and &quot;Skip the Events step&quot; configuration flags (omitComposeStep, omitEventsStep) now persist. They were present on the client model and the editor but missing from the server notification-config schema, so Mongoose strict mode silently dropped them on every save.</p>\n</li>\n<li><p>A &quot;Compose email&quot; button beside the Email Configuration selector on the Email Configurations page opens the Email Composer directly with that configuration pre-selected (via the config-id query param), so you can go straight from editing a configuration to sending with it. The button is hidden for built-in process configurations (forgot-password, walk, expense, contact-us, booking, member-sync), which are system-triggered and cannot be started by hand from the composer.</p>\n</li>\n</ul>\n"}