import { Router } from "express"; import { pool } from "../db.js"; import { submitLimiter } from "../middleware/rate-limit.js"; import { createIssue } from "../erpnext-client.js"; const router = Router(); const VALID_CATEGORIES = ["question", "support", "issue", "service_request", "quote"] as const; // POST /api/v1/tickets router.post("/api/v1/tickets", submitLimiter, async (req, res) => { try { const { category, subject, message, email, name, page } = req.body ?? {}; // Validate category if (!category || !VALID_CATEGORIES.includes(category)) { res.status(400).json({ error: `Category must be one of: ${VALID_CATEGORIES.join(", ")}`, }); return; } // Validate subject if (!subject || typeof subject !== "string" || subject.trim().length < 3) { res.status(400).json({ error: "Subject must be at least 3 characters." }); return; } if (subject.length > 200) { res.status(400).json({ error: "Subject must be under 200 characters." }); return; } // Validate message if (!message || typeof message !== "string" || message.trim().length < 10) { res.status(400).json({ error: "Message must be at least 10 characters." }); return; } if (message.length > 5000) { res.status(400).json({ error: "Message must be under 5000 characters." }); return; } // Optional email validation if (email && typeof email === "string" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { res.status(400).json({ error: "Invalid email format." }); return; } const ip = (req as any).clientIp || req.ip || ""; // Store locally in PostgreSQL (backup) const result = await pool.query( `INSERT INTO tickets (category, subject, message, email, name, page, ip_address) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, [category, subject.trim(), message.trim(), email || null, name || null, page || null, ip], ); const ticketId = result.rows[0]?.id; // Push to ERPNext as an Issue — non-blocking, don't fail the response let erpnextIssueName: string | undefined; try { const description = [ message.trim(), "", "---", `Category: ${category}`, name ? `Name: ${name}` : null, email ? `Email: ${email}` : null, page ? `Page: ${page}` : null, `IP: ${ip}`, `Source: performancewest.net support widget`, ] .filter(Boolean) .join("\n"); const erpIssue = await createIssue({ subject: subject.trim(), description, priority: "Medium", }); erpnextIssueName = (erpIssue as any)?.name; } catch (erpErr) { console.error("[tickets] ERPNext createIssue failed (non-fatal):", erpErr); } res.status(201).json({ success: true, message: "Request received. We'll get back to you within one business day.", ticket_id: erpnextIssueName || (ticketId ? String(ticketId) : undefined), }); } catch (err) { console.error("[tickets] Error:", err); res.status(500).json({ error: "Could not submit your request. Please try again." }); } }); export default router;