# QRIS Dynamic Payment Implementation - Sesuai Spec ## ✅ Implementasi Sesuai Task Specification ### 1. Environment Variables ✅ ```ini 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" (dari `qris_request_date`) **Implementation:** `QrisHelper::checkStatus()` **Response Handling:** - ✅ `status: success` → Check `qris_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:** 1. Lakukan request checkStatus 2. Jika `qris_status == 'paid'` → Return immediately 3. Jika `qris_status == 'unpaid'` → Wait 15 seconds, retry 4. 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:** ```json { "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_code` TEXT - ✅ `qris_invoiceid` VARCHAR(100) - ✅ `qris_nmid` VARCHAR(100) - ✅ `qris_request_date` DATETIME - ✅ `qris_expired_at` DATETIME - ✅ `qris_check_count` INT DEFAULT 0 - ✅ `qris_last_check_at` DATETIME - ✅ `qris_status` ENUM('unpaid', 'paid', 'expired') - ✅ `qris_payment_method` VARCHAR(50) - ✅ `qris_payment_customer_name` VARCHAR(255) - ✅ `qris_paid_at` DATETIME **Indexes:** - ✅ `idx_qris_invoiceid` - ✅ `idx_qris_status` - ✅ `idx_qris_expired_at` ### 9. Auto Approve Flow ✅ **Setelah QRIS Paid:** 1. ✅ Update `qris_status = 'paid'` 2. ✅ Store payment method & customer name 3. ✅ Call TIMO API: `payment/{token}` 4. ✅ Update `status_bayar = 'DIBAYAR'` 5. ✅ Set `tanggal_bayar` & `jumlah_bayar` 6. ✅ 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:** ```json { "token": "...", "no_sl": "...", "payment_method": "qris" } ``` **Response (QRIS):** ```json { "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:** ```json { "token": "...", "no_sl": "..." } ``` **Response (Paid):** ```json { "status": 200, "pesan": "Pembayaran berhasil", "data": { "status": "paid", "payment_method": "gopay", "customer_name": "John Doe" } } ``` **Response (Unpaid - Max Attempts):** ```json { "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 1. ✅ **API LIVE/PRODUCTION** - Menggunakan credentials production 2. ✅ **Real Payment** - Scan QRIS akan memotong saldo e-wallet 3. ✅ **No Auto Refund** - Tidak ada refund otomatis 4. ✅ **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 ```