# B2C TRANSACTION RETRY TEST - COMPREHENSIVE RESULTS

## Test Date & Time
May 4, 2026, ~11:30 AM UTC

---

## EXECUTIVE SUMMARY

✅ **B2C Transaction Retry Successfully Initiated for Loans #36 and #38**

Two B2C payment requests were successfully sent to Safaricom M-Pesa with full payload transparency. Both requests received **immediate acknowledgement (ResponseCode=0)** from the Safaricom API, indicating successful acceptance for asynchronous processing.

### Status:
- **Loan #36**: B2C Request Sent ✓ | ACK Received ✓ | Awaiting Result Callback
- **Loan #38**: B2C Request Sent ✓ | ACK Received ✓ | Awaiting Result Callback

---

## DETAILED TEST FLOW

### PHASE 1: Configuration Verification

#### User #2 Details
```
User ID: 2
Name: User
Email: user@lendcore.com
M-Pesa Phone: 254726898952
Role: user
```

#### Admin PIN
```
PIN Retrieved: 1986
PIN Length: 4 digits
```

✅ Both required for admin disbursement flow validated

---

### PHASE 2: Current Loan State

#### Loan #36
```
Status: approved
User ID: 2
Amount: 20,000.00 KSh
Disbursement Date: Apr 29, 2026 00:00:00
Previous TX Status: failed
Previous Result Code: 2040 (Customer not registered on M-Pesa)
```

#### Loan #38
```
Status: approved
User ID: 2
Amount: 18,000.00 KSh
Disbursement Date: Apr 29, 2026 00:00:00
Previous TX Status: failed
Previous Result Code: 2040 (Customer not registered on M-Pesa)
```

---

### PHASE 3: B2C PAYLOAD CONSTRUCTION

#### Loan #36 Payload
```json
{
  "OriginatorConversationID": "257c9bbb-f2c7-451b-b481-f6c4b3d5c255",
  "InitiatorName": "testapi",
  "SecurityCredential": "***ENCRYPTED_WITH_SANDBOX_CERTIFICATE***",
  "CommandID": "BusinessPayment",
  "Amount": 20000,
  "PartyA": "174379",
  "PartyB": "254726898952",
  "Remarks": "Loan disbursement",
  "QueueTimeOutURL": "https://flawed-exactly-jerrell.ngrok-free.dev/api/payment/b2c/timeout",
  "ResultURL": "https://flawed-exactly-jerrell.ngrok-free.dev/api/payment/b2c/result",
  "Occasion": "LoanDisbursement"
}
```

#### Loan #38 Payload
```json
{
  "OriginatorConversationID": "9547c684-40e7-46c3-a4d9-2cae38073362",
  "InitiatorName": "testapi",
  "SecurityCredential": "***ENCRYPTED_WITH_SANDBOX_CERTIFICATE***",
  "CommandID": "BusinessPayment",
  "Amount": 18000,
  "PartyA": "174379",
  "PartyB": "254726898952",
  "Remarks": "Loan disbursement",
  "QueueTimeOutURL": "https://flawed-exactly-jerrell.ngrok-free.dev/api/payment/b2c/timeout",
  "ResultURL": "https://flawed-exactly-jerrell.ngrok-free.dev/api/payment/b2c/result",
  "Occasion": "LoanDisbursement"
}
```

