Skip to content

Choose the sender of a broadcast variation or a transactional email - #509

Open
pierre-b wants to merge 8 commits into
devfrom
feat/send-time-sender
Open

pierre-b wants to merge 8 commits into
devfrom
feat/send-time-sender

Conversation

@pierre-b

@pierre-b pierre-b commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Closes #418.

Problem

A broadcast or a transactional email always left from its template's sender. Sending a campaign from another address meant editing or duplicating the template.

Change

  • Broadcasts: each variation (test_settings.variations[].sender_id), in an A/B test too, can choose one of the marketing email integration's senders, or keep its template's. The value is stored in the existing test_settings JSONB, so there is no migration.
  • Transactional API: email_options.sender_id picks one of the transactional integration's senders. It is checked before the contact is created or updated. from_name still applies on top of it. The SMTP bridge accepts it in a relayed JSON body.
  • Resolution: EmailProvider.ResolveSender(chosenID, templateSenderID). A chosen sender that is not one of the integration's is an error and never falls back to another address. Without a chosen sender, the template's sender applies, then the integration's default, as before.
  • Refusals (400 with the message): an unknown sender on create, update, schedule, resume, send-to-individual and transactional send.
  • Sender removed afterwards: that variation is queued for no further recipient until another sender is chosen.
    • The broadcast cannot be scheduled.
    • It cannot be resumed while that variation still has recipients to queue.
    • If it is queuing them, it pauses at the next run of its sending task, with a reason naming the variation and the sender.
    • An A/B test waiting for its winner is not paused for a losing variation's sender.
    • Emails already queued keep the sender they were queued with.
  • Console:
    • A Sender field next to each template in the broadcast drawer, offering only the marketing integration's senders. Cleared, the variation keeps the template's sender.
    • The broadcast page shows each variation's "From:" line.
    • A "Change sender" dialog appears next to Resume on a paused broadcast whose sender was removed.
    • All new strings are translated in the 7 catalogues.
  • OpenAPI and docs: BroadcastVariation.sender_id and email_options.sender_id in the schemas, with openapi.json re-bundled. CHANGELOG Feature bullet under ## [43.2] - WIP.

Plan: plans/feat-send-time-sender-plan.md.

Tests

  • Domain: ResolveSender, VariationSenderID, the same template twice with different senders, TestSettingsProvided, JSON round-trip.
  • Service:
    • create, update, schedule, resume and send-to-individual with known, unknown and removed senders;
    • transactional send with sender_id, refused before the contact upsert;
    • both broadcast senders pick the variation's sender, and their batch pre-check returns the pause error before any recipient.
  • Orchestrator:
    • paused with a reason naming the variation, and nothing sent;
    • a winner phase ignores a losing variation's sender;
    • test_completed and paused broadcasts are not paused again, and a paused run is not announced as sending started.
  • HTTP: 400 mapping on every refusing endpoint.
  • Integration (Mailpit): TestSendTimeSender covers a single broadcast, an A/B test with one sender per variation, a transactional sender_id, an unknown sender refused on create and on send, and a removed sender refusing schedule.
  • Console: the select, the drawer (choose, clear, refuse a removed sender, A/B), the change-sender dialog and the catalogues.
  • make test-unit, INTEGRATION_TESTS=true go test -race -tags integration,licdev ./tests/integration/ -run TestSendTimeSender and cd console && npx vitest run (2,173 tests) pass locally after merging dev.

Docs

Notifuse/docs#30 (broadcast campaigns and transactional API pages, docs openapi.json).

Known, not addressed

  • The message history preview shows the template's sender, not the one chosen on the broadcast.
  • A failed resume now shows the server's message, in English, instead of the translated generic sentence.
  • A resume that lands inside a run of a paused broadcast can send without the "sending started" annotation. The window is the template loading time.
  • A pause that commits while the last batch is queued leaves completed_at empty, so resuming still asks for a sender although every email is queued. This errs on the safe side.
  • The run in progress when a sender is removed keeps queuing with it until it ends, because the integration is read once per run. The next run pauses.
  • Some tests leave mutations alive: the variation number and count put in the pause error, the announcement for testing and scheduled broadcasts, the dialog's dedupe and query invalidation, and the page's resume toast.
  • Three functions share the name checkVariationSenders (domain, service, orchestrator), and the in-app API examples do not show sender_id.
  • A few translations say "variant" where the catalogue usually says "variation", and "modello"/"modelo" where it says "template".
  • The first commit's message says the broadcast "pauses before its next batch"; the later commits correct the behaviour and the texts.

A broadcast variation can now set sender_id to one of the marketing email
integration's senders, and a transactional send can set
email_options.sender_id. A sender that is not one of the integration's is
refused with a 400 on save and send; a broadcast whose chosen sender has
since left the integration cannot be scheduled or resumed, and pauses
before its next batch if already sending. The console offers a sender
under each template of the broadcast drawer and shows it on the
broadcast's details.
- The pause reason names the variation and the sender, instead of quoting
  the data feed pause sentinel.
- A broadcast waiting for its A/B winner, or already paused, is not paused
  again for a variation's sender.
- A paused broadcast whose chosen sender left the integration offers a
  "Change sender" dialog next to Resume; a failed resume shows the server's
  message.
- The sender field can be cleared once the integration has no sender left,
  and names no template's sender before a template is chosen.
- An integration without any sender is no longer reported as a missing
  chosen sender.
- CHANGELOG, OpenAPI and plan say the broadcast pauses at its next sending
  round, not before its next batch.
…cast

- The pause reason and the change-sender dialog number a variation that has
  no name, as the console saves them.
- The dialog is offered only when a variation still to send lost its
  sender, asks once per template, and a failed resume shows the server's
  message alone.
- A run on a paused broadcast is no longer announced as sending started.
- CHANGELOG, OpenAPI and plan state that only a variation still to send is
  stopped, and when it pauses.
- CHANGELOG and OpenAPI: a variation whose sender was removed is queued for
  no further recipient; emails already queued keep their sender, and saving
  test_settings checks every variation, a losing one's included.
- The change-sender dialog is not offered once every recipient is queued,
  since the queued emails keep the sender they were queued with.
# Conflicts:
#	CHANGELOG.md
#	config/config.go
#	console/src/i18n/locales/ca.po
#	console/src/i18n/locales/de.po
#	console/src/i18n/locales/en.po
#	console/src/i18n/locales/es.po
#	console/src/i18n/locales/fr.po
#	console/src/i18n/locales/it.po
#	console/src/i18n/locales/ja.po
#	console/src/i18n/locales/pt-BR.po
#	web_analytics_sdk/dist/notifuse-analytics.cjs.js
#	web_analytics_sdk/dist/notifuse-analytics.esm.js
#	web_analytics_sdk/dist/notifuse-analytics.min.js
#	web_analytics_sdk/package.json
@codecov

codecov Bot commented Nov 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.84%. Comparing base (b994c1a) to head (e56d5b0).

Files with missing lines Patch % Lines
internal/service/transactional_service.go 62.50% 3 Missing and 3 partials ⚠️
internal/service/broadcast/orchestrator.go 94.20% 2 Missing and 2 partials ⚠️
internal/service/broadcast_service.go 85.71% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #509      +/-   ##
==========================================
+ Coverage   88.82%   88.84%   +0.01%     
==========================================
  Files         368      368              
  Lines       71860    72012     +152     
==========================================
+ Hits        63832    63979     +147     
- Misses       5390     5391       +1     
- Partials     2638     2642       +4     
Flag Coverage Δ
unit 88.84% <92.85%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant