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 => { const { formData, receivedAt } = options; logger.info({ formData, receivedAt, formDataKeys: Object.keys(formData), }, 'Processing Formstack form submission'); // Extract data from Formstack payload const { FormID, UniqueID, name, signal_number, type_of_help_requested, type_of_organization, urgency_level, email_address, phone_number, address, description_of_issue, preferred_contact_method, available_times_for_contact, how_did_you_hear_about_us, preferred_language, } = formData; // Build full name const fullName = name ? `${name.first || ''} ${name.last || ''}`.trim() : 'Unknown'; // Get organization name from form data const organizationName = type_of_organization || ''; // Build ticket title - matching ngo-isac-uploader pattern let title = fullName; if (organizationName) { title += ` - ${organizationName}`; } if (type_of_help_requested) { title += ` - ${type_of_help_requested}`; } // Build article body - only description and metadata // All other fields go into custom Zammad ticket fields const body = description_of_issue ? `

${description_of_issue}


Submitted via Formstack | Form ID: ${FormID} | Submission ID: ${UniqueID} | Received: ${receivedAt}

` : `

No description provided


Submitted via Formstack | Form ID: ${FormID} | Submission ID: ${UniqueID} | Received: ${receivedAt}

`; // 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 { // Get or create user based on contact info // Priority: signal_number > phone_number > email_address let customer; // Try to find existing user by phone or email if (signal_number || phone_number) { const phoneToSearch = signal_number || phone_number; customer = await getUser(zammad, phoneToSearch); if (customer) { logger.info({ customerId: customer.id, method: 'phone' }, 'Found existing user by phone'); } } if (!customer && email_address) { // Search by email if phone search didn't work const emailResults = await zammad.user.search(`email:${email_address}`); if (emailResults.length > 0) { customer = emailResults[0]; logger.info({ customerId: customer.id, method: 'email' }, 'Found existing user by email'); } } if (!customer) { // Create new user with all available contact information logger.info('Creating new user from form submission'); customer = await zammad.user.create({ firstname: name?.first || '', lastname: name?.last || '', email: email_address || `${UniqueID}@formstack.local`, phone: signal_number || phone_number || '', note: `User created from Formstack submission ${UniqueID}`, }); } logger.info({ customerId: customer.id, customerEmail: customer.email, customerPhone: customer.phone, }, 'Customer identified/created'); // Build address parts const streetAddress = address?.address || ''; const cityValue = address?.city || ''; const stateValue = address?.state || ''; const zipValue = address?.zip || ''; // Create the ticket with custom fields mapped to Zammad attributes // Following the pattern from ngo-isac-uploader where all form data // goes into structured fields rather than HTML body const ticket = await zammad.ticket.create({ title, group: "Users", // Default group - you may want to make this configurable customer_id: customer.id, // Custom fields - these will be populated in Zammad's ticket attributes // NOTE: 'organization', 'formstack_form_id', 'formstack_submission_id' // fields could not be created due to naming conflicts, so metadata // is included in the ticket body instead signal_number: signal_number || undefined, type_of_help_requested: type_of_help_requested || undefined, type_of_organization: type_of_organization || undefined, urgency_level: urgency_level || undefined, city: cityValue || undefined, us_state: stateValue || undefined, zip_code: zipValue || undefined, street_address: streetAddress || undefined, preferred_contact_method: preferred_contact_method || undefined, available_times: available_times_for_contact || undefined, where_heard: how_did_you_hear_about_us || undefined, preferred_language: preferred_language || undefined, // Article with just the description article: { body, subject: title, content_type: "text/html", type: "note", }, }); 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;