diff --git a/apps/bridge-worker/tasks/formstack/create-ticket-from-form.ts b/apps/bridge-worker/tasks/formstack/create-ticket-from-form.ts new file mode 100644 index 0000000..a28eb89 --- /dev/null +++ b/apps/bridge-worker/tasks/formstack/create-ticket-from-form.ts @@ -0,0 +1,150 @@ +import { createLogger } from "@link-stack/logger"; +import { Zammad, getOrCreateUser } 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'; + + // Build address string + let addressString = ''; + if (address) { + const parts = [ + address.address, + address.city, + address.state, + address.zip, + ].filter(Boolean); + addressString = parts.join(', '); + } + + // Build ticket title + const title = `Help Request: ${type_of_help_requested || 'General'} - ${fullName}`; + + // Build ticket body with all form information + const body = `

Contact Information

+ + +

Request Details

+ + + ${description_of_issue ? `

Description of Issue

${description_of_issue}

` : ''} + + ${how_did_you_hear_about_us ? `

How they heard about us: ${how_did_you_hear_about_us}

` : ''} + +
+

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 phone number, Signal number, or email + let customer; + const contactInfo = signal_number || phone_number || email_address; + + if (contactInfo) { + customer = await getOrCreateUser(zammad, contactInfo); + } else { + // Create user with just the name if no contact info + customer = await zammad.user.create({ + firstname: name?.first, + lastname: name?.last, + note: `User created from Formstack submission ${UniqueID}`, + }); + } + + logger.info({ customerId: customer.id, customerEmail: customer.email }, 'Customer identified/created'); + + // Create the ticket + const ticket = await zammad.ticket.create({ + title, + group: "Users", // Default group - you may want to make this configurable + note: `This ticket was created automatically from Formstack form ${FormID}.`, + customer_id: customer.id, + 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; diff --git a/apps/link/app/api/formstack/route.ts b/apps/link/app/api/formstack/route.ts new file mode 100644 index 0000000..7df1486 --- /dev/null +++ b/apps/link/app/api/formstack/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createLogger } from "@link-stack/logger"; +import { getWorkerUtils } from "@link-stack/bridge-common"; + +const logger = createLogger('formstack-webhook'); + +export async function POST(req: NextRequest): Promise { + try { + // Get the shared secret from environment variable + const expectedSecret = process.env.FORMSTACK_SHARED_SECRET; + + if (!expectedSecret) { + logger.error('FORMSTACK_SHARED_SECRET environment variable is not configured'); + return NextResponse.json( + { error: "Server configuration error" }, + { status: 500 } + ); + } + + // Get the shared secret from the request body + const body = await req.json(); + const receivedSecret = body.HandshakeKey; + + // Verify the shared secret + if (receivedSecret !== expectedSecret) { + logger.warn({ receivedSecret }, 'Invalid shared secret received'); + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + // Log the entire webhook payload to see the data structure + logger.info({ + payload: body, + headers: Object.fromEntries(req.headers.entries()), + }, 'Received Formstack webhook'); + + // Enqueue a bridge-worker task to process this form submission + const worker = await getWorkerUtils(); + await worker.addJob('formstack/create-ticket-from-form', { + formData: body, + receivedAt: new Date().toISOString(), + }); + + logger.info('Formstack webhook task enqueued successfully'); + + return NextResponse.json({ + status: "success", + message: "Webhook received and queued for processing" + }); + + } catch (error) { + logger.error({ + error: error instanceof Error ? error.message : error, + stack: error instanceof Error ? error.stack : undefined, + }, 'Error processing Formstack webhook'); + + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/apps/link/middleware.ts b/apps/link/middleware.ts index 33117af..d8a6a78 100644 --- a/apps/link/middleware.ts +++ b/apps/link/middleware.ts @@ -130,6 +130,6 @@ export default withAuth(checkRewrites, { export const config = { matcher: [ - "/((?!ws|wss|api/signal|api/whatsapp|_next/static|_next/image|favicon.ico).*)", + "/((?!ws|wss|api/signal|api/whatsapp|api/formstack|_next/static|_next/image|favicon.ico).*)", ], }