# B2C Error Codes & Handling Reference

## Summary: Transaction Code Flow

```
┌─────────────────────────────────────────────────────────────────┐
│                  B2C DISBURSEMENT PROCESS                       │
└─────────────────────────────────────────────────────────────────┘

1. BEFORE REQUEST
   ├─ Admin enters 4-digit PIN ✓
   └─ OriginatorConversationID generated (UUID)
      └─ stored in transaction.mpesa_code

2. API REQUEST → Daraja
   └─ POST https://sandbox.safaricom.co.ke/mpesa/b2c/v3/paymentrequest
      └─ Payload includes OriginatorConversationID, credentials, amount, etc.

3. IMMEDIATE RESPONSE (ACK)
   ├─ ResponseCode: 0 (success) or error code
   ├─ ConversationID: Safaricom tracking ID
   └─ OriginatorConversationID: echoed back

4. ASYNC CALLBACK (30-300s later)
   ├─ ResultURL callback on success/failure
   │  └─ TransactionID: "SG632NMUAB" (customer receipt)
   │  └─ ConversationID: "AG_20260503_..." (Safaricom ID)
   │  └─ ResultCode: 0 (success) or error code
   │
   └─ QueueTimeOutURL callback on timeout
      └─ No TransactionID yet
      └─ Processing still ongoing at Safaricom

5. DB UPDATE
   ├─ transaction.mpesa_transaction_id = TransactionID
   ├─ transaction.mpesa_code = ConversationID (updates)
   ├─ transaction.status = completed | failed
   └─ transaction.callback_data = full result
```

---

## Daraja Response/Result Codes

### Immediate Response (from ACK)

| Code | Meaning | Action |
|------|---------|--------|
| 0 | Success | B2C request accepted, await callback |
| 401 | Unauthorized | Check access token, credentials |
| 422 | Invalid payload | Check field values and types |
| 500 | Server error | Retry after delay |

### Result Codes (in Callback)

| Code | Description | Root Cause | Admin Action |
|------|-------------|-----------|--------------|
| **0** | ✅ **Success** | Transaction processed | Loan auto-activates, repayment schedule generated |
| **1** | ❌ Insufficient balance | B2C Utility account empty | Move funds via M-Pesa portal: My Functions → Move Funds from MMF to Utility |
| **2** | Amount below minimum | Amount < KSh 10 | Increase loan amount ≥ KSh 10 |
| **3** | Amount below limit | Amount < B2C lower limit | Increase loan amount per business settings |
| **4** | Amount exceeds limit | Amount > KSh 250,000/txn | Split into multiple disbursements or contact support |
| **8** | Exceeds max balance | Customer balance > KSh 500,000 | Customer must spend down wallet |
| **11** | DebitParty invalid | B2C short code not active | Contact M-Pesa: M-PESABusiness@Safaricom.co.ke |
| **21** | ❌ **Initiator not allowed** | API user lacks B2C role | M-Pesa portal: Create API user with "ORG B2C API initiator" role |
| **2001** | ❌ **Initiator credentials invalid** | Wrong username, password, or certificate | Verify MPESA_INITIATOR_NAME, re-encrypt password, check cert path |
| **2006** | Account rule violation | B2C account disabled | Contact M-Pesa business support |
| **2028** | Product not assigned | Short code not whitelisted for B2C | Apply via: M-PESABusiness@Safaricom.co.ke |
| **2040** | ❌ **Customer not registered** | Phone not on M-Pesa | Borrower must register phone on M-Pesa first |
| **8006** | Security credential locked | Too many failed auth attempts | Business Admin unlocks in M-Pesa org portal |

---

## Common Error Scenarios in LendCore

### Scenario 1: ResultCode = 1 (Insufficient Funds)

```
Timeline:
─────────
12:05 Admin clicks Disburse, enters PIN ✓
      → B2C request sent
      → ConversationID: AG_20260503_010010031cpgqhfjz357 (ACK)

12:06 Safaricom calls ResultURL:
      { ResultCode: 1, ResultDesc: "The balance is insufficient..." }
      
Result:
───────
transaction.status = 'failed'
loan.status = 'approved' (no change)

Admin remedy:
─────────────
1. Login to https://org.ke.m-pesa.com/orglogin.action
2. Username: Business Admin username
3. Navigate: My Functions → Initiate Transaction
4. Select: Business Services - Move Funds from MMF Account to Utility Account
5. Enter amount, submit
6. Another operator approves in My Tasks
7. Retry disbursement
```

---

### Scenario 2: ResultCode = 21 (Initiator Not Allowed)

```
Timeline:
─────────
12:05 Admin clicks Disburse, enters PIN ✓
      → B2C request sent
      → ResponseCode: 0 (ACK accepted)

12:06 Safaricom calls ResultURL:
      { ResultCode: 21, ResultDesc: "The initiator is not allowed to initiate this request" }

Root cause:
───────────
API user in M-Pesa portal does NOT have "ORG B2C API initiator" role

Admin remedy:
─────────────
1. Login to https://org.ke.m-pesa.com/orglogin.action (Business Admin)
2. Navigate: My Functions → Operators
3. Find the API user (username in MPESA_INITIATOR_NAME)
4. Click "Operations" → "Assign Role"
5. Check: ORG B2C API Initiator ✓
6. Save
7. Retry disbursement
```

---

### Scenario 3: ResultCode = 2001 (Credentials Invalid)

