import express from "express"; import multer from "multer"; import cors from "cors"; import pdf from "pdf-parse"; import OpenAI from "openai"; import PDFDocument from "pdfkit"; process.on("uncaughtException", (err) => console.error("UNCAUGHT EXCEPTION:", err)); process.on("unhandledRejection", (err) => console.error("UNHANDLED REJECTION:", err)); const app = express(); app.use(cors({ origin: "*" })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 15 * 1024 * 1024 }, }); const PORT = process.env.PORT || 3000; app.get("/", (req, res) => res.send("chargeback-health-check-api up")); app.get("/health", (req, res) => res.send("ok")); const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); function normalizeJsonText(text) { let out = (text || "").trim(); out = out.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim(); const start = out.indexOf("{"); const end = out.lastIndexOf("}"); if (start !== -1 && end !== -1 && end > start) out = out.slice(start, end + 1); return out.trim(); } function formatMoney(n) { if (n == null || Number.isNaN(Number(n))) return "Not found"; return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(Number(n)); } function formatPct(n) { if (n == null || Number.isNaN(Number(n))) return "Not available"; return (Number(n) * 100).toFixed(2) + "%"; } function parseMoneyLoose(v) { if (v == null) return null; const n = Number(String(v).replace(/[$,()]/g, "").replace(/\s+/g, "").trim()); return Number.isFinite(n) ? n : null; } function monthNameToLabel(text) { if (!text) return "Unknown Period"; const m = text.match(/\b(January|February|March|April|May|June|July|August|September|October|November|December)\b[\s,/-]*(\d{4})/i); if (m) return `${m[1]} ${m[2]}`; const m2 = text.match(/\b(0?[1-9]|1[0-2])[\/-](\d{4})\b/); if (m2) return `${m2[1]}/${m2[2]}`; return "Unknown Period"; } function buildRiskAssessment(avgRatio, totalCount) { let risk_score = 95; let risk_label = "Low Risk"; const findings = []; const recommendations = []; if (avgRatio == null) { risk_score = 50; risk_label = "Needs Review"; findings.push("Unable to confidently calculate chargeback ratio from the uploaded statements."); recommendations.push("Upload statements that clearly show sales volume and chargeback totals."); return { risk_score, risk_label, findings, recommendations }; } if (avgRatio >= 0.01) { risk_score = 25; risk_label = "High Risk"; findings.push("Chargeback ratio is at or above 1.00%, which is generally considered elevated."); recommendations.push("Immediate review of refund policy, descriptor clarity, and chargeback prevention tools is recommended."); } else if (avgRatio >= 0.0075) { risk_score = 45; risk_label = "Elevated Risk"; findings.push("Chargeback ratio is elevated and may increase processor scrutiny."); recommendations.push("Review sources of disputes and tighten order verification and customer service workflows."); } else if (avgRatio >= 0.005) { risk_score = 65; risk_label = "Moderate Risk"; findings.push("Chargeback ratio is worth monitoring over the uploaded period."); recommendations.push("Track month-over-month dispute trends and consider early alert products."); } else { risk_score = 85; risk_label = "Low Risk"; findings.push("Chargeback ratio appears to be within a healthier range."); recommendations.push("Continue monitoring trends and maintain current prevention practices."); } if (totalCount >= 20) { risk_score -= 10; findings.push("Total chargeback count over the uploaded period is high."); recommendations.push("Investigate recurring root causes by product, channel, or fulfillment issue."); } else if (totalCount >= 10) { risk_score -= 5; findings.push("Chargeback count is meaningful enough to review for concentration patterns."); } risk_score = Math.max(0, Math.min(100, risk_score)); return { risk_score, risk_label, findings: findings.slice(0, 4), recommendations: recommendations.slice(0, 4) }; } async function extractStatementData(text) { const instruction = 'Extract sales and chargeback data from this merchant statement. Return ONLY valid JSON with exactly these keys: ' + '{"statement_month": string|null, "total_volume": number|null, "chargeback_count": number|null, "chargeback_amount": number|null}. ' + "Definitions: total_volume is total sales/card sales volume for the statement period. chargeback_count is the number of chargebacks for the period. chargeback_amount is total dollar value of chargebacks for the period. If not found, use null. Return JSON only."; const input = instruction + "\n\nSTATEMENT TEXT:\n" + text.slice(0, 200000); const resp = await openai.responses.create({ model: "gpt-4o-mini", input }); const out = normalizeJsonText(resp.output_text || ""); try { return JSON.parse(out); } catch { return {}; } } function buildSummaryPdfBuffer(result) { return new Promise((resolve, reject) => { try { const doc = new PDFDocument({ size: "LETTER", margin: 50 }); const chunks = []; doc.on("data", (c) => chunks.push(c)); doc.on("end", () => resolve(Buffer.concat(chunks))); const pageW = doc.page.width; const left = doc.page.margins.left; const right = doc.page.width - doc.page.margins.right; doc.rect(0, 0, pageW, 130).fill("#113A6B"); doc .fillColor("#FFFFFF") .font("Helvetica-Bold") .fontSize(22) .text("Chargeback Health Check", left, 28, { width: right - left, align: "center" }); doc .fillColor("#DBEAFE") .font("Helvetica-Bold") .fontSize(14) .text(result.business_name || "Business Name Not Provided", left, 82, { width: right - left, align: "center" }); doc.y = 155; doc.fillColor("#111827").font("Helvetica-Bold").fontSize(20).text(`Risk Score: ${result.risk_score ?? "N/A"} / 100`); doc.moveDown(0.2); doc.fillColor("#6B7280").font("Helvetica").fontSize(12).text(`Risk Label: ${result.risk_label || "N/A"}`); doc.moveDown(1); doc.fillColor("#111827").font("Helvetica-Bold").fontSize(15).text("3-Month Summary"); doc.moveDown(0.4); doc.fillColor("#111827").font("Helvetica").fontSize(12); doc.text(`Business Name: ${result.business_name || "Not provided"}`); doc.text(`3-Month Total Sales: ${formatMoney(result.three_month_total_volume)}`); doc.text(`3-Month Chargeback Count: ${result.three_month_chargeback_count ?? 0}`); doc.text(`3-Month Chargeback Amount: ${formatMoney(result.three_month_chargeback_amount)}`); doc.text(`3-Month Chargeback Ratio: ${formatPct(result.three_month_chargeback_ratio)}`); doc.moveDown(0.8); doc.fillColor("#111827").font("Helvetica-Bold").fontSize(15).text("Monthly Breakdown"); doc.moveDown(0.4); doc.fillColor("#111827").font("Helvetica").fontSize(12); (result.monthly_summaries || []).forEach((m, idx) => { doc.text( `${m.statement_month || `Statement ${idx + 1}`}: Sales ${formatMoney(m.total_volume)} | Chargebacks ${m.chargeback_count ?? 0} | Amount ${formatMoney(m.chargeback_amount)} | Ratio ${formatPct(m.chargeback_ratio_by_volume)}` ); }); doc.moveDown(0.8); doc.fillColor("#111827").font("Helvetica-Bold").fontSize(15).text("Findings"); doc.moveDown(0.4); doc.fillColor("#111827").font("Helvetica").fontSize(12); (result.findings || []).forEach((f) => doc.text(`• ${f}`)); if (!(result.findings || []).length) doc.text("No findings available."); doc.moveDown(0.8); doc.fillColor("#111827").font("Helvetica-Bold").fontSize(15).text("Recommendations"); doc.moveDown(0.4); doc.fillColor("#111827").font("Helvetica").fontSize(12); (result.recommendations || []).forEach((r) => doc.text(`• ${r}`)); if (!(result.recommendations || []).length) doc.text("No recommendations available."); doc.end(); } catch (e) { reject(e); } }); } async function sendViaResend({ to, subject, text, html, pdfBuffer }) { if (!process.env.RESEND_API_KEY || !process.env.EMAIL_FROM) { throw new Error("Missing RESEND_API_KEY or EMAIL_FROM"); } const payload = { from: process.env.EMAIL_FROM, to: [to], bcc: process.env.EMAIL_BCC ? [process.env.EMAIL_BCC] : undefined, subject, text, html, attachments: pdfBuffer ? [ { filename: "chargeback-health-check.pdf", content: pdfBuffer.toString("base64"), }, ] : [], }; const resp = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); const data = await resp.json().catch(() => ({})); if (!resp.ok) throw new Error(`Resend error: ${resp.status} ${JSON.stringify(data)}`); return data; } app.post("/analyze-chargebacks", upload.array("statements", 3), async (req, res) => { try { const files = req.files || []; if (!files.length) { return res.status(400).json({ error: "No statements uploaded." }); } if (!process.env.OPENAI_API_KEY) { return res.status(500).json({ error: "Missing OPENAI_API_KEY on server." }); } const business_name = (req.body?.business_name || "").trim(); const emailed_to = (req.body?.toEmail || req.body?.email || "").trim(); const monthly_summaries = []; for (const file of files) { const parsed = await pdf(file.buffer); const text = (parsed.text || "").trim(); if (!text) { throw new Error("One of the PDFs appears scanned/no text."); } const aiData = await extractStatementData(text); const total_volume = aiData.total_volume != null ? Number(aiData.total_volume) : null; const chargeback_count = aiData.chargeback_count != null ? Number(aiData.chargeback_count) : 0; const chargeback_amount = aiData.chargeback_amount != null ? Number(aiData.chargeback_amount) : 0; monthly_summaries.push({ statement_month: aiData.statement_month || monthNameToLabel(text), total_volume, chargeback_count, chargeback_amount: Number((chargeback_amount || 0).toFixed(2)), chargeback_ratio_by_volume: total_volume != null && total_volume > 0 ? Number(((chargeback_amount || 0) / total_volume).toFixed(6)) : null }); } const three_month_total_volume = monthly_summaries.reduce((s, m) => s + Number(m.total_volume || 0), 0); const three_month_chargeback_count = monthly_summaries.reduce((s, m) => s + Number(m.chargeback_count || 0), 0); const three_month_chargeback_amount = monthly_summaries.reduce((s, m) => s + Number(m.chargeback_amount || 0), 0); const three_month_chargeback_ratio = three_month_total_volume > 0 ? Number((three_month_chargeback_amount / three_month_total_volume).toFixed(6)) : null; const risk = buildRiskAssessment(three_month_chargeback_ratio, three_month_chargeback_count); const result = { business_name, emailed_to: emailed_to || null, monthly_summaries, three_month_total_volume: Number(three_month_total_volume.toFixed(2)), three_month_chargeback_count, three_month_chargeback_amount: Number(three_month_chargeback_amount.toFixed(2)), three_month_chargeback_ratio, avg_monthly_chargeback_ratio: three_month_chargeback_ratio, risk_score: risk.risk_score, risk_label: risk.risk_label, findings: risk.findings, recommendations: risk.recommendations }; if (emailed_to && process.env.RESEND_API_KEY && process.env.EMAIL_FROM) { try { const pdfBuffer = await buildSummaryPdfBuffer(result); const html = `
CHARGEBACK HEALTH CHECK
${result.business_name || "Business Name Not Provided"}
${result.risk_score ?? "N/A"} / 100
${result.risk_label || "N/A"}
3-Month Total Sales: ${formatMoney(result.three_month_total_volume)}
3-Month Chargeback Count: ${result.three_month_chargeback_count ?? 0}
3-Month Chargeback Amount: ${formatMoney(result.three_month_chargeback_amount)}
3-Month Chargeback Ratio: ${formatPct(result.three_month_chargeback_ratio)}
`; const text = [ `Business Name: ${result.business_name || "Not provided"}`, `Risk Score: ${result.risk_score ?? "N/A"} / 100`, `Risk Label: ${result.risk_label || "N/A"}`, `3-Month Total Sales: ${formatMoney(result.three_month_total_volume)}`, `3-Month Chargeback Count: ${result.three_month_chargeback_count ?? 0}`, `3-Month Chargeback Amount: ${formatMoney(result.three_month_chargeback_amount)}`, `3-Month Chargeback Ratio: ${formatPct(result.three_month_chargeback_ratio)}` ].join("\n"); await sendViaResend({ to: emailed_to, subject: "Your Chargeback Health Check", text, html, pdfBuffer }); } catch (err) { console.error("EMAIL FAILED:", err); } } return res.status(200).json(result); } catch (e) { console.error("Chargeback analyzer crash:", e); return res.status(500).json({ error: "Server error", message: e?.message || String(e), }); } }); app.listen(PORT, () => console.log("API listening on port", PORT));