D
Engineering

Withdraw Flow

Alur penarikan dana wallet — 3-layer security safeguard, DOKU payout, dan service fee collection.

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

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

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

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

plaintext
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: SUCCESS

Database Fields

WalletTransaction (Withdraw)

json
{
  "_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)

json
{
  "_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

ScenarioAction
Balance check failedRollback ke PENDING, save balance_check_info
DOKU API timeout (balance)Rollback ke PENDING, return 500
Payout failedhandleDokuFailure() → refund wallet + status FAILED
Transfer failedLog error, save failed info, tidak rollback (non-blocking)
Fee collector not configuredLog warning, skip transfer, withdrawal tetap SUCCESS

Reject Flow

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

javascript
// 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") }
})