278 lines
6.7 KiB
Markdown
278 lines
6.7 KiB
Markdown
# 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
|
|
```
|