Initial push of Plunk Next
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
import {describe, it, expect, beforeEach, vi, afterEach} from 'vitest';
|
||||
import {WorkflowTriggerType, WorkflowExecutionStatus} from '@plunk/db';
|
||||
import {EventService} from '../EventService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
// Mock Redis for caching tests - must be inline to avoid hoisting issues
|
||||
vi.mock('../../database/redis', () => {
|
||||
const store = new Map<string, {value: string; expiry?: number}>();
|
||||
return {
|
||||
redis: {
|
||||
get: vi.fn(async (key: string) => {
|
||||
const item = store.get(key);
|
||||
if (!item) return null;
|
||||
if (item.expiry && Date.now() > item.expiry) {
|
||||
store.delete(key);
|
||||
return null;
|
||||
}
|
||||
return item.value;
|
||||
}),
|
||||
set: vi.fn(async (key: string, value: string) => {
|
||||
store.set(key, {value});
|
||||
return 'OK';
|
||||
}),
|
||||
setex: vi.fn(async (key: string, seconds: number, value: string) => {
|
||||
store.set(key, {value, expiry: Date.now() + seconds * 1000});
|
||||
return 'OK';
|
||||
}),
|
||||
del: vi.fn(async (key: string) => {
|
||||
store.delete(key);
|
||||
return 1;
|
||||
}),
|
||||
incr: vi.fn(async (key: string) => {
|
||||
const current = store.get(key);
|
||||
const newValue = current ? parseInt(current.value) + 1 : 1;
|
||||
store.set(key, {value: String(newValue)});
|
||||
return newValue;
|
||||
}),
|
||||
expire: vi.fn(async (key: string, seconds: number) => {
|
||||
const item = store.get(key);
|
||||
if (!item) return 0;
|
||||
store.set(key, {...item, expiry: Date.now() + seconds * 1000});
|
||||
return 1;
|
||||
}),
|
||||
clear: () => store.clear(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('EventService', () => {
|
||||
let projectId: string;
|
||||
const prisma = getPrismaClient();
|
||||
|
||||
beforeEach(async () => {
|
||||
const {project} = await factories.createUserWithProject();
|
||||
projectId = project.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clear Redis mock
|
||||
const {redis} = await import('../../database/redis');
|
||||
if ('clear' in redis) {
|
||||
(redis as any).clear();
|
||||
}
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// EVENT TRACKING
|
||||
// ========================================
|
||||
describe('trackEvent', () => {
|
||||
it('should create an event record', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const event = await EventService.trackEvent(projectId, 'user.signup', contact.id, undefined, {
|
||||
source: 'web',
|
||||
plan: 'free',
|
||||
});
|
||||
|
||||
expect(event.projectId).toBe(projectId);
|
||||
expect(event.contactId).toBe(contact.id);
|
||||
expect(event.name).toBe('user.signup');
|
||||
expect(event.data).toEqual({source: 'web', plan: 'free'});
|
||||
});
|
||||
|
||||
it('should track event without contact (project-level event)', async () => {
|
||||
const event = await EventService.trackEvent(projectId, 'project.created', undefined, undefined, {
|
||||
plan: 'pro',
|
||||
});
|
||||
|
||||
expect(event.projectId).toBe(projectId);
|
||||
expect(event.contactId).toBeNull();
|
||||
expect(event.name).toBe('project.created');
|
||||
});
|
||||
|
||||
it('should track event with email reference', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
const email = await factories.createEmail(projectId, contact.id);
|
||||
|
||||
const event = await EventService.trackEvent(projectId, 'email.opened', contact.id, email.id, {
|
||||
userAgent: 'Mozilla/5.0',
|
||||
});
|
||||
|
||||
expect(event.emailId).toBe(email.id);
|
||||
expect(event.contactId).toBe(contact.id);
|
||||
});
|
||||
|
||||
it('should trigger workflows listening for the event', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
// Create workflow triggered by 'purchase.completed' event
|
||||
const workflow = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'purchase.completed'},
|
||||
});
|
||||
|
||||
// Add a delay step so workflow doesn't complete immediately
|
||||
const triggerStep = await prisma.workflowStep.findFirst({
|
||||
where: {workflowId: workflow.id, type: 'TRIGGER'},
|
||||
});
|
||||
|
||||
const delayStep = await prisma.workflowStep.create({
|
||||
data: {
|
||||
workflowId: workflow.id,
|
||||
type: 'DELAY',
|
||||
name: 'Wait',
|
||||
position: {x: 100, y: 0},
|
||||
config: {amount: 24, unit: 'hours'},
|
||||
},
|
||||
});
|
||||
|
||||
// Connect steps
|
||||
await prisma.workflowTransition.create({
|
||||
data: {
|
||||
fromStepId: triggerStep!.id,
|
||||
toStepId: delayStep.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Track the event
|
||||
await EventService.trackEvent(projectId, 'purchase.completed', contact.id, undefined, {
|
||||
amount: 99.99,
|
||||
product: 'Premium Plan',
|
||||
});
|
||||
|
||||
// Verify workflow execution was created
|
||||
const executions = await prisma.workflowExecution.findMany({
|
||||
where: {
|
||||
workflowId: workflow.id,
|
||||
contactId: contact.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(executions).toHaveLength(1);
|
||||
// Workflow should be in COMPLETED status since DELAY step completes and has no next step
|
||||
// (DELAY sets to WAITING then processNextSteps sees no transitions and completes it)
|
||||
expect([WorkflowExecutionStatus.WAITING, WorkflowExecutionStatus.COMPLETED]).toContain(executions[0].status);
|
||||
});
|
||||
|
||||
it('should NOT trigger disabled workflows', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
// Create disabled workflow
|
||||
const workflow = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: false, // Disabled
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'test.event'},
|
||||
});
|
||||
|
||||
await EventService.trackEvent(projectId, 'test.event', contact.id);
|
||||
|
||||
// No execution should be created
|
||||
const executions = await prisma.workflowExecution.findMany({
|
||||
where: {workflowId: workflow.id},
|
||||
});
|
||||
|
||||
expect(executions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should NOT trigger workflows for different event names', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const workflow = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'user.signup'},
|
||||
});
|
||||
|
||||
// Track different event
|
||||
await EventService.trackEvent(projectId, 'user.login', contact.id);
|
||||
|
||||
// No execution should be created
|
||||
const executions = await prisma.workflowExecution.findMany({
|
||||
where: {workflowId: workflow.id},
|
||||
});
|
||||
|
||||
expect(executions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should respect workflow re-entry settings', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const workflow = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
allowReentry: false, // Do not allow re-entry
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'repeat.event'},
|
||||
});
|
||||
|
||||
// First event - should create execution
|
||||
await EventService.trackEvent(projectId, 'repeat.event', contact.id);
|
||||
|
||||
let executions = await prisma.workflowExecution.findMany({
|
||||
where: {workflowId: workflow.id, contactId: contact.id},
|
||||
});
|
||||
|
||||
expect(executions).toHaveLength(1);
|
||||
|
||||
// Second event - should NOT create execution (re-entry not allowed)
|
||||
await EventService.trackEvent(projectId, 'repeat.event', contact.id);
|
||||
|
||||
executions = await prisma.workflowExecution.findMany({
|
||||
where: {workflowId: workflow.id, contactId: contact.id},
|
||||
});
|
||||
|
||||
expect(executions).toHaveLength(1); // Still only 1
|
||||
});
|
||||
|
||||
it('should allow re-entry when enabled', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const workflow = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
allowReentry: true, // Allow re-entry
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'repeat.event'},
|
||||
});
|
||||
|
||||
// First event
|
||||
await EventService.trackEvent(projectId, 'repeat.event', contact.id);
|
||||
|
||||
// Complete first execution
|
||||
const firstExecution = await prisma.workflowExecution.findFirst({
|
||||
where: {workflowId: workflow.id, contactId: contact.id},
|
||||
});
|
||||
|
||||
await prisma.workflowExecution.update({
|
||||
where: {id: firstExecution!.id},
|
||||
data: {status: WorkflowExecutionStatus.COMPLETED},
|
||||
});
|
||||
|
||||
// Second event - should create new execution
|
||||
await EventService.trackEvent(projectId, 'repeat.event', contact.id);
|
||||
|
||||
const executions = await prisma.workflowExecution.findMany({
|
||||
where: {workflowId: workflow.id, contactId: contact.id},
|
||||
});
|
||||
|
||||
expect(executions).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should trigger multiple workflows listening for same event', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const workflow1 = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'shared.event'},
|
||||
});
|
||||
|
||||
const workflow2 = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'shared.event'},
|
||||
});
|
||||
|
||||
await EventService.trackEvent(projectId, 'shared.event', contact.id);
|
||||
|
||||
const execution1 = await prisma.workflowExecution.findFirst({
|
||||
where: {workflowId: workflow1.id},
|
||||
});
|
||||
|
||||
const execution2 = await prisma.workflowExecution.findFirst({
|
||||
where: {workflowId: workflow2.id},
|
||||
});
|
||||
|
||||
expect(execution1).toBeDefined();
|
||||
expect(execution2).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// EVENT RETRIEVAL
|
||||
// ========================================
|
||||
describe('getContactEvents', () => {
|
||||
it('should get events for a specific contact', async () => {
|
||||
const contact1 = await factories.createContact({projectId});
|
||||
const contact2 = await factories.createContact({projectId});
|
||||
|
||||
await EventService.trackEvent(projectId, 'event.1', contact1.id);
|
||||
await EventService.trackEvent(projectId, 'event.2', contact1.id);
|
||||
await EventService.trackEvent(projectId, 'event.3', contact2.id);
|
||||
|
||||
const events = await EventService.getContactEvents(projectId, contact1.id);
|
||||
|
||||
expect(events).toHaveLength(2);
|
||||
expect(events.every(e => e.contactId === contact1.id)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return events in reverse chronological order (newest first)', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
await EventService.trackEvent(projectId, 'first', contact.id);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
await EventService.trackEvent(projectId, 'second', contact.id);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
await EventService.trackEvent(projectId, 'third', contact.id);
|
||||
|
||||
const events = await EventService.getContactEvents(projectId, contact.id);
|
||||
|
||||
expect(events[0].name).toBe('third'); // Newest
|
||||
expect(events[1].name).toBe('second');
|
||||
expect(events[2].name).toBe('first'); // Oldest
|
||||
});
|
||||
|
||||
it('should respect limit parameter', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
// Create 100 events
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await EventService.trackEvent(projectId, `event.${i}`, contact.id);
|
||||
}
|
||||
|
||||
const events = await EventService.getContactEvents(projectId, contact.id, 25);
|
||||
|
||||
expect(events).toHaveLength(25);
|
||||
});
|
||||
|
||||
it('should default to 50 events limit', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
// Create 60 events
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await EventService.trackEvent(projectId, `event.${i}`, contact.id);
|
||||
}
|
||||
|
||||
const events = await EventService.getContactEvents(projectId, contact.id);
|
||||
|
||||
expect(events).toHaveLength(50); // Default limit
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProjectEvents', () => {
|
||||
it('should get all events for a project', async () => {
|
||||
const contact1 = await factories.createContact({projectId});
|
||||
const contact2 = await factories.createContact({projectId});
|
||||
|
||||
await EventService.trackEvent(projectId, 'event.1', contact1.id);
|
||||
await EventService.trackEvent(projectId, 'event.2', contact2.id);
|
||||
await EventService.trackEvent(projectId, 'event.3');
|
||||
|
||||
const events = await EventService.getProjectEvents(projectId);
|
||||
|
||||
expect(events).toHaveLength(3);
|
||||
expect(events.every(e => e.projectId === projectId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by event name', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
await EventService.trackEvent(projectId, 'user.signup', contact.id);
|
||||
await EventService.trackEvent(projectId, 'user.login', contact.id);
|
||||
await EventService.trackEvent(projectId, 'user.signup', contact.id);
|
||||
|
||||
const events = await EventService.getProjectEvents(projectId, 'user.signup');
|
||||
|
||||
expect(events).toHaveLength(2);
|
||||
expect(events.every(e => e.name === 'user.signup')).toBe(true);
|
||||
});
|
||||
|
||||
it('should include contact email in results', async () => {
|
||||
const contact = await factories.createContact({
|
||||
projectId,
|
||||
email: '[email protected]',
|
||||
});
|
||||
|
||||
await EventService.trackEvent(projectId, 'test.event', contact.id);
|
||||
|
||||
const events = await EventService.getProjectEvents(projectId);
|
||||
|
||||
expect(events[0].contact?.email).toBe('[email protected]');
|
||||
});
|
||||
|
||||
it('should respect limit parameter', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
for (let i = 0; i < 150; i++) {
|
||||
await EventService.trackEvent(projectId, `event.${i}`, contact.id);
|
||||
}
|
||||
|
||||
const events = await EventService.getProjectEvents(projectId, undefined, 50);
|
||||
|
||||
expect(events).toHaveLength(50);
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// EVENT STATISTICS
|
||||
// ========================================
|
||||
describe('getEventStats', () => {
|
||||
it('should return event counts grouped by type', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
await EventService.trackEvent(projectId, 'user.signup', contact.id);
|
||||
await EventService.trackEvent(projectId, 'user.signup', contact.id);
|
||||
await EventService.trackEvent(projectId, 'user.login', contact.id);
|
||||
await EventService.trackEvent(projectId, 'purchase.completed', contact.id);
|
||||
await EventService.trackEvent(projectId, 'purchase.completed', contact.id);
|
||||
await EventService.trackEvent(projectId, 'purchase.completed', contact.id);
|
||||
|
||||
const stats = await EventService.getEventStats(projectId);
|
||||
|
||||
expect(stats).toHaveLength(3);
|
||||
|
||||
// Should be ordered by count (desc)
|
||||
expect(stats[0].name).toBe('purchase.completed');
|
||||
expect(stats[0].count).toBe(3);
|
||||
expect(stats[1].name).toBe('user.signup');
|
||||
expect(stats[1].count).toBe(2);
|
||||
expect(stats[2].name).toBe('user.login');
|
||||
expect(stats[2].count).toBe(1);
|
||||
});
|
||||
|
||||
it('should filter by date range', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const oldDate = new Date('2024-01-01');
|
||||
const recentDate = new Date('2024-06-01');
|
||||
|
||||
// Create old event directly
|
||||
await prisma.event.create({
|
||||
data: {
|
||||
projectId,
|
||||
contactId: contact.id,
|
||||
name: 'old.event',
|
||||
createdAt: oldDate,
|
||||
},
|
||||
});
|
||||
|
||||
// Create recent events
|
||||
await EventService.trackEvent(projectId, 'recent.event', contact.id);
|
||||
|
||||
const startDate = new Date('2024-05-01');
|
||||
const stats = await EventService.getEventStats(projectId, startDate);
|
||||
|
||||
expect(stats).toHaveLength(1);
|
||||
expect(stats[0].name).toBe('recent.event');
|
||||
});
|
||||
|
||||
it('should handle empty result', async () => {
|
||||
const stats = await EventService.getEventStats(projectId);
|
||||
|
||||
expect(stats).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUniqueEventNames', () => {
|
||||
it('should return unique event names ordered by frequency', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
await EventService.trackEvent(projectId, 'event.a', contact.id);
|
||||
await EventService.trackEvent(projectId, 'event.b', contact.id);
|
||||
await EventService.trackEvent(projectId, 'event.b', contact.id);
|
||||
await EventService.trackEvent(projectId, 'event.c', contact.id);
|
||||
await EventService.trackEvent(projectId, 'event.c', contact.id);
|
||||
await EventService.trackEvent(projectId, 'event.c', contact.id);
|
||||
|
||||
const names = await EventService.getUniqueEventNames(projectId);
|
||||
|
||||
expect(names).toHaveLength(3);
|
||||
expect(names[0]).toBe('event.c'); // Most frequent
|
||||
expect(names[1]).toBe('event.b');
|
||||
expect(names[2]).toBe('event.a'); // Least frequent
|
||||
});
|
||||
|
||||
it('should return empty array when no events exist', async () => {
|
||||
const names = await EventService.getUniqueEventNames(projectId);
|
||||
|
||||
expect(names).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// WORKFLOW CACHE MANAGEMENT
|
||||
// ========================================
|
||||
describe('invalidateWorkflowCache', () => {
|
||||
it('should invalidate workflow cache for project', async () => {
|
||||
const {redis} = await import('../../database/redis');
|
||||
|
||||
// Set cache
|
||||
const cacheKey = `workflows:enabled:${projectId}`;
|
||||
await redis.set(cacheKey, JSON.stringify([{id: 'test'}]));
|
||||
|
||||
// Verify cache exists
|
||||
const cached = await redis.get(cacheKey);
|
||||
expect(cached).toBeTruthy();
|
||||
|
||||
// Invalidate
|
||||
await EventService.invalidateWorkflowCache(projectId);
|
||||
|
||||
// Verify cache deleted
|
||||
const afterInvalidation = await redis.get(cacheKey);
|
||||
expect(afterInvalidation).toBeNull();
|
||||
});
|
||||
|
||||
it('should not throw error if cache does not exist', async () => {
|
||||
await expect(EventService.invalidateWorkflowCache(projectId)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// EDGE CASES
|
||||
// ========================================
|
||||
describe('edge cases', () => {
|
||||
it('should handle events with complex data structures', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const complexData = {
|
||||
user: {
|
||||
id: 123,
|
||||
profile: {
|
||||
name: 'John Doe',
|
||||
preferences: ['email', 'sms'],
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
source: 'mobile_app',
|
||||
version: '2.0.1',
|
||||
},
|
||||
items: [
|
||||
{id: 1, name: 'Item 1', price: 19.99},
|
||||
{id: 2, name: 'Item 2', price: 29.99},
|
||||
],
|
||||
};
|
||||
|
||||
const event = await EventService.trackEvent(projectId, 'complex.event', contact.id, undefined, complexData);
|
||||
|
||||
expect(event.data).toEqual(complexData);
|
||||
|
||||
// Verify data persists correctly
|
||||
const retrieved = await prisma.event.findUnique({
|
||||
where: {id: event.id},
|
||||
});
|
||||
|
||||
expect(retrieved?.data).toEqual(complexData);
|
||||
});
|
||||
|
||||
it('should handle events with null data', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const event = await EventService.trackEvent(projectId, 'simple.event', contact.id);
|
||||
|
||||
expect(event.data).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle event names with special characters', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const eventName = 'user:[email protected]/path-123';
|
||||
|
||||
const event = await EventService.trackEvent(projectId, eventName, contact.id);
|
||||
|
||||
expect(event.name).toBe(eventName);
|
||||
});
|
||||
|
||||
it('should not trigger workflows for events without contact when workflow expects contact', async () => {
|
||||
await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'test.event'},
|
||||
});
|
||||
|
||||
// Track event without contact
|
||||
await EventService.trackEvent(projectId, 'test.event');
|
||||
|
||||
// No execution should be created (event is not contact-specific)
|
||||
const executions = await prisma.workflowExecution.findMany({
|
||||
where: {workflow: {projectId}},
|
||||
});
|
||||
|
||||
expect(executions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event Data - Persistent vs Non-Persistent', () => {
|
||||
it('should store all event data (persistent + non-persistent) in event record', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const eventData = {
|
||||
totalSpent: 599.99, // Persistent
|
||||
orderId: {value: 'ORD-123', persistent: false}, // Non-persistent
|
||||
items: {value: [{name: 'Widget', qty: 2}], persistent: false}, // Non-persistent
|
||||
};
|
||||
|
||||
const event = await EventService.trackEvent(projectId, 'purchase', contact.id, undefined, eventData);
|
||||
|
||||
// Event should store ALL data for workflow access
|
||||
expect(event.data).toMatchObject({
|
||||
totalSpent: 599.99,
|
||||
orderId: {value: 'ORD-123', persistent: false},
|
||||
items: {value: [{name: 'Widget', qty: 2}], persistent: false},
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass all event data to workflow execution context', async () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const workflow = await factories.createWorkflow({
|
||||
projectId,
|
||||
enabled: true,
|
||||
triggerType: WorkflowTriggerType.EVENT,
|
||||
triggerConfig: {eventName: 'order_placed'},
|
||||
});
|
||||
|
||||
await factories.createWorkflowStep({
|
||||
workflowId: workflow.id,
|
||||
type: 'TRIGGER',
|
||||
name: 'Order Trigger',
|
||||
position: {x: 0, y: 0},
|
||||
config: {},
|
||||
});
|
||||
|
||||
const eventData = {
|
||||
amount: 99.99, // Persistent
|
||||
confirmationCode: {value: 'CONF-456', persistent: false}, // Non-persistent
|
||||
};
|
||||
|
||||
await EventService.trackEvent(projectId, 'order_placed', contact.id, undefined, eventData);
|
||||
|
||||
// Wait for async workflow creation
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
const execution = await prisma.workflowExecution.findFirst({
|
||||
where: {
|
||||
workflowId: workflow.id,
|
||||
contactId: contact.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Execution context should have ALL event data
|
||||
expect(execution).toBeDefined();
|
||||
expect(execution?.context).toMatchObject({
|
||||
amount: 99.99,
|
||||
confirmationCode: {value: 'CONF-456', persistent: false},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user