import express from "express"; import bodyParser from "body-parser"; import crypto from "crypto"; const app = express(); app.use(bodyParser.json()); /* ===== ENV ===== */ const PROJECT = process.env.PAKASIR_PROJECT; const API_KEY = process.env.PAKASIR_API_KEY; const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || "pakasir"; if (!PROJECT || !API_KEY) { throw new Error("PAKASIR_PROJECT & PAKASIR_API_KEY wajib diisi"); } /* ===== MEMORY STORE ===== */ const transactions = new Map(); // order_id => { amount, status } const clients = new Map(); // order_id => [res] /* ===== SSE HELPER ===== */ function sendEvent(order_id, payload) { const arr = clients.get(order_id) || []; for (const res of arr) { res.write(`data: ${JSON.stringify(payload)}\n\n`); } } /* ===== CREATE ===== */ app.post("/create", async (req, res) => { try { const { amount } = req.body; if (!amount || amount < 1000) { return res.status(400).json({ error: "amount minimal 1000" }); } const order_id = `INV-${Date.now()}`; const r = await fetch("https://app.pakasir.com/api/transactioncreate/qris", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ project: PROJECT, api_key: API_KEY, order_id, amount }) }); const j = await r.json(); const payment = j.payment || j; const qris = payment.payment_number || j.qris_string; if (!qris) { return res.status(500).json({ error: "gagal create qris", raw: j }); } transactions.set(order_id, { amount, status: "PENDING" }); res.json({ success: true, order_id, amount, qr_url: `https://quickchart.io/qr?text=${encodeURIComponent(qris)}&size=500` }); } catch (e) { console.error(e); res.status(500).json({ error: "create error" }); } }); /* ===== SSE REALTIME ===== */ app.get("/events/:order_id", (req, res) => { const { order_id } = req.params; res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }); res.write("\n"); const arr = clients.get(order_id) || []; arr.push(res); clients.set(order_id, arr); req.on("close", () => { const left = (clients.get(order_id) || []).filter(r => r !== res); if (left.length === 0) clients.delete(order_id); else clients.set(order_id, left); }); }); /* ===== WEBHOOK ===== */ app.post("/webhook", (req, res) => { try { const body = req.body; const order_id = body.order_id || body.payment?.order_id; let status = body.status || body.payment?.status; if (!order_id || !status) { return res.status(400).send("invalid payload"); } status = status.toUpperCase(); if (transactions.has(order_id)) { transactions.get(order_id).status = status; } sendEvent(order_id, { order_id, status }); res.json({ ok: true }); } catch (e) { console.error(e); res.status(500).send("webhook error"); } }); /* ===== STATUS (FIXED) ===== */ app.get("/status/:order_id", async (req, res) => { try { const { order_id } = req.params; const tx = transactions.get(order_id); if (!tx) { return res.json({ order_id, status: "NOT_FOUND" }); } // kalau sudah PAID dari webhook → langsung return if (tx.status === "PAID" || tx.status === "SUCCESS") { return res.json({ order_id, status: tx.status }); } // fallback polling (WAJIB amount) const params = new URLSearchParams({ project: PROJECT, api_key: API_KEY, order_id, amount: tx.amount }); const r = await fetch( `https://app.pakasir.com/api/transactiondetail?${params}` ); const j = await r.json(); const data = j.transaction || j; const status = (data.status || "PENDING").toUpperCase(); tx.status = status; transactions.set(order_id, tx); res.json({ order_id, status }); } catch (e) { console.error(e); res.status(500).json({ error: "status error" }); } }); /* ===== HEALTH ===== */ app.get("/", (req, res) => { res.json({ name: "Pakasir Realtime Gateway", status: "running" }); }); /* ===== START ===== */ const PORT = process.env.PORT || 7860; app.listen(PORT, () => console.log("RUNNING ON", PORT));