PHP code example of develupers / laravel-plan-usage

1. Go to this page and download the library: Download develupers/laravel-plan-usage library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

develupers / laravel-plan-usage example snippets


return [
    'tables' => [
        'billable' => 'accounts', // Your billable model's table
        'plans' => 'plans',
        'plan_prices' => 'plan_prices',
        'features' => 'features',
        'plan_features' => 'plan_features',
        'usages' => 'usages',
        'quotas' => 'quotas',
        'subscription_plan_changes' => 'subscription_plan_changes',
        'billing_webhook_events' => 'billing_webhook_events',
    ],

    'cache' => [
        'enabled' => true,
        'store' => 'redis',
        'ttl' => 3600,
    ],

    'quota' => [
        'throw_exception' => true,
        'soft_limit' => false,
        'grace_percentage' => 0,
        'warning_thresholds' => [80, 100],
        'trigger_once' => false, // Only fire each threshold event once per billing period
    ],

    // Billing provider configuration
    'billing' => [
        // 'auto' = detect from installed package
        // 'stripe' = force Stripe provider
        // 'paddle' = force Paddle provider
        // 'polar' = force Polar provider
        'provider' => env('BILLING_PROVIDER', 'auto'),
    ],

    // Subscription settings
    'subscription' => [
        // Default plan ID for new billables and cancelled subscriptions.
        // Set this to your free plan ID so users always have a plan.
        'default_plan_id' => env('DEFAULT_PLAN_ID', null),
    ],

    // Paddle-specific configuration (only needed if using Paddle)
    'paddle' => [
        'sandbox' => env('PADDLE_SANDBOX', true),
        'seller_id' => env('PADDLE_SELLER_ID'),
        'api_key' => env('PADDLE_API_KEY'),
        'webhook_secret' => env('PADDLE_WEBHOOK_SECRET'),
        'client_side_token' => env('PADDLE_CLIENT_SIDE_TOKEN'),
    ],

    // Polar-specific configuration (only needed if using Polar)
    // Webhook signature verification is handled by danestves/laravel-polar via
    // POLAR_WEBHOOK_SECRET in its own config, so there is no key for it here.
    'polar' => [
        'access_token' => env('POLAR_ACCESS_TOKEN'),
        'organization_id' => env('POLAR_ORGANIZATION_ID'),
        'server' => env('POLAR_SERVER', 'sandbox'),
    ],

];

// In config/plan-usage.php
'subscription' => [
    'default_plan_id' => 1, // Your Free plan ID
],

// Or via .env
DEFAULT_PLAN_ID=1

// The free plan is automatically assigned!
$account = Account::create([
    'name' => 'New Account',
    'owner_id' => $user->id,
]);

echo $account->plan->name; // "Free"

use Develupers\PlanUsage\Traits\HasPlanFeatures;
use Laravel\Cashier\Billable;

class Account extends Model
{
    use Billable, HasPlanFeatures;
}

use Develupers\PlanUsage\Traits\HasPlanFeatures;
use Laravel\Paddle\Billable;

class Account extends Model
{
    use Billable, HasPlanFeatures;
}

use Danestves\LaravelPolar\Billable;
use Develupers\PlanUsage\Traits\HasPlanFeatures;

class Account extends Model
{
    use Billable, HasPlanFeatures;
}

use Develupers\PlanUsage\Models\Plan;
use Develupers\PlanUsage\Models\PlanPrice;
use Develupers\PlanUsage\Models\Feature;
use Develupers\PlanUsage\Models\PlanFeature;

// Create a plan
$plan = Plan::create([
    'name' => 'Professional',
    'slug' => 'professional',
    'display_name' => 'Professional Plan',
    'description' => 'Perfect for growing businesses',
    'stripe_product_id' => 'prod_123456',          // Stripe Product ID (if using Stripe)
    'paddle_product_id' => 'pro_01abc123',         // Paddle Product ID (if using Paddle)
    'trial_days' => 14,
    'type' => 'public',
]);

// Create pricing options for the plan
$monthlyPrice = PlanPrice::create([
    'plan_id' => $plan->id,
    'stripe_price_id' => 'price_monthly123',       // Stripe Price ID
    'paddle_price_id' => 'pri_01xyz789',           // Paddle Price ID
    'polar_product_id' => 'prod_polar_monthly123', // Polar Product ID
    'price' => 49.00,
    'currency' => 'USD',
    'interval' => 'month',
    'is_default' => true, // Default pricing option
    'is_active' => true,
]);

