← quik.run
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
Endpointquik.run/r/refund-alertsRunFork
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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
 
// 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.

Fork snippet