```
Timeline:
─────────
12:05 Admin clicks Disburse, enters PIN ✓
      → B2C request sent
      → ResponseCode: 0 (ACK accepted)

12:06 Safaricom calls ResultURL:
      { ResultCode: 2001, ResultDesc: "The initiator information is invalid." }

Root causes:
────────────
1. MPESA_INITIATOR_NAME in .env is wrong
2. MPESA_INITIATOR_PASSWORD encrypted incorrectly
3. Security certificate expired or wrong path

Admin remedy:
─────────────
1. Go to https://daraja.safaricom.co.ke/dashboard/myapps
2. Select your sandbox app
3. Find "Test Credentials" section
4. Copy correct InitiatorName value → MPESA_INITIATOR_NAME
5. Verify InitiatorPassword and certificate path
6. Ensure certificate exists at: storage/app/mpesa/SandboxCertificate.cer
7. .env.example has default path:
   MPESA_SECURITY_CREDENTIAL_CERT_PATH=storage/app/mpesa/SandboxCertificate.cer
8. Restart app, retry
```

---

### Scenario 4: ResultCode = 2040 (Customer Not Registered)

```
Timeline:
─────────
12:05 Admin clicks Disburse, enters PIN ✓
      Loan.user.mpesa_phone = "254708374149"
      → B2C request sent
      → ConversationID issued (ACK)

12:06 Safaricom calls ResultURL:
      { ResultCode: 2040, ResultDesc: "Credit Party customer type...can't be supported" }

Root cause:
───────────
Phone number 254708374149 is NOT registered on M-Pesa

Admin remedy:
─────────────
1. Contact borrower: "Please register phone 254708374149 on M-Pesa"
2. Borrower must:
   - Dial *333# on the phone
   - Complete M-Pesa registration
   - Confirm they can send/receive money
3. Wait 5-10 minutes for registration
4. Retry disbursement

Alternative:
───────────
If borrower phone is different, update in LendCore:
1. User profile → Update M-Pesa phone
2. Retry disbursement
```

---

### Scenario 5: Timeout (No callback received)

```
Timeline:
─────────
12:05 Admin clicks Disburse, enters PIN ✓
      → B2C request sent
      → ConversationID: AG_20260503_... (ACK)
      
12:05:30 Request processing at Safaricom...

12:06:05 (61 seconds later)
      → QueueTimeOutURL called with timeout result
      
      { ResultCode: null, ResultDesc: "The service request is still being processed" }

Transaction handling:
────────────────────
transaction.status = 'failed'
loan.status = 'approved' (no change)
callback_data.timeout = { ... timeout payload ... }

What might happen:
──────────────────
1. Actual result arrives later:
   → Transaction status updates to 'completed' or 'failed'
   → Loan activates (if result = 0)
   
2. Never completes:
   → Admin must manually retry
   → Check M-Pesa org portal for transaction status
   → Query using OriginatorConversationID or receipt

Admin action:
─────────────
1. Wait 5-10 minutes for potential late callback
2. Check M-Pesa org portal:
   - My Functions → Transaction Status Query
   - Enter receipt or reference
3. If confirmed successful:
   - Manually mark transaction as completed
   - Activate loan in admin panel
4. If failed:
   - Check error code from portal
   - Fix issue
   - Retry disbursement
```

---

## Database Query Examples

### Find transaction by OriginatorConversationID

```php
$transaction = Transaction::where('mpesa_code', $originatorConversationId)->first();
```

### Find B2C transactions by status

```php
// All pending B2C disbursements
$pending = Transaction::where('type', 'disbursement')
    ->where('status', 'pending')
    ->get();

// Failed disbursements
$failed = Transaction::where('type', 'disbursement')
    ->where('status', 'failed')
    ->latest()
    ->paginate(20);
```

### Extract error details from callback

```php
$resultCode = $transaction->callback_data['Result']['ResultCode'] ?? null;
$resultDesc = $transaction->callback_data['Result']['ResultDesc'] ?? null;
$transactionId = $transaction->callback_data['Result']['TransactionID'] ?? null;

dd([
    'resultCode' => $resultCode,
    'resultDesc' => $resultDesc,
    'transactionId' => $transactionId,
    'storageIds' => [
        'mpesa_code' => $transaction->mpesa_code,
        'mpesa_transaction_id' => $transaction->mpesa_transaction_id,
    ],
]);
```

---

## Admin Dashboard Recommendations

Add these reports to admin panel:

```blade
{{-- Dashboard Widget: Recent Disbursements --}}
<table>
    <tr>
        <th>Loan ID</th>
        <th>Borrower</th>
        <th>Amount</th>
        <th>Status</th>
        <th>Result Code</th>
        <th>M-Pesa Receipt</th>
        <th>Action</th>
    </tr>
    @foreach ($disbursements as $txn)
        <tr>
            <td>#{{ $txn->loan_id }}</td>
            <td>{{ $txn->user->name }}</td>
            <td>KSh {{ number_format($txn->amount, 2) }}</td>
            <td>
                <span class="badge bg-{{ $txn->status === 'completed' ? 'success' : 'warning' }}">
                    {{ ucfirst($txn->status) }}
                </span>
            </td>
            <td>{{ $txn->callback_data['Result']['ResultCode'] ?? '-' }}</td>
            <td>{{ $txn->mpesa_transaction_id ?? '-' }}</td>
            <td>
                @if ($txn->status === 'failed')
                    <a href="#" class="btn btn-sm btn-warning">Retry</a>
                @endif
            </td>
        </tr>
    @endforeach
</table>
```

---

## Production Checklist

- [ ] PIN changed from default
- [ ] M-Pesa API credentials verified (sandbox → production)
- [ ] API user has "ORG B2C API initiator" role
- [ ] B2C short code whitelisted for production
- [ ] Minimum balance in B2C Utility account (KSh 10,000+)
- [ ] Callback URLs registered and publicly accessible
- [ ] Ngrok tunnel disabled (use real domain)
- [ ] Error notifications configured (admin email alerts)
- [ ] Transaction logs archived
- [ ] Audit trail enabled for all disbursements
