Overview
Withdraw adalah proses penarikan dana dari wallet toko ke rekening bank user. Admin Dazo mengimplementasikan 3-layer security safeguard untuk mencegah double-spend dan error.
Security Safeguards
1. Status Validation
if ($transaction->status !== 'PENDING') {
return response()->json(['message' => 'Transaksi sudah diproses sebelumnya'], 422);
}2. Optimistic Locking
Atomic MongoDB update — hanya 1 admin yang bisa lock:
$updated = DB::collection('wallet_transactions')
->where('_id', $transaction->id)
->where('status', 'PENDING')
->update([
'status' => 'PROCESSING',
'processed_by' => $admin->email,
'processing_started_at' => now(),
]);
if ($updated === 0) {
return response()->json(['message' => 'Transaksi sedang diproses oleh admin lain'], 409);
}3. DOKU Balance Check
$balanceCheck = $this->dokuService->getBalance($wallet->doku_sub_account_id);
$dokuBalance = $balanceCheck['data']['balance'] ?? 0;
if ($dokuBalance < $requiredAmount) {
// Rollback ke PENDING
$transaction->update(['status' => 'PENDING']);
return response()->json(['message' => 'Saldo DOKU tidak mencukupi'], 422);
}Complete Flow
1. Admin klik "Approve"
│
▼
2. ✅ Status check: PENDING?
└─ No → 422 "Already processed"
│
▼
3. ✅ Optimistic lock: PENDING → PROCESSING
└─ Failed → 409 "Admin lain sedang proses"
│
▼
4. ✅ DOKU balance check
└─ Insufficient → Rollback → 422
│
▼
5. 💰 DOKU Payout (ke rekening user)
└─ Failed → handleDokuFailure() → Refund + FAILED
│
▼
6. 💸 Transfer Service Fee (non-blocking)
└─ Failed → Log + continue (tidak affect withdrawal)
│
▼
7. ✅ Update: SUCCESSDatabase Fields
WalletTransaction (Withdraw)
{
"_id": "uuid",
"wallet_id": "uuid",
"type": "withdraw",
"amount": 100000,
"status": "SUCCESS",
"fees": {
"service_fee": 5000,
"doku_fee": 3000
},
"bank_account": {
"bank_code": "BCA",
"account_number": "1234567890",
"account_name": "John Doe"
},
"doku_payout_invoice": "INV-20260625-abc123",
"service_fee_transfer": {
"invoice": "TRF-20260625-def456",
"amount": 5000,
"destination": "SAC-xxxx-xxxxxxxxxxxxx",
"status": "SUCCESS",
"transferred_at": "2026-06-25T14:30:00.000000Z"
},
"processed_by": "admin@dazo.id",
"processing_started_at": "2026-06-25T14:29:00.000000Z",
"completed_at": "2026-06-25T14:30:00.000000Z"
}PlatformSetting (Fee Config)
{
"_id": "platform",
"withdraw": {
"service_fee": 5000,
"doku_fee": 3000,
"updated_at": "2026-06-25T00:00:00.000000Z",
"updated_by": "admin@dazo.id"
},
"fee_collector_account": {
"account_id": "SAC-xxxx-xxxxxxxxxxxxx",
"email": "fee-collector@dazo.co.id",
"name": "Dazo Platform Fee Collector",
"created_at": "2026-06-25T00:00:00.000000Z",
"created_by": "admin@dazo.id"
}
}Error Handling
| Scenario | Action |
|---|---|
| Balance check failed | Rollback ke PENDING, save balance_check_info |
| DOKU API timeout (balance) | Rollback ke PENDING, return 500 |
| Payout failed | handleDokuFailure() → refund wallet + status FAILED |
| Transfer failed | Log error, save failed info, tidak rollback (non-blocking) |
| Fee collector not configured | Log warning, skip transfer, withdrawal tetap SUCCESS |
Reject Flow
// WithdrawController::reject()
$transaction->update([
'status' => 'REJECTED',
'rejected_by' => $admin->email,
'rejection_reason' => $request->reason,
]);
// Refund ke wallet
$wallet->increment('balance', $transaction->amount);
$wallet->decrement('processing_balance', $transaction->amount);Monitoring Queries
// Failed balance checks (24h terakhir)
db.wallet_transactions.find({
balance_check_failed_at: { $gte: ISODate("2026-06-25T00:00:00Z") }
})
// Failed service fee transfers
db.wallet_transactions.find({
"service_fee_transfer.status": "FAILED"
})
// Stuck PROCESSING (>1 jam)
db.wallet_transactions.find({
status: "PROCESSING",
processing_started_at: { $lt: ISODate("2026-06-25T13:00:00Z") }
})