Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
100
api/src/routes/tickets.ts
Normal file
100
api/src/routes/tickets.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue