Nominal unik sebagai penanda
Setiap pembayaran baru mendapat offset rupiah unik dariAmountAllocator. Keunikan berlaku pada nominal akhir (baseAmount + offset), bukan offset saja.
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 }.accountIdadalah business merchant,merchantIdadalah 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 selama2 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 setelahexpiresAt 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.
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.