Link simulasi midtrans

<https://simulator.sandbox.midtrans.com/openapi/va/index?bank=bri>

Tabel di supabase

create table public.orders (
  id uuid not null default extensions.uuid_generate_v4 (),
  user_id uuid null,
  order_id text null,
  amount integer null,
  status text null default 'pending'::text,
  snap_token text null,
  created_at timestamp without time zone null default now(),
  updated_at timestamp with time zone null default now(),
  constraint orders_pkey primary key (id),
  constraint orders_order_id_key unique (order_id)
) TABLESPACE pg_default;

buat edge function : new-create-payment

<https://ufkkmgqkznzchxynvmfn.supabase.co/functions/v1/new-create-payment>
import { serve } from "<https://deno.land/std/http/server.ts>";

serve(async (req) => {

  // ✅ Handle preflight request
  if (req.method === "OPTIONS") {
    return new Response("ok", {
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
        "Access-Control-Allow-Methods": "POST, OPTIONS",
      },
    });
  }

  const { order_id, amount } = await req.json();

  const serverKey = Deno.env.get("MIDTRANS_SERVER_KEY");

  const response = await fetch("<https://app.sandbox.midtrans.com/snap/v1/transactions>", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + btoa(serverKey + ":")
    },
    body: JSON.stringify({
      transaction_details: {
        order_id,
        gross_amount: amount
      }
    })
  });

  const data = await response.json();

  return new Response(JSON.stringify(data), {
    headers: {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*", // ✅ penting
    }
  });
});

Configurasi di Midtrans

image.png

WEBHOOK

link :

<https://ufkkmgqkznzchxynvmfn.supabase.co/functions/v1/midtrans-webhook>

Edge Function Webhook/ install pakai CLI

import { serve } from "<https://deno.land/std/http/server.ts>";

serve(async (req) => {
  try {
    if (req.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    const body = await req.json();

    const {
      order_id,
      status_code,
      gross_amount,
      signature_key,
      transaction_status,
      fraud_status
    } = body;

    // ================================
    // 1️⃣ VERIFY SIGNATURE (WAJIB)
    // ================================
    const serverKey = Deno.env.get("MIDTRANS_SERVER_KEY");

    const dataToHash =
      order_id + status_code + gross_amount + serverKey;

    const hashBuffer = await crypto.subtle.digest(
      "SHA-512",
      new TextEncoder().encode(dataToHash)
    );

    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const computedSignature = hashArray
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");

    if (computedSignature !== signature_key) {
      console.error("❌ Invalid signature");
      return new Response("Invalid signature", { status: 403 });
    }

    // ================================
    // 2️⃣ MAP STATUS
    // ================================
    let newStatus = "pending";

    switch (transaction_status) {
      case "capture":
        if (fraud_status === "challenge") {
          newStatus = "challenge";
        } else if (fraud_status === "accept") {
          newStatus = "paid";
        }
        break;

      case "settlement":
        newStatus = "paid";
        break;

      case "pending":
        newStatus = "pending";
        break;

      case "deny":
      case "cancel":
        newStatus = "failed";
        break;

      case "expire":
        newStatus = "expired";
        break;

      case "refund":
      case "chargeback":
        newStatus = "refunded";
        break;
    }

    // ================================
    // 3️⃣ UPDATE DATABASE (SERVICE ROLE)
    // ================================
    const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");

    const updateRes = await fetch(
      `https://ufkkmgqkznzchxynvmfn.supabase.co/rest/v1/orders?order_id=eq.${order_id}`,
      {
        method: "PATCH",
        headers: {
          apikey: serviceRoleKey!,
          Authorization: `Bearer ${serviceRoleKey}`,
          "Content-Type": "application/json",
          Prefer: "return=representation",
        },
        body: JSON.stringify({
          status: newStatus,
          updated_at: new Date().toISOString(),
        }),
      }
    );

    if (!updateRes.ok) {
      const errText = await updateRes.text();
      console.error("❌ DB Update Failed:", errText);
      return new Response("Database update failed", { status: 500 });
    }

    console.log(`✅ Order ${order_id} updated to ${newStatus}`);

    return new Response("OK", { status: 200 });

  } catch (err) {
    console.error("❌ Webhook error:", err);
    return new Response("Internal Server Error", { status: 500 });
  }
});

RLS

Pertanyaan bagus sekali 👌 ini sudah level arsitektur production.

Jawaban singkatnya: