Skip to content

feat(campaigns): add transactional/subscriptions campaigns support#206

Open
RickjanHoornbeeck wants to merge 12 commits intomainfrom
feat/issue-191
Open

feat(campaigns): add transactional/subscriptions campaigns support#206
RickjanHoornbeeck wants to merge 12 commits intomainfrom
feat/issue-191

Conversation

@RickjanHoornbeeck
Copy link

  • Introduced a new transactional field in the Campaign model to differentiate between transactional and non-transactional campaigns.
  • Updated the campaign creation and update endpoints to handle the new transactional field.
  • Implemented validation to ensure that a campaign cannot be both transactional and linked to a subscription.
  • Modified the UI to include a toggle for setting a campaign as transactional and to conditionally display subscription options based on this setting.
  • Updated database schema with migrations to add the transactional column to the campaigns table.
  • Enhanced tests to cover new transactional logic and ensure proper handling of subscription channel compatibility.

- Introduced a new `transactional` field in the Campaign model to differentiate between transactional and non-transactional campaigns.
- Updated the campaign creation and update endpoints to handle the new `transactional` field.
- Implemented validation to ensure that a campaign cannot be both transactional and linked to a subscription.
- Modified the UI to include a toggle for setting a campaign as transactional and to conditionally display subscription options based on this setting.
- Updated database schema with migrations to add the `transactional` column to the campaigns table.
- Enhanced tests to cover new transactional logic and ensure proper handling of subscription channel compatibility.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for transactional campaigns (mutually exclusive with subscription-linked campaigns) across the management API, persistence layer, pubsub rendering behavior, and console UI, along with accompanying schema changes.

Changes:

  • Introduces a transactional flag on campaigns and enforces “transactional XOR subscription-linked” in create/update flows (incl. unsubscribe URL behavior).
  • Updates console campaign creation/edit flows to toggle transactional mode and conditionally select subscriptions (with channel compatibility filtering).
  • Adds DB migrations for the new campaign field and introduces new sender_identities storage + data migrations for provider/template sender metadata.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
internal/store/management/subscriptions.go Normalizes legacy subscription channel values (e.g. smstext) when mapping to OAPI models.
internal/store/management/migrations/1764106035_sender_identities.up.sql Adds sender_identities table and migrates provider/template sender data into it; backfills templates.sender_identity_id.
internal/store/management/migrations/1764106035_sender_identities.down.sql Attempts to reverse sender identity migration and restore provider/template JSON sender fields.
internal/store/management/migrations/1764106034_add_transactional_to_campaigns.up.sql Adds campaigns.transactional boolean column (default false).
internal/store/management/migrations/1764106034_add_transactional_to_campaigns.down.sql Drops campaigns.transactional.
internal/store/management/campaigns.go Persists/returns transactional; updates create/list/get/update queries and update semantics for clearing subscription_id when transactional.
internal/pubsub/consumer/campaigns.go Omits unsubscribe URL for transactional campaigns.
internal/pubsub/consumer/campaigns_test.go Adds tests covering unsubscribe URL inclusion/omission based on subscription linkage + transactional.
internal/http/controllers/v1/management/oapi/resources_gen.go Regenerates OAPI models to include transactional fields.
internal/http/controllers/v1/management/oapi/resources.yml Adds transactional to campaign schemas (and includes other API surface updates).
internal/http/controllers/v1/management/campaigns.go Adds request validation for transactional/subscription exclusivity + subscription channel compatibility; propagates transactional through create/update/duplicate.
internal/http/controllers/v1/management/campaigns_test.go Adds/extends tests for subscription channel mismatch, transactional clearing subscription, and invalid subscription updates.
console/src/views/campaign/template/mail/Setup.tsx Switches locale fetching to openapi-fetch client.
console/src/views/campaign/setup/Setup.tsx Adds transactional toggle and subscription selection to campaign setup; includes them in update payload.
console/src/views/campaign/CreateCampaign.tsx Updates create flow to select channel first, then optionally set transactional/subscription before POSTing via OAPI client.
console/src/views/campaign/Campaigns.tsx Switches campaigns list fetching to OAPI client and offset pagination; updates duplicate/delete calls.
console/src/views/campaign/CampaignDetails.tsx Adds transactional/subscription controls and updates campaign PATCH via OAPI client.
console/src/types.ts Extends campaign types to include transactional and updates create/update param types.
console/src/oapi/management.generated.ts Regenerates OpenAPI TS types (includes transactional fields and additional endpoints/schemas).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +39 to +44
UPDATE providers p
SET data = jsonb_set(p.data, '{default_from}', to_jsonb(si.traits->>'address'))
FROM sender_identities si
WHERE p.data->>'default_from' IS NOT NULL
AND p.data->>'default_from' ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
AND si.id = (p.data->>'default_from')::uuid;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for transactional campaigns (mutually exclusive with subscription-linked campaigns) across the management API, store layer, pubsub rendering, and the console UI. The PR also introduces a sizable sender identities schema + data migration affecting providers/templates.

