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.
Data model
Section titled “Data model”TumaPayment(tuma_payments) - one STK push attempt: linked to exactly one ofsale_idorinstallment_sale_id(never both, enforced at the request level - see below), astatusenum (Modules\Tuma\Enums\TumaPaymentStatus:Pending/Completed/Failed/Cancelled),merchant_request_id/checkout_request_idfrom the Tuma API, an unguessable per-paymentcallback_token, and the resultingmpesa_receipt_numberonce settled.TumaPaymentAccount(tuma_payment_accounts) - a store’s configured Tuma merchant account (bank name, account number, API key).api_keyis hidden from API responses ($hidden).
Initiating an STK push (TumaPaymentController)
Section titled “Initiating an STK push (TumaPaymentController)”accounts- activeTumaPaymentAccounts for the store, plus the globalstk_push_enabledadmin setting and an optionalstk_push_disabled_messageso the POS can show a disabled notice instead of a raw failure.initiate(InitiateStkPushRequest) - requirestuma_payment_account_id,amount,phone(254XXXXXXXXX), and exactly one ofsale_idorinstallment_sale_id(validated inwithValidator, not the rules array). For aSale: rejects ifamountexceedssale_total, then on initiation linkstuma_payment_idto the sale and sets its status topending_payment. For anInstallmentSale: checksisActive()and thatamountdoesn’t exceedcomputeBalance()['balance_due'](0.01 tolerance) - no header status changes, since an installment sale has nopending_paymentstate; the payment only materializes once the callback succeeds, viaProcessInstallmentPaymentJob.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.
Route surface
Section titled “Route surface”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) |
Response shape
Section titled “Response shape”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.
Related
Section titled “Related”- Sales -
Sale.tuma_payment_id,Sale.tuma, and thepending_paymentstatus this module sets and clears