PHPackages                             maestrodimateo/simple-mobile-money - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. maestrodimateo/simple-mobile-money

ActiveLibrary[Payment Processing](/categories/payments)

maestrodimateo/simple-mobile-money
==================================

A simple, unified Laravel API for Gabonese mobile money aggregators (E-Billing, SingPay, PViT)

v1.3.0(3w ago)186MITPHPPHP ^8.3 || ^8.4CI passing

Since Jul 10Pushed 3w agoCompare

[ Source](https://github.com/maestrodimateo/simple-mobile-money)[ Packagist](https://packagist.org/packages/maestrodimateo/simple-mobile-money)[ RSS](/packages/maestrodimateo-simple-mobile-money/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (21)Versions (6)Used By (0)

Simple Mobile Money
===================

[](#simple-mobile-money)

 [![Simple Mobile Money — one Laravel interface for every Gabonese mobile money aggregator (E-Billing, SingPay, PViT)](art/banner.png)](art/banner.png)

 [![Tests](https://github.com/maestrodimateo/simple-mobile-money/actions/workflows/tests.yml/badge.svg)](https://github.com/maestrodimateo/simple-mobile-money/actions/workflows/tests.yml) [![Quality](https://github.com/maestrodimateo/simple-mobile-money/actions/workflows/quality.yml/badge.svg)](https://github.com/maestrodimateo/simple-mobile-money/actions/workflows/quality.yml) [![Latest Version on Packagist](https://camo.githubusercontent.com/f5176c6b5b52d3bbfedfde0fca066701689986c3a0851267ca7ce7363eda1a76/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d61657374726f64696d6174656f2f73696d706c652d6d6f62696c652d6d6f6e65792e7376673f6c6162656c3d76657273696f6e)](https://packagist.org/packages/maestrodimateo/simple-mobile-money) [![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE.md)

> One simple, unified Laravel API to collect mobile money payments in Gabon — through **E-Billing**, **SingPay** and **PViT** (Airtel Money &amp; Moov Money).

You write the same code for every provider. The package hides each API's base URL, auth scheme, payload format and webhook shape behind neutral DTOs, a single facade, and Laravel events.

> 🇫🇷 **Nouveau sur le mobile money ?** Lis le **[Guide du débutant](GUIDE-DEBUTANT.md)** — une explication pas à pas (en français) du fonctionnement du package et de chaque agrégateur.

```
$response = MobileMoney::driver('singpay')->pay(new PaymentRequest(
    amount: 500,                 // XAF
    reference: 'ORDER-1042',     // your unique reference
    msisdn: '074000000',         // customer's number
    operator: Operator::AIRTEL,
));
```

---

Contents
--------

[](#contents)

- [Guide du débutant (FR)](GUIDE-DEBUTANT.md) — step-by-step beginner guide
- [How it works](#how-it-works) — read this first
- [Requirements](#requirements)
- [Installation](#installation)
- [Provider setup](#provider-setup)
- [Quickstart (end to end)](#quickstart-end-to-end)
- [Payout (cash-out)](#payout-cash-out)
- [API reference](#api-reference)
- [Webhooks in depth](#webhooks-in-depth)
- [Security model](#security-model)
- [Configuration reference](#configuration-reference)
- [Recipes &amp; FAQ](#recipes--faq)
- [Testing](#testing)
- [Roadmap](#roadmap)

---

How it works
------------

[](#how-it-works)

A mobile money collection is **asynchronous**: `pay()` only *starts* it. The real outcome (success / failure) arrives later, through a **webhook**, which the package turns into a Laravel **event**.

There are two flows, depending on the provider:

FlowProvidersWhat `pay()` returnsWhat you do**USSD push**SingPay (default), PViT, E-Billing (`flow: ussd_push`)`PaymentResponse` with `needsRedirect() === false`Tell the user to confirm the push (PIN) on their phone, then wait for the event**Hosted redirect**E-Billing (default), SingPay (`flow: ext`)`PaymentResponse` with a `redirectUrl`Redirect the user to that URL; they pay on the provider's page and come backThe full lifecycle:

```
  Your app                         Package                       Provider
     │  pay(PaymentRequest)           │                              │
     │ ─────────────────────────────► │  initiate + store txn        │
     │                                │ ───────────────────────────► │
     │ ◄───────────────────────────── │  PaymentResponse (PENDING)   │
     │  (redirect OR "confirm push")  │                              │
     │                                │        ┌── customer pays ────┤
     │                                │        │  (PIN / hosted page)│
     │                                │ ◄──────┴── webhook ───────── │
     │                                │  re-verify status via API    │
     │                                │ ───────────────────────────► │
     │ ◄── PaymentSucceeded event ─── │  update txn + dispatch event │
     │  fulfill the order             │                              │

```

> **Key idea:** never mark an order as paid from the `pay()` response. Fulfil it only when you receive the `PaymentSucceeded` event.

---

Requirements
------------

[](#requirements)

- PHP **8.3+**
- Laravel **12.x / 13.x**

---

Installation
------------

[](#installation)

```
composer require maestrodimateo/simple-mobile-money
```

Publish the config and run the migration (the transactions table is registered automatically):

```
php artisan vendor:publish --tag=mobile-money-config
php artisan migrate
```

---

Provider setup
--------------

[](#provider-setup)

Pick your default provider and fill in the credentials for the ones you use.

```
# ── default provider used when you call MobileMoney::driver() with no argument
MOBILE_MONEY_PROVIDER=singpay
```

### E-Billing (Digitech)

[](#e-billing-digitech)

Credentials come from your merchant profile (LAB or PROD). PAYIN auth defaults to HTTP Basic; it can also use OAuth2 (AWS Cognito) — see below.

```
EBILLING_USERNAME=
EBILLING_SHARED_KEY=
EBILLING_ENV=lab            # lab | production
EBILLING_FLOW=redirect      # redirect (hosted portal) | ussd_push (Airtel/Moov)
# EBILLING_EXPIRY_PERIOD=30 # bill validity in minutes (optional)

# Payout / disbursement (SHAP — separate OAuth2 credentials)
EBILLING_PAYOUT_API_ID=
EBILLING_PAYOUT_API_SECRET=
```

> E-Billing **requires `payer_email` and `payer_name`** — set `payerEmail` and `payerName` on the `PaymentRequest`, otherwise the driver throws. It is also **callback-only**: it exposes no status endpoint, so `status()` throws for this driver (see [How it works](#how-it-works)).

#### PAYIN OAuth2 (AWS Cognito)

[](#payin-oauth2-aws-cognito)

E-Billing is migrating the PAYIN API from HTTP Basic to **AWS Cognito OAuth2**(production cutover **2026-08-31**). The driver supports both; it stays on Basic until you flip `EBILLING_AUTH` to `oauth2`, so existing integrations are unaffected.

Once Digitech provisions your Cognito client (client id + secret, and the granted scopes), switch over:

```
EBILLING_AUTH=oauth2
EBILLING_OAUTH_CLIENT_ID=
EBILLING_OAUTH_CLIENT_SECRET=
# Scopes must match what your Cognito client is granted (space-separated),
# else the token request fails with invalid_scope. Default covers pay-in:
# EBILLING_OAUTH_SCOPE="ebilling-api/invoice:create ebilling-api/payment:create"
# Token endpoint defaults per env (lab.billing-easy.net / billing-easy.com); override if needed:
# EBILLING_OAUTH_TOKEN_URL=
# EBILLING_OAUTH_TOKEN_TTL=3300   # cache lifetime in seconds (token lives ~3600)
```

The driver runs the **client-credentials** flow (HTTP Basic `client_id:secret` + form `grant_type=client_credentials&scope=…`), caches the Bearer token, and authenticates every PAYIN call with it. Nothing else in your code changes — `pay()` works the same.

> **Scopes** map to what you do: `ebilling-api/invoice:create` (create a bill), `ebilling-api/payment:create` (USSD push). Add `…/invoice:read`, `…/payment:read`only if you request them in the portal. The scope name in the portal omits the `ebilling-api/` prefix — always request it **with** the prefix.
>
> **Secret rotation:** Digitech auto-rotates `client_secret` (lab every 30 days, prod every 90). You get an email; update `EBILLING_OAUTH_CLIENT_SECRET` during the grace period (lab 14 days, prod 30). Keep it in your secret manager, never in code.
>
> This toggle covers **PAYIN only** — the SHAP payout API keeps its own `EBILLING_PAYOUT_API_*` credentials, unchanged.

### SingPay

[](#singpay)

Credentials come from **SingPay Workspace** (`client.singpay.ga`). A test wallet is created automatically; the same base URL is used for test and production.

```
SINGPAY_CLIENT_ID=
SINGPAY_CLIENT_SECRET=
SINGPAY_WALLET=
# SINGPAY_DISBURSEMENT=       # required only for a production wallet
SINGPAY_FLOW=ussd_push        # ussd_push (direct) | ext (hosted page)
# For the ext flow (both required by SingPay; callbackUrl overrides success):
# SINGPAY_REDIRECT_SUCCESS=https://your-app.com/pay/success
# SINGPAY_REDIRECT_ERROR=https://your-app.com/pay/error
# SINGPAY_LOGO_URL=
```

### PViT (mypvit / BakoAI, API v2)

[](#pvit-mypvit--bakoai-api-v2)

Credentials come from your PViT merchant space. Auth is the `X-Secret` header.

```
PVIT_SECRET=sk_live_xxxxxxxx
PVIT_CODE_URL=              # the {codeUrl} in your endpoints
PVIT_ACCOUNT_OPERATION_CODE=
PVIT_CALLBACK_URL_CODE=
```

> Some provider details are not publicly documented (PViT's Airtel v2 operator code, E-Billing operator machine codes and status endpoint). They are **configurable** in `config/mobile-money.php` (`providers.*.operators`) rather than hard-coded — confirm them with each provider and adjust if needed.

---

Quickstart (end to end)
-----------------------

[](#quickstart-end-to-end)

### 1. Start the payment (controller)

[](#1-start-the-payment-controller)

```
use Illuminate\Http\Request;
use Maestrodimateo\MobileMoney\Facades\MobileMoney;
use Maestrodimateo\MobileMoney\Data\PaymentRequest;
use Maestrodimateo\MobileMoney\Enums\Operator;

class CheckoutController
{
    public function pay(Request $request)
    {
        $response = MobileMoney::driver('singpay')->pay(new PaymentRequest(
            amount: 500,                       // XAF, integer
            reference: 'ORDER-'.$order->id,    // your unique reference
            msisdn: $request->input('phone'),  // e.g. '074000000'
            operator: Operator::AIRTEL,
            description: "Order #{$order->id}",
        ));

        // Hosted flow (E-Billing): send the customer to the provider's page.
        if ($response->needsRedirect()) {
            return redirect()->away($response->redirectUrl);
        }

        // USSD-push flow (SingPay, PViT): the customer confirms on their phone.
        return view('checkout.pending', ['reference' => $response->reference]);
    }
}
```

`$response->status` is normally `PaymentStatus::PENDING` here. Do **not** treat it as paid yet.

### 2. Register the webhook URL

[](#2-register-the-webhook-url)

The package already exposes the route `POST /mobile-money/webhook/{provider}`. In each provider's merchant dashboard, register the matching public URL:

```
https://your-app.com/mobile-money/webhook/singpay
https://your-app.com/mobile-money/webhook/ebilling
https://your-app.com/mobile-money/webhook/pvit

```

### 3. Fulfil the order when the payment succeeds (listener)

[](#3-fulfil-the-order-when-the-payment-succeeds-listener)

```
use Maestrodimateo\MobileMoney\Events\PaymentSucceeded;

class FulfilOrder
{
    public function handle(PaymentSucceeded $event): void
    {
        // $event->result is a CallbackResult (provider-neutral)
        $order = Order::where('reference', $event->result->reference)->firstOrFail();

        if ($order->isPaid()) {
            return; // idempotent: the same webhook may arrive more than once
        }

        $order->markPaid();
    }
}
```

Register it in your `EventServiceProvider` (or with an attribute listener). A `PaymentFailed` event is dispatched for failed / cancelled / expired payments.

That's the whole loop: **initiate → (redirect or push) → event → fulfil**.

---

Payout (cash-out)
-----------------

[](#payout-cash-out)

Send money **out** to a beneficiary (refund, cashback, withdrawal). Payout is a separate capability — only providers implementing `SupportsPayout` offer it.

ProviderPayout**E-Billing**✅ Pays any beneficiary by number via SHAP (own OAuth2 credentials, see setup). `balance()` + `payoutStatus()` available.**PViT**✅ "Rendu monnaie" — pays any beneficiary via the `GIVE_CHANGE` transaction on `/rest`. `balance()` + `payoutStatus()` available.SingPay⛔ Not supported — calling `payout()` throws a clear error.> SingPay's only cash-out is a *transfer* tied to a prior collection and a pre-registered recipient — it does not pay an arbitrary number, so it isn't exposed here. For arbitrary payouts (refunds, winnings), use **E-Billing** or **PViT**.

For Gabonese operators (Airtel / Moov), an E-Billing payout is **synchronous**: the call returns the final status directly (no webhook). A PViT "rendu monnaie" uses the same neutral API — swap the driver name.

```
use Maestrodimateo\MobileMoney\Data\PayoutRequest;
use Maestrodimateo\MobileMoney\Enums\Operator;
use Maestrodimateo\MobileMoney\Enums\PayoutType;

$response = MobileMoney::driver('ebilling')->payout(new PayoutRequest(
    amount: 5000,                       // XAF
    reference: 'PAYOUT-42',             // your unique reference
    msisdn: '074000000',                // beneficiary
    operator: Operator::AIRTEL,
    type: PayoutType::REFUND,           // refund | cashback | withdrawal
));

$response->status; // PaymentStatus::SUCCESS (synchronous)

MobileMoney::driver('ebilling')->balance();                 // ['airtelmoney' => 305394, ...] (XAF)
MobileMoney::driver('ebilling')->payoutStatus('PAYOUT-42'); // PaymentStatus
```

Events: `PayoutInitiated`, then `PayoutSucceeded` / `PayoutFailed` (fired immediately since the payout is synchronous). Payouts are stored in the same table with `type = payout`.

---

API reference
-------------

[](#api-reference)

### Facade — `MobileMoney`

[](#facade--mobilemoney)

```
MobileMoney::driver(?string $provider = null): Gateway   // null = default provider
MobileMoney::fake(): MobileMoneyFake                      // swap in a test double (see Testing)
```

Each `Gateway` exposes:

MethodReturnsPurpose`pay(PaymentRequest $request)``PaymentResponse`Start a collection`status(string $providerReference)``PaymentStatus`Poll the current status (throws if `supportsStatusQuery()` is false)`supportsStatusQuery()``bool`Whether the provider exposes status polling (E-Billing: `false`)`verify(string $merchantRef, ?string $providerRef)``PaymentStatus`Re-verify bound to the merchant reference (used internally by the webhook)`parseWebhook(Request $request)``CallbackResult`Normalise a raw webhook (used internally)`name()``string`The provider key### `PaymentRequest` (you build this)

[](#paymentrequest-you-build-this)

FieldTypeRequiredNotes`amount``int`yesXAF, integer (no decimals)`reference``string`yesYour unique reference (PViT: ≤ 15 chars)`msisdn``string`yesCustomer number, e.g. `074000000``operator``Operator`yes`Operator::AIRTEL` or `Operator::MOOV``description``?string`noShown to the customer`payerName``?string`no`payerEmail``?string`no`callbackUrl``?string`noPer-request return URL (hosted flow)`metadata``array`noEchoed back to you; never sent to the provider### `PaymentResponse` (returned by `pay()`)

[](#paymentresponse-returned-by-pay)

PropertyTypeNotes`status``PaymentStatus`Usually `PENDING``reference``string`Your reference`providerReference``?string`Provider transaction / bill id`redirectUrl``?string`Set for hosted flows`raw``array`Raw provider response`needsRedirect()``bool``true` when a redirect is required### `PaymentStatus` (enum)

[](#paymentstatus-enum)

`PENDING`, `SUCCESS`, `FAILED`, `CANCELLED`, `EXPIRED`, `AMBIGUOUS`, `UNKNOWN`.

```
$status->isFinal();       // true for SUCCESS / FAILED / CANCELLED / EXPIRED
$status->isSuccessful();  // true only for SUCCESS
```

### `Operator` (enum)

[](#operator-enum)

`Operator::AIRTEL`, `Operator::MOOV` — with `->label()` ("Airtel Money" / "Moov Money").

### `CallbackResult` (carried by webhook events)

[](#callbackresult-carried-by-webhook-events)

`reference`, `status`, `providerReference`, `amount`, `operator`, `raw`.

### Events

[](#events)

EventWhenProperties`PaymentInitiated`after `pay()``provider`, `request`, `response`, `transaction``PaymentSucceeded`webhook re-verified as success`provider`, `result`, `transaction``PaymentFailed`webhook re-verified as failed/cancelled/expired`provider`, `result`, `transaction``transaction` is the stored `Transaction` model (or `null` if persistence is off).

### `Transaction` model

[](#transaction-model)

Stored in `mobile_money_transactions`: `provider`, `reference`, `provider_reference`, `status` (cast to `PaymentStatus`), `amount`, `currency`, `msisdn`, `operator` (cast to `Operator`), `description`, `metadata`, `raw`.

### Exceptions

[](#exceptions)

- `InvalidConfigurationException` — missing credential or unmapped operator.
- `ProviderRequestException` — the provider returned an HTTP error (`->provider`, `->statusCode`, `->context`).
- Both extend `MobileMoneyException`.

---

Webhooks in depth
-----------------

[](#webhooks-in-depth)

- **Endpoint:** `POST {webhooks.path}/{provider}` (default `mobile-money/webhook/{provider}`), route name `mobile-money.webhook`.
- **No CSRF:** the route is registered outside the `web` group, so external POSTs work without a token.
- **Acknowledgement:** the endpoint replies `{ "responseCode": 200, "transactionId": "..." }` — the exact shape PViT requires; the others just need HTTP 200.
- **Testing locally:** expose your app with a tunnel (e.g. `php artisan expose` or ngrok) and register the public tunnel URL in the dashboards.
- **Disable entirely:** set `MOBILE_MONEY_WEBHOOKS=false` (the route won't be registered).

---

Security model
--------------

[](#security-model)

None of the three aggregators cryptographically sign their webhooks, so the callback body is treated as an **untrusted hint** — never as truth. On every webhook the package:

1. optionally restricts callbacks to an **IP allowlist**(`webhooks.allowed_ips`) — configure TrustedProxies so `request()->ip()` is the real client IP behind a load balancer;
2. matches the callback to a **stored transaction** by merchant reference and **rejects** any webhook pointing to a different provider transaction than the one on record (reference-confusion protection);
3. **re-verifies the status against the provider API** using the *stored*provider reference — never the one in the webhook body (`webhooks.verify_status`, on by default — keep it on). For SingPay's `ext`flow (where no provider reference is known until the webhook), the outcome is additionally **bound to the merchant reference**: the re-verified provider transaction must belong to the expected order, or it is rejected;
4. **fails closed** (HTTP 4xx, no success dispatched) when it cannot re-verify or is unsure (`UNKNOWN` / `AMBIGUOUS`), and never moves a transaction out of a terminal state. For E-Billing (callback-only), a matching **amount is mandatory** and a partial payment is never promoted to a full success;
5. dispatches terminal events **only on the transition** into the final state, so a replayed webhook cannot re-trigger fulfilment;
6. is **rate-limited** by default (`webhooks.middleware`), since each call triggers a synchronous outbound verification.

> These guarantees rely on the stored transaction, so **keep persistence enabled**(`store.enabled`) in production. With storage off, the package can only do a best-effort check and your application must validate the reference ↔ provider-reference mapping itself.

---

Configuration reference
-----------------------

[](#configuration-reference)

`config/mobile-money.php`:

KeyDefaultPurpose`default``ebilling`Provider used by `MobileMoney::driver()``store.enabled``true`Persist transactions (also gates the auto-migration)`store.store_raw``true`Persist providers' raw payloads (contain PII) — set `false` to omit`store.table``mobile_money_transactions`Table name`store.model``Transaction::class`Model used for persistence (a custom one **must extend** `Transaction`)`webhooks.enabled``true`Register the webhook route`webhooks.path``mobile-money/webhook`Base path (provider is appended)`webhooks.middleware``['throttle:60,1']`Middleware for the webhook route (throttling recommended)`webhooks.verify_status``true`Re-verify status before trusting a success`webhooks.allowed_ips``[]`Per-provider IP allowlist (`[]` = no IP check)`providers.*`—Credentials, base URLs and operator maps---

Recipes &amp; FAQ
-----------------

[](#recipes--faq)

**Use a custom Transaction model** — point `store.model` at your own model (extend the package's `Transaction`, or match its columns).

**Go stateless** — set `store.enabled=false`. You then persist whatever you need from the `PaymentInitiated` / `PaymentSucceeded` events yourself. Read the [security note](#security-model) first.

**Make fulfilment idempotent** — the same webhook can arrive more than once. Guard your listener (`if ($order->isPaid()) return;`) as shown in the quickstart.

**Poll instead of / in addition to webhooks** — call `MobileMoney::driver('pvit')->status($providerReference)` where `$providerReference` is `PaymentResponse->providerReference` (or the stored `Transaction->provider_reference`). Not available for E-Billing, which is callback-only (`supportsStatusQuery() === false`).

**Test your integration** — the package uses Laravel's HTTP client, so fake it:

```
use Illuminate\Support\Facades\Http;

Http::fake([
    'gateway.singpay.ga/*' => Http::response(['transaction' => ['id' => 'tx-1', 'status' => 'Start']]),
]);

$response = MobileMoney::driver('singpay')->pay(/* ... */);
expect($response->providerReference)->toBe('tx-1');
```

---

Testing
-------

[](#testing)

Run the package's own suite:

```
composer test        # Pest
composer lint        # Pint (code style)
composer analyse     # PHPStan / Larastan level 6
```

### Faking in your app's tests

[](#faking-in-your-apps-tests)

`MobileMoney::fake()` swaps the manager for a test double, so you can test your checkout / payout code against the neutral package API — no HTTP, and without stubbing each provider's raw payloads. It returns a `PENDING` payment and a `SUCCESS` payout by default; override per test and assert what was sent:

```
use Maestrodimateo\MobileMoney\Enums\PaymentStatus;
use Maestrodimateo\MobileMoney\Facades\MobileMoney;

it('places an order', function () {
    MobileMoney::fake();                 // stub every call, record nothing on the wire

    $this->post('/checkout', ['amount' => 500])->assertOk();

    MobileMoney::assertPaid(fn ($request, $provider) => $request->amount === 500);
});

it('handles a declined payment', function () {
    MobileMoney::fake()->respondToPayments(PaymentStatus::FAILED);

    // ... assert your code reacts to the FAILED response
});
```

Stub configuration (all fluent, all optional):

MethodEffect`respondToPayments(PaymentStatus $s)`Status returned by `pay()` (default `PENDING`)`respondToPayouts(PaymentStatus $s)`Status returned by `payout()` (default `SUCCESS`)`respondToStatus(PaymentStatus $s)`Status returned by `status()` / `verify()``respondToBalance(array $b)`Value returned by `balance()`Assertions:

MethodAsserts`assertPaid($callback | int | null)`A payment was made (callback receives `PaymentRequest`, `string $provider`; int asserts a count)`assertPayout($callback | int | null)`A payout was made (same signature, with `PayoutRequest`)`assertNothingSent()`No payment or payout was made> The fake covers the `pay()` / `payout()` side only. It does **not** persist transactions or dispatch events — those are webhook-driven. To test that path, `Http::fake()` the provider and POST the webhook route (see [Webhooks in depth](#webhooks-in-depth)).

---

Roadmap
-------

[](#roadmap)

- Disbursement / payout (cash-out).
- SingPay hosted payment page (`/ext`) as an alternative to the USSD-push flow.
- Direct Airtel Money / Moov Money integrations (operator contracts required).

---

License
-------

[](#license)

MIT.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance95

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Total

5

Last Release

22d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/92a2c64304e341345f2854b5bc9c43806b9e54f0b5b9b57135d37d69dd7c5c67?d=identicon)[mebalenoel](/maintainers/mebalenoel)

---

Top Contributors

[![maestrodimateo](https://avatars.githubusercontent.com/u/40523415?v=4)](https://github.com/maestrodimateo "maestrodimateo (7 commits)")

---

Tags

laravelpaymentmobile-moneyebillingairtel-moneymoov-moneypvitgabonsingpay

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/maestrodimateo-simple-mobile-money/health.svg)

```
[![Health](https://phpackages.com/badges/maestrodimateo-simple-mobile-money/health.svg)](https://phpackages.com/packages/maestrodimateo-simple-mobile-money)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k161.4k2](/packages/mike-bronner-laravel-model-caching)[forjedio/inertia-table

Backend-driven dynamic tables for Laravel + Inertia.js

272.0k](/packages/forjedio-inertia-table)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45844.8k1](/packages/pressbooks-pressbooks)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5226.7k](/packages/simplestats-io-laravel-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
