# 🔴 COMPREHENSIVE CODE AUDIT REPORT
## Project: cbtQ - Laravel CBT System with Token System

**Audit Date:** 2026-03-25  
**Auditor:** Senior Backend Engineer  
**Focus:** Data Integrity, Race Conditions, Transaction Safety, Architecture

---

## 🚨 CRITICAL ISSUES (MUST FIX IMMEDIATELY)

### 1. RACE CONDITION IN WITHDRAWAL APPROVAL ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/SuperAdmin/WithdrawController.php` (Line 72-74)

**Vulnerable Code:**
```php
// Deduct from sales commission balance
$sales = $withdraw->sales;
if ($sales) {
    $sales->commission_balance -= $withdraw->amount;
    $sales->save();
}
```

**Problem:**
- Uses read-modify-write pattern: Read balance → Modify → Save
- Vulnerable to concurrent requests
- **Example Scenario:**
  - Request A: Reads balance = 100,000
  - Request B: Reads balance = 100,000
  - Request A: Deducts 50,000, saves balance = 50,000
  - Request B: Deducts 30,000, saves balance = 70,000
  - **Result:** Should be 20,000, but is 70,000 - 50,000 lost!

**Impact:**
- Double-spending of commissions
- Financial data corruption
- Sales can withdraw more than they have

**Fix:**
```php
$sales->decrement('commission_balance', $withdraw->amount);
```

---

### 2. RACE CONDITION IN WITHDRAWAL CREATION ⚠️ SEVERITY: CRITICAL

**Location:** `app/Services/Sales/SalesWithdrawService.php` (Line 97-99)

**Vulnerable Code:**
```php
// Update balance
$sales->update([
    'commission_balance' => $sales->commission_balance - $validated['amount'],
]);
```

**Problem:**
- Same race condition issue as above
- User can create multiple withdrawal requests simultaneously
- Balance check happens before update, not atomic

**Impact:**
- Users can withdraw more than their balance
- Negative balances possible
- Financial loss

**Fix:**
```php
$sales->decrement('commission_balance', $validated['amount']);
```

**Additional Fix Needed:** Add validation BEFORE decrement:
```php
if ($sales->commission_balance < $validated['amount']) {
    return response()->json([
        'success' => false,
        'message' => 'Saldo tidak mencukupi.'
    ], 400);
}

$sales->decrement('commission_balance', $validated['amount']);
```

---

### 3. RACE CONDITION IN WITHDRAWAL REJECTION ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/SuperAdmin/WithdrawController.php` (Line 103-106)

**Vulnerable Code:**
```php
// Refund commission balance to sales
$sales = $withdraw->sales;
if ($sales) {
    $sales->commission_balance += $withdraw->amount;
    $sales->save();
}
```

**Problem:**
- Non-atomic increment operation
- Race condition can cause balance corruption

**Impact:**
- Incorrect refund amounts
- Duplicate refunds in concurrent rejections

**Fix:**
```php
$sales->increment('commission_balance', $withdraw->amount);
```

---

### 4. MISSING TRANSACTION IN TOKEN ORDER APPROVAL ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/SuperAdmin/TokenController.php` (Line 145-169)

**Vulnerable Code:**
```php
if ($request->status === 'completed') {
    DB::beginTransaction();
    try {
        // Find LBB
        $lbb = Lbb::findOrFail($order->lbb_id);

        // Update LBB token balance
        $lbb->token_balance += $order->token_amount;
        $lbb->save();

        // Create token transaction record
        TokenTransaction::create([...]);

        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        return redirect()->route('super-admin.token.transactions')
            ->with('error', 'Gagal menyelesaikan order: ' . $e->getMessage());
    }
}

// Update status and admin notes
$order->status = $request->status;
if ($request->has('admin_notes')) {
    $order->admin_notes = $request->admin_notes;
}
$order->save();
```

**Problem:**
- Transaction only wraps token balance update
- Order status update happens OUTSIDE transaction
- If order save fails, tokens already credited
- **Partial commit scenario:**
  1. Token balance updated (committed)
  2. Transaction record created (committed)
  3. Transaction committed
  4. Order status update fails (outside transaction)
  5. **Result:** Tokens credited but order not marked completed

**Impact:**
- Tokens given without proper order tracking
- Data inconsistency between orders and token transactions
- Financial loss