Changes:

  • Add transactional field to campaigns (DB + store + OpenAPI models) and enforce “transactional XOR subscription-linked” in create/update flows.
  • Update render context generation to omit unsubscribe URLs for transactional campaigns; add tests for this behavior.
  • Update console campaign creation/setup/details UI to toggle transactional mode and select a compatible subscription; migrate several console calls to oapiClient.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
internal/store/management/subscriptions.go Normalizes subscription channel output (e.g., sms → OAPI text).
internal/store/management/migrations/1764106035_add_transactional_to_campaigns.up.sql Adds transactional boolean column to campaigns.
internal/store/management/migrations/1764106035_add_transactional_to_campaigns.down.sql Removes transactional column.
internal/store/management/migrations/1764106034_sender_identities.up.sql Introduces sender_identities + migrates provider/template “from” data into identities and a new templates.sender_identity_id column.
internal/store/management/migrations/1764106034_sender_identities.down.sql Attempts to reverse identity migration back into JSON fields and drop schema.
internal/store/management/campaigns.go Adds Transactional field, persists it, exposes it via OAPI, and clears subscription on transactional updates.
internal/pubsub/consumer/campaigns.go Omits unsubscribe_url when campaign is transactional.
internal/pubsub/consumer/campaigns_test.go Tests unsubscribe URL presence/absence based on transactional/subscription state.
internal/http/controllers/v1/management/oapi/resources.yml Adds transactional to Campaign schemas and update/create payloads.
internal/http/controllers/v1/management/oapi/resources_gen.go Regenerated OAPI models with transactional fields.
internal/http/controllers/v1/management/campaigns.go Validates transactional/subscription exclusivity and subscription channel compatibility; includes transactional in create/update/duplicate.
internal/http/controllers/v1/management/campaigns_test.go Expands tests for subscription validation and transactional clearing behavior.
console/src/views/campaign/template/mail/Setup.tsx Switches locale fetching to oapiClient.
console/src/views/campaign/setup/Setup.tsx Adds transactional toggle + subscription selector with channel filtering.
console/src/views/campaign/CreateCampaign.tsx Adds transactional toggle + subscription selector during creation; uses oapiClient.
console/src/views/campaign/Campaigns.tsx Switches to offset pagination + oapiClient for list/duplicate/delete.
console/src/views/campaign/CampaignDetails.tsx Adds transactional toggle + subscription selector; switches updates to oapiClient.
console/src/types.ts Extends Campaign types to include transactional.
console/src/oapi/management.generated.ts Updates generated OpenAPI TS types (includes campaign transactional and a template test endpoint).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +40 to +44
SET data = jsonb_set(p.data, '{default_from}', to_jsonb(si.traits->>'address'))
FROM sender_identities si
WHERE p.data->>'default_from' IS NOT NULL
AND p.data->>'default_from' ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
AND si.id = (p.data->>'default_from')::uuid;
Comment on lines +46 to +53
-- Reverse: restore email template from.email from UUID → address
UPDATE templates t
SET data = jsonb_set(t.data, '{from,email}', to_jsonb(si.traits->>'address'))
FROM sender_identities si
WHERE t.data->'from'->>'email' IS NOT NULL
AND t.data->'from'->>'email' ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
AND si.id = (t.data->'from'->>'email')::uuid;

Comment on lines +55 to +60
UPDATE templates t
SET data = jsonb_set(t.data, '{from}', to_jsonb(si.traits->>'address'))
FROM sender_identities si
WHERE t.data->>'from' IS NOT NULL
AND t.data->>'from' ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
AND si.id = (t.data->>'from')::uuid;
Comment on lines +1 to +25
-- Sender identities table
-- Stores registered sender addresses (email, SMS, etc.) scoped to a provider.
-- Channel-specific metadata lives in the traits JSONB column; the address is
-- always stored as traits->>'address'.

CREATE TABLE sender_identities (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
provider_id UUID NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
channel VARCHAR(255) NOT NULL,
traits JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX sender_identities_project_id_idx ON sender_identities(project_id);
CREATE INDEX sender_identities_provider_id_idx ON sender_identities(provider_id);
CREATE UNIQUE INDEX sender_identities_provider_channel_address_uniq
ON sender_identities (provider_id, channel, (traits->>'address'));

CREATE TRIGGER set_updated_at_sender_identities BEFORE UPDATE ON sender_identities FOR EACH ROW EXECUTE PROCEDURE set_updated_at();

--------------------------------------------------------------------------------
-- Data migration: provider default_from → sender_identities
--------------------------------------------------------------------------------
Comment on lines +31 to +36
UPDATE providers p
SET data = data || jsonb_build_object('default_from_name', inn.from_name)
FROM identity_names inn
WHERE (p.data->>'default_from')::UUID = inn.identity_id
AND p.data->>'default_from' IS NOT NULL
AND p.data->>'default_from' ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';
@github-actions
Copy link

github-actions bot commented Mar 19, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@RickjanHoornbeeck
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

signbot9000 bot added a commit to lunogram/cla that referenced this pull request Mar 19, 2026
@jeroenrinzema jeroenrinzema force-pushed the main branch 2 times, most recently from f1f4c61 to 734e288 Compare March 22, 2026 10:01
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.

2 participants