Skip to content

Tuma

Location: dg_smart_pos_api/modules/Tuma. Owns initiating and reconciling Tuma STK push payments against either a Sales Sale or an installment sale (App\Models\Installment\InstallmentSale), plus the per-store merchant accounts used to initiate them. It does not own the sale or installment records themselves, only the payment attempt and its callback handling.

  • TumaPayment (tuma_payments) - one STK push attempt: linked to exactly one of sale_id or installment_sale_id (never both, enforced at the request level - see below), a status enum (Modules\Tuma\Enums\TumaPaymentStatus: Pending/Completed/Failed/Cancelled), merchant_request_id/checkout_request_id from the Tuma API, an unguessable per-payment callback_token, and the resulting mpesa_receipt_number once settled.
  • TumaPaymentAccount (tuma_payment_accounts) - a store’s configured Tuma merchant account (bank name, account number, API key). api_key is hidden from API responses ($hidden).

Initiating an STK push (TumaPaymentController)

Section titled “Initiating an STK push (TumaPaymentController)”
  • accounts - active TumaPaymentAccounts for the store, plus the global stk_push_enabled admin setting and an optional stk_push_disabled_message so the POS can show a disabled notice instead of a raw failure.
  • initiate (InitiateStkPushRequest) - requires tuma_payment_account_id, amount, phone (254XXXXXXXXX), and exactly one of sale_id or installment_sale_id (validated in withValidator, not the rules array). For a Sale: rejects if amount exceeds sale_total, then on initiation links tuma_payment_id to the sale and sets its status to pending_payment. For an InstallmentSale: checks isActive() and that amount doesn’t exceed computeBalance()['balance_due'] (0.01 tolerance) - no header status changes, since an installment sale has no pending_payment state; the payment only materializes once the callback succeeds, via ProcessInstallmentPaymentJob.
  • status - polls a payment’s current status; returns 404 if the payment doesn’t belong to the given store.

Handling the M-Pesa callback (TumaCallbackController + TumaPaymentService::handleCallback)

Section titled “Handling the M-Pesa callback (TumaCallbackController + TumaPaymentService::handleCallback)”

A public, unauthenticated endpoint - Tuma calls it directly, there’s no user session. The payment is looked up by checkout_request_id (falling back to tuma_payment_id), then the $token path segment is checked with hash_equals against the payment’s stored callback_token. A payment created before the token column existed has callback_token === null and is accepted unverified through the token-less legacy URL - an intentional, time-limited fallback per the code comment, not a bug.

On success: dispatches ProcessInstallmentPaymentJob (installment) or ProcessSaleStockJob (sale) - stock deduction, marking the sale paid, and receipt generation all happen asynchronously in that job, not inline in the callback. On failure: hits a RateLimiter throttle per phone number (max 3 failures / 60s, enforced on the next initiate call via TumaStkThrottledException) and fires a PaymentFailed event. A Sale’s pending_payment state is only reverted if it’s still in that state - a stale/duplicate callback for a sale a later attempt already resolved is logged and ignored rather than incorrectly reopening it.

Separately, central-level management of TumaBusiness records (creating/editing the merchant businesses admins assign to stores) lives entirely outside this module, in App\Http\Controllers\Admin\AdminTumaBusinessController and App\Services\TumaBusinessService - see Admin.

Registered as a tenant route module (Tuma is in ModulesServiceProvider::TENANT_ROUTE_MODULES), required once from routes/tenant.php. All three: auth:sanctum, prefix tuma (from modules/Tuma/Routes/tenant.php):

Method Path Controller method
GET stores/{store}/accounts TumaPaymentController::accounts
POST stores/{store}/initiate TumaPaymentController::initiate
GET stores/{store}/payments/{payment}/status TumaPaymentController::status

Central, unauthenticated, registered in routes/api.php (not a tenant route module file):

Method Path Controller method
POST tuma/callback/{tenantId}/{token} TumaCallbackController::handle
POST tuma/callback/{tenantId} TumaCallbackController::handle (legacy, unverified)

A TumaPayment (TumaPaymentResource):

{
"id": 1,
"sale_id": 42,
"installment_sale_id": null,
"store_id": 3,
"tuma_payment_account_id": 1,
"tuma_payment_id": "TP-000001",
"merchant_request_id": "29115-...-1",
"checkout_request_id": "ws_CO_...",
"status": "pending",
"amount": "500.00",
"phone": "254712345678",
"mpesa_receipt_number": null,
"transaction_date": null,
"callback_received_at": null,
"description": "Payment for sale #A25A25-000001",
"error_message": null,
"created_at": "2026-01-01T00:00:00.000000Z"
}

TumaPaymentAccountResource returns id, store_id, tuma_business_id, name, bank_name, account_number, email, mobile, is_active - api_key is never exposed.

  • Sales - Sale.tuma_payment_id, Sale.tuma, and the pending_payment status this module sets and clears