# C2B Queue Worker and Supporting Scripts

## Overview

This document explains how C2B non-STK confirmations move through the LendCore system after they are received by the relay layer and before the payment is applied to loans.

The core design is intentionally simple:

1. The relay service forwards a callback payload to the LendCore callback endpoint.
2. LendCore stores the payload and enqueues a job into the database-backed queue.
3. A queue worker processes the job, matches the payment to a user, and applies repayment allocation logic.
4. If the job cannot be processed successfully, the payment is preserved in suspense for manual review.

This architecture exists because M-Pesa callbacks must return quickly, while the payment processing itself can be heavier and more failure-prone.

## Implementation references

The main implementation points for this flow are:

- [app/Http/Controllers/Api/MpesaCallbackController.php](../app/Http/Controllers/Api/MpesaCallbackController.php) — accepts the callback payload and queues the processing job.
- [app/Jobs/ProcessC2bNonStkConfirmation.php](../app/Jobs/ProcessC2bNonStkConfirmation.php) — processes the queued confirmation, matches the payment to a user, and applies repayment allocation.
- [app/Services/C2bUserMatchingService.php](../app/Services/C2bUserMatchingService.php) — resolves a user by matching the incoming BillRefNumber to the user's `national_id`.
- [app/Services/RepaymentService.php](../app/Services/RepaymentService.php) — allocates the payment against active loans and repayments.
- [app/Services/SuspenseService.php](../app/Services/SuspenseService.php) — stores unmatched or failed transactions for later review.
- [app/Services/QueueWorkerTriggerService.php](../app/Services/QueueWorkerTriggerService.php) and [app/Http/Middleware/TriggerQueueWorker.php](../app/Http/Middleware/TriggerQueueWorker.php) — attempt to launch the worker automatically when pending C2B work exists, using the current PHP binary and full artisan path. The trigger now checks for pending jobs every 5 seconds and only starts a worker when there are unprocessed queue entries.

---

## End-to-end queue flow

### 1. Callback arrives at the endpoint

The non-STK confirmation endpoint is exposed at:

- /api/payment/c2b/nonstk/confirmation

When Safaricom or relay-box sends the callback, the controller logs the request and extracts key fields such as:

- TransID
- BillRefNumber
- TransAmount
- BusinessShortCode
- MSISDN

The controller then checks whether a job for the same transaction is already pending in the queue. If one exists, the callback is treated as a duplicate and acknowledged without creating a second job.

### 2. Job is queued

If the transaction is new, LendCore dispatches a job named:

- App\Jobs\ProcessC2bNonStkConfirmation

The job is pushed onto the database queue configured as:

- queue connection: database
- queue name: c2b

The queue is backed by the jobs table, which makes it suitable for shared hosting and environments where a long-running daemon is not always available.

### 3. Worker consumes the queued job

The queue worker reads entries from the jobs table and executes the job handler. The handler performs the actual business logic:

- validates the payload
- checks whether the transaction was already processed
- tries to match the payment to a user using the BillRefNumber
- applies repayment allocation against the user’s active loans
- records successful transactions and clears suspense entries when appropriate

### 4. Processing result

If the payment is matched and allocated successfully, the transaction is treated as completed. If there is no matching user or no payable balance remains, the process may store the transaction in suspense instead of failing the whole pipeline.

### 5. Transaction timestamps in the C2B reports

The C2B transaction list and detail views display two separate timestamps:

- **M-Pesa Transaction time** — read from the callback payload's `TransTime` field. The expected M-Pesa format is `YYYYMMDDHHMMSS`, for example `20260824050648`, which is displayed as `2026-08-24 05:06:48`.
- **Processed** — remains `N/A` while a suspense transaction is unmatched. After a suspense payment is manually settled, this displays `applied_at`, the time the manual settlement was completed. For a direct completed C2B transaction, this displays `processed_at`.

The `transaction_time` model accessor parses the payload value for both direct C2B and suspense records. It does not use the processing timestamp as a fallback, so the M-Pesa event time and the LendCore settlement time cannot be confused. Existing records are supported automatically because their stored callback payloads are read at display time; no payload rewrite or separate timestamp column is required.

---

## Core queue worker components

### Job handler

The main processing logic lives in:

- app/Jobs/ProcessC2bNonStkConfirmation.php

This class is responsible for:

- receiving the callback payload from the queue
- extracting transaction identifiers and amount
- deciding whether the payload is valid
- calling the user-matching service
- invoking repayment allocation
- logging success or failure outcomes
- moving failures into suspense as a safeguard

### Queue trigger service

The queue launcher logic is handled by:

- app/Services/QueueWorkerTriggerService.php

