Wallet adalah jalur uang merchant. Ada dua alur: topup (kredit dari pembayaran order) dan withdraw (debit ke rekening bank). Pemrosesan withdraw sebenarnya tidak ada di dazoapp — hanya request, lalu disimpan PENDING.
Ringkasan alur
TOPUP (kredit):
Customer bayar via DOKU
→ webhook DokuNotificationController/DokuCheckoutController
→ set order.payment_status='paid' + $order->save()
→ OrderObserver@updated trigger (hanya jika isDirty payment_status)
→ IF payment_method='instant_payment':
WalletService::processDeposit()
→ WalletTransaction type='deposit' status='success'
→ $wallet->balance += $amount (TANPA lock)
WITHDRAW (debit):
POST /api/wallet/withdraw
→ WalletController@withdraw
→ validasi PIN (inline, TANPA tracking gagal)
→ validasi amount >= 50000
→ WalletTransaction type='withdraw' status='PENDING'
→ DB::collection('wallets')->update(['$inc' => ['balance' => -$amount]]) ← atomic
→ selesai. Tidak ada approval/payout di dazoapp.
→ status PENDING → ? (dikelola sistem eksternal, tidak ada di repo ini)1. Topup flow
Endpoint & controller
Tidak ada endpoint topup langsung. Topup wallet terjadi implisit lewat pembayaran order.
| Endpoint | Handler | Fungsi |
|---|---|---|
POST /api/payment/create-checkout (routes/api.php:801) | DokuCheckoutController@createCheckout | Buat payment URL via DOKU |
POST /api/payment/engine-bot/create-checkout (routes/api.php:805) | EngineBotPaymentController@createCheckout | Variant untuk Engine Bot |
GET /payment/pending/{orderId} (routes/api.php:812) | DokuPaymentController@getPendingPayment | Cek status pending |
POST /payment/check-status/{orderId} (routes/api.php:813) | DokuPaymentController@checkPaymentStatus | Polling status real-time |
Webhook DOKU
| Route | Handler | Verifikasi |
|---|---|---|
POST /api/doku/notification (routes/api.php:798) | DokuNotificationController@handle (:18) | HMAC-SHA256 inline (:36-53) |
POST /api/doku/checkout-notification (routes/api.php:802) | DokuCheckoutController@handleNotification (:311) | DokuApiService@verifyCheckoutSignature (:321) |
Catatan: Duitku (
POST /api/duitku/webhook) hanya untuk subscription, tidak sentuh wallet.
Trigger update balance
- Webhook set
order->payment_status = 'paid'lalu$order->save()(DokuNotificationController.php:78-79,DokuCheckoutController.php:385-400) OrderObserver@updated(app/Observers/OrderObserver.php:18) menangkap transisipayment_status→'paid'(cekisDirtydi:21-24)- Hanya jika
payment_method === 'instant_payment'(:32), panggilWalletService@processDeposit(:34, implementasiWalletService.php:340) processDepositmembuatWalletTransactiontype='deposit'status='success'(:352-365) lalu$wallet->balance += $amount(:367-368)
2. Withdraw flow
Endpoint
POST /api/wallet/withdraw (routes/api.php:782) → WalletController@withdraw (:209). Middleware: auth, completed, loginlog, throttle:100,1.
Validasi
| Field | Aturan | Sumber |
|---|---|---|
amount | min:50000 + cek eksplisit < 50000 | WalletController.php:212,258-260 |
pin | size:6, diverifikasi Hash::check inline | WalletController.php:213,253-255 |
bank_account_id | required | WalletController.php:214 |
Yang dilakukan dazoapp
Request-only → PENDING. Controller membuat WalletTransaction dengan status='PENDING' dan mengurangi balance atomik, tanpa memanggil DOKU payout:
\DB::collection('wallets')
->where('id', $wallet->id)
->update([
'$inc' => ['balance' => -$totalDeducted],
...
]);(WalletController.php:300-309)
Field yang di-set saat request withdraw
| Field | Nilai | Sumber |
|---|---|---|
id | UUID baru | WalletController.php:277 |
wallet_id | $wallet->id | :279 |
store_id | $userStore->store_id | :280 |
user_id | auth()->user()->id | :281 |
type | 'withdraw' (literal, bukan konstanta TYPE_WITHDRAWAL='withdrawal') | :282 |
amount | integer | :283 |
fees | array dari PlatformSetting::getWithdrawFees() | :284 |
net_amount | = amount (user terima full) | :285 |
status | 'PENDING' (uppercase, bukan konstanta 'pending' lowercase) | :286 |
reference_id | 'WDR-{timestamp}-{hash}' | :287 |
description | "Penarikan ke {bank_name} - {account_number}" | :288 |
doku_payout_invoice | 'WD-{timestamp}-{hash}' | :289 |
rejection_reason | null | :290 |
bank_account | {id, bank_code, bank_name, account_number, account_name} | :291-297 |
Status transisi: PENDING → ?
Definisi di WalletTransaction.php:25-38:
pending → processing → success → failed → cancelled
PENDING → APPROVED → PROCESSING → SUCCESS / FAILED
PENDING → REJECTEDAPPROVED dan REJECTED didokumentasikan sebagai status yang ditangani admin (komentar WalletTransaction.php:33-38). Tapi:
WalletService::requestWithdrawal() adalah dead code
Method lengkap (WalletService.php:211) mengimplementasikan DOKU payout + rollback — tapi tidak pernah dipanggil dari controller/route mana pun. Controller WalletController@withdraw punya implementasi inline sendiri dengan field berbeda (fees, bank_account, doku_payout_invoice di controller vs metadata.bank_code di service).
3. Balance & transaction model
Model Wallet (app/Models/Wallet.php)
- Collection:
wallets, MongoDB,use SoftDeletes $guarded = [](mass-assignable semua field)$hidden = ['pin_hash']- Field utama:
balance(int),pending_balance(int),status('active'/'inactive', default'inactive'),doku_sub_account_id,bank_accounts(array embedded) - Appended attributes (virtual, di-recompute saat baca):
processing_balance— sumWalletTransactiontype='withdraw'status='PENDING'(Wallet.php:78-84)available_balance— query real-time ke DOKUgetSubAccountBalanceminus pending withdraw (:100-123)total_withdrawn— sum transaksiwithdrawstatus='SUCCESS'(:89-95)
Model WalletTransaction (app/Models/WalletTransaction.php)
- Collection:
wallet_transactions, tidak SoftDeletes - Type konstanta:
deposit,withdrawal,settlement— tapi controller pakai'withdraw'literal - Status konstanta (lowercase):
pending,processing,success,failed,cancelled— tapi controller pakai'PENDING'uppercase - Casts:
metadata→array,amount/fee/net_amount→integer,completed_at/created_at/updated_at→datetime
4. Race condition & security
Tidak ada pessimistic lock
WalletController@withdrawpakaiDB::beginTransaction()(:275) + MongoDB atomic$inc(:300-309) — atomic di level dokumen, tapi tidak cegah double-request antar request paralel sebelum validasi balanceWalletService@processDeposit(:367-368) danrequestWithdrawal(:264-266) tanpa transaksi/lock sama sekalilockForUpdatehanya dipakai diOrderController(:2248,2257) — bukan wallet
Idempotency webhook tidak fully aman
Tidak ada idempotency key eksplisit. Yang ada hanya pengecekan status:
DokuNotificationController.php:73-75: skip jikaorder.payment_status === 'paid'DokuCheckoutController.php:376-382: sama
Race antara 2 webhook sampai bersamaan bisa menyebabkan processDeposit terpanggil 2x (OrderObserver trigger di updated event, dan 2 save paralel bisa lewat cek isDirty).
Minimum amount & PIN
- Controller:
min:50000(WalletController.php:212) - Config:
doku.wallet.min_withdrawal = 10000(config/doku.php:27) — hanya dipakai diWalletService@requestWithdrawalyang tidak pernah dipanggil
5. Cross-reference ke admin-dazo
Inkonsistensi yang ditemukan
| Masalah | Detail |
|---|---|
| Type literal vs konstanta | WalletTransaction::TYPE_WITHDRAWAL = 'withdrawal' tapi controller tulis type='withdraw'. Filter processing_balance di Wallet.php:81 pakai 'withdraw' (cocok controller, tidak cocok konstanta) |
| Status case | STATUS_PENDING = 'pending' (lowercase) tapi controller tulis 'PENDING' (uppercase). Filter Wallet.php:82 pakai 'PENDING' |
| Dead code | WalletService@requestWithdrawal tidak pernah dipanggil |
| Implementasi paralel | WalletService@processDeposit & controller withdraw tidak share logic — dua implementasi paralel |
Referensi file
| File | Peran |
|---|---|
app/Models/Wallet.php | Model + accessor virtual |
app/Models/WalletTransaction.php | Model transaksi + konstanta |
app/Http/Controllers/WalletController.php | Endpoint withdraw + PIN verification |
app/Services/WalletService.php | Deposit logic + dead code withdrawal |
app/Services/DokuApiService.php | Integrasi DOKU (payout tidak dipakai) |
app/Observers/OrderObserver.php | Trigger deposit saat order paid |
app/Http/Controllers/DokuCheckoutController.php | Create checkout + handle webhook |
app/Http/Controllers/DokuNotificationController.php | Handle webhook generik |