NP@naman-parashar-s-team / Refund alertsPublic
Refund alerts
A JS snippet on QuikRun, callable at quik.run/r/refund-alerts.
JS· node:20· 1 runs· 0 forks· updated 13h ago
Refund alerts.js
// refund-alerts — a Stripe-style webhook receiver on QuikRun.// POST a Stripe "charge.refunded" event and get back a clean, structured alert// (money formatted, who, why, when) plus a Slack-ready message preview. GET// renders a live status page with the expected shape and a rendered example.// No external fetch required.const BRAND = {bg: "#0b0b0c",surface: "#111214",surfaceHi: "#0d0e10",border: "#23252a",text: "#eaeaea",muted: "#6b6f76",accent: "#E6FF55",};// Stripe amounts are in the currency's minor unit. Zero-decimal currencies (JPY,// KRW, etc.) are not multiplied by 100, so format them without cents.const ZERO_DECIMAL = new Set(["bif", "clp", "djf", "gnf", "jpy", "kmf", "krw", "mga","pyg", "rwf", "ugx", "vnd", "vuv", "xaf", "xof", "xpf",]);function formatMoney(amountMinor, currency) {const code = String(currency || "usd").toLowerCase();const cur = code.toUpperCase();const raw = Number(amountMinor);if (!Number.isFinite(raw)) return `${cur} n/a`;const value = ZERO_DECIMAL.has(code) ? raw : raw / 100;try {return new Intl.NumberFormat("en-US", {style: "currency",currency: cur,}).format(value);} catch {const fixed = ZERO_DECIMAL.has(code) ? value.toFixed(0) : value.toFixed(2);return `${fixed} ${cur}`;}}// Stripe uses machine reasons like "requested_by_customer"; make them readable.function humanReason(reason) {if (!reason) return "No reason provided";return String(reason).replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());}function formatWhen(unixSeconds) {const n = Number(unixSeconds);const ms = Number.isFinite(n) && n > 0 ? n * 1000 : Date.now();const d = new Date(ms);try {return (new Intl.DateTimeFormat("en-US", {dateStyle: "medium",timeStyle: "short",timeZone: "UTC",}).format(d) + " UTC");} catch {return d.toISOString();}}// Escape any value that lands inside HTML markup.function esc(v) {return String(v == null ? "" : v).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");}// Turn a raw Stripe-style event into the normalized alert we emit + would Slack.function buildAlert(event) {const obj = (event && event.data && event.data.object) || {};const type = (event && event.type) || "charge.refunded";const amount =obj.amount_refunded != null ? obj.amount_refunded : obj.amount;const money = formatMoney(amount, obj.currency);const reason =(obj.refunds &&obj.refunds.data &&obj.refunds.data[0] &&obj.refunds.data[0].reason) ||obj.reason;return {kind: "refund",event: type,headline: `Refund issued: ${money}`,amount: money,amount_minor: amount == null ? null : Number(amount),currency: String(obj.currency || "usd").toUpperCase(),customer:obj.customer ||(obj.billing_details && obj.billing_details.email) ||obj.receipt_email ||"unknown",charge_id: obj.id || (event && event.id) || "unknown",reason: humanReason(reason),when: formatWhen(obj.created || (event && event.created)),};}// ---- Example payload shown on the GET status page (and used for the preview) ----const EXAMPLE_EVENT = {id: "evt_1P9x2c2eZvKYlo2C",type: "charge.refunded",created: 1721390400,data: {object: {id: "ch_3P9x2c2eZvKYlo2C1a2b3c4d",amount: 4200,amount_refunded: 4200,currency: "usd",customer: "cus_Q7pLmN4kXyZ",created: 1721390400,refunds: { data: [{ reason: "requested_by_customer" }] },},},};const EXPECTED_SHAPE = `POST / content-type: application/json{"type": "charge.refunded","data": {"object": {"id": "ch_3P9x2c...","amount_refunded": 4200,"currency": "usd","customer": "cus_Q7pLmN4kXyZ","created": 1721390400,"refunds": { "data": [{ "reason": "requested_by_customer" }] }}}}`;function metaCell(label, value, accent) {return `<div class="cell"><div class="cell-label">${esc(label)}</div><div class="cell-value${accent ? " accent" : ""}">${esc(value)}</div></div>`;}function renderStatusPage(alert, hasSlack) {const cells = [metaCell("Amount refunded", alert.amount, true),metaCell("Customer", alert.customer),metaCell("Reason", alert.reason),metaCell("Charge", alert.charge_id),metaCell("When", alert.when),metaCell("Event", alert.event),].join("");const slackNote = hasSlack? "env.SLACK_WEBHOOK is set. Real events would be posted to your channel.": "Set env.SLACK_WEBHOOK to fan real refunds out to Slack. Nothing is hardcoded.";const slackMessage = `:money_with_wings: ${alert.headline} for ${alert.customer} (${alert.reason}) at ${alert.when}. Charge ${alert.charge_id}.`;return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>refund-alerts · webhook endpoint</title><style>:root {--bg: ${BRAND.bg};--surface: ${BRAND.surface};--surface-hi: ${BRAND.surfaceHi};--border: ${BRAND.border};--text: ${BRAND.text};--muted: ${BRAND.muted};--accent: ${BRAND.accent};--mono: ui-monospace, "SF Mono", Menlo, monospace;--sans: system-ui, -apple-system, "Segoe UI", sans-serif;}* { box-sizing: border-box; }html { -webkit-text-size-adjust: 100%; }body {margin: 0;background:radial-gradient(1100px 520px at 82% -18%, rgba(230,255,85,0.12), transparent 58%),radial-gradient(760px 420px at 0% 108%, rgba(230,255,85,0.04), transparent 60%),var(--bg);color: var(--text);font-family: var(--sans);-webkit-font-smoothing: antialiased;text-rendering: optimizeLegibility;padding: 64px 20px 96px;}.wrap { max-width: 760px; margin: 0 auto; }.top {display: flex; align-items: center; justify-content: space-between;gap: 16px; flex-wrap: wrap; margin-bottom: 30px;}.live {display: inline-flex; align-items: center; gap: 8px;padding: 6px 12px 6px 11px; border-radius: 999px;border: 1px solid rgba(230,255,85,0.28);background: rgba(230,255,85,0.06);font: 500 11px/1 var(--mono);letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent);}.dot {width: 7px; height: 7px; border-radius: 999px; background: var(--accent);box-shadow: 0 0 0 0 rgba(230,255,85,0.55); animation: pulse 2.2s infinite;}@keyframes pulse {0% { box-shadow: 0 0 0 0 rgba(230,255,85,0.5); }70% { box-shadow: 0 0 0 8px rgba(230,255,85,0); }100% { box-shadow: 0 0 0 0 rgba(230,255,85,0); }}.brand {font: 500 11px/1 var(--mono); letter-spacing: 0.18em;text-transform: uppercase; color: var(--muted);}.brand b { color: var(--text); font-weight: 600; }h1 {font-size: clamp(30px, 6vw, 42px); line-height: 1.08;letter-spacing: -0.025em; margin: 0 0 14px; font-weight: 600;}.sub {color: var(--muted); font-size: 15.5px; line-height: 1.65;margin: 0 0 40px; max-width: 56ch;}.sub code {font: 0.86em/1 var(--mono); color: var(--accent);background: rgba(230,255,85,0.08);border: 1px solid rgba(230,255,85,0.14);padding: 2px 6px; border-radius: 6px;}.card {background: var(--surface); border: 1px solid var(--border);border-radius: 18px; padding: 24px; margin-bottom: 20px;}.card-head {display: flex; align-items: baseline; justify-content: space-between;gap: 12px; margin: 0 0 18px;}.card-head h2 {font: 500 11px/1 var(--mono); letter-spacing: 0.16em;text-transform: uppercase; color: var(--muted); margin: 0;}.tag {font: 500 10.5px/1 var(--mono); letter-spacing: 0.12em;text-transform: uppercase; color: var(--accent);border: 1px solid rgba(230,255,85,0.22);background: rgba(230,255,85,0.05);padding: 4px 9px; border-radius: 999px; white-space: nowrap;}pre {margin: 0; padding: 18px 20px; overflow-x: auto;background: var(--surface-hi); border: 1px solid var(--border);border-radius: 12px;font: 12.5px/1.75 var(--mono); color: #c7c9cf;}.grid {display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));gap: 1px; background: var(--border);border: 1px solid var(--border); border-radius: 14px; overflow: hidden;}.cell { background: var(--surface); padding: 16px 18px; min-width: 0; }.cell-label {font: 500 10.5px/1.2 var(--mono); letter-spacing: 0.12em;text-transform: uppercase; color: var(--muted); margin-bottom: 8px;}.cell-value {font: 14px/1.4 var(--mono); color: var(--text);font-variant-numeric: tabular-nums;overflow-wrap: anywhere;}.cell-value.accent {color: var(--accent); font-weight: 600; font-size: 20px;letter-spacing: -0.01em;}.slack {display: flex; gap: 14px; align-items: flex-start;background:linear-gradient(180deg, rgba(230,255,85,0.06), rgba(230,255,85,0.02));border: 1px solid rgba(230,255,85,0.20);border-radius: 14px; padding: 16px 18px;}.slack svg { flex-shrink: 0; margin-top: 2px; }.slack-body { min-width: 0; }.slack-note { font-size: 13.5px; line-height: 1.55; color: #d3d5da; }.slack-msg {margin-top: 12px; padding: 12px 14px;background: var(--surface-hi); border: 1px solid var(--border);border-radius: 10px; color: #aeb1b8;font: 12px/1.6 var(--mono); overflow-wrap: anywhere;}.foot {margin-top: 38px; color: var(--muted);font: 12px/1.7 var(--mono);}.foot b { color: var(--text); font-weight: 600; }@media (max-width: 560px) {body { padding: 40px 16px 72px; }.card { padding: 18px; border-radius: 16px; }.grid { grid-template-columns: 1fr; }.cell-value.accent { font-size: 18px; }}</style></head><body><div class="wrap"><div class="top"><span class="live"><span class="dot"></span> Webhook endpoint · live</span><span class="brand">running on <b>QuikRun</b></span></div><h1>Refund alerts, ready to receive.</h1><p class="sub">POST a Stripe-style <code>charge.refunded</code> event to this URL and get back a clean, structured alert: money formatted, who was refunded, why, and when. Configure a webhook secret and it fans out to Slack.</p><div class="card"><div class="card-head"><h2>Expected event shape</h2><span class="tag">POST · JSON</span></div><pre>${esc(EXPECTED_SHAPE)}</pre></div><div class="card"><div class="card-head"><h2>Example alert produced</h2><span class="tag">${esc(alert.event)}</span></div><div class="grid">${cells}</div></div><div class="slack"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="${BRAND.accent}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4 20-7Z"/></svg><div class="slack-body"><div class="slack-note">${esc(slackNote)}</div><div class="slack-msg">${esc(slackMessage)}</div></div></div><p class="foot">Try it: <b>curl -X POST</b> this URL with the JSON above, or just reload to see the example rendered.</p></div></body></html>`;}export default async function handler(req, env) {const hasSlack = !!(env && env.SLACK_WEBHOOK);// Browser / health check: render the status page.if (req.method === "GET" || req.method === "HEAD") {const previewAlert = buildAlert(EXAMPLE_EVENT);return new Response(renderStatusPage(previewAlert, hasSlack), {status: 200,headers: { "content-type": "text/html; charset=utf-8" },});}if (req.method !== "POST") {return new Response(JSON.stringify({ok: false,error: "Method not allowed. Use POST for events, GET for status.",}),{status: 405,headers: { "content-type": "application/json", allow: "GET, POST" },},);}// req.body is already parsed for JSON. Fall back gracefully if it isn't an object.let event = req.body;if (typeof event === "string") {try {event = JSON.parse(event);} catch {event = null;}}if (!event || typeof event !== "object") {return {ok: false,error: "Expected a JSON event body, e.g. a Stripe charge.refunded event.",hint: "POST content-type: application/json",};}const alert = buildAlert(event);return {ok: true,received: alert.event,alert,slack: {configured: hasSlack,// We never expose the webhook URL, only whether one is set.note: hasSlack? "Would POST this alert to env.SLACK_WEBHOOK.": "env.SLACK_WEBHOOK not set. Add it in the snippet's secrets to enable Slack delivery.",message_preview: `:money_with_wings: ${alert.headline} for ${alert.customer} (${alert.reason}) at ${alert.when}. Charge ${alert.charge_id}.`,},};}
Fork this and make it yours
Describe a change in plain English and QuikRun deploys it to your own URL.