**Fix:** Move order status update INSIDE transaction:
```php
if ($request->status === 'completed') {
    DB::beginTransaction();
    try {
        $lbb = Lbb::findOrFail($order->lbb_id);
        $lbb->increment('token_balance', $order->token_amount);

        TokenTransaction::create([...]);

        // Update order INSIDE transaction
        $order->status = $request->status;
        if ($request->has('admin_notes')) {
            $order->admin_notes = $request->admin_notes;
        }
        $order->save();

        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        return redirect()->route('super-admin.token.transactions')
            ->with('error', 'Gagal menyelesaikan order: ' . $e->getMessage());
    }
} else {
    // Handle non-completed status changes
    $order->status = $request->status;
    if ($request->has('admin_notes')) {
        $order->admin_notes = $request->admin_notes;
    }
    $order->save();
}
```

---

### 5. RACE CONDITION IN TOKEN INJECTION ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/SuperAdmin/TokenController.php` (Line 103-105)

**Vulnerable Code:**
```php
// ✅ Atomic update
$lbb->increment('token_balance', $request->amount);
```

**Status:** ✅ FIXED - Using atomic `increment()` - GOOD!

---

### 6. MISSING TRANSACTION IN WITHDRAWAL APPROVAL ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/SuperAdmin/WithdrawController.php` (Line 56-81)

**Problem:**
- No transaction wrapping withdrawal approval
- Balance deduction and withdraw record update not atomic
- If withdraw update fails, balance already deducted
- **Partial commit scenario:**
  1. Sales balance deducted
  2. Withdraw proof uploaded
  3. Withdraw status update fails
  4. **Result:** Balance deducted but withdrawal not recorded

**Impact:**
- Money lost without proper withdrawal record
- Data inconsistency
- Financial loss

**Fix:**
```php
DB::beginTransaction();
try {
    $withdraw = Withdraw::lockForUpdate()->findOrFail($id);

    if ($withdraw->status != 'pending') {
        DB::rollBack();
        return redirect()->route('super-admin.withdraw.index')
            ->with('error', 'Pencairan ini sudah diproses.');
    }

    // Upload proof
    if ($request->hasFile('proof')) {
        $proofPath = $request->file('proof')->store('withdraw-proofs', 'public');
        $withdraw->proof = $proofPath;
    }

    $withdraw->status = 'approved';
    $withdraw->notes = $request->notes;
    $withdraw->processed_by = auth()->id();
    $withdraw->save();

    // Deduct from sales commission balance
    $sales = $withdraw->sales;
    if ($sales) {
        $sales->decrement('commission_balance', $withdraw->amount);
    }

    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    return redirect()->route('super-admin.withdraw.index')
        ->with('error', 'Gagal menyetujui pencairan: ' . $e->getMessage());
}
```

---

### 7. MISSING TRANSACTION IN WITHDRAWAL REJECTION ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/SuperAdmin/WithdrawController.php` (Line 83-106)

**Problem:**
- Same issue as approval
- No transaction wrapping

**Fix:** Same pattern as approval, with `increment()` for refund.

---

### 8. DOUBLE DEDUCTION BUG IN WITHDRAWALS ⚠️ SEVERITY: CRITICAL

**Location:** 
- `app/Services/Sales/SalesWithdrawService.php` (Line 97-99)
- `app/Http/Controllers/SuperAdmin/WithdrawController.php` (Line 72-74)

**Problem:**
1. When user creates withdrawal: Balance deducted immediately
2. When admin approves: Balance deducted AGAIN
3. **Result:** Double deduction!

**Example:**
- User has 100,000
- User requests 50,000 withdrawal
- `withdraw()` deducts 50,000 → Balance = 50,000
- Admin approves
- `approve()` deducts 50,000 → Balance = 0
- **Actual amount sent:** 50,000
- **Balance lost:** 50,000

**Impact:**
- Severe financial loss
- Every withdrawal loses money

**Fix Options:**

**Option A (Recommended):** Don't deduct on creation, only on approval
```php
// In SalesWithdrawService.php - withdraw()
// REMOVE the balance deduction here
// Withdraw::create([...]);
// $sales->update([...]); // DELETE THIS

// Balance only deducted in approve()
```

**Option B:** Don't deduct on approval, only on creation
```php
// In WithdrawController.php - approve()
// Remove balance deduction here
// $sales->commission_balance -= $withdraw->amount; // DELETE THIS
```

**Recommended:** Option A is better because:
- Clearer flow: Request → Review → Approve → Deduct
- Easier to handle rejections
- Better audit trail

---

### 9. TOKEN USAGE NOT IMPLEMENTED ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/Siswa/SiswaCBTController.php` - `start()` method

