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
149
api/src/routes/quotes.ts
Normal file
149
api/src/routes/quotes.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { Router } from "express";
|
||||
import { pool } from "../db.js";
|
||||
import { submitLimiter } from "../middleware/rate-limit.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { createResource, createServiceOrder } from "../erpnext-client.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// POST /api/v1/quotes — Request a custom quote for a service
|
||||
router.post("/api/v1/quotes", submitLimiter, async (req, res) => {
|
||||
try {
|
||||
const { name, email, company, phone, service_slug, details } = req.body ?? {};
|
||||
|
||||
if (!name || typeof name !== "string" || name.trim().length < 2) {
|
||||
res.status(400).json({ error: "Name is required (at least 2 characters)." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email || typeof email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
res.status(400).json({ error: "Valid email address is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!service_slug || typeof service_slug !== "string") {
|
||||
res.status(400).json({ error: "Service selection is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO quotes (name, email, company, phone, service_slug, details, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'pending')
|
||||
RETURNING id`,
|
||||
[
|
||||
name.trim(),
|
||||
email.toLowerCase().trim(),
|
||||
company || null,
|
||||
phone || null,
|
||||
service_slug.trim(),
|
||||
details || null,
|
||||
],
|
||||
);
|
||||
|
||||
const quoteId = result.rows[0]?.id;
|
||||
|
||||
// Push to ERPNext as an Opportunity — non-blocking, don't fail the response
|
||||
try {
|
||||
await createResource("Opportunity", {
|
||||
opportunity_from: "Lead",
|
||||
party_name: email.toLowerCase().trim(),
|
||||
contact_email: email.toLowerCase().trim(),
|
||||
opportunity_type: "Sales",
|
||||
custom_service_slug: service_slug.trim(),
|
||||
notes: [
|
||||
`Name: ${name.trim()}`,
|
||||
company ? `Company: ${company}` : null,
|
||||
phone ? `Phone: ${phone}` : null,
|
||||
details ? `Details: ${details}` : null,
|
||||
`Source: performancewest.net quote form`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
});
|
||||
} catch (erpErr) {
|
||||
console.error("[quotes] ERPNext Opportunity creation failed (non-fatal):", erpErr);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "Quote request received. We'll send your quote within one business day.",
|
||||
quote_id: quoteId ? String(quoteId) : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[quotes] Error:", err);
|
||||
res.status(500).json({ error: "Could not submit your request. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v1/orders — Place a fixed-price service order
|
||||
router.post("/api/v1/orders", submitLimiter, async (req, res) => {
|
||||
try {
|
||||
const { name, email, company, service_slug, amount_cents, quote_id } = req.body ?? {};
|
||||
|
||||
if (!name || typeof name !== "string" || name.trim().length < 2) {
|
||||
res.status(400).json({ error: "Name is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email || typeof email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
res.status(400).json({ error: "Valid email address is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!service_slug || typeof service_slug !== "string") {
|
||||
res.status(400).json({ error: "Service selection is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate order number: PW-YYYY-XXXX
|
||||
const year = new Date().getFullYear();
|
||||
const short = uuidv4().split("-")[0]!.toUpperCase();
|
||||
const orderNumber = `PW-${year}-${short}`;
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO orders (order_number, quote_id, service_slug, name, email, company, status, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'received', $7)
|
||||
RETURNING id, order_number`,
|
||||
[
|
||||
orderNumber,
|
||||
quote_id || null,
|
||||
service_slug.trim(),
|
||||
name.trim(),
|
||||
email.toLowerCase().trim(),
|
||||
company || null,
|
||||
amount_cents || null,
|
||||
],
|
||||
);
|
||||
|
||||
const row = result.rows[0];
|
||||
|
||||
// Push to ERPNext as a Sales Order — non-blocking, don't fail the response
|
||||
try {
|
||||
await createServiceOrder({
|
||||
customer: name.trim(),
|
||||
service_slug: service_slug.trim(),
|
||||
notes: [
|
||||
`Order: ${orderNumber}`,
|
||||
company ? `Company: ${company}` : null,
|
||||
quote_id ? `Quote ref: ${quote_id}` : null,
|
||||
`Source: performancewest.net`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
});
|
||||
} catch (erpErr) {
|
||||
console.error("[orders] ERPNext Sales Order creation failed (non-fatal):", erpErr);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "Order received. We'll begin processing within one business day.",
|
||||
order_number: row?.order_number,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[orders] Error:", err);
|
||||
res.status(500).json({ error: "Could not place your order. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Loading…
Add table
Add a link
Reference in a new issue