fix: Enhance email processing to support campaign types and improve unsubscribe logic

This commit is contained in:
Dries Augustyns
2026-04-02 14:09:01 +02:00
parent fb5aa8796a
commit 9e2400c6de
4 changed files with 35 additions and 6 deletions
+4 -1
View File
@@ -59,6 +59,7 @@ export async function createEmailWorker() {
contact: true, contact: true,
project: true, project: true,
template: {select: {type: true}}, template: {select: {type: true}},
campaign: {select: {type: true}},
}, },
}); });
@@ -112,7 +113,9 @@ export async function createEmailWorker() {
contact: email.contact, contact: email.contact,
project: email.project, project: email.project,
includeUnsubscribe: 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 // Use fromName from database if available, otherwise fall back to project name
+5 -4
View File
@@ -293,9 +293,8 @@ export class EmailService {
include: { include: {
contact: true, contact: true,
project: true, project: true,
template: { template: {select: {type: true}},
select: {type: true}, campaign: {select: {type: true}},
},
}, },
}); });
@@ -363,7 +362,9 @@ export class EmailService {
contact: email.contact, contact: email.contact,
project: email.project, project: email.project,
includeUnsubscribe: 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 // Use explicit fromName if provided, otherwise fall back to project name
@@ -1,5 +1,5 @@
import {beforeEach, describe, expect, it, vi, type Mock} from 'vitest'; 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 {ActionSchemas} from '@plunk/shared';
import {EmailService} from '../EmailService'; import {EmailService} from '../EmailService';
import {sendRawEmail} from '../SESService'; import {sendRawEmail} from '../SESService';
@@ -216,6 +216,29 @@ describe('EmailService', () => {
expect(email.error).toMatch(/unsubscribed/i); 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 <a href="https://example.com/unsubscribe">unsubscribe</a>',
});
const email = await EmailService.sendCampaignEmail({
projectId,
contactId: contact.id,
campaignId: campaign.id,
subject: 'Newsletter',
body: campaign.body,
from: '[email protected]',
});
expect(email.sourceType).toBe(EmailSourceType.CAMPAIGN);
expect(email.status).toBe(EmailStatus.PENDING);
});
it('should keep CAMPAIGN sourceType when campaign uses headless template', async () => { it('should keep CAMPAIGN sourceType when campaign uses headless template', async () => {
const contact = await factories.createContact({projectId, subscribed: true}); const contact = await factories.createContact({projectId, subscribed: true});
+2
View File
@@ -67,6 +67,7 @@ export interface CampaignFactoryOptions {
status?: CampaignStatus; status?: CampaignStatus;
scheduledFor?: Date | null; scheduledFor?: Date | null;
segmentId?: string | null; segmentId?: string | null;
type?: TemplateType;
} }
export interface WorkflowFactoryOptions { export interface WorkflowFactoryOptions {
@@ -224,6 +225,7 @@ export class TestFactories {
status: options.status || CampaignStatus.DRAFT, status: options.status || CampaignStatus.DRAFT,
scheduledFor: options.scheduledFor, scheduledFor: options.scheduledFor,
segmentId: options.segmentId, segmentId: options.segmentId,
type: options.type || TemplateType.MARKETING,
}, },
}); });
} }