PSA/server/migrations/20250713000000_fix_system_email_processing_workflow.cjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

215 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// server/migrations/20250713000000_fix_system_email_processing_workflow.cjs
// Fixed workflow definition without dynamic imports
const fixedEmailProcessingWorkflowCode = `
async function execute(context) {
const { actions, data, logger, setState } = context;
const triggerEvent = context.input?.triggerEvent;
// Extract email data from the INBOUND_EMAIL_RECEIVED event payload
const emailData = triggerEvent?.payload?.emailData;
const providerId = triggerEvent?.payload?.providerId;
const tenant = triggerEvent?.payload?.tenant;
if (!emailData || !providerId || !tenant) {
logger.error('Missing required email data in trigger event');
setState('ERROR_MISSING_DATA');
return;
}
setState('PROCESSING_INBOUND_EMAIL');
logger.info('Processing inbound email: ' + emailData.subject + ' from ' + emailData.from.email);
// Store relevant data in workflow context
data.set('emailData', emailData);
data.set('providerId', providerId);
data.set('tenant', tenant);
data.set('processedAt', new Date().toISOString());
try {
// Step 1: Check if this is a threaded email (reply to existing ticket)
setState('CHECKING_EMAIL_THREADING');
logger.info('Checking if email is part of existing conversation thread');
const existingTicket = await actions.find_ticket_by_email_thread({
threadId: emailData.threadId,
inReplyTo: emailData.inReplyTo,
references: emailData.references,
originalMessageId: emailData.inReplyTo
});
if (existingTicket && existingTicket.success && existingTicket.ticket) {
// This is a reply to an existing ticket - add as comment
logger.info('Email is part of existing ticket: ' + existingTicket.ticket.ticketId);
await actions.create_comment_from_email({
ticket_id: existingTicket.ticket.ticketId,
content: emailData.body.html || emailData.body.text,
format: emailData.body.html ? 'html' : 'text',
source: 'email',
author_type: 'contact',
metadata: {
emailSource: true,
emailId: emailData.id,
fromEmail: emailData.from.email,
fromName: emailData.from.name,
emailSubject: emailData.subject,
emailReceivedAt: emailData.receivedAt,
isReply: true,
replyToMessageId: emailData.inReplyTo
}
});
setState('EMAIL_PROCESSED');
logger.info('Email reply processed successfully');
data.set('ticketId', existingTicket.ticket.ticketId);
return;
}
// Step 2: This is a new email - find or match client
setState('MATCHING_EMAIL_CLIENT');
logger.info('Attempting to match email sender to existing client');
const matchedClient = await actions.find_contact_by_email({
email: emailData.from.email
});
let clientInfo = null;
if (matchedClient && matchedClient.success && matchedClient.contact) {
logger.info('Found exact email match: ' + matchedClient.contact.company_name);
clientInfo = {
companyId: matchedClient.contact.company_id,
contactId: matchedClient.contact.contact_id
};
} else {
logger.info('No exact email match found, creating ticket without client association');
}
// Step 3: Create new ticket from email
setState('CREATING_TICKET');
logger.info('Creating new ticket from email');
// Get default IDs (simplified for migration)
const defaultChannelId = await actions.find_channel_by_name({
name: 'Email'
});
const defaultStatusId = await actions.find_status_by_name({
name: 'New',
item_type: 'ticket'
});
const defaultPriorityId = await actions.find_priority_by_name({
name: 'Medium'
});
const ticketResult = await actions.create_ticket_from_email({
title: emailData.subject,
description: emailData.body.text,
company_id: clientInfo?.companyId,
contact_id: clientInfo?.contactId,
source: 'email',
channel_id: defaultChannelId?.success ? defaultChannelId.channel.id : null,
status_id: defaultStatusId?.success ? defaultStatusId.status.id : null,
priority_id: defaultPriorityId?.success ? defaultPriorityId.priority.id : null,
email_metadata: {
messageId: emailData.id,
threadId: emailData.threadId,
from: emailData.from,
inReplyTo: emailData.inReplyTo,
references: emailData.references,
providerId: providerId
}
});
logger.info('Ticket created with ID: ' + ticketResult.ticket_id);
data.set('ticketId', ticketResult.ticket_id);
// Step 4: Create initial comment with original email content
await actions.create_comment_from_email({
ticket_id: ticketResult.ticket_id,
content: emailData.body.html || emailData.body.text,
format: emailData.body.html ? 'html' : 'text',
source: 'email',
author_type: 'system',
metadata: {
emailSource: true,
originalEmailId: emailData.id,
fromEmail: emailData.from.email,
fromName: emailData.from.name,
emailSubject: emailData.subject,
emailReceivedAt: emailData.receivedAt
}
});
setState('EMAIL_PROCESSED');
logger.info('Email processing completed successfully');
} catch (error) {
logger.error('Error processing inbound email: ' + error.message);
setState('ERROR_PROCESSING_EMAIL');
}
}
`;
exports.up = async function(knex) {
console.log('Fixing System Email Processing Workflow code...');
// Find the system email processing workflow registration
const emailProcessingRegistration = await knex('system_workflow_registrations')
.where({ name: 'System Email Processing' })
.first();
if (emailProcessingRegistration) {
console.log('Found System Email Processing Workflow registration:', emailProcessingRegistration.registration_id);
// Update the code in the current version
const updatedRows = await knex('system_workflow_registration_versions')
.where({
registration_id: emailProcessingRegistration.registration_id,
is_current: true
})
.update({
code: fixedEmailProcessingWorkflowCode.trim(),
updated_at: new Date().toISOString()
});
console.log(`✅ Updated ${updatedRows} workflow version(s) with fixed code.`);
} else {
console.log(' System Email Processing Workflow not found, skipping fix.');
}
};
exports.down = async function(knex) {
console.log('Reverting System Email Processing Workflow code fix...');
// Original problematic code with dynamic import
const originalCode = `
async function execute(context) {
// This is a placeholder - the actual execution happens in
// /shared/workflow/workflows/system-email-processing-workflow.ts
const { systemEmailProcessingWorkflow } = await import('/shared/workflow/workflows/system-email-processing-workflow.ts');
return await systemEmailProcessingWorkflow(context);
}
`;
// Find the system email processing workflow registration
const emailProcessingRegistration = await knex('system_workflow_registrations')
.where({ name: 'System Email Processing' })
.first();
if (emailProcessingRegistration) {
// Revert to the original problematic code
const updatedRows = await knex('system_workflow_registration_versions')
.where({
registration_id: emailProcessingRegistration.registration_id,
is_current: true
})
.update({
code: originalCode.trim(),
updated_at: new Date().toISOString()
});
console.log(`🔄 Reverted ${updatedRows} workflow version(s) to original code.`);
} else {
console.log(' System Email Processing Workflow not found, skipping revert.');
}
};