Skip to main content
MerchantId dibangun di atas satu kendala: API privat provider tidak membawa order reference milik aplikasi Anda. Nominal akhir menjadi pembeda utama. Halaman ini menjelaskan ide yang membuat pendekatan itu aman.

Nominal unik sebagai penanda

Setiap pembayaran baru mendapat offset rupiah unik dari AmountAllocator. Keunikan berlaku pada nominal akhir (baseAmount + offset), bukan offset saja.
Scoping per base amount saja tidak cukup: dengan jendela offset 999, base amount yang berbeda kurang dari itu bisa menghasilkan nominal final sama. 3500 + 1 dan 3499 + 2 sama-sama 3501. Karena nominal adalah satu-satunya pembeda yang dimiliki poller, keduanya tidak boleh aktif pada scope yang sama. Slot terkecil selalu dipilih lebih dulu. Lihat AmountAllocator.

PaymentScope

Setiap pembayaran menyimpan kepemilikan eksplisit sehingga transaksi satu provider atau store tidak pernah melunasi pembayaran lain.
  • GoPay: { provider: "gopay", merchantId }.
  • Shopee: { provider: "shopee", accountId: session.merchant.id, merchantId: session.storeId }. accountId adalah business merchant, merchantId adalah store pemilik transaksi - satu merchant dapat memiliki beberapa store.
PaymentStore bersama wajib menghormati scope pada listActive(scope). PaymentService memfilter ulang hasilnya sebagai pertahanan tambahan. Lihat PaymentStore.

Karantina nominal

Ketika sebuah pembayaran meninggalkan himpunan aktif (paid, expired, atau cancelled), nominalnya dikarantina selama 2 x clockSkewMs. Tanpa karantina, pesanan baru berharga sama dapat langsung menerima nominal bekas sementara transaksi lamanya masih di jendela matcher, sehingga satu transfer melunasi dua pesanan. Alasan 2 x clockSkewMs: sebuah transaksi bisa melunasi pembayaran yang dibuat hingga clockSkewMs sebelum waktu transaksi, dan timestamp transaksi itu sendiri bisa clockSkewMs setelah slot dilepas. Dua skew setelah slot dibebaskan, tidak ada transaksi milik pembayaran lama yang masih bisa jatuh di jendela pembayaran baru.

Transaksi terkonsumsi

reconcile menjamin satu transaksi dipakai paling banyak sekali dalam satu call. PaymentService memperluas jaminan itu lintas tick: id transaksi yang sudah melunasi pembayaran diingat selama masih mungkin muncul di jendela feed (rolling lookback 24 jam), sehingga row lama yang di-fetch ulang tidak pernah melunasi pesanan kedua.

Reconcile sebelum expire

Rekonsiliasi selalu berjalan sebelum penandaan kedaluwarsa. Feed mengindeks transaksi dengan jeda, jadi pembeli yang membayar tepat waktu bisa muncul di feed setelah expiresAt lewat. Dua invariant yang dikunci:
  • Matching berjalan sebelum expiry pada setiap tick.
  • Pembayaran baru menjadi expired setelah expiresAt + clockSkewMs, yaitu persis saat matcher juga berhenti menerima transaksi untuknya.
Ini menahan jeda indexing feed agar uang yang dibayar tepat waktu tidak kehilangan pesanannya. Lihat Model pembayaran.

Fail-open untuk data tak dikenal

Matcher menolak status gagal yang dikenal, tetapi menerima status kosong/tidak dikenal dan timestamp yang tidak dapat diparse. Feed privat dapat berubah label; mengubah ini menjadi fail-closed dapat membuat pembayaran sah tidak pernah terdeteksi. Perubahan kebijakan ini wajib disertai test yang membuktikan arah sukses dan gagal.

Satu antrean tulis

createPayment, settle, cancel, dan expire diserialkan dalam satu antrean per PaymentService. cancelPayment yang balapan dengan settlement tidak boleh menimpa status paid. Ini hanya menjaga satu proses; deployment multi-process harus menegakkan keunikan dan atomicity di store persisten. Lihat PaymentStore.

Langkah berikutnya

Utilitas QRIS

Cara nominal ditanam ke payload EMV.

Model pembayaran

Siklus create, poll, settle, expire.