**Problem:**
- Exam attempts don't deduct tokens
- Token balance never decreases
- Token system is broken

**Code Flow:**
```php
public function start(Request $request, $id)
{
    // ... validation ...
    
    // Create new attempt
    DB::beginTransaction();
    try {
        $attempt = ExamAttempt::create([...]);
        DB::commit();
    } catch (\Throwable $e) {
        DB::rollBack();
        return response()->json([...], 500);
    }
    // NO TOKEN DEDUCTION HERE!
}
```

**Impact:**
- LBBs get unlimited free exams
- No revenue from token sales
- Business model broken

**Fix:**
```php
public function start(Request $request, $id)
{
    // ... existing validation ...
    
    DB::beginTransaction();
    try {
        // Lock LBB row for update to prevent race condition
        $lbb = $student->lbb->lockForUpdate();
        
        // Check token balance
        $tokenCost = 1; // Or get from exam config
        if ($lbb->token_balance < $tokenCost) {
            DB::rollBack();
            return response()->json([
                'success' => false,
                'message' => 'Token tidak mencukupi!'
            ], 403);
        }
        
        // Deduct token atomically
        $lbb->decrement('token_balance', $tokenCost);
        
        // Create usage transaction
        TokenTransaction::create([
            'lbb_id' => $lbb->id,
            'amount' => $tokenCost,
            'type' => 'usage',
            'notes' => 'Exam: ' . $exam->name,
            'created_by' => $student->id,
        ]);
        
        // Create attempt
        $attempt = ExamAttempt::create([...]);
        
        DB::commit();
    } catch (\Throwable $e) {
        DB::rollBack();
        return response()->json([
            'success' => false,
            'message' => $e->getMessage()
        ], 500);
    }
    
    // ... rest of code ...
}
```

---

### 10. NO VALIDATION OF AVAILABLE TOKENS IN EXAM START ⚠️ SEVERITY: CRITICAL

**Location:** `app/Http/Controllers/Siswa/SisuwaCBTController.php` - `start()` method

**Problem:**
- No check if LBB has enough tokens
- Students can take exams even with 0 balance
- Race condition: Multiple students could start exam simultaneously with insufficient tokens

**Impact:**
- Negative token balance possible
- Free exam access without payment
- Business model broken

**Fix:** See Fix #9 above - includes validation and atomic deduction with row locking.

---

## 🟠 HIGH PRIORITY ISSUES

### 11. NESTED TRANSACTION IN TOKEN INJECTION

**Location:** `app/Http/Controllers/SuperAdmin/TokenController.php` (Line 87-127)

**Problem:**
```php
DB::beginTransaction();

try {
    DB::transaction(function () use ($request) {
        // ... logic ...
    });
    // ...
}
```

- Uses `DB::transaction()` inside `DB::beginTransaction()`
- Nested transactions can cause unexpected behavior
- Confusing structure

**Fix:** Remove the outer `DB::beginTransaction()`:
```php
try {
    DB::transaction(function () use ($request) {
        // ... logic ...
    });
    
    return redirect()->route('super-admin.token.index')
        ->with('success', "Berhasil inject {$request->amount} token ke {$lbb->name}");
    
} catch (\Exception $e) {
    return redirect()->route('super-admin.token.index')
        ->with('error', 'Gagal inject token: ' . $e->getMessage());
}
```

---

### 12. NO ROW LOCKING IN APPROVAL/REJECTION OPERATIONS

**Location:** 
- `app/Http/Controllers/SuperAdmin/WithdrawController.php`
- `app/Http/Controllers/SuperAdmin/TokenController.php`

**Problem:**
- Multiple admins could process same order/withdrawal simultaneously
- Race conditions even with atomic operations

**Impact:**
- Double processing of withdrawals
- Duplicate token injections
- Data corruption

**Fix:** Use `lockForUpdate()` when fetching records:
```php
$withdraw = Withdraw::lockForUpdate()->findOrFail($id);
```

---

### 13. TOKEN ORDER CREATION DOESN'T CHECK BALANCE

**Location:** `app/Services/Admin/AdminTokenService.php` - `purchaseToken()` method

**Problem:**
- No validation if LBB can afford the purchase
- LBB could create unlimited orders without payment

**Impact:**
- Financial risk
- Bad debt accumulation

**Fix:**
```php
// In purchaseToken() method
if ($lbb->token_balance < $tokenAmount) {
    return redirect()->route('admin.token')
        ->with('error', 'Saldo token tidak mencukupi. Silakan beli token terlebih dahulu.');
}
```

