224 lines
9 KiB
TypeScript
224 lines
9 KiB
TypeScript
import { createLogger } from "@link-stack/logger";
|
|
import { Zammad, getUser } from "../../lib/zammad.js";
|
|
|
|
const logger = createLogger('create-ticket-from-form');
|
|
|
|
export interface CreateTicketFromFormOptions {
|
|
formData: any;
|
|
receivedAt: string;
|
|
}
|
|
|
|
const createTicketFromFormTask = async (
|
|
options: CreateTicketFromFormOptions,
|
|
): Promise<void> => {
|
|
const { formData, receivedAt } = options;
|
|
|
|
logger.info({
|
|
formData,
|
|
receivedAt,
|
|
formDataKeys: Object.keys(formData),
|
|
}, 'Processing Formstack form submission');
|
|
|
|
// Extract data from Formstack payload - matching Python ngo-isac-uploader field names
|
|
const {
|
|
FormID,
|
|
UniqueID,
|
|
Name,
|
|
Email,
|
|
Phone,
|
|
'Signal Account': signalAccount,
|
|
City,
|
|
State,
|
|
'Zip Code': zipCode,
|
|
'What organization are you affiliated with and/or employed by (if applicable)?': organization,
|
|
'What type of support do you wish to receive (to the extent you know)?': typeOfSupport,
|
|
'Is there a specific deadline associated with this request (e.g., a legal or legislative deadline)?': specificDeadline,
|
|
'Please provide the deadline': deadline,
|
|
'Do you have an insurance provider that provides coverage for the types of services you seek (e.g., public official, professional liability insurance, litigation insurance)?': hasInsuranceProvider,
|
|
'Have you approached the insurance provider for assistance?': approachedProvider,
|
|
'Are you seeking help on behalf of an individual or an organization?': typeOfUser,
|
|
'What is the structure of the organization?': orgStructure,
|
|
'Are you currently a candidate for elected office, a government officeholder, or a government employee?': governmentAffiliated,
|
|
'Where did you hear about the Democracy Protection Network?': whereHeard,
|
|
'Do you or the organization work on behalf of any of the following communities or issues? Please select all that apply.': relatedIssues,
|
|
'Do you or the organization engage in any of the following types of work? Please select all that apply.': typeOfWork,
|
|
'Why are you seeking support? Please briefly describe the circumstances that have brought you to the DPN, including, as applicable, dates, places, and the people or entities involved. We coordinate crisis-response services and some resilience-building services (e.g., assistance establishing good-governance or security practices). If you are seeking resilience-building services, please note that in the text box below.': descriptionOfIssue,
|
|
'What is your preferred communication method?': preferredContactMethod,
|
|
} = formData;
|
|
|
|
// Build full name - matching Python pattern
|
|
const firstName = Name?.first || '';
|
|
const lastName = Name?.last || '';
|
|
const fullName = (firstName && lastName)
|
|
? `${firstName} ${lastName}`.trim()
|
|
: firstName || lastName || 'Unknown';
|
|
|
|
// Build ticket title - exactly matching Python ngo-isac-uploader pattern
|
|
// Pattern: [Name] - [Organization] - [Type of support]
|
|
let title = fullName;
|
|
if (organization) {
|
|
title += ` - ${organization}`;
|
|
}
|
|
if (typeOfSupport) {
|
|
// Handle array format (Formstack sends arrays for multi-select)
|
|
const supportText = Array.isArray(typeOfSupport) ? typeOfSupport.join(', ') : typeOfSupport;
|
|
title += ` - ${supportText}`;
|
|
}
|
|
|
|
// Build article body - format all fields as HTML like Python does
|
|
const formatAllFields = (data: any): string => {
|
|
let html = '';
|
|
for (const [key, value] of Object.entries(data)) {
|
|
if (key === 'HandshakeKey' || key === 'FormID' || key === 'UniqueID') continue;
|
|
if (value === null || value === undefined || value === '') continue;
|
|
|
|
const displayValue = Array.isArray(value) ? value.join(', ') :
|
|
typeof value === 'object' ? JSON.stringify(value) : value;
|
|
html += `<strong>${key}:</strong><br>${displayValue}<br>`;
|
|
}
|
|
return html;
|
|
};
|
|
|
|
const body = formatAllFields(formData);
|
|
|
|
// Get Zammad configuration from environment
|
|
const zammadUrl = process.env.ZAMMAD_URL || 'http://zammad-nginx:8080';
|
|
const zammadToken = process.env.ZAMMAD_API_TOKEN;
|
|
|
|
if (!zammadToken) {
|
|
logger.error('ZAMMAD_API_TOKEN environment variable is not configured');
|
|
throw new Error('ZAMMAD_API_TOKEN is required');
|
|
}
|
|
|
|
const zammad = Zammad({ token: zammadToken }, zammadUrl);
|
|
|
|
try {
|
|
// Look up the article type ID for cdr_signal
|
|
let cdrSignalTypeId: number | undefined;
|
|
try {
|
|
const articleTypes = await zammad.get('ticket_article_types');
|
|
const cdrSignalType = articleTypes.find((t: any) => t.name === 'cdr_signal');
|
|
cdrSignalTypeId = cdrSignalType?.id;
|
|
if (cdrSignalTypeId) {
|
|
logger.info({ cdrSignalTypeId }, 'Found cdr_signal article type');
|
|
} else {
|
|
logger.warn('cdr_signal article type not found, ticket will use default type');
|
|
}
|
|
} catch (error: any) {
|
|
logger.warn({ error: error.message }, 'Failed to look up cdr_signal article type');
|
|
}
|
|
|
|
// Determine contact method and phone number - matching Python logic
|
|
// Priority: Signal > SMS/Phone > Email
|
|
const useSignal = preferredContactMethod?.includes('Signal') || preferredContactMethod?.includes('ignal');
|
|
const useSMS = preferredContactMethod?.includes('SMS');
|
|
const phoneNumber = useSignal ? signalAccount : (useSMS || Phone) ? Phone : '';
|
|
|
|
// Get or create user - matching Python pattern
|
|
let customer;
|
|
|
|
if (phoneNumber) {
|
|
// Try to find by phone (Signal or regular)
|
|
customer = await getUser(zammad, phoneNumber);
|
|
if (customer) {
|
|
logger.info({ customerId: customer.id, method: 'phone' }, 'Found existing user by phone');
|
|
}
|
|
}
|
|
|
|
if (!customer && Email) {
|
|
// Search by email if phone search didn't work
|
|
const emailResults = await zammad.user.search(`email:${Email}`);
|
|
if (emailResults.length > 0) {
|
|
customer = emailResults[0];
|
|
logger.info({ customerId: customer.id, method: 'email' }, 'Found existing user by email');
|
|
}
|
|
}
|
|
|
|
if (!customer) {
|
|
// Create new user - matching Python user creation pattern
|
|
logger.info('Creating new user from form submission');
|
|
customer = await zammad.user.create({
|
|
firstname: firstName,
|
|
lastname: lastName,
|
|
email: Email || `${UniqueID}@formstack.local`,
|
|
phone: phoneNumber || '',
|
|
roles: ['Customer'],
|
|
});
|
|
}
|
|
|
|
logger.info({
|
|
customerId: customer.id,
|
|
customerEmail: customer.email,
|
|
customerPhone: customer.phone,
|
|
}, 'Customer identified/created');
|
|
|
|
// Helper function to format field values (handle arrays and null values)
|
|
const formatFieldValue = (value: any): string | undefined => {
|
|
if (value === null || value === undefined || value === '') return undefined;
|
|
if (Array.isArray(value)) return value.join(', ');
|
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
return String(value);
|
|
};
|
|
|
|
// Create the ticket with custom fields - EXACTLY matching Python ngo-isac-uploader field names
|
|
const ticketData: any = {
|
|
title,
|
|
group: "Imports", // Matching Python - uses "Imports" group
|
|
customer_id: customer.id,
|
|
|
|
// Custom fields - matching Python field names EXACTLY
|
|
us_state: formatFieldValue(State),
|
|
zip_code: formatFieldValue(zipCode),
|
|
city: formatFieldValue(City),
|
|
type_of_support: formatFieldValue(typeOfSupport),
|
|
specific_deadline: formatFieldValue(specificDeadline),
|
|
deadline: formatFieldValue(deadline),
|
|
has_insurance_provider: formatFieldValue(hasInsuranceProvider),
|
|
approached_provider: formatFieldValue(approachedProvider),
|
|
type_of_user: formatFieldValue(typeOfUser),
|
|
org_structure: formatFieldValue(orgStructure),
|
|
government_affiliated: formatFieldValue(governmentAffiliated),
|
|
where_heard: formatFieldValue(whereHeard),
|
|
related_issues: formatFieldValue(relatedIssues),
|
|
type_of_work: formatFieldValue(typeOfWork),
|
|
|
|
// Article with all formatted fields
|
|
article: {
|
|
body,
|
|
subject: title,
|
|
content_type: "text/html",
|
|
type: useSignal ? "cdr_signal" : "note",
|
|
from: phoneNumber || Email || 'unknown',
|
|
sender: "Customer",
|
|
},
|
|
};
|
|
|
|
const ticket = await zammad.ticket.create(ticketData);
|
|
|
|
// Update the ticket with the cdr_signal article type
|
|
// This must be done after creation as Zammad doesn't allow setting this field during creation
|
|
if (cdrSignalTypeId) {
|
|
await zammad.ticket.update(ticket.id, { create_article_type_id: cdrSignalTypeId });
|
|
logger.info({ ticketId: ticket.id, cdrSignalTypeId }, 'Updated ticket with cdr_signal article type');
|
|
}
|
|
|
|
logger.info({
|
|
ticketId: ticket.id,
|
|
customerId: customer.id,
|
|
formId: FormID,
|
|
submissionId: UniqueID,
|
|
}, 'Zammad ticket created successfully');
|
|
|
|
} catch (error: any) {
|
|
logger.error({
|
|
error: error.message,
|
|
stack: error.stack,
|
|
output: error.output,
|
|
formId: FormID,
|
|
submissionId: UniqueID,
|
|
}, 'Failed to create Zammad ticket');
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export default createTicketFromFormTask;
|