A powerful Laravel billing package for payments and subscriptions, inspired by Laravel Cashier. Built specifically for West African markets with FedaPay integration, supporting Mobile Money (MTN, Moov, Togocel), one-time payments, recurring subscriptions, and marketplace features with commissions and payouts.
- Multiple Payment Providers - Extensible architecture with FedaPay as the default provider
- Mobile Money Support - MTN Mobile Money, Moov Money, Togocel T-Money with USSD Push
- Subscriptions - Full subscription lifecycle management with trials, grace periods, and plan swapping
- One-time Payments - Simple charge API for single payments
- Marketplace Module - Commission calculation, transaction tracking, and seller payouts
- Webhook Handling - Automatic webhook processing with signature verification
- Multi-currency - Support for XOF and other currencies
- Localization - French and English translations included
- PHP 8.4+
- Laravel 11.x or 12.x
Install the package via Composer:
composer require ratoufa/laravel-billingRun the install command to publish configuration and migrations:
php artisan billing:installOr publish manually:
# Publish configuration
php artisan vendor:publish --tag="billing-config"
# Publish migrations
php artisan vendor:publish --tag="billing-migrations"
# Publish translations (optional)
php artisan vendor:publish --tag="billing-translations"Run the migrations:
php artisan migrateAdd your FedaPay credentials to your .env file:
BILLING_PROVIDER=fedapay
FEDAPAY_PUBLIC_KEY=pk_sandbox_xxxxxxxx
FEDAPAY_SECRET_KEY=sk_sandbox_xxxxxxxx
FEDAPAY_ENV=sandbox
FEDAPAY_CURRENCY=XOF
FEDAPAY_WEBHOOK_SECRET=whsec_xxxxxxxx// config/billing.php
return [
// Default billing provider
'default' => env('BILLING_PROVIDER', 'fedapay'),
// Provider configurations
'providers' => [
'fedapay' => [
'driver' => 'fedapay',
'public_key' => env('FEDAPAY_PUBLIC_KEY'),
'secret_key' => env('FEDAPAY_SECRET_KEY'),
'environment' => env('FEDAPAY_ENV', 'sandbox'),
'currency' => env('FEDAPAY_CURRENCY', 'XOF'),
'webhook_secret' => env('FEDAPAY_WEBHOOK_SECRET'),
],
],
// Custom model classes (optional)
'models' => [
'plan' => \Ratoufa\Billing\Models\Plan::class,
'subscription' => \Ratoufa\Billing\Models\Subscription::class,
'payment' => \Ratoufa\Billing\Models\Payment::class,
],
// Subscription settings
'subscription' => [
'grace_days' => 3,
'retry' => [
'enabled' => true,
'max_attempts' => 3,
'interval_hours' => 24,
],
],
// Marketplace module (optional)
'marketplace' => [
'enabled' => false,
'commission' => [
'default_rate' => 0.10, // 10%
'min_payout_amount' => 1000,
],
'payout' => [
'mode' => 'instant', // or 'batch'
],
],
];Add the Billable trait to your User model (or any model that can make payments):
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Ratoufa\Billing\Concerns\Billable;
final class User extends Authenticatable
{
use Billable;
// ...
}Add the required columns to your users table:
// In a migration
Schema::table('users', function (Blueprint $table) {
$table->string('billing_provider')->nullable();
$table->string('billing_provider_id')->nullable();
$table->string('phone')->nullable();
});Create subscription plans in your database:
use Ratoufa\Billing\Models\Plan;
$plan = Plan::create([
'name' => 'Pro Plan',
'slug' => 'pro',
'description' => 'Access to all features',
'price' => 5000, // 5000 XOF
'currency' => 'XOF',
'interval' => 'month',
'interval_count' => 1,
'is_active' => true,
]);// Create a subscription with a trial period
$user->newSubscription('default', 'pro')
->trialDays(14)
->create();
// Or with a specific plan model
$plan = Plan::where('slug', 'pro')->first();
$user->newSubscription('default', $plan)->create();
// With additional metadata
$user->newSubscription('default', 'pro')
->withMetadata(['campaign' => 'launch'])
->create();// Check if the user has a valid subscription
if ($user->subscribed('default')) {
// User has an active subscription
}
// Check for a specific plan
if ($user->subscribed('default', 'pro')) {
// User is subscribed to the pro plan
}
// Other status checks
$user->onTrial('default'); // Is on trial?
$user->onGracePeriod('default'); // Is in grace period after cancellation?
$user->hasActiveSubscription(); // Has any active subscription?
// Get the subscription instance
$subscription = $user->subscription('default');
$subscription->active(); // Is active?
$subscription->cancelled(); // Is cancelled?
$subscription->pastDue(); // Is past due?
$subscription->paused(); // Is paused?
$subscription = $user->subscription('default');
// Cancel at end of billing period
$subscription->cancel();
// Cancel immediately
$subscription->cancelNow();
// Resume a cancelled subscription (during grace period)
$subscription->resume();
// Pause a subscription
$subscription->pause();
// Unpause
$subscription->unpause();
// Swap to a different plan
$subscription->swap('premium');
// or
$subscription->swap($premiumPlan);The package dispatches events for subscription lifecycle:
use Ratoufa\Billing\Events\SubscriptionCreated;
use Ratoufa\Billing\Events\SubscriptionCancelled;
use Ratoufa\Billing\Events\SubscriptionResumed;
use Ratoufa\Billing\Events\SubscriptionSwapped;
// In EventServiceProvider
protected $listen = [
SubscriptionCreated::class => [
SendWelcomeEmail::class,
],
SubscriptionCancelled::class => [
SendCancellationEmail::class,
],
];// Create a payment and get checkout URL
$paymentIntent = $user->pay(5000, [
'description' => 'Product purchase',
'return_url' => route('payment.success'),
]);
// Redirect to checkout
return redirect($paymentIntent['checkout_url']);
// Or get just the checkout URL
$checkoutUrl = $user->getCheckoutUrl(5000, [
'description' => 'Product purchase',
]);// Create a payment record
$payment = $user->charge(5000, [
'description' => 'One-time purchase',
]);For direct Mobile Money payments with USSD push (customer receives a prompt on their phone):
use Ratoufa\Billing\Facades\Billing;
// Auto-detect operator from phone number
$result = Billing::chargeWithMobileMoney(
billable: $user,
amount: 5000,
phoneNumber: '+22890123456'
);
// Specify operator manually
$result = Billing::chargeWithMobileMoney(
billable: $user,
amount: 5000,
phoneNumber: '+22890123456',
mode: 'mtn_open' // mtn_open, moov_tg, togocel, etc.
);
// Access the result
$result->paymentId; // Payment record ID
$result->mode; // Mobile Money mode used
$result->modeName; // Human-readable name
$result->message; // Message to show user| Mode | Operator | Countries |
|---|---|---|
mtn_open |
MTN Mobile Money | Benin, Ivory Coast, Cameroon |
mtn_ci |
MTN Ivory Coast | Ivory Coast |
moov_bj |
Moov Money | Benin |
moov_tg |
Moov Money | Togo |
togocel |
T-Money | Togo |
// Create customer in the billing provider
$customerId = $user->createAsCustomer([
'phone' => '+22890123456',
]);
// Update customer information
$user->updateCustomer([
'phone' => '+22890123456',
]);
// Sync (create or update)
$user->syncCustomer();The package automatically registers webhook routes. Configure your webhook endpoint in your payment provider:
https://your-app.com/billing/webhook/fedapay
use Ratoufa\Billing\Events\PaymentSucceeded;
use Ratoufa\Billing\Events\PaymentFailed;
protected $listen = [
PaymentSucceeded::class => [
ActivateUserAccess::class,
],
PaymentFailed::class => [
NotifyUserOfFailure::class,
],
];Enable signature verification in production:
BILLING_VERIFY_WEBHOOK_SIGNATURE=true
FEDAPAY_WEBHOOK_SECRET=whsec_your_secretProcess recurring subscription payments with the Artisan command:
# Process all due subscriptions
php artisan billing:process-payments
# Dry run (see what would be processed)
php artisan billing:process-payments --dry-run
# Limit number of subscriptions
php artisan billing:process-payments --limit=100Add to your scheduler (app/Console/Kernel.php):
$schedule->command('billing:process-payments')->daily();Enable the marketplace module for handling commissions and seller payouts.
BILLING_MARKETPLACE_ENABLED=true
BILLING_COMMISSION_RATE=0.10
BILLING_MIN_PAYOUT=1000
BILLING_PAYOUT_MODE=instantAdd the Commissionable trait to your seller/organization model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Ratoufa\Billing\Concerns\Commissionable;
final class Organization extends Model
{
use Commissionable;
protected $fillable = [
'name',
'payout_phone',
'payout_name',
// ...
];
}use Ratoufa\Billing\Services\CommissionCalculator;
$calculator = app(CommissionCalculator::class);
// Calculate commission
$commission = $calculator->calculate(
grossAmount: 10000,
commissionRate: 0.10 // 10% platform fee
);
// $commission->grossAmount = 10000
// $commission->commissionRate = 0.10
// $commission->commissionAmount = 1000
// $commission->netAmount = 9000
// Record the transaction
$transaction = $organization->recordTransaction($commission, [
'customer_email' => 'customer@example.com',
'provider_transaction_id' => 'txn_123',
]);// Get pending payout amount
$pending = $organization->pendingPayoutAmount();
// Get total earnings
$total = $organization->totalEarnings();
// Get total paid out
$paidOut = $organization->totalPaidOut();
// Get transactions awaiting payout
$transactions = $organization->transactionsAwaitingPayout();use Ratoufa\Billing\Services\InstantPayoutService;
use Ratoufa\Billing\Data\PayoutData;
use Ratoufa\Billing\Enums\PayoutMethodEnum;
$payoutService = app(InstantPayoutService::class);
// Create and process a payout
$payoutData = new PayoutData(
amount: $organization->pendingPayoutAmount(),
currency: 'XOF',
method: PayoutMethodEnum::MobileMoney,
recipientPhone: $organization->payout_phone,
recipientName: $organization->payout_name,
);
$payout = $payoutService->processPayout($organization, $payoutData);# Process scheduled payouts
php artisan billing:process-payouts
# Dry run
php artisan billing:process-payouts --dry-runuse Ratoufa\Billing\Events\PayoutCompleted;
use Ratoufa\Billing\Events\PayoutFailed;
use Ratoufa\Billing\Events\SaleCompleted;
protected $listen = [
SaleCompleted::class => [
NotifySellerOfSale::class,
],
PayoutCompleted::class => [
NotifySellerOfPayout::class,
],
];use Ratoufa\Billing\Facades\Billing;
// Get the default provider
$provider = Billing::driver();
// Use a specific provider
$provider = Billing::driver('fedapay');
// Create a payment intent
$intent = Billing::createPaymentIntent($user, 5000, [
'description' => 'Purchase',
]);
// Check provider capabilities
Billing::supportsRecurringPayments(); // bool
Billing::supportsPaymentMethods(); // boolCreate a custom billing provider:
<?php
namespace App\Billing\Providers;
use Ratoufa\Billing\Providers\AbstractProvider;
use Illuminate\Database\Eloquent\Model;
final class CustomProvider extends AbstractProvider
{
public function name(): string
{
return 'custom';
}
public function createCustomer(Model $billable, array $options = []): string
{
// Implementation
}
public function charge(Model $billable, int $amount, array $options = []): Payment
{
// Implementation
}
public function createPaymentIntent(Model $billable, int $amount, array $options = []): array
{
// Implementation
}
// ... implement other methods
}Register your provider in a service provider:
use Ratoufa\Billing\BillingManager;
public function boot(): void
{
$this->app->extend(BillingManager::class, function (BillingManager $manager) {
$manager->extend('custom', function ($config) {
return new CustomProvider($config);
});
return $manager;
});
}# Run all tests
composer test
# Run specific test suites
composer test:unit # Pest tests
composer test:types # PHPStan analysis
composer test:lint # Pint code style
composer test:type-coverage # Type coverage (100%)
# Fix code style
composer lintPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.