---

### 14. MISSING AUDIT TRAIL FOR FINANCIAL OPERATIONS

**Location:** Multiple files

**Problem:**
- No comprehensive audit logging
- Difficult to trace financial transactions
- Hard to debug issues

**Recommendation:**
- Create audit log table
- Log all financial operations with:
  - User who performed action
  - Timestamp
  - Before/after values
  - IP address
  - Request ID

---

### 15. NO DATABASE CONSTRAINTS FOR CRITICAL RULES

**Location:** Database migrations

**Problem:**
- No check constraints for negative balances
- No triggers for balance validation
- Relies on application logic only

**Recommendation:**
- Add database-level constraints where possible
- Consider using stored procedures for complex validations
- Add indexes for performance on frequently queried columns

---

## 🟡 MEDIUM PRIORITY ISSUES

### 16. INCONSISTENT ERROR HANDLING

**Location:** Various controllers

**Problem:**
- Some methods return JSON responses
- Some methods return redirect with flash messages
- Inconsistent error handling patterns

**Recommendation:** Standardize error handling approach.

---

### 17. NO VALIDATION OF COMMISSION BALANCE BEFORE INCREMENT

**Location:** `app/Http/Controllers/SuperAdmin/TokenController.php` (Line 115-125)

**Problem:**
- Commission increment happens without validation
- Could cause overflow issues in extreme cases

**Recommendation:** Add validation or use BIGINT for balance fields.

---

### 18. MAGIC NUMBERS IN CODE

**Location:** Multiple files

**Problem:**
- Hardcoded values like `1000`, `100000`, etc.
- Difficult to maintain

**Example:**
```php
if ($validated['amount'] < $minWithdraw) {
    // ...
}
```

**Recommendation:** Use constants or configuration values.

---

## 🟢 GOOD PRACTICES FOUND

### ✅ Proper Use of Service Layer
- Controllers are thin
- Business logic in service classes
- Good separation of concerns

### ✅ Atomic Operations in Some Places
- Token injection uses `increment()`
- Good example to follow elsewhere

### ✅ Transaction Usage (Some Places)
- Some operations wrapped in transactions
- Need to expand to all critical operations

### ✅ Validation Present
- Request validation in place
- Form requests used in some areas

### ✅ Relationship Definitions
- Model relationships well-defined
- Eloquent relationships used properly

---

## 🧠 ARCHITECTURE FLOW ANALYSIS

### Token Purchase Flow

```
1. Admin submits purchase request
   ↓
2. AdminTokenService::purchaseToken()
   - Validates request
   - Creates TokenOrder (status: pending)
   - Creates TokenTransaction (type: inject) ❌ BUG: Creates inject record before approval
   - Redirects to payment page
   ↓
3. Admin uploads proof
   - AdminTokenService::uploadProof()
   - Updates order status to 'verification'
   - Saves proof file
   ↓
4. SuperAdmin approves order
   - TokenController::updateOrderStatus()
   - Updates LBB token_balance (non-atomic) ❌ BUG: Race condition
   - Creates TokenTransaction (type: inject) - DUPLICATE!
   - Updates order status (outside transaction) ❌ BUG: Partial commit
   ↓
5. Tokens available to LBB
```

**Issues:**
- Double creation of TokenTransaction
- Transaction created before approval
- Non-atomic balance update
- Order status outside transaction

---

### Withdrawal Flow

```
1. Sales creates withdrawal request
   ↓
2. SalesWithdrawService::withdraw()
   - Validates amount
   - Creates Withdraw record (status: pending)
   - Deducts from commission_balance (non-atomic) ❌ BUG: Race condition
   ↓
3. SuperAdmin approves/rejects
   ↓
4A. Approve:
   - WithdrawController::approve()
   - Uploads proof
   - Updates withdraw status
   - Deducts from commission_balance (non-atomic) ❌ BUG: Double deduction!
   - No transaction ❌ BUG: Partial commit risk
   ↓
4B. Reject:
   - WithdrawController::reject()
   - Updates withdraw status
   - Increments commission_balance (non-atomic) ❌ BUG: Race condition
   - No transaction ❌ BUG: Partial commit risk
```

**Issues:**
- Double deduction
- No transactions
- Race conditions everywhere
- Balance updates not atomic

---

### Exam Flow

