6.7 KiB
6.7 KiB
QRIS Dynamic Payment Implementation - Sesuai Spec
✅ Implementasi Sesuai Task Specification
1. Environment Variables ✅
QRIS_API_KEY=139139250221910
QRIS_MID=126670220
QRIS_NMID=ID1025466699168
QRIS_BASE_URL=https://qris.interactive.co.id/restapi/qris
QRIS_EXPIRED_MINUTES=30
2. API Generate Invoice ✅
Endpoint: GET {QRIS_BASE_URL}/qris/show_qris.php
Parameters:
do: "create-invoice"apikey: "{QRIS_API_KEY}"mID: "{QRIS_MID}"cliTrxNumber: "{invoice_code}" (no_trx)cliTrxAmount: "{total_amount}"useTip: "no"
Implementation: QrisHelper::createInvoice()
Validasi:
- Minimum amount: Rp 100 ✅
- Maximum untuk QRIS: Rp 70.000 ✅
3. Invoice Handling ✅
Store to Database:
- ✅
invoice_code→no_trx - ✅
qris_invoiceid→qris_invoiceid - ✅
qris_content→qris_qr_code - ✅
qris_request_date→qris_request_date - ✅
amount→jumlah_tagihan + biaya_admin - ✅
status: unpaid→qris_status = 'unpaid' - ✅
created_at→tanggal_request
UI Display:
- ✅ QR Code content tersedia di response
- ✅ NMID tersedia di response
- ✅ Expired countdown: 30 menit
4. Expired Policy ✅
Expired After: 30 menit (dari qris_request_date)
Behavior:
- ✅ Mark status:
expired - ✅ Disable scan: User tidak bisa check status lagi
Implementation:
- Check di
cekStatusQris()sebelum check status - Update
qris_status = 'expired'jika expired
5. API Check Status ✅
Endpoint: GET {QRIS_BASE_URL}/qris/checkpaid_qris.php
Parameters:
do: "checkStatus"apikey: "{QRIS_API_KEY}"mID: "{QRIS_MID}"invid: "{qris_invoiceid}"trxvalue: "{total_amount}"trxdate: "YYYY-MM-DD" (dariqris_request_date)
Implementation: QrisHelper::checkStatus()
Response Handling:
- ✅
status: success→ Checkqris_status - ✅
qris_status: paid→ Auto approve - ✅
qris_status: unpaid→ Continue checking
6. Status Check Policy ✅
Request Timeout: 15 seconds ✅
Retry Rule:
- ✅ Max attempts: 3
- ✅ Retry interval: 15 seconds
- ✅ Total max duration: 45 seconds (3 x 15)
Implementation: QrisHelper::checkStatusWithRetry()
Logic:
- Lakukan request checkStatus
- Jika
qris_status == 'paid'→ Return immediately - Jika
qris_status == 'unpaid'→ Wait 15 seconds, retry - Hentikan jika attempts >= 3
Allowed Triggers:
- ✅ User click check payment button (
/timo/cek_status_qris) - ✅ Controlled backend process (future: cronjob)
Forbidden Patterns:
- ❌ Continuous polling
- ❌ Interval under 15 seconds
- ❌ Auto check on page refresh
- ❌ Infinite loop check
7. Fallback If Still Unpaid ✅
After 3 Attempts:
- ✅ Show upload payment proof form (
show_upload_proof: true) - ✅ Show contact CS (
show_contact_cs: true) - ✅ Store customer phone number (via user data)
- ✅ Mark transaction status:
pending_verification
Response Format:
{
"status": 200,
"pesan": "Silahkan upload bukti pembayaran atau hubungi customer service",
"data": {
"status": "pending_verification",
"check_count": 3,
"message": "Pembayaran belum terdeteksi setelah 3x pengecekan...",
"show_upload_proof": true,
"show_contact_cs": true
}
}
8. Database Schema ✅
File: database/qris_migration.sql
Fields:
- ✅
qris_qr_codeTEXT - ✅
qris_invoiceidVARCHAR(100) - ✅
qris_nmidVARCHAR(100) - ✅
qris_request_dateDATETIME - ✅
qris_expired_atDATETIME - ✅
qris_check_countINT DEFAULT 0 - ✅
qris_last_check_atDATETIME - ✅
qris_statusENUM('unpaid', 'paid', 'expired') - ✅
qris_payment_methodVARCHAR(50) - ✅
qris_payment_customer_nameVARCHAR(255) - ✅
qris_paid_atDATETIME
Indexes:
- ✅
idx_qris_invoiceid - ✅
idx_qris_status - ✅
idx_qris_expired_at
9. Auto Approve Flow ✅
Setelah QRIS Paid:
- ✅ Update
qris_status = 'paid' - ✅ Store payment method & customer name
- ✅ Call TIMO API:
payment/{token} - ✅ Update
status_bayar = 'DIBAYAR' - ✅ Set
tanggal_bayar&jumlah_bayar - ✅ Send WhatsApp notification ke user
10. Webhook (Future) ✅
Architecture Ready:
- Endpoint planned:
/api/webhook/qris - Whitelist domain:
https://timo.wipay.id - Future use: Auto update status, validate signature, prevent double payment
📋 Endpoints
POST /timo/request_pembayaran
Support QRIS:
{
"token": "...",
"no_sl": "...",
"payment_method": "qris"
}
Response (QRIS):
{
"status": 200,
"data": {
"no_trx": "#TIMO...",
"qris_qr_code": "000201010212...",
"qris_invoiceid": 123456,
"qris_nmid": "ID1025466699168",
"qris_request_date": "2025-01-15 14:00:00",
"qris_expired_at": "2025-01-15 14:30:00",
"qris_status": "unpaid"
}
}
POST /timo/cek_status_qris
Check Status dengan Retry:
{
"token": "...",
"no_sl": "..."
}
Response (Paid):
{
"status": 200,
"pesan": "Pembayaran berhasil",
"data": {
"status": "paid",
"payment_method": "gopay",
"customer_name": "John Doe"
}
}
Response (Unpaid - Max Attempts):
{
"status": 200,
"pesan": "Silahkan upload bukti pembayaran...",
"data": {
"status": "pending_verification",
"check_count": 3,
"show_upload_proof": true,
"show_contact_cs": true
}
}
✅ Compliance dengan Constraints
- ✅ API LIVE/PRODUCTION - Menggunakan credentials production
- ✅ Real Payment - Scan QRIS akan memotong saldo e-wallet
- ✅ No Auto Refund - Tidak ada refund otomatis
- ✅ No Aggressive Polling - Max 3 attempts dengan interval 15 detik
🎯 Deliverables
- ✅ QRIS Invoice Generator Service (
QrisHelper::createInvoice()) - ✅ QR Code Renderer (content tersedia di response)
- ✅ Transaction Persistence Layer (database fields)
- ✅ Manual Status Checker (
cekStatusQris()dengan retry) - ✅ Webhook-ready Architecture (endpoint planned)
📝 Testing Checklist
- Test generate invoice untuk amount < 70rb
- Test generate invoice untuk amount > 70rb (should fail)
- Test check status (unpaid) - max 3 attempts
- Test check status (paid) - auto approve
- Test expired QRIS (after 30 minutes)
- Test WhatsApp notification setelah paid
- Test fallback setelah 3 attempts failed
🔄 Flow Diagram
User Request QRIS (< 70rb)
↓
Generate Invoice via API
↓
Store: qris_content, qris_invoiceid, qris_request_date
↓
Display QR Code (30 menit countdown)
↓
User Scan & Pay
↓
User Click "Check Status"
↓
Check Status (max 3x, interval 15s)
↓
If Paid → Auto Approve → WhatsApp
If Unpaid (after 3x) → Show Upload Proof Form