Modul Wallet & Payment adalah jalur uang — perlu kehati-hatian ekstra. Mencakup saldo, PIN, withdraw, 4 payment gateway, webhook, dan sinkronisasi ke order.
File kunci
| File | Baris | Peran |
|---|---|---|
app/Services/WalletService.php | 423 | Logic wallet: balance, PIN, withdraw, deposit |
app/Services/DokuApiService.php | 337 | Wrapper DOKU API |
app/Http/Controllers/WalletController.php | 706 | Endpoint wallet API |
app/Http/Controllers/DokuCheckoutController.php | 514 | Checkout DOKU + webhook |
app/Http/Controllers/DuitkuController.php | 708 | Pembayaran subscription via Duitku |
app/Http/Controllers/Api/AspireController.php | 614 | Eksperimen Aspire (sandbox) |
app/Http/Controllers/Api/MidtransController.php | — | Tidak terdaftar webhook |
app/Models/Wallet.php | 124 | Model wallet |
app/Models/WalletTransaction.php | 103 | Riwayat transaksi |
config/doku.php | — | Config DOKU, limit, PIN policy |
Model Wallet
MongoDB, SoftDeletes. Field utama:
| Field | Catatan |
|---|---|
balance | Cast integer, default 0 |
pending_balance | Cast integer, default 0 |
status | 'inactive' / 'active' |
pin_hash | Hash PIN (Hash::make), di-hidden dari response |
doku_sub_account_id | ID sub-account DOKU untuk split payment |
activated_at | Cast datetime |
bank_accounts | Array rekening bank embed |
Accessor virtual (bukan field tersimpan)
| Accessor | Fungsi | Catatan |
|---|---|---|
processing_balance | Sum withdraw status PENDING | — |
total_withdrawn | Sum withdraw status SUCCESS | — |
available_balance | Live dari DOKU - pending | HTTP call ke DOKU tiap dipanggil |
WalletTransaction
| Field | Catatan |
|---|---|
wallet_id | FK ke Wallet |
store_id | Multi-tenant scope |
user_id | User inisiator |
type | deposit / withdrawal / settlement |
amount | Cast integer |
fee | Cast integer, default 0 |
net_amount | Auto-fill di creating = amount - fee |
status | pending / processing / success / failed / cancelled |
reference_id | Untuk deposit: ID order |
reference_type | Untuk deposit: 'order' |
metadata | Cast array — bank_code, invoice_number, dll |
completed_at | Cast datetime |
Payment gateway — 4 gateway
| Gateway | Untuk apa | Status |
|---|---|---|
| DOKU | Checkout customer (instant payment) + withdraw ke bank | Aktif, utama |
| Duitku | Pembayaran subscription platform | Aktif |
| Aspire | Eksperimen, masih sandbox | Hardcode credentials |
| Midtrans | Tidak terdaftar webhook | Tidak jalan |
DOKU — sub-account & split payment
Sub-account DOKU = virtual account di bawah merchant utama untuk satu store. Dipakai sebagai mekanisme multi-tenant pemisahan saldo.
- Dibuat saat
WalletService::activateWallet()→DokuApiService::createSubAccount() - Pembayaran customer otomatis split ke sub-account store via
DokuCheckoutController::createCheckout() - Withdraw keluar dari sub-account via
DokuApiService::payout()
Webhook pembayaran
| Path | Gateway | Fungsi |
|---|---|---|
POST /api/doku/notification | DOKU | Mark order paid → trigger OrderObserver |
POST /api/doku/checkout-notification | DOKU Checkout | Mark order paid + notify Engine Bot |
POST /api/duitku/webhook | Duitku | Set invoice subscription paid |
POST /api/aspire/callback | Aspire | Validate signature saja |
POST /api/payment/engine-bot/create-checkout | Engine Bot | Buat checkout internal |
POST /api/payment/engine-bot/check-status/{orderNumber} | Engine Bot | Cek status |
POST /api/doku/sandbox/simulate-webhook | DOKU Sandbox | Dev only |
OrderObserver & wallet sync
Saat payment_status order transisi ke 'paid' dan payment_method === 'instant_payment', OrderObserver memanggil WalletService::processDeposit():
$this->walletService->processDeposit(
storeId: $storeId,
referenceId: $order->id,
amount: (int) $order->total_price,
description: 'Pembayaran order #' . $order->order_number
);Backfill tool: php artisan wallet:sync-orders {--store_id=} — sinkronkan order paid yang belum punya record deposit.
PIN management
PIN disimpan sebagai pin_hash (hash Laravel). Counter attempt & lock state disimpan di Cache, bukan di MongoDB.
Konfigurasi
DOKU_PIN_MAX_ATTEMPTS=5
DOKU_PIN_LOCK_MINUTES=30Alur
| Operasi | Lokasi | Catatan |
|---|---|---|
| Set PIN | WalletController::activate() → WalletService::activateWallet() | Saat aktivasi wallet pertama |
| Verify PIN | WalletService::verifyPin() | Lock otomatis setelah 5x salah, 30 menit |
| Reset PIN (PIN lama) | WalletController::resetPin() | Validasi PIN lama + baru |
| Reset PIN (OTP email) | WalletController::sendResetPinOtp() + resetPinWithOtp() | OTP 6 digit, expired 5 menit |
Withdraw flow
Alur request withdraw (WalletController::withdraw())
- Request:
amount(min 50.000),pin,bank_account_id - Verify PIN langsung via
Hash::check(bypass lock service — lihat catatan di bawah) - Cek saldo:
total = amount + fees(fee dariPlatformSetting::getWithdrawFees()) - Create
WalletTransactionstatusPENDING,type='withdraw' - Mutate wallet balance via raw MongoDB
$inc: { balance: -$totalDeducted } - Commit
Setelah ini, transaksi PENDING diproses di admin-dazo:
- Admin approval/reject
- Call DOKU payout ke rekening bank
- Update status ke
SUCCESS/FAILED
Fee
'service_fee' => 5000 (default)
'doku_fee' => 0 (default)
'total_fee' = service_fee + doku_feeDisimpan di koleksi platform_settings.
Limit
| Aspek | Nilai | Sumber |
|---|---|---|
| Minimum (controller) | Rp 50.000 | WalletController.php:212 |
| Minimum (config) | Rp 10.000 | DOKU_MIN_WITHDRAWAL (tidak dipakai controller) |
| Maksimum basic | Rp 2.000.000 | DOKU_MAX_WITHDRAWAL_BASIC (tidak di-enforce) |
| Maksimum premium | Rp 20.000.000 | DOKU_MAX_WITHDRAWAL_PREMIUM (tidak di-enforce) |
Duitku — subscription payment
Hanya untuk pembayaran paket berlangganan platform, bukan order customer.
| Method | Fungsi |
|---|---|
get_payment_method | List metode pembayaran |
create_invoice | Buat invoice + dispatch PaymentTimer job |
get_transaction | Cek status + upgrade/downgrade store |
handle_callback | Webhook — set invoice paid |
Aspire — eksperimen sandbox
Masih sandbox dengan credentials hardcode di source code (AspireController.php:74-75). Risiko keamanan — perlu dirotasi.
Pola khusus: JWT dari frontend di-forward ke API Aspire via session()->put("third_party_api_token", Crypt::encryptString($token)).
Langkah berikutnya
- Butuh memahami order flow? Baca Order.
- Butuh kirim notif WA setelah pembayaran? Baca WhatsApp.
- Detail auth untuk API call? Baca Auth & JWT.