```
1. Student starts exam
   ↓
2. SiswaCBTController::start()
   - Validates student access
   - Selects random package
   - Creates ExamAttempt
   - NO TOKEN DEDUCTION ❌ BUG: Broken token system
   ↓
3. Student takes exam
   - Saves answers periodically
   ↓
4. Student submits exam
   - SiswaCBTController::submit()
   - Calculates score
   - Updates attempt status
   - No refund logic (if exam aborted)
   ↓
5. Result shown
```

**Issues:**
- No token usage
- No token deduction
- No refund mechanism for failed/incomplete exams

---

## 🔧 REFACTORED CODE EXAMPLES

### Fix #1, #2, #3: Withdrawal Operations with Proper Transaction and Atomic Operations

```php
// app/Http/Controllers/SuperAdmin/WithdrawController.php

public function approve(Request $request, $id)
{
    $request->validate([
        'proof' => 'required|image|max:5120',
        'notes' => 'nullable|string|max:500',
    ]);

    DB::beginTransaction();
    try {
        // Lock row to prevent concurrent modifications
        $withdraw = Withdraw::lockForUpdate()->findOrFail($id);

        if ($withdraw->status != 'pending') {
            DB::rollBack();
            return redirect()->route('super-admin.withdraw.index')
                ->with('error', 'Pencairan ini sudah diproses.');
        }

        // Upload proof
        if ($request->hasFile('proof')) {
            $proofPath = $request->file('proof')->store('withdraw-proofs', 'public');
            $withdraw->proof = $proofPath;
        }

        $withdraw->status = 'approved';
        $withdraw->notes = $request->notes;
        $withdraw->processed_by = auth()->id();
        $withdraw->save();

        // Deduct from sales commission balance atomically
        $sales = $withdraw->sales;
        if ($sales) {
            $sales->decrement('commission_balance', $withdraw->amount);
        }

        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        \Log::error('Withdraw approval error: ' . $e->getMessage());
        return redirect()->route('super-admin.withdraw.index')
            ->with('error', 'Gagal menyetujui pencairan: ' . $e->getMessage());
    }

    return redirect()->route('super-admin.withdraw.index')
        ->with('success', "Pencairan dari {$withdraw->sales->user->name} berhasil disetujui!");
}

public function reject(Request $request, $id)
{
    $request->validate([
        'notes' => 'required|string|max:500',
    ]);

    DB::beginTransaction();
    try {
        // Lock row to prevent concurrent modifications
        $withdraw = Withdraw::lockForUpdate()->findOrFail($id);

        if ($withdraw->status != 'pending') {
            DB::rollBack();
            return redirect()->route('super-admin.withdraw.index')
                ->with('error', 'Pencairan ini sudah diproses.');
        }

        $withdraw->status = 'rejected';
        $withdraw->notes = $request->notes;
        $withdraw->processed_by = auth()->id();
        $withdraw->save();

        // Refund commission balance atomically
        $sales = $withdraw->sales;
        if ($sales) {
            $sales->increment('commission_balance', $withdraw->amount);
        }

        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        \Log::error('Withdraw rejection error: ' . $e->getMessage());
        return redirect()->route('super-admin.withdraw.index')
            ->with('error', 'Gagal menolak pencairan: ' . $e->getMessage());
    }

    return redirect()->route('super-admin.withdraw.index')
        ->with('success', "Pencairan dari {$withdraw->sales->user->name} berhasil ditolak! Saldo telah dikembalikan.");
}
```

---

### Fix #2: Withdrawal Creation without Balance Deduction

