new-site/api/src/routes/refunds.ts
justin f8cd37ac8c 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>
2026-04-27 06:54:22 -05:00

319 lines
12 KiB
TypeScript

/**
* Refund management routes.
*
* Handles refunds for formation orders and compliance service orders
* when the failure was not the customer's fault.
*
* Refund flow:
* 1. System or admin initiates refund (POST /api/v1/admin/refunds)
* 2. Admin reviews + approves (PATCH /api/v1/admin/refunds/:id)
* 3. Admin sends refund via Relay dashboard (manual ACH)
* 4. Admin marks as sent/confirmed
* 5. Customer receives email notification at each step
*
* Auto-refund triggers (created by the formation worker):
* - State portal charged the card but rejected the filing
* - Payment went through but automation crashed before filing completed
* - Name collision discovered after payment was processed
*/
import { Router } from "express";
import { pool } from "../db.js";
import { requireAdmin } from "../middleware/admin-auth.js";
import { v4 as uuidv4 } from "uuid";
const router = Router();
// =====================================================================
// Admin: Create a refund
// =====================================================================
router.post("/api/v1/admin/refunds", requireAdmin, async (req, res) => {
try {
const {
order_type, order_id, order_number,
customer_name, customer_email,
original_amount_cents, refund_amount_cents, refund_type,
reason_category, reason_detail,
state_fee_recoverable, refund_method,
admin_notes,
} = req.body ?? {};
if (!order_number || !customer_email || !refund_amount_cents || !reason_category) {
res.status(400).json({ error: "Missing required fields: order_number, customer_email, refund_amount_cents, reason_category" });
return;
}
const year = new Date().getFullYear();
const short = uuidv4().split("-")[0]!.toUpperCase();
const refundNumber = `REF-${year}-${short}`;
const result = await pool.query(
`INSERT INTO refunds (
refund_number, order_type, order_id, order_number,
customer_name, customer_email,
original_amount_cents, refund_amount_cents, refund_type,
reason_category, reason_detail,
state_fee_recoverable, refund_method,
admin_notes, requested_by, status
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,'pending')
RETURNING id, refund_number`,
[
refundNumber,
order_type || "formation",
order_id || null,
order_number,
customer_name || "",
customer_email,
original_amount_cents || 0,
refund_amount_cents,
refund_type || "full",
reason_category,
reason_detail || null,
state_fee_recoverable || false,
refund_method || "relay_ach",
admin_notes || null,
`admin:${req.admin!.username}`,
],
);
const refund = result.rows[0];
// Link refund to the order
if (order_id) {
const table = order_type === "formation" ? "formation_orders" : "orders";
await pool.query(
`UPDATE ${table} SET refund_id = $1, refunded = FALSE WHERE id = $2`,
[refund.id, order_id],
);
}
// Log to audit
if (order_id) {
await pool.query(
`INSERT INTO order_audit_log (order_type, order_id, order_number, action, actor_type, actor_id, actor_name, note)
VALUES ($1, $2, $3, 'refund_initiated', 'admin', $4, $5, $6)`,
[order_type || "formation", order_id, order_number, req.admin!.id, req.admin!.username,
`Refund ${refundNumber}: $${(refund_amount_cents / 100).toFixed(2)}${reason_category}`],
);
}
res.status(201).json({
success: true,
refund_number: refund.refund_number,
refund_id: refund.id,
message: "Refund created. Review and approve to process.",
});
} catch (err) {
console.error("[refunds] Create error:", err);
res.status(500).json({ error: "Could not create refund." });
}
});
// =====================================================================
// Admin: List refunds
// =====================================================================
router.get("/api/v1/admin/refunds", requireAdmin, async (req, res) => {
try {
const status = req.query.status as string || "";
const limit = Math.min(parseInt(req.query.limit as string, 10) || 50, 200);
const offset = parseInt(req.query.offset as string, 10) || 0;
let where = "WHERE 1=1";
const params: any[] = [];
let idx = 1;
if (status) { where += ` AND status = $${idx++}`; params.push(status); }
const countResult = await pool.query(`SELECT COUNT(*) as total FROM refunds ${where}`, params);
params.push(limit, offset);
const result = await pool.query(
`SELECT * FROM refunds ${where} ORDER BY created_at DESC LIMIT $${idx++} OFFSET $${idx++}`,
params,
);
res.json({
refunds: result.rows,
total: parseInt(countResult.rows[0].total, 10),
limit, offset,
});
} catch (err) {
console.error("[refunds] List error:", err);
res.status(500).json({ error: "Could not load refunds." });
}
});
// =====================================================================
// Admin: Get single refund
// =====================================================================
router.get("/api/v1/admin/refunds/:id", requireAdmin, async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const result = await pool.query("SELECT * FROM refunds WHERE id = $1", [id]);
if (result.rows.length === 0) {
res.status(404).json({ error: "Refund not found." });
return;
}
res.json({ refund: result.rows[0] });
} catch (err) {
console.error("[refunds] Get error:", err);
res.status(500).json({ error: "Could not load refund." });
}
});
// =====================================================================
// Admin: Update refund status (approve, send, confirm, deny)
// =====================================================================
router.patch("/api/v1/admin/refunds/:id", requireAdmin, async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const { status, admin_notes, relay_transaction_id, denied_reason, refund_method } = req.body ?? {};
const current = await pool.query("SELECT * FROM refunds WHERE id = $1", [id]);
if (current.rows.length === 0) {
res.status(404).json({ error: "Refund not found." });
return;
}
const refund = current.rows[0];
const updates: string[] = [];
const params: any[] = [];
let pIdx = 1;
if (status) {
updates.push(`status = $${pIdx++}`); params.push(status);
if (status === "approved") {
updates.push(`approved_by = $${pIdx++}`); params.push(req.admin!.id);
updates.push("approved_at = now()");
}
if (status === "sent") {
updates.push("sent_at = now()");
}
if (status === "confirmed") {
updates.push("confirmed_at = now()");
// Mark the order as refunded
if (refund.order_id) {
const table = refund.order_type === "formation" ? "formation_orders" : "orders";
await pool.query(`UPDATE ${table} SET refunded = TRUE WHERE id = $1`, [refund.order_id]);
}
}
if (status === "denied") {
updates.push(`denied_reason = $${pIdx++}`); params.push(denied_reason || "Refund denied");
}
}
if (admin_notes !== undefined) { updates.push(`admin_notes = $${pIdx++}`); params.push(admin_notes); }
if (relay_transaction_id) { updates.push(`relay_transaction_id = $${pIdx++}`); params.push(relay_transaction_id); }
if (refund_method) { updates.push(`refund_method = $${pIdx++}`); params.push(refund_method); }
if (updates.length > 0) {
updates.push("updated_at = now()");
params.push(id);
await pool.query(
`UPDATE refunds SET ${updates.join(", ")} WHERE id = $${pIdx}`,
params,
);
}
// Audit log
if (status && refund.order_id) {
await pool.query(
`INSERT INTO order_audit_log (order_type, order_id, order_number, action, from_status, to_status, actor_type, actor_id, actor_name, note)
VALUES ($1, $2, $3, $4, $5, $6, 'admin', $7, $8, $9)`,
[refund.order_type, refund.order_id, refund.order_number,
`refund_${status}`, refund.status, status,
req.admin!.id, req.admin!.username,
`Refund ${refund.refund_number}: status → ${status}${admin_notes ? '. ' + admin_notes : ''}`],
);
}
res.json({ success: true, message: `Refund status updated to: ${status || 'updated'}` });
} catch (err) {
console.error("[refunds] Update error:", err);
res.status(500).json({ error: "Could not update refund." });
}
});
// =====================================================================
// Admin: Dashboard stats for refunds
// =====================================================================
router.get("/api/v1/admin/refunds/stats", requireAdmin, async (req, res) => {
try {
const result = await pool.query(`
SELECT
COUNT(*) FILTER (WHERE status = 'pending') as pending,
COUNT(*) FILTER (WHERE status = 'approved') as approved,
COUNT(*) FILTER (WHERE status = 'processing') as processing,
COUNT(*) FILTER (WHERE status = 'sent') as sent,
COUNT(*) FILTER (WHERE status = 'confirmed') as confirmed,
COUNT(*) FILTER (WHERE status = 'denied') as denied,
COUNT(*) as total,
COALESCE(SUM(refund_amount_cents) FILTER (WHERE status IN ('sent','confirmed')), 0) as total_refunded_cents,
COALESCE(SUM(refund_amount_cents) FILTER (WHERE status = 'pending'), 0) as pending_refund_cents
FROM refunds
`);
res.json({ stats: result.rows[0] });
} catch (err) {
console.error("[refunds] Stats error:", err);
res.status(500).json({ error: "Could not load refund stats." });
}
});
// =====================================================================
// System: Auto-create refund (called by formation worker on failure)
// =====================================================================
router.post("/api/v1/internal/refunds/auto", async (req, res) => {
// This endpoint is called internally by the worker when a state charges
// the card but the filing fails. No admin auth — uses internal secret.
const secret = req.headers["x-internal-secret"];
if (!secret || secret !== process.env.WEBHOOK_SECRET) {
res.status(401).json({ error: "Unauthorized" });
return;
}
try {
const {
order_type, order_id, order_number,
customer_name, customer_email,
amount_cents, reason_category, reason_detail,
} = req.body ?? {};
const year = new Date().getFullYear();
const short = uuidv4().split("-")[0]!.toUpperCase();
const refundNumber = `REF-${year}-${short}`;
const result = await pool.query(
`INSERT INTO refunds (
refund_number, order_type, order_id, order_number,
customer_name, customer_email,
original_amount_cents, refund_amount_cents, refund_type,
reason_category, reason_detail,
requested_by, status
) VALUES ($1,$2,$3,$4,$5,$6,$7,$7,'full',$8,$9,'system','pending')
RETURNING id, refund_number`,
[refundNumber, order_type || "formation", order_id, order_number,
customer_name, customer_email, amount_cents,
reason_category, reason_detail],
);
// Create ERPNext issue to alert admin
console.log(`[refunds] Auto-refund created: ${result.rows[0].refund_number} for ${order_number}$${(amount_cents / 100).toFixed(2)}`);
res.status(201).json({
success: true,
refund_number: result.rows[0].refund_number,
});
} catch (err) {
console.error("[refunds] Auto-create error:", err);
res.status(500).json({ error: "Could not create auto-refund." });
}
});
export default router;