Home / Uncategorized / poe.com/s/uJJ53Op1PrldJVu7rsJO

poe.com/s/uJJ53Op1PrldJVu7rsJO

Bot image for GPT-5-mini

Shared conversation

GPT-5-mini ·

Bagus — saya lanjutkan dengan membuat contoh implementasi backend minimal menggunakan Node.js + Express untuk dua opsi: Midtrans (Snap) dan Xendit. Saya sertakan:

  • Kode server lengkap (minimal) untuk membuat transaksi (create-transaction) dan menangani webhook.
  • Penjelasan environment variables yang diperlukan.
  • Contoh panggilan dari front-end (single-file HTML) ke endpoint backend tadi.
  • Petunjuk pengujian lokal (ngrok) dan keamanan singkat.

Pilih salah satu atau minta keduanya. Karena Anda menulis "lanjutkan" lagi, saya akan berikan kedua contoh (Midtrans dan Xendit). Pada akhirnya Anda bisa pakai salah satu gateway.

Perhatian: kode ini contoh minimal untuk development/testing. Jangan gunakan credentials produksi di client; selalu simpan secret di server dan gunakan HTTPS di production.

  1. Struktur proyek (sederhana)
  • project-root/
    • server.js (Express server)
    • package.json
    • README.md
  1. Environment variables (contoh)
  • Untuk Midtrans:
    • MIDTRANS_SERVER_KEY=your_midtrans_server_key
    • MIDTRANS_CLIENT_KEY=your_midtrans_client_key (hanya dipakai jika Anda pakai Snap di client; jangan publish server key)
    • NODE_ENV=development
  • Untuk Xendit:
    • XENDIT_SECRET_API_KEY=your_xendit_secret_api_key
  • Porta server default: 3000
  1. server.js — implementasi (Node.js + Express)
    Simpan sebagai server.js
javascript
// server.js - contoh minimal create-transaction + webhook untuk Midtrans Snap & Xendit
// Untuk demo/development saja. Gunakan HTTPS & verifikasi lebih ketat di production.

const express = require('express');
const fetch = require('node-fetch'); // or built-in fetch (Node 18+)
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Simple in-memory "DB" (replace with real DB)
const ORDERS = {};

// Helper: generate order id
function genOrderId() {
  return 'ORD-' + Date.now();
}

/* ===========================
   MIDTRANS (Snap) - Server
   ===========================
   Flow:
   - Client POST /midtrans/create-transaction -> server membuat order, memanggil API Snap (server-side)
   - Server mengembalikan {snap_token, redirect_url?} ke client
   - Client memanggil Midtrans Snap JS (client_key) atau redirect ke redirect_url
   - Midtrans webhook -> POST /midtrans/webhook -> server verifikasi signature & update order
*/

// Config (ambil dari env)
const MIDTRANS_SERVER_KEY = process.env.MIDTRANS_SERVER_KEY || '';
const MIDTRANS_CLIENT_KEY = process.env.MIDTRANS_CLIENT_KEY || ''; // dipakai di client only
const MIDTRANS_BASE = 'https://api.sandbox.midtrans.com/v2'; // sandbox

// Basic auth header for Midtrans server key
function midtransAuthHeader() {
  const sk = MIDTRANS_SERVER_KEY;
  return 'Basic ' + Buffer.from(sk + ':').toString('base64');
}