$yearlyPrice = PlanPrice::create([
    'plan_id' => $plan->id,
    'stripe_price_id' => 'price_yearly456',
    'paddle_price_id' => 'pri_01abc456',
    'polar_product_id' => 'prod_polar_yearly456',
    'price' => 490.00, // Discounted yearly price
    'currency' => 'USD',
    'interval' => 'year',
    'is_active' => true,
]);

// Create features
$apiFeature = Feature::create([
    'name' => 'API Calls',
    'slug' => 'api-calls',
    'type' => 'quota',
    'reset_period' => 'month',
]);

// Link feature to plan with a value
PlanFeature::create([
    'plan_id' => $plan->id,
    'feature_id' => $apiFeature->id,
    'value' => '5000', // 5000 API calls per month
]);

$account = Account::find(1);
$account->plan_id = $plan->id;
$account->save();

// CheckQuota gates the request, ConsumeQuota enforces + logs on success
Route::middleware(['check-quota:api-calls,1', 'consume-quota:api-calls,1'])
    ->post('/api/generate', 'ApiController@generate');

// consume() does everything: checks quota, increments, and logs usage
if ($account->consume('api-calls', 1, ['endpoint' => '/api/generate'])) {
    // Success -- quota was available
} else {
    // Quota exceeded
}

// Check if feature is in the plan
$account->hasFeature('api-calls');

// Read-only quota check (no side effects)
$account->checkQuota('api-calls', 10);

// Log usage without quota enforcement
$account->logUsage('api-calls', 1, ['source' => 'import']);

// Check remaining quota
$remaining = $account->getRemainingQuota('api-calls');

// Get detailed usage information
$usage = $account->getFeatureUsage('api-calls');
// Returns: ['limit' => 5000, 'used' => 1250, 'remaining' => 3750]
// Returns null when there's nothing to meter — see "getFeatureUsage() can
// return null" under Usage Tracking & Analytics.

// Only show public plans on pricing page
$availablePlans = Plan::availableForPurchase()->get(); // active + public

// Get legacy plans for existing customers
$legacyPlans = Plan::legacy()->get();

// Get hidden plans (admin use only)
$hiddenPlans = Plan::hidden()->get();

// Check plan type
if ($plan->isAvailableForPurchase()) {
    // Show "Subscribe" button
}

if ($plan->isHidden()) {
    // Only show in admin panel
}

// Plan type lifecycle: public → legacy (when retired)
// Private plans are for gated access (access codes, invitations)
// Hidden plans are never exposed to users (lifetime deals, internal use)

$plan = Plan::create([
    'name' => 'Growth Lifetime',
    'slug' => 'growth-lifetime',
    'display_name' => 'Growth Lifetime',
    'description' => 'Growth plan with lifetime access',
    'type' => 'hidden',        // Not shown on public pricing pages
    'is_lifetime' => true,     // Exempt from subscription enforcement
    'is_active' => true,
]);

// Get all lifetime plans
$lifetimePlans = Plan::lifetime()->get();

// Get plans that  is lifetime
if ($plan->isLifetime()) {
    // Skip subscription enforcement
}

// Plans can have multiple pricing options
$plan = Plan::find(1);

// Monthly pricing
$plan->prices()->create([
    'stripe_price_id' => 'price_monthly',
    'price' => 29.00,
    'currency' => 'USD',
    'interval' => 'month',
    'is_default' => true,
]);

// Annual pricing with discount
$plan->prices()->create([
    'stripe_price_id' => 'price_yearly',
    'price' => 290.00, // Save $58!
    'currency' => 'USD',
    'interval' => 'year',
]);

// Lifetime deal
$plan->prices()->create([
    'stripe_price_id' => 'price_lifetime',
    'price' => 999.00,
    'currency' => 'USD',
    'interval' => 'lifetime',
]);

// Get default price
$defaultPrice = $plan->defaultPrice;

// Get price by interval
$monthlyPrice = $plan->getMonthlyPrice();
$yearlyPrice = $plan->getYearlyPrice();
$customPrice = $plan->getPriceByInterval('week');

// Get all active prices
$activePrices = $plan->activePrices;

// Find plan by any provider price ID (auto-detects current provider)
$plan = Plan::findByProviderPriceId('price_monthly123');