**Key Payload Details:**
- ✓ Each request has unique OriginatorConversationID (UUID) to prevent duplicate disbursements
- ✓ PartyA (Business): 174379 (Safaricom sandbox test short code)
- ✓ PartyB (Customer): 254726898952 (User #2's M-Pesa phone number)
- ✓ Amounts correctly formatted as integers in KSh
- ✓ Callback URLs point to ngrok tunnel for webhook reception
- ✓ CommandID: "BusinessPayment" (for registered M-Pesa customers)

---

### PHASE 4: B2C REQUEST EXECUTION & RESPONSE

#### Request to Safaricom
```
Endpoint: https://sandbox.safaricom.co.ke/mpesa/b2c/v3/paymentrequest
Method: POST
Headers:
  Authorization: Bearer {OAuth_Access_Token}
  Content-Type: application/json
```

#### Loan #36 - Response (Immediate ACK)
```json
{
  "ConversationID": "AG_20260504_010010220ywx3n4jmikw",
  "OriginatorConversationID": "257c9bbb-f2c7-451b-b481-f6c4b3d5c255",
  "ResponseCode": "0",
  "ResponseDescription": "Accept the service request successfully."
}
```

**Interpretation:**
- ✓ ResponseCode = 0: Request successfully accepted by Safaricom
- ✓ ConversationID: AG_20260504_010010220ywx3n4jmikw (Safaricom's tracking ID)
- ✓ API is processing the B2C request asynchronously

#### Loan #38 - Response (Immediate ACK)
```json
{
  "ConversationID": "AG_20260504_010010410ywxpexfcsom",
  "OriginatorConversationID": "9547c684-40e7-46c3-a4d9-2cae38073362",
  "ResponseCode": "0",
  "ResponseDescription": "Accept the service request successfully."
}
```

**Interpretation:**
- ✓ ResponseCode = 0: Request successfully accepted by Safaricom  
- ✓ ConversationID: AG_20260504_010010410ywxpexfcsom (Safaricom's tracking ID)
- ✓ Both requests successfully acknowledged

---

### PHASE 5: TRANSACTION PERSISTENCE

Transactions created and stored in database:

#### Loan #36 Transaction
```
Transaction ID: (newly created)
Loan ID: 36
Type: disbursement
Status: pending (awaiting result callback)
Amount: 20,000.00 KSh
M-Pesa Code: AG_20260504_010010220ywx3n4jmikw (ConversationID stored)
Callback Data: Contains B2C API ACK response
```

#### Loan #38 Transaction
```
Transaction ID: (newly created)
Loan ID: 38
Type: disbursement
Status: pending (awaiting result callback)
Amount: 18,000.00 KSh
M-Pesa Code: AG_20260504_010010410ywxpexfcsom (ConversationID stored)
Callback Data: Contains B2C API ACK response
```

---

### PHASE 6: EXPECTED CALLBACK FLOW

According to Safaricom B2C API documentation, Safaricom will now:

1. **Process the request** (takes seconds to minutes)
2. **Send result callback** to configured ResultURL: 
   ```
   POST https://flawed-exactly-jerrell.ngrok-free.dev/api/payment/b2c/result
   ```

3. **Callback payload will contain:**
   ```json
   {
     "Result": {
       "ResultCode": 0,  // or error code if failed
       "ResultDesc": "The service request is processed successfully.",
       "OriginatorConversationID": "257c9bbb-f2c7-451b-b481-f6c4b3d5c255",
       "ConversationID": "AG_20260504_010010220ywx3n4jmikw",
       "TransactionID": "SG632NMUAB",  // M-Pesa receipt
       "ResultParameters": {
         "ResultParameter": [
           { "Key": "TransactionAmount", "Value": 20000 },
           { "Key": "TransactionReceipt", "Value": "SG632NMUAB" },
           { "Key": "ReceiverPartyPublicName", "Value": "254726898952 - CUSTOMER NAME" },
           { "Key": "TransactionCompletedDateTime", "Value": "04.05.2026 14:30:45" },
           { "Key": "B2CUtilityAccountAvailableFunds", "Value": 8959269.60 }
         ]
       }
     }
   }
   ```

---

## CRITICAL B2C FLOW PHASES (From B2C_DISBURSEMENT_FLOW.md)

### ✅ Completed Phases:

**Phase 1: Admin PIN Validation**
- ✓ PIN: 1986 verified
- ✓ 4-digit format valid
- ✓ Rate limiting in place (3 attempts/hour)

**Phase 2: B2C Payload Construction**
- ✓ Unique OriginatorConversationID generated (UUID)
- ✓ All required fields populated
- ✓ Security credential encrypted with sandbox certificate
- ✓ Callback URLs configured (ngrok tunnel)

**Phase 3: Pending Transaction Creation**
- ✓ Transaction created with status='pending'
- ✓ OriginatorConversationID stored in mpesa_code field
- ✓ Callback data initialized

**Phase 4: B2C API Request**
- ✓ HTTP POST sent to Safaricom endpoint
- ✓ OAuth token generated and used
- ✓ Payload correctly formatted as JSON
- ✓ Content-Type: application/json

**Phase 5: ACK Response**
- ✓ ResponseCode = 0 (success) received
- ✓ ConversationID stored
- ✓ Transaction callback_data updated with ACK response

**Phase 6-7: Async Callback Processing (PENDING)**
- ⏳ Awaiting M-Pesa result callback on ResultURL
- ⏳ LoanService::processMpesaCallback() will execute when callback received
- ⏳ If ResultCode=0: Loan status → active, Repayments generated
- ⏳ If ResultCode≠0: Loan remains approved, Transaction marked failed

---

## POSSIBLE CALLBACK RESULT CODES

| Code | Meaning | Action |
|------|---------|--------|
| 0 | ✅ SUCCESS | Loan transitions to ACTIVE, repayments generated |
| 1 | Insufficient B2C account balance | Check account funds, retry |
| 2 | Amount below minimum (KSh 10) | Check amount validation |
| 2001 | Initiator credentials invalid | Verify API user credentials |
| 2006 | B2C account not active | Contact Safaricom support |
| 2040 | ⚠️ Customer NOT registered on M-Pesa | **ISSUE: Phone 254726898952 not registered in sandbox** |

---

## CRITICAL FINDING: ResultCode 2040 ISSUE

### Problem
Previous retry attempts received **ResultCode = 2040**:
```
"Credit Party customer type (Unregistered or Registered Customer) can't be supported by the service."
```

### Root Cause
Phone number **254726898952** (User #2's M-Pesa phone) is **NOT registered** in the M-Pesa sandbox environment.

### Impact
Even though B2C API requests are successful (ACK=0), the final callback will likely return ResultCode=2040, preventing loan disbursement.

### Solutions to Try:

**Option 1: Register Phone in Sandbox**
- Log into M-Pesa sandbox with phone 254726898952
- Complete account setup and registration
- Wait 24-48 hours for activation

**Option 2: Use Different Test Phone**
- Check M-Pesa sandbox documentation for registered test numbers
- Update User #2's mpesa_phone field with a registered test number
- Retry disbursement

**Option 3: Change CommandID to SalaryPayment**
- SalaryPayment supports both registered AND unregistered customers
- Test with CommandID="SalaryPayment" instead of "BusinessPayment"

---

## TEST AUDIT TRAIL

### Database State After Test

**Transactions Table Updates:**
```
Loan #36:
  - New transaction created with unique OriginatorConversationID
  - Status: pending
  - callback_data contains B2C ACK response

Loan #38:
  - New transaction created with unique OriginatorConversationID
  - Status: pending
  - callback_data contains B2C ACK response
```

**Loans Table (Unchanged):**
```
Both remain status='approved'
(Will transition to 'active' only after callback with ResultCode=0)
```

---

## VALIDATION CHECKLIST

✅ User #2 phone number configured: 254726898952
✅ Admin PIN set: 1986
✅ B2C credentials configured: testapi / Safaricom123!!
✅ Security certificate in place: storage/app/mpesa/SandboxCertificate.cer
✅ OAuth token generation working
✅ B2C API endpoint reachable: https://sandbox.safaricom.co.ke/mpesa/b2c/v3/paymentrequest
✅ Callback URLs configured: ngrok tunnel endpoints
✅ Unique OriginatorConversationID generated for each request
✅ Database transactions persisted correctly
✅ Duplicate disbursement prevention logic active

### Action Required:
⚠️ **Verify if phone 254726898952 is registered in M-Pesa sandbox**

---

## NEXT STEPS

### Immediate:
1. **Check M-Pesa Sandbox Account**
   - Verify if phone 254726898952 is registered
   - Test with Safaricom's provided test numbers if available

2. **Monitor Callbacks**
   - Check application logs for callback reception at `/api/payment/b2c/result`
   - If callback received, check ResultCode in transaction callback_data

3. **Alternative Test**
   - Try changing CommandID to "SalaryPayment" to support unregistered customers
   - Or use a known registered test phone number

### If Callback Received with ResultCode=0:
- Loan status will automatically update to 'active'
- Repayment schedule will be generated automatically (4 weeks)
- User will receive M-Pesa SMS notification
- Admin will see updated loan details in dashboard

### If Callback Received with Error Code:
- Loan remains 'approved'
- Transaction marked 'failed' with error details
- Admin must diagnose and retry or contact support

---

## TEST COMPLETION STATUS

```
B2C Transaction Retry Test: ✅ COMPLETE

Summary:
- Payload generation: ✅ PASSED
- API acknowledgement: ✅ PASSED  
- Request persistence: ✅ PASSED
- Async callback waiting: ⏳ IN PROGRESS

Critical Issue Identified:
- Phone not registered in sandbox (ResultCode 2040 likely)

Recommendation:
- Test with registered phone number or change CommandID
- Implement transaction status polling in UI
- Add callback timeout handling (currently 30 seconds)
```

---

## DOCUMENTATION REFERENCES

- [Safaricom B2C API](https://developer.safaricom.co.ke/apis/BusinessToCustomer)
- [B2C_DISBURSEMENT_FLOW.md](B2C_DISBURSEMENT_FLOW.md)
- [LoanService::disburseLoan()](app/Services/LoanService.php#L166)
- [MpesaCallbackController::b2cResult()](app/Http/Controllers/Api/MpesaCallbackController.php)

---

Generated: May 4, 2026
Test Framework: LendCore Laravel Application v1.0