```php
// app/Services/Sales/SalesWithdrawService.php

public function withdraw(Request $request)
{
    $user = Auth::user();

    $sales = Sales::where('user_id', $user->id)->first();
    if (!$sales) {
        return response()->json([
            'success' => false,
            'message' => 'Data sales tidak ditemukan.'
        ], 404);
    }

    // Validate input
    $validated = $request->validate([
        'amount' => 'required|numeric|min:1000',
    ]);

    // Minimum withdraw
    $minWithdrawSetting = Setting::where('key_name', 'min_withdrawal')->first();
    $minWithdraw = $minWithdrawSetting ? (int)$minWithdrawSetting->value : 100000;

    // Withdrawal fee
    $withdrawFeeSetting = Setting::where('key_name', 'withdrawal_fee')->first();
    $withdrawFee = $withdrawFeeSetting ? (int)$withdrawFeeSetting->value : 1000;

    // Balance check (atomic check to prevent race condition)
    $currentBalance = $sales->commission_balance;
    if ($currentBalance < $validated['amount']) {
        return response()->json([
            'success' => false,
            'message' => 'Saldo tidak mencukupi.'
        ], 400);
    }

    // Minimum check
    if ($validated['amount'] < $minWithdraw) {
        return response()->json([
            'success' => false,
            'message' => 'Nominal penarikan kurang dari minimum.'
        ], 400);
    }

    // Fee calculation
    $receivedAmount = $validated['amount'] - $withdrawFee;

    DB::beginTransaction();
    try {
        // Lock sales row to prevent race condition
        $lockedSales = Sales::lockForUpdate()->findOrFail($sales->id);
        
        // Double-check balance after lock
        if ($lockedSales->commission_balance < $validated['amount']) {
            DB::rollBack();
            return response()->json([
                'success' => false,
                'message' => 'Saldo tidak mencukupi.'
            ], 400);
        }
        
        // Create withdraw record
        Withdraw::create([
            'sales_id' => $lockedSales->id,
            'amount' => $validated['amount'],
            'admin_fee' => $withdrawFee,
            'received_amount' => $receivedAmount,
            'date' => now(),
            'status' => 'pending',
        ]);

        // Deduct balance atomically
        $lockedSales->decrement('commission_balance', $validated['amount']);

        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        \Log::error('Withdraw creation error: ' . $e->getMessage());
        return response()->json([
            'success' => false,
            'message' => 'Terjadi kesalahan: ' . $e->getMessage()
        ], 500);
    }

    return response()->json([
        'success' => true,
        'message' => 'Penarikan berhasil diajukan. Menunggu persetujuan admin.',
        'redirect' => route('sales.withdraw.index')
    ]);
}
```

---

### Fix #4, #8: Token Order Approval with Proper Transaction

```php
// app/Http/Controllers/SuperAdmin/TokenController.php

public function updateOrderStatus(Request $request, $id)
{
    $order = TokenOrder::findOrFail($id);

    $request->validate([
        'status' => 'required|in:pending,completed,cancelled',
        'admin_notes' => 'nullable|string|max:500',
    ]);

    // Check if tenant has uploaded proof before approving
    if ($request->status === 'completed' && !$order->proof) {
        return redirect()->route('super-admin.token.transactions')
            ->with('error', 'Tidak dapat menyetujui order ini. Tenant belum mengupload bukti transfer.');
    }

    // Check if order is already in the requested status
    if ($order->status === $request->status) {
        return redirect()->route('super-admin.token.transactions')
            ->with('info', 'Order ini sudah memiliki status ' . ucfirst($order->status));
    }

    DB::beginTransaction();
    try {
        // Lock order to prevent concurrent modifications
        $lockedOrder = TokenOrder::lockForUpdate()->findOrFail($id);

        // Double-check status
        if ($lockedOrder->status !== $order->status) {
            DB::rollBack();
            return redirect()->route('super-admin.token.transactions')
                ->with('error', 'Order ini sedang diproses oleh admin lain. Silakan refresh halaman.');
        }

        // If completing order, inject tokens to LBB
        if ($request->status === 'completed') {
            // Find and lock LBB
            $lbb = Lbb::lockForUpdate()->findOrFail($lockedOrder->lbb_id);

            // Inject tokens atomically
            $lbb->increment('token_balance', $lockedOrder->token_amount);

            // Create token transaction record
            TokenTransaction::create([
                'lbb_id' => $lbb->id,
                'amount' => (int) $lockedOrder->token_amount,
                'type' => 'inject',
                'notes' => 'Pembelian Token - Order #' . $lockedOrder->id . ' (' . $lbb->name . ')',
                'created_by' => auth()->id(),
            ]);
        }

        // Update status and admin notes
        $lockedOrder->status = $request->status;
        if ($request->has('admin_notes')) {
            $lockedOrder->admin_notes = $request->admin_notes;
        }
        $lockedOrder->save();

        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        \Log::error('Order status update error: ' . $e->getMessage());
        return redirect()->route('super-admin.token.transactions')
            ->with('error', 'Gagal mengubah status order: ' . $e->getMessage());
    }

    return redirect()->route('super-admin.token.transactions')
        ->with('success', "Status order token {$order->lbb->name} berhasil diubah menjadi " . ucfirst($order->status));
}
```

---

### Fix #9, #10: Exam Start with Token Deduction