// Calculate savings
$yearlyPrice = $plan->getYearlyPrice();
$monthlyPrice = $plan->getMonthlyPrice();
$savings = $yearlyPrice->calculateSavings($monthlyPrice);
echo "Save {$savings}% with yearly billing!";

// In bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'check-feature' => \Develupers\PlanUsage\Http\Middleware\CheckFeature::class,
        'check-quota' => \Develupers\PlanUsage\Http\Middleware\CheckQuota::class,
        'consume-quota' => \Develupers\PlanUsage\Http\Middleware\ConsumeQuota::class,
    ]);
})

// In routes/web.php
Route::get('/analytics', function () {
    // Only accessible if user has 'advanced-analytics' feature
})->middleware('check-feature:advanced-analytics');

Route::post('/api/generate', function () {
    // Enforces quota, increments, and logs usage on success
})->middleware('consume-quota:api-calls,1');

// Gate + consume (check first, consume after success)
Route::middleware(['check-quota:api-calls,5', 'consume-quota:api-calls,5'])
    ->post('/api/bulk', 'ApiController@bulk');

// Get comprehensive usage details for a feature
$usage = $account->getFeatureUsage('api-calls');
// Returns: ['limit' => 5000, 'used' => 1250, 'remaining' => 3750]

// Get usage percentage
$percentage = $account->getFeatureUsagePercentage('api-calls');
echo "You've used {$percentage}% of your API calls";

// Get all features status
$featuresStatus = $account->getFeaturesStatus();
foreach ($featuresStatus as $status) {
    echo "{$status['name']}: {$status['used']}/{$status['limit']}\n";
}

$usage = $account->getFeatureUsage('api-calls');

if ($usage === null) {
    // One of:
    //  - the feature slug doesn't exist, or it's a boolean feature
    //    (boolean features have no usage to report)
    //  - the billable has no plan, or its plan doesn't ing']} left";
}

use Develupers\PlanUsage\Facades\PlanUsage;

// Consume: enforce quota + increment + log (returns false if exceeded)
PlanUsage::consume($account, 'api-calls', 10, [
    'endpoint' => '/api/generate',
    'ip' => $request->ip(),
]);

// Get usage history
$history = PlanUsage::usage()->getHistory($account, 'api-calls');

// Get usage statistics
$stats = PlanUsage::usage()->getStatistics(
    $account,
    'api-calls',
    now()->subMonth(),
    now(),
    'day'
);

// Get current usage
$used = PlanUsage::quotas()->getUsed($account, 'api-calls');

// Get remaining quota
$remaining = PlanUsage::quotas()->getRemaining($account, 'api-calls');

// Get usage percentage
$percentage = PlanUsage::quotas()->getUsagePercentage($account, 'api-calls');

// Enforce quota (returns false if would exceed)
$canProceed = PlanUsage::quotas()->enforce($account, 'api-calls', 10);

// In your EventServiceProvider or via Laravel auto-discovery
protected $listen = [
    \Develupers\PlanUsage\Events\QuotaWarning::class => [
        \App\Listeners\SendQuotaWarningNotification::class,
    ],
    \Develupers\PlanUsage\Events\QuotaExceeded::class => [
        \App\Listeners\HandleQuotaExceeded::class,
    ],
    \Develupers\PlanUsage\Events\PlanRevoked::class => [
        \App\Listeners\NotifyPlanRevoked::class,
    ],
];

// config/plan-usage.php
'quota' => [
    'warning_thresholds' => [80, 100],
    'trigger_once' => true,
],

// Get the current provider's price ID for a plan price
$priceId = $planPrice->getProviderPriceId();

// Find a plan by its provider's price/product ID
$plan = Plan::findByProviderPriceId($priceId);

// Get/set the provider's product ID
$productId = $plan->getProviderProductId();
$plan->setProviderProductId('prod_ABC123');

// Create a provider checkout for a plan price
$checkout = app(\Develupers\PlanUsage\Actions\Subscription\CreateCheckoutSessionAction::class)
    ->executeForPlanPrice($billable, $planPrice, [
        'success_url' => route('billing.success'),
        'cancel_url' => route('billing.index'),
    ]);

return $checkout->redirect();

use Develupers\PlanUsage\Contracts\BillingProvider;
use Develupers\PlanUsage\Contracts\SubscriptionPlanChangeProvider;
use Develupers\PlanUsage\Enums\SubscriptionChangeTiming;

$provider = app(BillingProvider::class);