This service is designed to start the queue worker automatically when the application notices that C2B work is pending. It creates a lock file to avoid repeatedly spawning duplicate workers and then runs a Laravel queue worker command.

The command it launches is conceptually:

- php artisan queue:work database --queue=c2b --tries=3 --timeout=120 --stop-when-empty

### Middleware trigger

The actual invocation is wired through middleware:

- app/Http/Middleware/TriggerQueueWorker.php

This middleware runs after a web request completes and asks the trigger service whether a queue worker should be started. This is a lightweight fallback mechanism so callback requests or API calls can kick off processing without relying on a separate daemon always being present.

### Web queue controller

The application also has an in-request processing controller:

- app/Http/Controllers/Api/WebQueueController.php

This endpoint is useful for environments where the queue worker is not running continuously. When the callback endpoint is hit, it can attempt to drain a small batch of jobs from the queue within the same request. This reduces the chance that a callback sits in the database queue indefinitely.

---

## Scripts involved

### 1. Relay and callback test scripts

These scripts are used to simulate the relay and callback flow locally or during debugging:

- temp_post_relaybox.php
- temp_post_relaybox_validation.php
- temp_post_relaybox_fixed.php
- temp_relay_forward_verify.php
- temp_relay_payloads.php

They are useful for:

- sending test confirmation payloads to the relay endpoint
- validating callback routing
- replaying payloads after a failure
- confirming that the relay layer forwards data correctly

### 2. Queue-related helper scripts

Several helper and recovery scripts exist around the queue and callback flow:

- simulate_callbacks.php
- register_nonstk_urls.php
- reconcile_fee_payments.php
- check_loans.php
- check_loan15.php
- check_loan36.php

These scripts help verify whether:

- callback registration is active
- payments were applied to the correct loans
- transactions remain stuck in suspense
- jobs are present in the queue table
- payment allocation behavior is consistent

### 3. Recovery and replay tooling

The recovery flow is critical for production resilience. The system stores unmatched or failed transactions in suspense and can replay them later. This is especially important when:

- the database was down during the original callback
- a user was not yet available when the payment arrived
- a temporary queue-processing gap occurred

These recovery operations are meant to avoid permanent data loss and make it possible to process callbacks later without needing the original client to resend them.

---

## How payment matching works

Once the queue job runs, the payment is matched through the shared service:

- app/Services/C2bUserMatchingService.php

This service builds candidate identifiers from the BillRefNumber and checks whether they match:

- national_id
- id_number

If a user is found, the system proceeds to allocate the payment against the user’s active loans. If no user is found, the payment is stored in suspense for later review.

---

## How repayment allocation works

After the user is identified, the payment is sent to:

- app/Services/RepaymentService.php

This service applies the incoming amount to active loans and their corresponding repayments. It may:

- allocate the payment across multiple installments
- preserve any remaining amount as a guarantee deposit balance
- record allocation details for auditing

The queue job’s success depends on this service completing without throwing an exception. If it fails for a non-actionable reason, the system logs the event and prevents the job from being retried indefinitely.

For legacy-imported loans, a successfully allocated C2B/API payment also increments the loan's `legacy_paid` snapshot by the amount allocated to that loan. This keeps the legacy loan's authoritative paid total, repayment display, and legacy fine foundation current after new payments arrive. Non-legacy loans do not update `legacy_paid`; they continue using the normal repayment `amount_paid` totals. Any payment remainder sent to the user's guarantee deposit is not added to a loan's `legacy_paid`.

---

## Why the queue worker matters

The queue worker is the component that turns an incoming callback into a real financial update. Without it, the callback just sits in the queue table and becomes invisible to the business logic.

That is why the system includes both:

- a background worker launcher
- an in-request fallback processor

Together these reduce the chance that callbacks remain stuck in the database queue after relay-box forwards them.

---

## Monitoring and troubleshooting

To verify that the queue pipeline is healthy, watch for the following indicators:

- queue worker logs in storage/logs/queue-worker.log
- Laravel application logs in storage/logs/laravel.log
- entries in the jobs table
- entries in suspense_transactions when matching fails
- completed payment transactions in the transactions table

When troubleshooting, the usual questions are:

1. Did the callback reach the endpoint?
2. Was the job written to the jobs table?
3. Did the worker start and consume the job?
4. Did the repayment allocation succeed?
5. Was the payment preserved in suspense instead of being lost?

---

## Operational summary

The queue pipeline is built around three cooperating layers:

- the callback endpoint that accepts inbound payloads
- the database-backed queue that preserves jobs safely
- the worker/controller logic that actually applies the payment

In practice, the system is resilient because it combines asynchronous processing with immediate fallback processing and suspense-based recovery. This makes it suitable for production environments where connectivity, timing, and worker availability can vary.
