Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {StepExecutionStatus, WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db';
import {
EmailSourceType,
StepExecutionStatus,
TemplateType,
WorkflowExecutionStatus,
WorkflowStepType,
} from '@plunk/db';
import {toPrismaJson} from '@plunk/types';
import {WorkflowExecutionService} from '../WorkflowExecutionService';
import {QueueService} from '../QueueService';
import {WorkflowService} from '../WorkflowService';
import {factories, getPrismaClient} from '../../../../../test/helpers';

vi.mock('node:dns/promises', () => ({
Expand Down Expand Up @@ -40,10 +48,99 @@ describe('WorkflowExecutionService - Integration Tests', () => {
const prisma = getPrismaClient();

beforeEach(async () => {
vi.clearAllMocks();
const {project} = await factories.createUserWithProject();
projectId = project.id;
});

describe('Send email template binding', () => {
it('executes an API-created email step without a duplicate template ID in config', async () => {
const contact = await factories.createContact({projectId, subscribed: false});
const template = await factories.createTemplate({
projectId,
type: TemplateType.TRANSACTIONAL,
subject: 'Confirm your email',
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
const emailStep = await WorkflowService.addStep(projectId, workflow.id, {
type: WorkflowStepType.SEND_EMAIL,
name: 'Send confirmation',
position: {x: 100, y: 0},
config: {recipient: {type: 'CONTACT'}},
templateId: template.id,
});
const execution = await factories.createWorkflowExecution(workflow.id, contact.id, {
status: WorkflowExecutionStatus.RUNNING,
});
await prisma.workflowExecution.update({
where: {id: execution.id},
data: {currentStepId: triggerStep.id},
});

await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);

const completed = await prisma.workflowExecution.findUniqueOrThrow({where: {id: execution.id}});
const stepExecution = await prisma.workflowStepExecution.findFirstOrThrow({
where: {executionId: execution.id, stepId: emailStep.id},
});
const email = await prisma.email.findFirstOrThrow({where: {workflowExecutionId: execution.id}});

expect(completed.status).toBe(WorkflowExecutionStatus.COMPLETED);
expect(stepExecution.status).toBe(StepExecutionStatus.COMPLETED);
expect(email).toMatchObject({
contactId: contact.id,
templateId: template.id,
subject: template.subject,
sourceType: EmailSourceType.TRANSACTIONAL,
});
expect(QueueService.queueEmail).toHaveBeenCalledWith(email.id, EmailSourceType.TRANSACTIONAL, undefined);
});

it('uses the template relation when legacy config contains a different template ID', async () => {
const contact = await factories.createContact({projectId, subscribed: false});
const relationalTemplate = await factories.createTemplate({
projectId,
type: TemplateType.TRANSACTIONAL,
subject: 'Relational template',
});
const staleConfigTemplate = await factories.createTemplate({
projectId,
type: TemplateType.TRANSACTIONAL,
subject: 'Stale config template',
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
await WorkflowService.addStep(projectId, workflow.id, {
type: WorkflowStepType.SEND_EMAIL,
name: 'Send confirmation',
position: {x: 100, y: 0},
config: {
templateId: staleConfigTemplate.id,
recipient: {type: 'CONTACT'},
},
templateId: relationalTemplate.id,
});
const execution = await factories.createWorkflowExecution(workflow.id, contact.id, {
status: WorkflowExecutionStatus.RUNNING,
});
await prisma.workflowExecution.update({
where: {id: execution.id},
data: {currentStepId: triggerStep.id},
});

await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);

const email = await prisma.email.findFirstOrThrow({where: {workflowExecutionId: execution.id}});
expect(email.templateId).toBe(relationalTemplate.id);
expect(email.subject).toBe(relationalTemplate.subject);
});
});

// ========================================
// CONDITIONAL BRANCHING (CONDITION STEPS)
// ========================================
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ export const WorkflowSchemas = {

export const WorkflowStepConfigSchemas = {
sendEmail: z.object({
templateId: uuid,
// The WorkflowStep.template relation is authoritative. This deprecated
// JSON copy is accepted when present for compatibility with older clients.
templateId: uuid.optional(),
recipient: z
.object({
type: z.enum(['CONTACT', 'CUSTOM']),
Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export interface EmailRecipientConfig {
* Configuration for SEND_EMAIL workflow step
*/
export interface SendEmailStepConfig {
/** Template ID to use for the email */
templateId: string;
/** @deprecated The WorkflowStep.template relation is authoritative. */
templateId?: string;
/** Recipient configuration */
recipient?: EmailRecipientConfig;
}