D
Modul

Wallet & Payment

Jalur uang. Wallet saldo, PIN, withdraw via DOKU, webhook pembayaran, dan integrasi 4 payment gateway.

Modul Wallet & Payment adalah jalur uang — perlu kehati-hatian ekstra. Mencakup saldo, PIN, withdraw, 4 payment gateway, webhook, dan sinkronisasi ke order.

File kunci

FileBarisPeran
app/Services/WalletService.php423Logic wallet: balance, PIN, withdraw, deposit
app/Services/DokuApiService.php337Wrapper DOKU API
app/Http/Controllers/WalletController.php706Endpoint wallet API
app/Http/Controllers/DokuCheckoutController.php514Checkout DOKU + webhook
app/Http/Controllers/DuitkuController.php708Pembayaran subscription via Duitku
app/Http/Controllers/Api/AspireController.php614Eksperimen Aspire (sandbox)
app/Http/Controllers/Api/MidtransController.phpTidak terdaftar webhook
app/Models/Wallet.php124Model wallet
app/Models/WalletTransaction.php103Riwayat transaksi
config/doku.phpConfig DOKU, limit, PIN policy

Model Wallet

MongoDB, SoftDeletes. Field utama:

FieldCatatan
balanceCast integer, default 0
pending_balanceCast integer, default 0
status'inactive' / 'active'
pin_hashHash PIN (Hash::make), di-hidden dari response
doku_sub_account_idID sub-account DOKU untuk split payment
activated_atCast datetime
bank_accountsArray rekening bank embed

Accessor virtual (bukan field tersimpan)

AccessorFungsiCatatan
processing_balanceSum withdraw status PENDING
total_withdrawnSum withdraw status SUCCESS
available_balanceLive dari DOKU - pendingHTTP call ke DOKU tiap dipanggil

WalletTransaction

FieldCatatan
wallet_idFK ke Wallet
store_idMulti-tenant scope
user_idUser inisiator
typedeposit / withdrawal / settlement
amountCast integer
feeCast integer, default 0
net_amountAuto-fill di creating = amount - fee
statuspending / processing / success / failed / cancelled
reference_idUntuk deposit: ID order
reference_typeUntuk deposit: 'order'
metadataCast array — bank_code, invoice_number, dll
completed_atCast datetime

Payment gateway — 4 gateway

GatewayUntuk apaStatus
DOKUCheckout customer (instant payment) + withdraw ke bankAktif, utama
DuitkuPembayaran subscription platformAktif
AspireEksperimen, masih sandboxHardcode credentials
MidtransTidak terdaftar webhookTidak 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

PathGatewayFungsi
POST /api/doku/notificationDOKUMark order paid → trigger OrderObserver
POST /api/doku/checkout-notificationDOKU CheckoutMark order paid + notify Engine Bot
POST /api/duitku/webhookDuitkuSet invoice subscription paid
POST /api/aspire/callbackAspireValidate signature saja
POST /api/payment/engine-bot/create-checkoutEngine BotBuat checkout internal
POST /api/payment/engine-bot/check-status/{orderNumber}Engine BotCek status
POST /api/doku/sandbox/simulate-webhookDOKU SandboxDev only

OrderObserver & wallet sync

Saat payment_status order transisi ke 'paid' dan payment_method === 'instant_payment', OrderObserver memanggil WalletService::processDeposit():

php
$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

plaintext
DOKU_PIN_MAX_ATTEMPTS=5
DOKU_PIN_LOCK_MINUTES=30

Alur

OperasiLokasiCatatan
Set PINWalletController::activate()WalletService::activateWallet()Saat aktivasi wallet pertama
Verify PINWalletService::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())

  1. Request: amount (min 50.000), pin, bank_account_id
  2. Verify PIN langsung via Hash::check (bypass lock service — lihat catatan di bawah)
  3. Cek saldo: total = amount + fees (fee dari PlatformSetting::getWithdrawFees())
  4. Create WalletTransaction status PENDING, type='withdraw'
  5. Mutate wallet balance via raw MongoDB $inc: { balance: -$totalDeducted }
  6. Commit

Setelah ini, transaksi PENDING diproses di admin-dazo:

  • Admin approval/reject
  • Call DOKU payout ke rekening bank
  • Update status ke SUCCESS/FAILED

Fee

php
'service_fee' => 5000 (default)
'doku_fee'    => 0 (default)
'total_fee'   = service_fee + doku_fee

Disimpan di koleksi platform_settings.

Limit

AspekNilaiSumber
Minimum (controller)Rp 50.000WalletController.php:212
Minimum (config)Rp 10.000DOKU_MIN_WITHDRAWAL (tidak dipakai controller)
Maksimum basicRp 2.000.000DOKU_MAX_WITHDRAWAL_BASIC (tidak di-enforce)
Maksimum premiumRp 20.000.000DOKU_MAX_WITHDRAWAL_PREMIUM (tidak di-enforce)

Duitku — subscription payment

Hanya untuk pembayaran paket berlangganan platform, bukan order customer.

MethodFungsi
get_payment_methodList metode pembayaran
create_invoiceBuat invoice + dispatch PaymentTimer job
get_transactionCek status + upgrade/downgrade store
handle_callbackWebhook — 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.