smartguycodes / billing
A configurable Laravel billing package for SaaS businesses in Kenya. Handles M-Pesa STK Push, subscriptions, dunning, and renewal reminders.
Package info
github.com/SmartGuyCodes/smart-billing
Language:Blade
pkg:composer/smartguycodes/billing
Requires
- php: ^8.3
- guzzlehttp/guzzle: ^7.0
- laravel/framework: ^11.0|^12.0
- nesbot/carbon: ^3.11
Requires (Dev)
- orchestra/testbench: ^9.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
This package is auto-updated.
Last update: 2026-04-07 13:22:07 UTC
README
Drop-in Laravel billing for the Kenyan SaaS market.
Zero plumbing. Define your plans, configure M-Pesa, and your billing loop is sorted — subscriptions, STK Push, callbacks, dunning, renewal reminders, and an admin UI all included.
Architecture
smart-billing/
├── src/
│ ├── BillingServiceProvider.php # Auto-discovery, publishes, registers
│ ├── Concerns/Billable.php # Trait for your User model
│ ├── Contracts/PaymentDriver.php # Interface for all payment drivers
│ ├── Drivers/MpesaDriver.php # Full Daraja API — STK Push, C2B, Refunds
│ ├── Facades/Billing.php
│ ├── Services/
│ │ ├── BillingManager.php # Extensible driver manager
│ │ ├── PaymentService.php # Orchestrates initiate → callback → renew
│ │ ├── SubscriptionService.php # subscribe, cancel, resume, changePlan
│ │ └── DunningService.php # Retry logic, suspension, cancellation
│ ├── Models/ # 5 Eloquent models with scopes & helpers
│ ├── Http/Controllers/ # Admin UI + REST API + Webhook handlers
│ ├── Events/ # 8 events for your listeners
│ ├── Notifications/RenewalReminder
│ ├── Console/Commands/ # billing:install/renewals/dunning/reminders
│ └── Support/PaymentResult.php # Typed result value object
├── config/billing.php # All configurable — driver, dunning, reminders
├── database/migrations/ # 6 tables created in one migration
├── resources/views/admin/ # Dark-themed admin UI — dashboard, all CRUD
└── routes/web.php + api.php
- Drivers: M-Pesa (Daraja) out of the box, with a simple interface to add Stripe, Flutterwave, etc.
- Transactions: Every payment attempt is stored with a unique reference, status, and metadata
- Subscriptions: Plan-based billing with trial periods, grace periods, and dunning support
- Admin UI: View transactions, manage plans, and monitor subscription statuses
- Scheduler: Daily commands to process renewals, send reminders, and handle dunning
Requirements
| PHP | ^8.2 |
| Laravel | ^11 | ^12 |
| M-Pesa | Daraja API credentials |
Installation
composer require smartguycodes/billing
php artisan billing:install
This publishes the config, runs migrations, and optionally seeds sample plans.
Configuration
.env
# Core BILLING_DRIVER=mpesa BILLING_CURRENCY=KES BILLING_CURRENCY_SYMBOL=KSh # M-Pesa (Daraja) MPESA_ENV=sandbox # sandbox | production MPESA_CONSUMER_KEY=your_key MPESA_CONSUMER_SECRET=your_secret MPESA_SHORTCODE=174379 MPESA_PASSKEY=your_passkey MPESA_TYPE=paybill # paybill | till MPESA_CALLBACK_URL=https://yourapp.com/api/billing/webhooks/mpesa/callback MPESA_TIMEOUT_URL=https://yourapp.com/api/billing/webhooks/mpesa/timeout # Admin UI BILLING_ADMIN_PREFIX=billing-admin # Invoice BILLING_INVOICE_PREFIX=INV BILLING_COMPANY_NAME="My SaaS Co" BILLING_REF_PREFIX=TXN
Billable Model
Add the Billable trait to your User model:
use SmartGuyCodes\Billing\Concerns\Billable; class User extends Authenticatable { use Billable; }
Usage
Initiate an M-Pesa STK Push
use SmartGuyCodes\Billing\Facades\Billing; $result = $user->charge([ 'amount' => 999, 'account_number' => '0712345678', // Customer phone 'account_type' => 'mobile', 'transaction_type' => 'income', 'description' => 'Starter Plan - March 2025', ]); if ($result->isPending()) { // STK Push sent — wait for callback $checkoutId = $result->checkoutRequestId; }
Subscribe to a Plan
// By slug $plan = BillingPlan::where('slug', 'pro')->first(); $subscription = $user->subscribeTo($plan); // With trial $subscription = $user->subscribeTo($plan, ['trial_days' => 14]);
Check Subscription Status
$user->isSubscribed(); // bool $user->onTrial(); // bool $user->subscribedTo('pro'); // bool $sub = $user->activeSubscription(); $sub->daysUntilRenewal(); // int $sub->isActive(); // bool $sub->onGracePeriod(); // bool
Cancel & Resume
$user->cancelSubscription(); // At period end $user->cancelSubscription(immediately: true); // Right now // Resume within grace period $sub->resume();
Verify a Transaction
$transaction = BillingTransaction::where('reference_no', 'TXN-...')->first(); $result = app(\SmartGuyCodes\Billing\Services\PaymentService::class)->verify($transaction);
Transaction Data Model
Every transaction stores the full API User Layer:
| Field | Description |
|---|---|
reference_no |
System-generated — TXN-250101120000-ABCD |
invoice_number |
INV-2025-00042 |
client_no |
Your identifier for the customer |
account_number |
Mobile number, bank account, or card number |
account_type |
mobile | bank | card |
transaction_type |
income | expense |
amount |
Float |
currency |
e.g. KES |
status |
pending → completed | failed | refunded |
driver |
mpesa | stripe | flutterwave |
gateway_ref |
M-Pesa receipt number or Stripe charge ID |
M-Pesa Webhook URLs
Register these in the Safaricom Daraja portal:
| Type | URL |
|---|---|
| STK Callback | https://yourapp.com/api/billing/webhooks/mpesa/callback |
| STK Timeout | https://yourapp.com/api/billing/webhooks/mpesa/timeout |
| C2B Validation | https://yourapp.com/api/billing/webhooks/mpesa/validation |
| C2B Confirmation | https://yourapp.com/api/billing/webhooks/mpesa/confirmation |
REST API
All routes are prefixed /api/billing and require auth:sanctum.
POST /api/billing/pay
GET /api/billing/transactions/{ref}
POST /api/billing/transactions/{ref}/verify
GET /api/billing/plans
POST /api/billing/subscribe
GET /api/billing/subscription
DELETE /api/billing/subscription
POST /api/billing/subscription/resume
GET /api/billing/invoices
GET /api/billing/invoices/{id}
Example: Initiate Payment
POST /api/billing/pay
Authorization: Bearer {token}
Content-Type: application/json
{
"amount": 999,
"account_number": "0712345678",
"account_type": "mobile",
"transaction_type": "income",
"description": "Pro Plan Renewal"
}
Response:
{
"success": true,
"status": "pending",
"reference": "TXN-250401120000-XKQZ",
"checkout_request_id": "ws_CO_...",
"message": "Enter your M-Pesa PIN to complete the payment."
}
Admin Interface
Visit /{BILLING_ADMIN_PREFIX} (default: /billing-admin).
The middleware billing.admin gates access. By default it checks $user->is_admin. Override by publishing the middleware:
php artisan vendor:publish --tag=billing-views
Or define a gate in your AuthServiceProvider:
Gate::define('billing-admin', fn($user) => $user->role === 'superadmin');
Scheduler
Add to routes/console.php (Laravel 11+):
use Illuminate\Support\Facades\Schedule; Schedule::command('billing:renewals')->dailyAt('00:05'); Schedule::command('billing:dunning')->dailyAt('02:00'); Schedule::command('billing:reminders')->dailyAt('08:00');
| Command | Purpose |
|---|---|
billing:renewals |
Charge subscriptions due today |
billing:dunning |
Retry failed payments per dunning config |
billing:reminders |
Send renewal reminder notifications |
billing:install |
One-time setup wizard |
Adding a Custom Driver
// In a ServiceProvider app('billing')->extend('mpesa', function ($app) { return new MpesaDriver(config('billing.drivers.mpesa')); });
Your driver must implement SmartGuyCodes\Billing\Contracts\PaymentDriver:
interface PaymentDriver { public function initiate(array $payload): PaymentResult; public function verify(string $reference): PaymentResult; public function handleCallback(array $payload): PaymentResult; public function refund(string $reference, float $amount): PaymentResult; public function driverName(): string; public function validateConfig(): void; }
Events
Listen to these events in your application:
use SmartGuyCodes\Billing\Events\PaymentCompleted; Event::listen(PaymentCompleted::class, function ($event) { // $event->transaction — BillingTransaction // $event->result — PaymentResult Log::info("Payment received: {$event->transaction->reference_no}"); });
| Event | Fired When |
|---|---|
PaymentInitiated |
STK Push sent |
PaymentCompleted |
Callback confirms success |
PaymentFailed |
Callback confirms failure |
SubscriptionCreated |
User subscribes to a plan |
SubscriptionRenewed |
Subscription period renewed |
SubscriptionCancelled |
Subscription cancelled |
SubscriptionSuspended |
Dunning suspension |
DunningAttempted |
Retry payment attempted |
Plans Config (Optional)
If you prefer config-driven plans over database plans, set BILLING_PLANS_SOURCE=config and define in config/billing.php:
'plans' => [ [ 'name' => 'Starter', 'slug' => 'starter', 'price' => 999, 'interval' => 'monthly', 'trial_days' => 14, 'features' => ['Up to 5 users', '10GB storage'], ], ],
Usage
- Integrate billing features into your Laravel app using provided facades, models, and API endpoints.
- Access the billing dashboard at
/billing(if enabled).
Contributing
Contributions are welcome! Please open issues or submit pull requests to help improve the package.
License
This package is open-sourced software licensed under the MIT License.