app.post('/midtrans/create-transaction', async (req, res) => {
  try {
    const { name, email, ticketType, price } = req.body;
    if (!name || !email || !ticketType || !price) {
      return res.status(400).json({ error: 'Missing fields' });
    }

    const orderId = genOrderId();
    // Simpan order (pending)
    ORDERS[orderId] = {
      orderId, name, email, ticketType, price, status: 'pending', createdAt: Date.now()
    };

    // Prepare payload for Midtrans charge (snap)
    const payload = {
      transaction_details: {
        order_id: orderId,
        gross_amount: Number(price)
      },
      credit_card: {
        secure: true
      },
      customer_details: {
        first_name: name,
        email: email
      }
    };

    // Call Midtrans Snap (create transaction)
    const resp = await fetch(`${MIDTRANS_BASE}/transactions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': midtransAuthHeader()
      },
      body: JSON.stringify(payload)
    });

    if (!resp.ok) {
      const text = await resp.text();
      console.error('Midtrans error:', resp.status, text);
      return res.status(502).json({ error: 'Midtrans API error', detail: text });
    }

    const data = await resp.json();
    // data contains: token, redirect_url (depends)
    // Simpan response reference
    ORDERS[orderId].midtrans = data;

    return res.json({ orderId, midtrans: data, midtransClientKey: MIDTRANS_CLIENT_KEY });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'server_error' });
  }
});

// Midtrans webhook
// Midtrans sends JSON; to verify signature: for /charge_notification (v2), use order_id + status_code + gross_amount + server_key (server-key) hashed with sha512
app.post('/midtrans/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    // Midtrans docs: signature_key = sha512(order_id + status_code + gross_amount + server_key)
    const rawBody = req.body.toString('utf8');
    const payload = JSON.parse(rawBody);
    const { order_id, status_code, gross_amount, signature_key } = payload;
    const expected = crypto.createHash('sha512').update(order_id + status_code + gross_amount + MIDTRANS_SERVER_KEY).digest('hex');

    if (expected !== signature_key) {
      console.warn('Midtrans webhook signature mismatch', expected, signature_key);
      return res.status(400).send('invalid signature');
    }

    // Update order
    const order = ORDERS[order_id];
    if (!order) {
      console.warn('Order not found for', order_id);
      // still respond 200 so Midtrans won't retry too many times, or 404 if you want retries.
      return res.status(200).send('order not found');
    }

    // Example: handle settlement/paid
    if (payload.transaction_status === 'settlement' || payload.transaction_status === 'capture' || payload.transaction_status === 'credit_card' || payload.transaction_status === 'settlement'){
      order.status = 'paid';
      order.midtransNotification = payload;
      // TODO: generate ticket (PDF/QR), send email
      console.log('Order paid', order_id);
    } else if (payload.transaction_status === 'pending') {
      order.status = 'pending';
    } else if (payload.transaction_status === 'deny' || payload.transaction_status === 'expire' || payload.transaction_status === 'cancel') {
      order.status = 'failed';
    } else {
      order.midtransNotification = payload;
    }

    res.status(200).send('ok');
  } catch (err) {
    console.error('Webhook handling error', err);
    res.status(500).send('server error');
  }
});

/* ===========================
   XENDIT - Server
   ===========================
   Flow similar: create charge (invoice/ewallet/VA), return payment_url to client; webhook -> /xendit/webhook
*/

// Xendit config
const XENDIT_SECRET_API_KEY = process.env.XENDIT_SECRET_API_KEY || '';
const XENDIT_BASE = 'https://api.xendit.co';

// Create invoice (example)
app.post('/xendit/create-invoice', async (req, res) => {
  try {
    const { name, email, ticketType, price } = req.body;
    if (!name || !email || !ticketType || !price) {
      return res.status(400).json({ error: 'Missing fields' });
    }
    const orderId = genOrderId();
    ORDERS[orderId] = { orderId, name, email, ticketType, price, status: 'pending', createdAt: Date.now() };

    // Xendit Create Invoice
    const body = {
      external_id: orderId,
      amount: Number(price),
      payer_email: email,
      description: `Ticket ${ticketType} - ${orderId}`
    };

    const resp = await fetch(`${XENDIT_BASE}/v2/invoices`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + Buffer.from(XENDIT_SECRET_API_KEY + ':').toString('base64')
      },
      body: JSON.stringify(body)
    });

    if (!resp.ok) {
      const t = await resp.text();
      console.error('Xendit error', resp.status, t);
      return res.status(502).json({ error: 'Xendit API error', detail: t });
    }

    const data = await resp.json();
    ORDERS[orderId].xendit = data;
    return res.json({ orderId, invoice: data });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'server_error' });
  }
});

// Xendit webhook endpoint (verify with x-callback-signature)
app.post('/xendit/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const signature = req.headers['x-callback-signature'] || '';
    const payload = req.body.toString('utf8');
    const expected = crypto.createHmac('sha256', XENDIT_SECRET_API_KEY).update(payload).digest('hex');

    if (signature !== expected) {
      console.warn('Xendit webhook signature mismatch', signature, expected);
      return res.status(400).send('invalid signature');
    }

    const data = JSON.parse(payload);
    // Example: invoice.status = 'PAID'
    if (data.type === 'invoice' && data.data && data.data.status) {
      const external_id = data.data.external_id;
      const order = ORDERS[external_id];
      if (order) {
        if (data.data.status === 'PAID') {
          order.status = 'paid';
          order.xenditNotification = data;
          console.log('Order paid via Xendit', external_id);
          // TODO: generate ticket + notify buyer
        } else {
          order.xenditNotification = data;
        }
      }
    }

    res.status(200).send('ok');
  } catch (err) {
    console.error(err);
    res.status(500).send('server error');
  }
});

/* ================
   Utility endpoints (for demo)
   ================ */

// Get order (debug)
app.get('/order/:id', (req, res) => {
  const id = req.params.id;
  const order = ORDERS[id];
  if (!order) return res.status(404).json({ error: 'not found' });
  res.json(order);
});

// Health
app.get('/', (req, res) => res.send('Payment demo server running'));

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log('Server listening on', PORT));
  1. package.json (minimal)
    Simpan sebagai package.json
json
{
  "name": "payment-demo",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "body-parser": "^1.20.0",
    "express": "^4.18.2",
    "node-fetch": "^2.6.7"
  }
}

Catatan: Jika Anda gunakan Node 18+ yang telah menyediakan fetch global, Anda bisa menghapus node-fetch dan menyesuaikan.

  1. Contoh panggilan dari front-end (Single-file HTML)
    Tambahkan fungsi JS yang memanggil endpoint server saat user klik Buy. Contoh (client-side):
  • Untuk Midtrans: panggil POST /midtrans/create-transaction dengan JSON {name,email,ticketType,price}, server mengembalikan midtrans token/redirect_url. Jika server mengembalikan redirect_url, arahkan user ke sana. Jika mengembalikan snap_token, Anda bisa menggunakan Snap JS di client (script Snap JS harus dimuat dengan client key) — tapi Snap client memerlukan midtrans client key (tidak server key).

  • Untuk Xendit: panggil POST /xendit/create-invoice, server mengembalikan invoice data (invoice.invoice_url) — redirect user ke invoice_url.

Contoh JS singkat untuk panggilan POST:

javascript
async function checkoutWithMidtrans(name,email,ticketType,price){
  const resp = await fetch('/midtrans/create-transaction', {
    method:'POST',
    headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ name, email, ticketType, price })
  });
  const data = await resp.json();
  if(data.midtrans && data.midtrans.redirect_url){
    window.location = data.midtrans.redirect_url;
  } else if (data.midtrans && data.midtrans.token){
    // menggunakan Snap JS (pastikan Anda sudah load Snap JS with client key)
    window.snap.pay(data.midtrans.token, {
      onSuccess: function(result){ console.log('success', result); },
      onError: function(err){ console.error(err); }
    });
  } else {
    console.error('Invalid midtrans response', data);
  }
}

async function checkoutWithXendit(name,email,ticketType,price){
  const resp = await fetch('/xendit/create-invoice', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ name, email, ticketType, price })
  });
  const data = await resp.json();
  if(data.invoice && data.invoice.invoice_url){
    window.location = data.invoice.invoice_url;
  } else {
    console.error('Invalid xendit response', data);
  }
}
  1. Testing lokal dengan ngrok
  1. Generasi tiket & QR code (opsional)
  • Setelah status order berubah menjadi paid, Anda bisa:
    • Generate QR code (library: qrcode, qrcode-svg, or server-side API).
    • Generate PDF ticket dengan QR (Puppeteer to render HTML -> PDF, atau libraries seperti PDFKit).
    • Simpan file PDF dan kirim melalui email (SendGrid, Mailgun, SES).
  1. Keamanan & produksi
  • Jangan simpan API keys di repo. Gunakan secret manager or environment variables.
  • Gunakan HTTPS.
  • Batasi dan validasi semua input.
  • Simpan order & transaksi ke database persistent (Postgres, MongoDB).
  • Verifikasi webhook signature & replay attack prevention (timestamp, nonce if available).
  • Pastikan idempotency handling pada webhook (jika gateway resend).
  1. Mau saya tambahkan:
  • Contoh implementasi generate PDF ticket + QR code (Node.js + qrcode + Puppeteer)?
  • Contoh lengkap front-end single-file terintegrasi dengan endpoint create-transaction (mengganti simulasi client-side dengan panggilan server)?
  • Contoh implementasi Midtrans Snap JS di halaman (note: membutuhkan MIDTRANS_CLIENT_KEY dan Snap script di client)?

Beritahu saya pilihan Anda (contoh: "Ya, buat contoh PDF+QR" atau "Tambahkan Snap JS di halaman single-file"). Saya akan menyediakan kode tambahan sesuai pilihannya.

Bot image for GPT-5-mini
Go to @GPT-5-mini on Poe