if ($provider instanceof SubscriptionPlanChangeProvider
    && $provider->supportsTiming(SubscriptionChangeTiming::NextPeriod)) {
    // Offer "downgrade at renewal" in the UI
}

use Develupers\PlanUsage\Actions\Subscription\CancelSubscriptionAction;
use Develupers\PlanUsage\Enums\SubscriptionChangeTiming;

// Upgrade now. The provider invoices the prorated price difference immediately,
// then the package grants the prorated quota difference after confirmation.
$change = $account->changePlan($growthMonthlyPrice);

// Downgrade on renewal (Polar only). Current plan limits and quotas remain
// unchanged until the provider reports that the pending product is now current.
$change = $account->changePlan($starterMonthlyPrice, SubscriptionChangeTiming::NextPeriod);

// Inspect or remove a scheduled downgrade before it takes effect.
$pending = $account->pendingPlanChange();
$account->cancelPendingPlanChange();

// The same operations are available through the facade:
// PlanUsage::changePlan($account, $growthMonthlyPrice);
// PlanUsage::cancelPendingPlanChange($account);

// Cancel at period end, resume during the grace period, or revoke now.
app(CancelSubscriptionAction::class)->execute($account);
app(CancelSubscriptionAction::class)->resume($account);
app(CancelSubscriptionAction::class)->execute($account, immediately: true);

use Illuminate\Support\Facades\Schedule;

// Reset expired quotas every hour
Schedule::command('plan-usage:reset-quotas --dispatch')->hourly();

use Develupers\PlanUsage\Jobs\ResetExpiredQuotasJob;

// Queued
ResetExpiredQuotasJob::dispatch();

// Synchronous
ResetExpiredQuotasJob::dispatchSync();

use Illuminate\Support\Facades\Schedule;

// Enforce plan subscriptions daily
Schedule::command('plan-usage:enforce-subscriptions --dispatch')->daily();

use Develupers\PlanUsage\Events\PlanRevoked;

// In your EventServiceProvider or via Laravel auto-discovery
protected $listen = [
    PlanRevoked::class => [
        \App\Listeners\NotifyPlanRevoked::class,
    ],
];

// Plans connect to Stripe Products
$plan->stripe_product_id = 'prod_ABC123';

// Each price connects to Stripe Prices
$price->stripe_price_id = 'price_XYZ789';

// Plans connect to Paddle Products
$plan->paddle_product_id = 'pro_01ABC123';

// Each price connects to Paddle Prices
$price->paddle_price_id = 'pri_01XYZ789';

$monthlyPrice->polar_product_id = 'prod_01MONTHLY';
$yearlyPrice->polar_product_id = 'prod_01YEARLY';

use Illuminate\Support\Facades\Schedule;

// Reset expired quotas (e.g. monthly credit resets)
Schedule::command('plan-usage:reset-quotas --dispatch')->hourly();

// Revoke plans from accounts without active subscriptions (lifetime plans exempt)
Schedule::command('plan-usage:enforce-subscriptions --dispatch')->daily();

// Reconcile local subscriptions with billing provider to catch missed webhooks
Schedule::command('subscriptions:reconcile')->daily();

$comparison = PlanUsage::plans()->comparePlans($currentPlanId, $newPlanId);

foreach ($comparison as $featureSlug => $data) {
    echo "{$data['feature']}: ";
    echo "{$data['plan1']} → {$data['plan2']}";
    if ($data['difference'] > 0) {
        echo " (+{$data['difference']})";
    }
}
bash
php artisan config:clear
bash
php artisan plan-usage:install
bash
php artisan vendor:publish --tag="plan-usage-config"
php artisan vendor:publish --tag="plan-usage-migrations"
bash
php artisan migrate
bash
php artisan vendor:publish --tag=plan-usage-migrations
php artisan migrate
bash
# Run synchronously
php artisan plan-usage:reset-quotas

# Dispatch as a queued job
php artisan plan-usage:reset-quotas --dispatch
bash
# Run synchronously
php artisan plan-usage:enforce-subscriptions

# Dispatch as a queued job
php artisan plan-usage:enforce-subscriptions --dispatch
bash
# Reconcile with configured provider
php artisan subscriptions:reconcile

# Reconcile with specific provider
php artisan subscriptions:reconcile --provider=stripe
php artisan subscriptions:reconcile --provider=paddle
php artisan subscriptions:reconcile --provider=polar

# Preview changes
php artisan subscriptions:reconcile --dry-run
bash
composer analyse