Integration first pass
This commit is contained in:
parent
11563a794e
commit
320b9c1b38
3 changed files with 215 additions and 1 deletions
150
apps/bridge-worker/tasks/formstack/create-ticket-from-form.ts
Normal file
150
apps/bridge-worker/tasks/formstack/create-ticket-from-form.ts
Normal file
|
|
@ -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<void> => {
|
||||||
|
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 = `<h3>Contact Information</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Name:</strong> ${fullName}</li>
|
||||||
|
${email_address ? `<li><strong>Email:</strong> ${email_address}</li>` : ''}
|
||||||
|
${phone_number ? `<li><strong>Phone:</strong> ${phone_number}</li>` : ''}
|
||||||
|
${signal_number ? `<li><strong>Signal Number:</strong> ${signal_number}</li>` : ''}
|
||||||
|
${addressString ? `<li><strong>Address:</strong> ${addressString}</li>` : ''}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Request Details</h3>
|
||||||
|
<ul>
|
||||||
|
${type_of_help_requested ? `<li><strong>Type of Help:</strong> ${type_of_help_requested}</li>` : ''}
|
||||||
|
${type_of_organization ? `<li><strong>Organization Type:</strong> ${type_of_organization}</li>` : ''}
|
||||||
|
${urgency_level ? `<li><strong>Urgency Level:</strong> ${urgency_level}</li>` : ''}
|
||||||
|
${preferred_contact_method ? `<li><strong>Preferred Contact Method:</strong> ${preferred_contact_method}</li>` : ''}
|
||||||
|
${available_times_for_contact ? `<li><strong>Available Times:</strong> ${available_times_for_contact}</li>` : ''}
|
||||||
|
${preferred_language ? `<li><strong>Preferred Language:</strong> ${preferred_language}</li>` : ''}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
${description_of_issue ? `<h3>Description of Issue</h3><p>${description_of_issue}</p>` : ''}
|
||||||
|
|
||||||
|
${how_did_you_hear_about_us ? `<p><strong>How they heard about us:</strong> ${how_did_you_hear_about_us}</p>` : ''}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p><em>Form ID: ${FormID} | Submission ID: ${UniqueID} | Received: ${receivedAt}</em></p>`;
|
||||||
|
|
||||||
|
// 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;
|
||||||
64
apps/link/app/api/formstack/route.ts
Normal file
64
apps/link/app/api/formstack/route.ts
Normal file
|
|
@ -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<NextResponse> {
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -130,6 +130,6 @@ export default withAuth(checkRewrites, {
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
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).*)",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue