Skip to main content

Merchant MDR Classification Rule

Spec dokumen untuk klasifikasi MDR (Merchant Discount Rate) per merchant — dipakai oleh:

  • mobile app saat user submit registrasi (Step Profil Usaha)
  • backend merchant_core_api saat stamp mdr_rate ke merchant_registration_requests dan carry-over ke merchants
  • backend transaction handler saat hitung mdr_fee_amount per transaksi

Status: draft v8 (revisi 2026-05-05). Update: (v5) Tabel MDR QRIS resmi dikonfirmasi dari Siaran Pers RDG Februari 2025 — berlaku efektif 15 Maret 2025. (v6) §3.2 dikoreksi ke PP Nomor 7 Tahun 2021; BESAR ditambah (migration 045); omzet buckets mobile diperluas. (v7) Scoring 3 faktor: typeScale dari ref_business_type.typical_scale_id — migration 046. (v8) MCC sebagai internal scoring engine: suggested_mcc_code di ref_business_type (migration 047) → MCC check untuk deteksi Khusus, compliance is_allowed, pre-fill admin, QRIS payload tag 52. MCC tidak ditampilkan ke merchant. Algoritma §3.3 diperluas jadi 3 step: skala → MCC check → mdr_classification. Yang masih perlu sign-off: threshold karyawan UMI BI (Q4), mekanisme deteksi sektor khusus (Q3), audit trail (Q5), backfill historis (Q6).

1. Context

1.1 Masalah yang diselesaikan

Sebelum migration 107 (107_merchant_mdr_rate.sql):

  • merchant.merchants dan merchant.merchant_registration_requests tidak punya kolom mdr_rate.
  • Handler INSERT di merchant_core_api/internal/transactionstatus/store.go:272 hardcode mdr_rate = 0, mdr_fee_amount = 0 → mayoritas transaksi tercatat dengan MDR 0% (loss revenue).
  • Halaman Laporan mobile menampilkan total MDR Rp 942 dari gross Rp 2,472,367,540 (seharusnya ~Rp 17.3 juta @ 0.7%) — confirmation symptom.

Migration 107 menambah kolom mdr_rate numeric(5,4) NOT NULL DEFAULT 0.0070 CHECK (mdr_rate BETWEEN 0 AND 1) di kedua tabel; dokumen ini menetapkan rule BAGAIMANA value tersebut diisi mengikuti aturan BI QRIS.

1.2 Sumber-of-truth

LayerSumberCatatan
Master skala usahadb_reference.business_scalesRange omzet/modal (PP 7/2021) + karyawan (BPS Sensus Ekonomi 2016)
Master jenis usaha / sektordb_reference.business_types (existing)Diperlukan untuk identifikasi sektor khusus (Pendidikan/SPBU/Government) — belum ada flag sektor MDR khusus, perlu enrichment (lihat §6 Q3)
Klasifikasi MDR per merchant (registration)merchant.merchant_registration_requests.mdr_rateStamped saat user submit Step "Profil Usaha"
Klasifikasi MDR per merchant (post-approval)merchant.merchants.mdr_rateCarry-over dari registration_requests; admin dapat override
MDR per transaksi (snapshot)merchant.transactions.mdr_rateDibaca dari merchants.mdr_rate saat INSERT — untuk merchant UMI dihitung kondisional per-transaction-amount (§3.3)

2. Aturan Resmi BI QRIS

2.1 Klasifikasi MDR menurut BI

Sumber resmi: Tabel MDR QRIS per Siaran Pers RDG Februari 2025, berlaku efektif 15 Maret 2025. MDR ditanggung merchant, tidak boleh dibebankan ke konsumen.

BI QRIS menetapkan dua Jenis Merchant utama: Reguler dan Khusus.

Jenis Merchant: REGULER

Sub-kategoriKondisiMDR
UMI (Usaha Mikro)Nominal transaksi ≤ Rp 500.0000%
UMI (Usaha Mikro)Nominal transaksi > Rp 500.0000.3%
UKE / UME / UBEUsaha Kecil, Menengah, Besar — semua nominal0.7%

Jenis Merchant: KHUSUS

Sub-kategoriCakupanMDR
PendidikanSekolah, kampus, lembaga pendidikan0.6%
SPBUStasiun Pengisian Bahan Bakar Umum0.4%
BLU / PSO / G2P / P2GBadan Layanan Umum (BLU), Public Service Obligation (PSO), Government to People / Bansos (G2P), People to Government: pajak, paspor, donasi sosial nirlaba (P2G)0%

Tidak ada MDR di atas 0.7%. 0.7% adalah batas tertinggi skema standar QRIS BI.

Catatan: Kategori TRANSPORTASI (0.4%) dan RUMAH_SAKIT (0.4%) yang ada di merchant.ref_mcc_code (migration 014) tidak tercantum secara eksplisit dalam tabel resmi BI ini. Kedua kategori tersebut adalah asumsi internal berdasarkan praktik industri — perlu konfirmasi ke acquirer Mandiri apakah RS/transportasi umum mendapat tarif khusus atau masuk Reguler UMI/UKE. Sementara ini data MCC tetap dipertahankan dengan rate 0.4% sebagai best-effort estimate.

2.2 Aturan kondisional UMI (Usaha Mikro)

Merchant berklasifikasi UMI dikenakan MDR berdasarkan nominal transaksi, bukan flat rate:

Nominal transaksiMDR
≤ Rp 500,0000%
> Rp 500,0000.3%

Implikasi desain: kolom tunggal merchants.mdr_rate tidak cukup untuk UMI. Lihat §4.2 untuk skema kolom tambahan + logic per-transaksi.

2.2b Kategori GOVERNMENT — Cakupan Lengkap (Khusus, MDR 0%)

Berdasarkan tabel BI resmi, kategori 0% Khusus mencakup empat sub-tipe:

Sub-tipeSingkatanContoh
Badan Layanan UmumBLURSUD, perguruan tinggi negeri, Perum Bulog
Public Service ObligationPSOSubsidi pemerintah — KAI ekonomi, Damri, Pelni
Government to PeopleG2PBantuan Sosial (Bansos) — PKH, BPNT, BLT
People to GovernmentP2GPajak (DJP), paspor (imigrasi), donasi sosial nirlaba (ZIS)

Implikasi kode: kolom mdr_classification enum perlu diperluas atau GOVERNMENT dijadikan umbrella untuk keempat sub-tipe ini. Untuk audit trail, disarankan menyimpan sub-tipe (BLU/PSO/G2P/P2G) di field terpisah atau sebagai government_sub_type VARCHAR di tabel merchants.

2.3 Skema bagi-hasil (revenue share) — assumed v1

Per konfirmasi business owner (2026-05-05), skema sementara:

MDR yang dibayar merchantBagian Bank (acquirer Mandiri)Bagian KeslesCatatan
0.7% (Reguler — UKE/UME/UBE)0.4%0.3%Sumber Kesles share saat ini
0.3% (UMI > Rp 500,000)TBD0% (assumed)Skema bagi-hasil belum tersedia, sementara asumsikan Kesles tidak dapat share
0% (UMI ≤ Rp 500,000 / Government / bansos)0%Tidak ada MDR di-collect, otomatis Kesles share 0
0.6% (Pendidikan)TBD0% (assumed)Sama dengan 0.3% — share belum dikonfirmasi
0.4% (SPBU)TBD0% (assumed)Sama dengan 0.3% — share belum dikonfirmasi

Implikasi: revenue MDR Kesles hanya non-zero untuk merchant Reguler (klasifikasi 0.7%) saat ini. Inilah alasan §2.4 menetapkan akuisisi target = Reguler 0.7%.

Angka 0.3% Kesles share di qrisplus-selling-strategy-analysis.md, qrisplus-variable-cost-analysis.md line 27, dan merchant-financial-analytics-spec.md:50 merujuk khusus ke kasus MDR 0.7% (Reguler) — Kesles dapat 0.3% dari 0.7% itu (sisanya 0.4% ke Mandiri). Bukan "merchant tier Kecil bayar 0.3%". Untuk tier non-Reguler, Kesles share = 0% sampai skema baru tersedia.

2.4 Target fokus produk Kesles

Saat ini target fokus akuisisi Kesles adalah merchant yang masuk klasifikasi MDR 0.7% (Reguler / UKE / UME / UBE). Tier UMI (Mikro) dan sektor khusus (Pendidikan/SPBU/Government) bukan prioritas akuisisi — onboarding tetap diizinkan tapi revenue share economics belum optimal sebelum skema bagi-hasil tier 0.3%/0.6%/0.4%/0% tersedia.

2.5 Threshold UMI menurut BI (PENTING — beda dari skala UU 20/2008)

Klasifikasi MDR UMI vs Reguler menggunakan threshold BI QRIS, bukan definisi UU 20/2008 yang dipakai master db_reference.business_scales. Indikator UMI per konfirmasi business owner:

IndikatorThreshold UMISumber
Omzet tahunan< Rp 2,000,000,000 / tahun (~ Rp 166,666,667 / bulan)BI QRIS rule
Jumlah karyawanindikator pelengkap (threshold belum dikonfirmasi numerik)BI QRIS rule

Konsekuensi: master business_scales PP 7/2021 (Mikro ≤ 166.6jt/bulan = 2M/tahun; Kecil 166.6jt–1.25M/bulan = 2M–15M/tahun) tidak 1:1 dengan klasifikasi BI UMI. Threshold BI UMI adalah omzet < 2M/tahun — kebetulan sama persis dengan batas atas MIKRO PP 7/2021, sehingga MIKRO (PP 7/2021) ≈ UMI (BI) secara praktis. Merchant ber-skala PP 7/2021 Kecil (omzet > 166.6jt/bulan) otomatis masuk Reguler BI (0.7%). Skala PP 7/2021 digunakan untuk display badge kategori usaha; klasifikasi MDR tetap pakai BI threshold terpisah.

Rule penentuan UMI vs Reguler (untuk dipakai di mobile registration + dashboard review):

omzetTahunan = omzetPerBulan × 12
isUMIByRevenue = omzetTahunan < 2_000_000_000
isUMIByEmployees = jumlahKaryawan < <threshold> // belum dikonfirmasi numerik (lihat §6 Q4)

mdrClassification =
isUMIByRevenue AND isUMIByEmployees ? UMI // strict (default rekomendasi)
: REGULAR // higher tier wins (konsisten §3.2)

Rule kombinasi AND (strict) atau OR (lenient) atau higher-tier-wins belum final — lihat §6 Q4. Rekomendasi default: higher tier wins (konsisten dengan tie-break skala usaha) — kalau salah satu indikator menunjukkan Reguler, klasifikasi naik ke Reguler. Lebih konservatif untuk Kesles revenue (tarif 0.7% > 0%/0.3% UMI).


2.6 ⚠️ Diskrepansi — 012_cleanup_payment_cards_mdr_rates.sql (OUTDATED)

Migration 012 di db_reference berisi rate MDR yang sudah tidak sesuai dengan ketentuan BI per 15 Maret 2025. File ini menggunakan skema lama (sebelum update RDG Februari 2025):

KategoriRate di 012 (LAMA)Rate Resmi BI (15 Mar 2025)Status
Pendidikan (EDUCATION)0.30%0.60%❌ SALAH
SPBU (FUEL)0.30%0.40%❌ SALAH
PPOB / Bill Payment (PPOB)0.50%Tidak ada (masuk P2G = 0%)❌ KATEGORI TIDAK ADA
Government / Retribusi (GOVERNMENT)0.50%0% (P2G)❌ SALAH
Merchant Mikro (MUK)Threshold "≤ Rp 500jt/tahun"Skala UMI per BI QRIS⚠️ Definisi berbeda

Tindakan yang diperlukan: Buat migration 044_fix_mdr_rates_bi_march_2025.sql di db_reference untuk:

  1. Update merchant_tier = 'EDUCATION' → set biaya_mdr = 0.60
  2. Update merchant_tier = 'FUEL' → set biaya_mdr = 0.40
  3. Update merchant_tier = 'PPOB' → reklasifikasi ke GOVERNMENT dengan biaya_mdr = 0.00
  4. Update merchant_tier = 'GOVERNMENT' → set biaya_mdr = 0.00
  5. Update view public.v_qris_mdr_matrix jika diperlukan

Catatan: Migration 012 hanya menyentuh tabel ref_payment_jenis_kartu_psp (data PSP/bank acquirer), bukan merchant.ref_mcc_code. Tabel MCC (014) sudah menggunakan rate yang benar (Pendidikan 0.60%, SPBU 0.40%). Perbaikan diprioritaskan pada tabel PSP.


3. Penentuan Klasifikasi MDR di Mobile Registration

3.1 Dua master yang BERBEDA — jangan ditukar

MasterTujuanSumber threshold
db_reference.business_scalesDisplay "Skala Usaha" merchantPP 7/2021 (modal & omzet/tahun) + BPS Sensus Ekonomi 2016 (karyawan) — Mikro/Kecil/Menengah/Besar
rule kode §2.5Penentuan mdr_classification (BI QRIS)BI QRIS — UMI threshold omzet < 2M/tahun + indikator karyawan

Mobile Step "Profil Usaha" pakai kedua-duanya untuk tujuan berbeda: skala PP 7/2021 untuk badge "Skala Usaha: Mikro/Kecil/dst" (transparansi), dan threshold BI untuk tentukan mdr_classification.

3.2 Master business_scales (PP 7/2021 + BPS 2016 — display only)

Sumber: PP Nomor 7 Tahun 2021 untuk modal usaha & omzet/tahun. BPS Sensus Ekonomi 2016 untuk range jumlah karyawan (sebagai indikator pelengkap tie-break). Bukan UU 20/2008 (sudah dicabut).

codenameomzet_per_bulan_min..max (Rp)jumlah_karyawan_min..maxsort_order
MIKROMikro0 – 166,666,6671 – 41
KECILKecil166,666,668 – 1,250,000,0005 – 192
MENENGAHMenengah1,250,000,001 – 4,166,666,66720 – 993
BESARBesar4,166,666,668+100+4

Algoritma resolve finalScale — 3 faktor, higher-tier-wins:

  1. revenueScale = row di ref_business_scale yang range omzet_per_bulan_min..max match omzet input
  2. employeeScale = row yang range jumlah_karyawan_min..max match karyawan input
  3. typeScale = ref_business_type.typical_scale_id dari jenis usaha yang dipilih (added migration 046)
  4. Higher-tier-wins: finalScale = argmax(revenueScale.sort_order, employeeScale.sort_order, typeScale.sort_order)
  5. Output _BusinessScaleDecision { finalScale, revenueScale, employeeScale, typeScale } ditampilkan di Step Profil Usaha + Ringkasan sebagai kategori usaha, bukan sebagai sumber MDR.

Contoh: merchant melaporkan omzet Rp 100jt/bulan (→ MIKRO), 3 karyawan (→ MIKRO), tapi memilih jenis usaha MINIMARKET (typeScale = KECIL, benchmark Rp 245jt/bulan). Higher-tier-wins → finalScale = KECIL → MDR 0.7% Reguler. Ini mencegah merchant minimarket mendapat MDR UMI 0%/0.3%.

3.2b ref_business_type.typical_scale_id — sumber typeScale

Ditambahkan oleh migration 046_add_typical_scale_to_business_type.sql. Setiap jenis usaha mendapat typical_scale_id berdasarkan benchmark_omzet_per_bulan = benchmark_transactions × benchmark_ticket_size × 30:

KategoriJenis UsahaBenchmark omzet/bulanTypical scale
PerdaganganMinimarket / Convenience StoreRp 245 jtKECIL
PerdaganganToko Pakaian & AksesorisRp 315 jtKECIL
PerdaganganApotek / Toko ObatRp 228 jtKECIL
PerdaganganToko Kosmetik & PerawatanRp 222 jtKECIL
PerdaganganToko Elektronik & Aksesoris GadgetRp 561 jtKECIL
PerdaganganToko Kelontong / SembakoRp 144 jtMikro
PerdaganganToko Buku / ATK / FotokopiRp 101 jtMikro
Industri PengolahanRestoran / Rumah MakanRp 226 jtKECIL
Industri PengolahanToko Kue / DessertRp 188 jtKECIL
Industri PengolahanKatering / Pesanan HarianRp 258 jtKECIL
Industri PengolahanKedai Kopi / Coffee ShopRp 125 jtMikro
Industri PengolahanBakery / Toko RotiRp 134 jtMikro
Industri PengolahanMinuman Kekinian / Juice / TeaRp 118 jtMikro
Industri PengolahanGerai Jajanan / Street FoodRp 90 jtMikro
JasaBengkel Motor / MobilRp 144 jtMikro
JasaServis Gadget / ElektronikRp 132 jtMikro
JasaSalon / BarbershopRp 89 jtMikro
JasaStudio Foto / PrintingRp 83 jtMikro
JasaCounter Pulsa / PPOBRp 71 jtMikro
JasaCuci Motor / MobilRp 74 jtMikro
JasaAgen Pengiriman / PaketRp 65 jtMikro
JasaLaundry KiloanRp 46 jtMikro

Benchmark adalah estimasi dari data seed — bukan data aktual. typeScale berfungsi sebagai floor (batas bawah), bukan ceiling. Merchant minimarket dengan omzet aktual Rp 50jt/bulan tetap di-score KECIL karena typeScale = KECIL. Admin dapat override di Registration Queue (§4.5).

3.2c ref_business_type.suggested_mcc_code — MCC sebagai scoring engine internal

Ditambahkan oleh migration 047_add_suggested_mcc_to_business_type.sql. MCC tidak ditampilkan ke merchant di Step 1, namun dipakai internal untuk tiga fungsi scoring:

FungsiMekanismeLayer
Konfirmasi mdr_categoryJOIN suggested_mcc_codemerchant.ref_mcc_code.mdr_categoryBackend (auto)
Deteksi sektor KhususJika mdr_category IN ('PENDIDIKAN','SPBU','DONASI','BANSOS') → flag mdr_classification_hint = KHUSUSBackend (auto)
Pre-fill adminSuggested MCC ditampilkan di Registration Queue sebagai rekomendasi awalAdmin review
Compliance checkis_allowed = FALSE di ref_mcc_code → block onboarding otomatisBackend (auto)
QRIS payload tag 52MCC final (setelah admin konfirmasi) di-stamp ke merchants.mcc_codePost-approve

Mapping 22 jenis usaha → MCC primer (semua REGULER untuk tipe usaha saat ini):

Jenis UsahaMCCDeskripsimdr_category
Toko Kelontong / Sembako5499Toko kelontong / makanan lainnyaREGULER
Minimarket / Convenience Store5411Supermarket / Grocery StoresREGULER
Toko Pakaian & Aksesoris5651Toko pakaian keluargaREGULER
Apotek / Toko Obat5912ApotekREGULER
Toko Kosmetik & Perawatan5977Toko kosmetikREGULER
Toko Elektronik & Aksesoris Gadget5732Toko elektronikREGULER
Toko Buku / ATK / Fotokopi5943Toko ATK / alat kantorREGULER
Kedai Kopi / Coffee Shop5812Restoran / rumah makanREGULER
Restoran / Rumah Makan5812Restoran / rumah makanREGULER
Bakery / Toko Roti5462Toko roti / bakeryREGULER
Minuman Kekinian / Juice / Tea5814Fast Food RestaurantREGULER
Gerai Jajanan / Street Food5812Restoran / rumah makanREGULER
Toko Kue / Dessert5441Toko permen, kacang, confectioneryREGULER
Katering / Pesanan Harian5811CateringREGULER
Laundry Kiloan7211Laundry keluarga & komersialREGULER
Salon / Barbershop7230Salon & barbershopREGULER
Bengkel Motor / Mobil7538Bengkel servis mobilREGULER
Cuci Motor / Mobil7542Cuci mobil / Car WashREGULER
Agen Pengiriman / Paket4789Jasa transportasi lainnyaREGULER
Counter Pulsa / PPOB4812Konter pulsa / telekomunikasiREGULER
Servis Gadget / Elektronik7379Jasa maintenance komputerREGULER
Studio Foto / Printing7221Studio fotoREGULER

Semua 22 jenis usaha saat ini REGULER — tidak ada yang auto-trigger KHUSUS dari MCC. Deteksi Khusus (PENDIDIKAN/SPBU/GOVERNMENT) dilakukan melalui field "Jenis Merchant" yang merchant deklarasi sendiri di Step 1 (Q3 §6). Jika business_type baru ditambahkan untuk sekolah/SPBU/instansi, suggested_mcc_code akan otomatis trigger Khusus melalui mdr_category check.

MCC 5122 dilarang (is_allowed = FALSE) meski terdengar mirip "apotek". Apotek yang benar menggunakan MCC 5912. Backend wajib check is_allowed dari ref_mcc_code saat onboarding.

3.3 Penentuan mdr_classification (BI threshold)

Algoritma terpisah, hasil di-stamp ke merchants.mdr_classification + merchants.mdr_rate:

// ── Step 1 — Skala usaha (3 faktor, higher-tier-wins) ───────────────────────
revenueScale = lookup ref_business_scale WHERE omzet_per_bulan_min ≤ monthlyRevenue ≤ max
employeeScale = lookup ref_business_scale WHERE jumlah_karyawan_min ≤ employeeCount ≤ max
typeScale = lookup ref_business_scale via ref_business_type.typical_scale_id // mig 046
finalScale = argmax(revenueScale.sort_order, employeeScale.sort_order, typeScale.sort_order)

// ── Step 2 — MCC internal check (tidak terlihat merchant) ───────────────────
mccRow = lookup merchant.ref_mcc_code WHERE mcc_code = businessType.suggested_mcc_code
if mccRow.is_allowed == FALSE:
BLOCK onboarding → return error "Jenis usaha tidak dapat menggunakan QRIS"

mccCategory = mccRow.mdr_category // REGULER | PENDIDIKAN | SPBU | DONASI | BANSOS | FORBIDDEN

// ── Step 3 — MDR classification ─────────────────────────────────────────────
// Merchant deklarasi "Jenis Merchant" di Step 1 (Reguler / Khusus-Pendidikan / dst)
// Backend mengkonfirmasi via mccCategory; admin dapat override di Review Queue

if merchantDeclaredJenisMerchant == "KHUSUS_PENDIDIKAN"
OR mccCategory == "PENDIDIKAN":
mdr_classification = "PENDIDIKAN" // MDR 0.6%

elif merchantDeclaredJenisMerchant == "KHUSUS_SPBU"
OR mccCategory == "SPBU":
mdr_classification = "SPBU" // MDR 0.4%

elif merchantDeclaredJenisMerchant == "KHUSUS_GOVERNMENT"
OR mccCategory IN ("DONASI", "BANSOS"):
mdr_classification = "GOVERNMENT" // MDR 0%

elif finalScale.business_scale_name == "Mikro":
mdr_classification = "UMI" // MDR 0%/0.3% conditional

else: // Kecil / Menengah / Besar
mdr_classification = "REGULAR" // MDR 0.7%

typeScale adalah floor — hanya bisa menaikkan skala. mccCategory berfungsi sebagai konfirmator sektor Khusus — jika business_type baru ditambahkan dengan MCC PENDIDIKAN/SPBU, akan otomatis terdeteksi Khusus. Prioritas: deklarasi merchant → konfirmasi MCC → override admin.

3.4 Mapping mdr_classificationmerchants.mdr_rate value

mdr_classificationmerchants.mdr_rate (snapshot)Runtime behaviorUI display
UMI0.0030 (representative)conditional per transaction (≤500k → 0%, >500k → 0.3%) — lihat §3.5"0% transaksi ≤ Rp 500rb, 0.3% transaksi > Rp 500rb (UMI)"
REGULAR0.0070flat × gross_amount"0.7% (Reguler)"
PENDIDIKAN0.0060flat × gross_amount"0.6% (Sektor Pendidikan / Khusus)"
SPBU0.0040flat × gross_amount"0.4% (SPBU / Khusus)"
GOVERNMENT0.00000"0% (BLU / PSO / G2P / P2G / Khusus)"
(fallback)0.0070flat × gross_amount"0.7% (Reguler)"

Catatan UI: merchant kategori Khusus sebaiknya menampilkan label "Jenis Merchant: Khusus" sesuai terminologi BI resmi, bukan hanya nama sektor. Ini penting agar merchant memahami konteks regulasi.

Kolom merchants.mdr_rate untuk UMI disimpan 0.0030 sebagai representative rate — angka yang ditampilkan saat audit/admin. Runtime handler tetap menerapkan logic kondisional §3.5 (gross ≤ 500k → 0%).

3.5 Kolom & logic tambahan untuk klasifikasi UMI (PENDING design)

Karena merchants.mdr_rate sebagai single numeric field tidak menangkap rule kondisional UMI, perlu migration 108 (DRAFT) menambah:

ALTER TABLE merchant.merchants
ADD COLUMN IF NOT EXISTS mdr_classification varchar(32) NOT NULL DEFAULT 'REGULAR'
CHECK (mdr_classification IN ('UMI','REGULAR','PENDIDIKAN','SPBU','GOVERNMENT'));

ALTER TABLE merchant.merchant_registration_requests
ADD COLUMN IF NOT EXISTS mdr_classification varchar(32) NOT NULL DEFAULT 'REGULAR'
CHECK (mdr_classification IN ('UMI','REGULAR','PENDIDIKAN','SPBU','GOVERNMENT'));

Transaction handler logic (replacing hardcode mdr_rate = 0 di transactionstatus/store.go:272):

// computeTransactionMdr menghitung tarif MDR dan bagian Kesles per
// transaksi sesuai BI rule + skema bagi-hasil §2.3.
//
// Return:
// - rate : MDR yang dibebankan ke merchant (snapshot ke transactions.mdr_rate)
// - feeTotal : MDR total nominal Rp (snapshot ke transactions.mdr_fee_amount)
// - feeKeslesShare : bagian Kesles dalam Rp (snapshot ke transactions.mdr_fee_kesles)
func computeTransactionMdr(merchant Merchant, grossAmount int64) (rate float64, feeTotal int64, feeKeslesShare int64) {
switch merchant.MDRClassification {
case "UMI":
if grossAmount <= 500_000 {
return 0, 0, 0 // ≤ Rp500k gratis untuk UMI
}
rate = 0.0030
feeTotal = roundIDR(float64(grossAmount) * rate)
return rate, feeTotal, 0 // Kesles share = 0% (assumed)
case "PENDIDIKAN":
rate = 0.0060
feeTotal = roundIDR(float64(grossAmount) * rate)
return rate, feeTotal, 0 // Kesles share = 0% (assumed)
case "SPBU":
rate = 0.0040
feeTotal = roundIDR(float64(grossAmount) * rate)
return rate, feeTotal, 0 // Kesles share = 0% (assumed)
case "GOVERNMENT":
return 0, 0, 0 // 0% MDR
case "REGULAR":
fallthrough
default:
rate = merchant.MdrRate // default 0.0070, admin overridable
feeTotal = roundIDR(float64(grossAmount) * rate)
feeKeslesShare = roundIDR(float64(grossAmount) * 0.0030) // Kesles 0.3% dari 0.7%
return rate, feeTotal, feeKeslesShare
}
}

Catatan implementasi: feeKeslesShare untuk Reguler dihitung sebagai gross × 0.003 (flat 0.3%), bukan feeTotal × (0.3/0.7) — tetap konsisten secara aritmatika tapi lebih jelas mengikuti contract: "Kesles dapat 0.3% dari gross". Kalau admin override merchants.mdr_rate di luar 0.7% (mis. promo 0.5%), feeKeslesShare tetap 0.3% kecuali ada kebijakan baru — lihat §6 Q9.

Sektor khusus (PENDIDIKAN/SPBU/GOVERNMENT) butuh mekanisme deteksi — lihat §6 Q3.


4. Implementasi

4.1 Mobile — Step Profil Usaha & Ringkasan

apps/mobile_user/lib/features/merchant/presentation/pages/merchant_registration_page.dart:

static double _mdrRateForScale(String scaleName) {
switch (scaleName.trim().toLowerCase()) {
case 'mikro': return 0.0030; // representative — runtime conditional ≤500k=0%
case 'kecil': return 0.0070; // Reguler UKE
case 'menengah': return 0.0070; // Reguler UME
case 'besar': return 0.0070; // Reguler UBE
default: return 0.0070;
}
}

Status (2026-06-23): Kecil/Menengah/Besar di kode mobile sudah 0.0070 (Reguler). Yang masih PENDING: Mikro masih 0.0000 — perlu update ke 0.0030 (representative) dengan UI note kondisional. Lihat _mdrRateForScale di merchant_registration_page.dart.

Ringkasan menampilkan:

  • Skala Usaha: <name>
  • Klasifikasi MDR: 0.7% (Reguler) / 0%–0.3% (UMI) (Mikro) — note dynamic perlu ditambahkan

4.2 Backend — Intake & Validation

merchant_core_api/internal/merchant/merchant.go helper mdrRateForBusinessScale:

func mdrRateForBusinessScale(name string) float64 {
switch strings.ToLower(strings.TrimSpace(name)) {
case "mikro": return 0.0030 // representative — runtime kondisional di transaksi
case "kecil": return 0.0070
case "menengah": return 0.0070
case "besar": return 0.0070
default: return 0.0070
}
}

Status (2026-06-23): nilai backend sudah sesuai BI Rule — Mikro = 0.0030 (representative, per-tx kondisional), Kecil/Menengah/Besar = 0.0070. Lihat mdrRateForBusinessScale di merchant_core_api/internal/merchant/merchant.go.

4.3 Backend — Persistence (sudah berjalan)

PathTindakan
CreateMerchantRegistration UPDATE-draft pathSET mdr_rate = $46::numeric
CreateMerchantRegistration INSERT-new pathcolumn mdr_rate di INSERT
CreateManualMerchantRegistration (dashboard)column mdr_rate di INSERT
loadMerchantRegistrationForUpdateByStatusesSELECT COALESCE(mrr.mdr_rate, 0.0070)
ApproveMerchantRegistration INSERT INTO merchant.merchantscolumn mdr_rate

4.4 Backend — Per-transaction stamping (Layer 3, PENDING)

Saat ini transactionstatus/store.go:272 masih hardcode mdr_rate = 0. Fix yang akan datang:

  1. Read merchants.mdr_rate + merchants.mdr_classification (setelah migration 108).
  2. Apply computeTransactionMdr(merchant, grossAmount) (§3.5) untuk hasilkan rate + fee per transaksi.
  3. INSERT ke merchant.transactions dengan mdr_rate + mdr_fee_amount actual (bukan hardcode 0).
  4. Race condition antara transactionstatus/store.go (ON CONFLICT DO UPDATE excludes mdr fields) dan psp_event_receiver.go (ON CONFLICT DO NOTHING) — keduanya harus jalankan logic compute yang sama dan tidak saling menimpa setelah snapshot pertama.

4.5 Dashboard — Registration Queue / Review (PENDING)

Wajib: pengecekan MDR di dashboard Registration Queue saat admin Review (sebelum approve / reject) — agar mismatch antara klasifikasi yang user pilih di mobile vs aturan BI ketahuan sebelum merchant aktif. Skenario contoh: user "Mikro" tapi omzet/tahun > 2M → harus naik ke Reguler 0.7%.

Lokasi UI: panel detail registrasi di Registration Queue. Implementasi saat ini di apps/merchant_dashboard/lib/dashboard/features/home/presentation/widgets/forms/manual_registration_wizard_dialog.dart; saat refactor section ini, ekstrak ke folder dedicated apps/merchant_dashboard/lib/dashboard/features/registration/ (planned, belum dibuat) — tambahkan section "Klasifikasi MDR" sebelum tombol Approve.

Konten section:

  1. Submitted values (read-only, dari merchant_registration_requests):

    • Skala Usaha (UU 20/2008): business_scale_name
    • Omzet/bulan: average_monthly_revenue
    • Omzet/tahun (computed): average_monthly_revenue × 12
    • Jumlah karyawan: employee_count
    • MDR yang merchant submit: mdr_rate + mdr_classification
  2. Computed-by-rule (dari §3.3 algorithm, server-side recompute):

    • Klasifikasi rekomendasi: UMI / REGULAR / PENDIDIKAN / SPBU / GOVERNMENT
    • MDR rate rekomendasi: 0.0030 / 0.0070 / 0.0060 / 0.0040 / 0.0000
  3. Comparison + flag:

    • Hijau ✅ kalau submitted value match server compute
    • Kuning ⚠️ kalau mismatch ringan (mis. user pilih Mikro tapi server compute UMI — sebenarnya konsisten, hanya beda label)
    • Merah ❌ kalau mismatch signifikan (mis. user submit mdr_rate=0.0030 UMI tapi omzet/tahun = 5M = di atas threshold UMI → harusnya REGULAR 0.7%)
  4. Override control (admin-only):

    • Dropdown final mdr_classification (UMI/REGULAR/PENDIDIKAN/SPBU/GOVERNMENT)
    • Input mdr_rate final (numeric, 0..1, validated server-side terhadap CHECK constraint)
    • Required field: override_reason (text, min 10 char) — disimpan ke audit history (§6 Q5)
    • Default value mengikuti server compute, bukan submitted value — admin harus aktif konfirmasi sebelum lanjut.

Backend endpoint baru: PATCH /api/dashboard/merchant-registrations/{id}/mdr-classification — body { mdr_classification, mdr_rate, override_reason, reviewed_by_user_id }. Validate: status registrasi pending / pending_review, mdr_rate ∈ [0, 1], override_reason non-empty.

Lifecycle:

  • Saat admin click "Review" → fetch detail registrasi → server hitung & return rekomendasi → UI render comparison.
  • Admin boleh accept rekomendasi (klik "Pakai Rekomendasi") atau override manual.
  • Saat admin click "Approve", values yang aktif (override jika ada, else rekomendasi) di-stamp ke merchant_registration_requests.mdr_* lalu carry-over ke merchants.mdr_*.

Pencegahan double-stamp: kalau approve dijalankan tanpa Review terlebih dahulu, server tetap apply rekomendasi otomatis (server-side recompute) supaya tidak ada path yang menulis hardcoded value tanpa cek aturan BI.


5. Backfill historis

Setelah Layer 3 + migration 108 deploy, jalankan one-shot backfill:

-- DRAFT — JANGAN dijalankan tanpa review legal/finance
UPDATE merchant.transactions t
SET mdr_rate = CASE
WHEN m.mdr_classification = 'UMI' AND t.gross_amount <= 500000 THEN 0
WHEN m.mdr_classification = 'UMI' THEN 0.0030
WHEN m.mdr_classification = 'PENDIDIKAN' THEN 0.0060
WHEN m.mdr_classification = 'SPBU' THEN 0.0040
WHEN m.mdr_classification = 'GOVERNMENT' THEN 0
ELSE m.mdr_rate
END,
mdr_fee_amount = ROUND(t.gross_amount * (CASE ... same expression ... END), 0)
FROM merchant.merchants m
WHERE t.merchant_id = m.id
AND t.status = 'success'
AND t.mdr_rate = 0
AND t.transaction_at >= '<cut-off date>';

Cut-off date ditentukan saat sign-off (lihat §6 Q5).


6. Open Questions (perlu sign-off)

  1. Angka tier finalCLOSED: BI rule dikonfirmasi via Siaran Pers RDG Februari 2025, efektif 15 Maret 2025. Struktur Reguler (UMI + UKE/UME/UBE) dan Khusus (Pendidikan 0.6%, SPBU 0.4%, BLU/PSO/G2P/P2G 0%). Tidak ada tier > 0.7%.
  2. Tier BesarCLOSED: Besar masuk Reguler (UBE) → 0.7%, sama dengan Kecil/Menengah.
  3. Mekanisme deteksi sektor khususPARTIALLY CLOSED (2026-05-05): Sektor Khusus per BI adalah: Pendidikan, SPBU, dan BLU/PSO/G2P/P2G (Government). Pilihan mekanisme yang direkomendasikan: (b) manual pick saat registrasi — tambahkan step/field "Jenis Merchant" dengan opsi dropdown Reguler / Khusus - Pendidikan / Khusus - SPBU / Khusus - BLU/PSO/G2P/P2G. Default = Reguler. Admin dapat override di Registration Queue (§4.5). Auto-derive dari business_types terlalu kompleks dan rawan salah — lebih aman merchant deklarasi sendiri dengan admin review.
    • Yang masih open: apakah field "Jenis Merchant Khusus" perlu sub-tipe BLU/PSO/G2P/P2G (untuk audit), atau cukup satu label GOVERNMENT?
  4. Threshold karyawan untuk UMI + rule kombinasi — angka karyawan max untuk klasifikasi UMI BI belum dikonfirmasi numerik. Rule kombinasi (omzet < 2M/thn AND karyawan < threshold) (strict-AND) vs higher-tier-wins (rekomendasi). Rekomendasi: higher-tier-wins — paling conservative untuk Kesles revenue.
  5. Audit trail overridemerchant_mdr_rate_history table untuk track perubahan mdr_rate + mdr_classification per merchant + timestamp + actor + reason?
  6. Backfill cut-off — backfill merchant.transactions.mdr_rate historis dari kapan? Dari awal data, atau dari tanggal go-live policy?
  7. Skema bagi-hasil tier non-0.7%CLOSED (assumed v1): Kesles share = 0% untuk tier 0.3% / 0.6% / 0.4% / 0%. Update saat skema bank tersedia.
  8. Fokus akuisisiCLOSED: target = merchant Reguler UKE/UME/UBE (MDR 0.7%). UMI & Khusus boleh onboard, revenue Kesles 0% sampai skema baru.
  9. Override merchants.mdr_rate non-0.7% — jika admin override ke nilai non-standar (promo, partner agreement), bagaimana skema bagi-hasil Kesles? Tetap 0.3% flat, proporsional (rate − 0.4%), atau kolom mdr_kesles_share_rate terpisah?
  10. NEW — Fix migration 012 — Buat 044_fix_mdr_rates_bi_march_2025.sql untuk koreksi rate Pendidikan (0.3% → 0.6%), SPBU (0.3% → 0.4%), PPOB/Government (0.5% → 0%) di tabel ref_payment_jenis_kartu_psp. Lihat §2.6.
  11. NEW — Konfirmasi TRANSPORTASI & RUMAH_SAKIT — Rate 0.4% di merchant.ref_mcc_code tidak tercantum eksplisit di tabel BI resmi. Perlu konfirmasi ke Mandiri/BI apakah RS dan transportasi umum mendapat tarif Khusus atau Reguler.

Setelah Q4–Q6 + Q9–Q11 dijawab, dokumen di-promote dari draft v5v1.0 dan §2.1 + §2.5 + §3.3 + §3.4 + §3.5 + §4.5 di-freeze sebagai contract; migration 108 + migration 032 + update kode mobile/backend/dashboard dieksekusi.


7. Action items pasca-sign-off

  1. [URGENT] Migration 044 (db_reference): buat 044_fix_mdr_rates_bi_march_2025.sql — koreksi rate di ref_payment_jenis_kartu_psp sesuai BI 15 Maret 2025 (lihat §2.6). Ini independent dari migration 108 dan bisa dijalankan segera.
  2. Migration 108: tambah mdr_classification di merchants + merchant_registration_requests (enum check UMI/REGULAR/PENDIDIKAN/SPBU/GOVERNMENT).
  3. Update mobile (merchant_registration_page.dart):
    • Replace _mdrRateForScale(scale) dengan _resolveMdrClassification(omzetPerBulan, employeeCount, businessTypeCode) yang return tuple (mdrClassification, mdrRate) per §3.3 + §3.4.
    • Step Profil Usaha + Ringkasan: tampilkan klasifikasi (UMI/Reguler/dst) dengan note kondisional khusus UMI.
    • Body POST tambahkan field mdr_classification.
  4. Update backend (merchant.go):
    • Helper resolveMdrClassification(omzetPerBulan, employeeCount, businessTypeCode) server-side — single source of truth (mobile only mengusulkan, backend selalu recompute & overwrite).
    • INSERT/UPDATE registration_requests + INSERT merchants tambahkan kolom mdr_classification.
  5. Dashboard Registration Queue Review (apps/merchant_dashboard/lib/dashboard/features/registration/, planned folder — current code di features/home/.../forms/manual_registration_wizard_dialog.dart) — implement §4.5:
    • Section "Klasifikasi MDR" di panel detail registrasi.
    • Endpoint baru PATCH /api/dashboard/merchant-registrations/{id}/mdr-classification.
    • Comparison submitted vs server-compute + flag warna + override control.
    • Approve flow stamp values aktif (rekomendasi atau override) ke merchants.mdr_*.
  6. Layer 3 fix: implement computeTransactionMdr di transaction INSERT path (§3.5).
  7. Backfill historis (§5) post Layer 3.
  8. Update merchant-financial-analytics-spec.md §3: mdr_fee_kesles formula dependent ke mdr_classification — saat ini hardcode 0.3% × gross, hanya valid untuk merchant Reguler (klasifikasi 0.7%). Untuk UMI/Pendidikan/SPBU/Government, formula harus return 0 sesuai assumption v1 §2.3.
  9. Update qrisplus-variable-cost-analysis.md:27 dan qrisplus-device-landed-cost-estimation.md:213-214: tegaskan bahwa "MDR Kesles 0.3%" = pass-through dari merchant Reguler 0.7%, tidak berlaku untuk tier lain. Tambahkan footnote ke §2.3 dokumen ini.

8. Referensi

  • [UTAMA] Tabel MDR QRIS Resmi BI: Siaran Pers RDG Februari 2025 — berlaku efektif 15 Maret 2025 — sumber: qris.id / bi.go.id
  • Migration 107: merchant_database/db_kesles_merchant/migrations/legacy/107_merchant_mdr_rate.sql
  • Migration 014 (MCC): merchant_database/db_reference/sql/014_create_ref_mcc_code.sql
  • Migration 012 (OUTDATED rates): merchant_database/db_reference/sql/012_cleanup_payment_cards_mdr_rates.sql ⚠️ Rate Pendidikan & SPBU salah — lihat §2.6
  • Master skala: db_reference.ref_business_scale (UU 20/2008 — display only, bukan penentu MDR)
  • Master jenis usaha: db_reference.ref_business_type (perlu enrichment flag sektor Khusus)
  • Master MCC: merchant.ref_mcc_code (ISO 18245 — rate sudah benar di 014)
  • Kesles share economics: qrisplus-selling-strategy-analysis.md, qrisplus-variable-cost-analysis.md, merchant-financial-analytics-spec.md
  • Mobile UI: apps/mobile_user/lib/features/merchant/presentation/pages/merchant_registration_page.dart
  • Backend: merchant_core_api/internal/merchant/merchant.go (resolveRegistrationMdrRate, mdrRateForBusinessScale)