```php
// app/Http/Controllers/Siswa/SiswaCBTController.php

public function start(Request $request, $id)
{
    $examId = $id;
    $examCode = $request->input('exam_code');
    
    $exam = Exam::findOrFail($examId);
    $student = Auth::user()->student->where('lbb_id', session('current_lbb_id'))->first();
    
    if (!$student) {
        return response()->json([
            'success' => false,
            'message' => 'Data siswa tidak ditemukan!'
        ], 404);
    }

    DB::beginTransaction();
    try {
        // Get LBB and lock for update
        $lbb = $student->lbb;
        $lockedLbb = Lbb::lockForUpdate()->findOrFail($lbb->id);

        // Get random package
        $package = ExamPackage::with('examPackageQuestions')
            ->where('is_active','TRUE')
            ->where('exam_id', $exam->id)
            ->inRandomOrder()
            ->first();
            
        if (!$package) {
            DB::rollBack();
            return response()->json([
                'success' => false,
                'message' => 'Paket soal tidak tersedia!'
            ], 404);
        }

        // Get participant
        $participant = ExamParticipant::where('exam_id', $exam->id)
            ->where('student_id', $student->id)
            ->first();

        if (!$participant) {
            DB::rollBack();
            return response()->json([
                'success' => false,
                'message' => 'Anda tidak terdaftar dalam ujian ini!'
            ], 403);
        }
        
        // Check attempt limit
        $attempts = ExamAttempt::where('exam_participant_id', $participant->id)->count();
        if ($attempts >= $exam->max_attempt) {
            DB::rollBack();
            return response()->json([
                'success' => false,
                'message' => 'Anda sudah mencapai batas maksimal percobaan!'
            ], 403);
        }
        
        // Check exam availability
        $now = now();
        if ($now->lt($exam->start_date) || $now->gt($exam->end_date)) {
            DB::rollBack();
            return response()->json([
                'success' => false,
                'message' => 'Ujian tidak tersedia saat ini!'
            ], 403);
        }

        // Token cost (can be configured per exam if needed)
        $tokenCost = 1;

        // Check token balance
        if ($lockedLbb->token_balance < $tokenCost) {
            DB::rollBack();
            return response()->json([
                'success' => false,
                'message' => 'Token tidak mencukupi. Silakan hubungi admin.'
            ], 403);
        }

        // Deduct token atomically
        $lockedLbb->decrement('token_balance', $tokenCost);

        // Create usage transaction
        TokenTransaction::create([
            'lbb_id' => $lockedLbb->id,
            'amount' => $tokenCost,
            'type' => 'usage',
            'notes' => 'Exam: ' . $exam->name . ' - Student: ' . $student->name,
            'created_by' => $student->user_id,
        ]);

        // Create new attempt
        $attempt = ExamAttempt::create([
            'exam_participant_id' => $participant->id,
            'exam_package_id' => $package->id,
            'start_time' => now(),
            'status' => 'in_progress',
            'total_questions' => $package->examPackageQuestions->count(),
        ]);

        DB::commit();
    } catch (\Throwable $e) {
        DB::rollBack();
        \Log::error('Exam start error: ' . $e->getMessage());
        return response()->json([
            'success' => false,
            'message' => $e->getMessage()
        ], 500);
    }

    // Redirect to CBT page
    return response()->json([
        'success' => true,
        'redirect_url' => route('siswa.cbt.show', $attempt->id)
    ]);
}
```

---

### Fix #13: Token Purchase with Balance Validation

```php
// app/Services/Admin/AdminTokenService.php

public function purchaseToken($request)
{
    // Debug: Log all request data
    \Log::info('Purchase Token Request:', $request->all());
    
    // Validate request
    $validated = $request->validate([
        'token_amount' => 'required|integer|min:10',
        'token_price' => 'required|numeric',
        'payment_method' => 'required|string',
        'admin_fee' => 'required|numeric|min:0',
    ]);
    
    // Debug: Log validated data
    \Log::info('Validated Data:', $validated);
    
    // Validate payment method exists in database (case-insensitive)
    $bankAccount = BankAccount::whereRaw('LOWER(bank) = ?', [strtolower($validated['payment_method'])])->first();
    if (!$bankAccount) {
        return redirect()->route('admin.token')->with('error', 'Metode pembayaran tidak valid.');
    }

    // Get current admin user
    $user = Auth::user();
    $lbb = $user->lbb;

    if (!$lbb) {
        return redirect()->route('admin.token')->with('error', 'Data LBB tidak ditemukan.');
    }

    // Calculate total price
    $tokenAmount = $validated['token_amount'];
    $tokenPrice = $validated['token_price'];
    $adminFee = $validated['admin_fee'];
    $totalPrice = ($tokenAmount * $tokenPrice) + $adminFee;

    DB::beginTransaction();
    try {
        // Create token order (status: pending - tokens not yet credited)
        $tokenOrder = TokenOrder::create([
            'lbb_id' => $lbb->id,
            'user_id' => $user->id,
            'token_amount' => $tokenAmount,
            'token_price' => $tokenPrice,
            'total_price' => $totalPrice,
            'status' => 'pending',
            'date' => now(),
            'payment_method' => $validated['payment_method'],
        ]);

        // DO NOT create TokenTransaction here - only after approval
        // DO NOT deduct balance - tokens will be credited on approval

        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        \Log::error('Token order creation error: ' . $e->getMessage());
        return redirect()->route('admin.token')
            ->with('error', 'Gagal membuat order: ' . $e->getMessage());
    }

    return redirect()->route('admin.token.index')
        ->with('success', "Permintaan pembelian {$tokenAmount} token berhasil dibuat. Silakan lakukan pembayaran dan tunggu verifikasi dari admin.");
}
```

