From 9e2400c6de6f13c498f02c489f956ebf89de36b0 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Thu, 2 Apr 2026 14:09:01 +0200 Subject: [PATCH] fix: Enhance email processing to support campaign types and improve unsubscribe logic --- apps/api/src/jobs/email-processor.ts | 5 +++- apps/api/src/services/EmailService.ts | 9 ++++--- .../services/__tests__/EmailService.test.ts | 25 ++++++++++++++++++- test/helpers/factories.ts | 2 ++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/api/src/jobs/email-processor.ts b/apps/api/src/jobs/email-processor.ts index cceb5f0..77662df 100644 --- a/apps/api/src/jobs/email-processor.ts +++ b/apps/api/src/jobs/email-processor.ts @@ -59,6 +59,7 @@ export async function createEmailWorker() { contact: true, project: true, template: {select: {type: true}}, + campaign: {select: {type: true}}, }, }); @@ -112,7 +113,9 @@ export async function createEmailWorker() { contact: email.contact, project: email.project, includeUnsubscribe: - email.sourceType !== EmailSourceType.TRANSACTIONAL && email.template?.type !== 'HEADLESS', + email.sourceType !== EmailSourceType.TRANSACTIONAL && + email.template?.type !== 'HEADLESS' && + email.campaign?.type !== 'HEADLESS', }); // Use fromName from database if available, otherwise fall back to project name diff --git a/apps/api/src/services/EmailService.ts b/apps/api/src/services/EmailService.ts index abe638d..85ce5cd 100644 --- a/apps/api/src/services/EmailService.ts +++ b/apps/api/src/services/EmailService.ts @@ -293,9 +293,8 @@ export class EmailService { include: { contact: true, project: true, - template: { - select: {type: true}, - }, + template: {select: {type: true}}, + campaign: {select: {type: true}}, }, }); @@ -363,7 +362,9 @@ export class EmailService { contact: email.contact, project: email.project, includeUnsubscribe: - email.sourceType !== EmailSourceType.TRANSACTIONAL && email.template?.type !== 'HEADLESS', + email.sourceType !== EmailSourceType.TRANSACTIONAL && + email.template?.type !== 'HEADLESS' && + email.campaign?.type !== 'HEADLESS', }); // Use explicit fromName if provided, otherwise fall back to project name diff --git a/apps/api/src/services/__tests__/EmailService.test.ts b/apps/api/src/services/__tests__/EmailService.test.ts index 134d274..7e4626e 100644 --- a/apps/api/src/services/__tests__/EmailService.test.ts +++ b/apps/api/src/services/__tests__/EmailService.test.ts @@ -1,5 +1,5 @@ import {beforeEach, describe, expect, it, vi, type Mock} from 'vitest'; -import {EmailSourceType, EmailStatus} from '@plunk/db'; +import {EmailSourceType, EmailStatus, TemplateType} from '@plunk/db'; import {ActionSchemas} from '@plunk/shared'; import {EmailService} from '../EmailService'; import {sendRawEmail} from '../SESService'; @@ -216,6 +216,29 @@ describe('EmailService', () => { expect(email.error).toMatch(/unsubscribed/i); }); + it('should keep CAMPAIGN sourceType when campaign type is HEADLESS (no template)', async () => { + const contact = await factories.createContact({projectId, subscribed: true}); + + // Campaign typed HEADLESS directly — no template involved (inline body) + const campaign = await factories.createCampaign({ + projectId, + type: TemplateType.HEADLESS, + body: 'Content with unsubscribe', + }); + + const email = await EmailService.sendCampaignEmail({ + projectId, + contactId: contact.id, + campaignId: campaign.id, + subject: 'Newsletter', + body: campaign.body, + from: 'news@example.com', + }); + + expect(email.sourceType).toBe(EmailSourceType.CAMPAIGN); + expect(email.status).toBe(EmailStatus.PENDING); + }); + it('should keep CAMPAIGN sourceType when campaign uses headless template', async () => { const contact = await factories.createContact({projectId, subscribed: true}); diff --git a/test/helpers/factories.ts b/test/helpers/factories.ts index 9b2914d..ed91769 100644 --- a/test/helpers/factories.ts +++ b/test/helpers/factories.ts @@ -67,6 +67,7 @@ export interface CampaignFactoryOptions { status?: CampaignStatus; scheduledFor?: Date | null; segmentId?: string | null; + type?: TemplateType; } export interface WorkflowFactoryOptions { @@ -224,6 +225,7 @@ export class TestFactories { status: options.status || CampaignStatus.DRAFT, scheduledFor: options.scheduledFor, segmentId: options.segmentId, + type: options.type || TemplateType.MARKETING, }, }); }