/** * 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;