---

## 📊 SUMMARY STATISTICS

### Critical Issues: 10
- Race Conditions: 6
- Missing Transactions: 4
- Double Deduction Bug: 1
- Broken Token System: 1

### High Priority Issues: 5
- Nested Transactions: 1
- Missing Row Locking: 1
- No Balance Validation: 1
- Missing Audit Trail: 1
- Missing DB Constraints: 1

### Medium Priority Issues: 3
- Inconsistent Error Handling: 1
- No Validation for Increments: 1
- Magic Numbers: 1

### Good Practices: 4

---

## 🎯 RECOMMENDED ACTION PLAN

### Phase 1: Critical Fixes (IMMEDIATE - 1-2 days)
1. Fix double deduction bug in withdrawals
2. Add transactions to all withdrawal operations
3. Fix race conditions with atomic operations
4. Implement token deduction in exam flow
5. Fix token order approval transaction

### Phase 2: High Priority (1 week)
1. Add row locking to all approval/rejection operations
2. Implement audit logging system
3. Add database constraints
4. Remove nested transactions
5. Add balance validations

### Phase 3: Medium Priority (2 weeks)
1. Standardize error handling
2. Remove magic numbers
3. Add comprehensive tests
4. Improve documentation

### Phase 4: Long Term (1 month)
1. Consider implementing event sourcing for financial operations
2. Add reconciliation system
3. Implement automatic balance checks
4. Add monitoring and alerting

---

## ⚠️ RISK ASSESSMENT

**Current Risk Level:** 🚨 CRITICAL

**Potential Impact:**
- Financial loss: HIGH (double deductions, missing tokens)
- Data corruption: HIGH (race conditions, partial commits)
- Business impact: CRITICAL (token system broken)
- User trust: CRITICAL (financial data inconsistencies)

**Recommended Actions:**
1. STOP: Do not deploy to production until critical issues fixed
2. FIX: Implement Phase 1 fixes immediately
3. TEST: Thoroughly test all financial operations
4. MONITOR: Implement monitoring for financial operations
5. AUDIT: Regular audits of financial data

---

## 📝 ADDITIONAL RECOMMENDATIONS

### 1. Implement Database-Level Protections
```sql
-- Add check constraint for non-negative balances
ALTER TABLE lbbs ADD CONSTRAINT check_token_balance_non_negative 
CHECK (token_balance >= 0);

ALTER TABLE sales ADD CONSTRAINT check_commission_balance_non_negative 
CHECK (commission_balance >= 0);
```

### 2. Add Comprehensive Logging
```php
// Create AuditLog model and migration
// Log all financial operations with:
// - Operation type
// - User
// - Before/after values
// - IP address
// - Timestamp
// - Request ID
```

### 3. Implement Rate Limiting
```php
// Prevent rapid consecutive withdrawal requests
RateLimiter::for('withdrawals', function (Request $request) {
    return Limit::perMinute(3)->by($request->user()->id);
});
```

### 4. Add Automated Tests
```php
// Test race conditions with concurrent requests
// Test transaction rollback scenarios
// Test edge cases (negative balances, etc.)
```

### 5. Consider Using IDempotency Keys
```php
// Prevent duplicate operations
// Add idempotency_key to requests
// Check for existing operations with same key
```

---

## 📞 CONTACT

For questions or clarifications about this audit, please review the code examples provided and consult with a senior backend engineer familiar with Laravel and financial systems.

---

**Audit Completed:** 2026-03-25  
**Next Audit Recommended:** After Phase 1 fixes implemented