Overview
Product Plans define how customers pay for your products. While a Product represents what you’re selling (like “Pro” or “Enterprise”), a Plan specifies the pricing details - how much it costs, how often customers are billed, and in what currency.
Each product can have multiple plans. For example, your “Pro” product might have:
- Pro Monthly ($29/month)
- Pro Yearly ($290/year - save 17%)
- Pro Quarterly ($84 every 3 months)
Plans are always attached to a specific product and currency. To offer the same pricing in multiple currencies, create separate plans for each currency.
Quick Example
use Eufaturo\Billing\Billing;
use Eufaturo\Billing\ProductPlans\Enums\Interval;
use Eufaturo\Billing\ProductPlans\Enums\ProductPlanType;
// Get your product and currency
$pro = Billing::products()->find('pro');
$usd = Billing::currencies()->find('USD');
// Create monthly plan
$monthly = Billing::plans()->create(
product: $pro,
currency: $usd,
type: ProductPlanType::FlatRate,
name: 'Pro Monthly',
slug: 'pro-monthly',
isActive: true,
price: 2900, // $29.00 in cents
interval: Interval::Monthly,
intervalCount: 1,
);
// Create yearly plan with trial
$yearly = Billing::plans()->create(
product: $pro,
currency: $usd,
type: ProductPlanType::FlatRate,
name: 'Pro Yearly',
slug: 'pro-yearly',
isActive: true,
price: 29000, // $290.00 in cents
interval: Interval::Yearly,
intervalCount: 1,
trialInterval: Interval::Daily,
trialIntervalCount: 14, // 14-day trial
);
Creating Your First Plans
Step 1: Prepare Prerequisites
Before creating plans, you need:
- A Product - The product these plans belong to
- A Currency - The currency for pricing
use Eufaturo\Billing\Billing;
// Get or create product
$product = Billing::products()->find('pro')
?? Billing::products()->create(name: 'Pro');
// Get or create currency
$currency = Billing::currencies()->find('USD')
?? Billing::currencies()->create(
isoCode: 'USD',
name: 'US Dollar',
symbol: '$',
);
Step 2: Create a Basic Monthly Plan
use Eufaturo\Billing\ProductPlans\Enums\Interval;
use Eufaturo\Billing\ProductPlans\Enums\ProductPlanType;
$monthly = Billing::plans()->create(
product: $product,
currency: $currency,
type: ProductPlanType::FlatRate,
name: 'Pro Monthly',
slug: 'pro-monthly',
isActive: true,
price: 2900, // $29.00
interval: Interval::Monthly,
intervalCount: 1,
);
Step 3: Add an Annual Plan
$yearly = Billing::plans()->create(
product: $product,
currency: $currency,
type: ProductPlanType::FlatRate,
name: 'Pro Yearly',
slug: 'pro-yearly',
isActive: true,
price: 29000, // $290.00 (saves ~17%)
interval: Interval::Yearly,
intervalCount: 1,
description: 'Save 17% with annual billing',
);
Annual plans typically offer 10-25% discounts to incentivize longer commitments. Calculate yearly price as: (monthly_price * 12) * discount_factor
Understanding Pricing
Price Storage
All prices are stored as integers in cents (minor units):
// ✅ Correct
$plan->price = 2900; // $29.00
$plan->price = 9999; // $99.99
$plan->price = 100; // $1.00
$plan->price = 5; // $0.05
// ❌ Wrong - Never use floats!
$plan->price = 29.00; // Will be stored as 29 cents!
Why cents?
- Avoids floating-point precision errors
- Works consistently across all currencies
- Standard practice in payment processing
Displaying Prices
Convert cents to dollars for display:
// Basic conversion
$dollars = $plan->price / 100; // 2900 → 29.00
// Formatted
$formatted = '$' . number_format($plan->price / 100, 2);
// Output: "$29.00"
// In Blade templates
{{ '$' . number_format($plan->price / 100, 2) }}
Monthly Equivalent Pricing
Show monthly cost for annual plans:
$yearly = Billing::plans()->find('pro-yearly');
// Calculate monthly equivalent
$monthlyEquivalent = $yearly->price / 12 / 100;
echo "$" . number_format($monthlyEquivalent, 2) . "/mo";
// Output: "$24.17/mo" (for $290/year)
{{-- In your pricing table --}}
<div class="plan">
<h3>{{ $plan->name }}</h3>
<div class="price">
${{ number_format($plan->price / 100, 2) }}
<span class="interval">/ {{ $plan->interval->value }}</span>
</div>
@if($plan->interval->value === 'yearly')
<p class="monthly-equivalent">
${{ number_format(($plan->price / 12) / 100, 2) }}/mo
</p>
@endif
</div>
Billing Intervals
Choose how often customers are billed:
Available Intervals
use Eufaturo\Billing\ProductPlans\Enums\Interval;
Interval::Daily // Every day
Interval::Weekly // Every week
Interval::Monthly // Every month (most common)
Interval::Yearly // Every year
Interval Count
Use intervalCount for custom frequencies:
// Every 3 months (quarterly)
$quarterly = Billing::plans()->create(
// ...
interval: Interval::Monthly,
intervalCount: 3,
);
// Every 6 months (bi-annually)
$biannual = Billing::plans()->create(
// ...
interval: Interval::Monthly,
intervalCount: 6,
);
// Every 2 weeks
$biweekly = Billing::plans()->create(
// ...
interval: Interval::Weekly,
intervalCount: 2,
);
Common Patterns:
- Monthly:
interval: Monthly, intervalCount: 1
- Yearly:
interval: Yearly, intervalCount: 1
- Quarterly:
interval: Monthly, intervalCount: 3
Adding Trial Periods
Give customers risk-free access before charging:
$yearly = Billing::plans()->create(
product: $product,
currency: $currency,
type: ProductPlanType::FlatRate,
name: 'Pro Yearly',
slug: 'pro-yearly',
isActive: true,
price: 29000,
interval: Interval::Yearly,
intervalCount: 1,
trialInterval: Interval::Daily,
trialIntervalCount: 14, // 14-day trial
);
Common Trial Periods:
- 7 days:
trialInterval: Daily, trialIntervalCount: 7
- 14 days:
trialInterval: Daily, trialIntervalCount: 14
- 30 days:
trialInterval: Daily, trialIntervalCount: 30
- 1 month:
trialInterval: Monthly, trialIntervalCount: 1
// Check if plan has trial
if ($plan->hasTrial()) {
echo "Try free for {$plan->trial_interval_count} days";
}
Best Practices:
- Longer trials for annual plans (14-30 days)
- Shorter trials for monthly plans (7-14 days)
- Credit card upfront increases conversion
- No credit card increases trial signups but lower conversion
Plan Types
Flat Rate Plans
Fixed price per billing cycle (most common):
$flatRate = Billing::plans()->create(
product: $product,
currency: $currency,
type: ProductPlanType::FlatRate,
name: 'Pro Monthly',
slug: 'pro-monthly',
isActive: true,
price: 2900, // Fixed $29/month
interval: Interval::Monthly,
intervalCount: 1,
);
Use Cases:
- Standard SaaS subscriptions
- Unlimited usage within plan limits
- Predictable revenue
- Simple for customers to understand
Usage-Based Plans
Pay per unit consumed:
$usageBased = Billing::plans()->create(
product: $product,
currency: $currency,
type: ProductPlanType::UsageBased,
name: 'Pro Pay-as-you-go',
slug: 'pro-usage',
isActive: true,
price: 1000, // $10 base fee
pricePerUnit: 25, // $0.25 per unit
interval: Interval::Monthly,
intervalCount: 1,
);
How it Works:
price: Base fee charged each cycle
pricePerUnit: Cost per unit of usage
- Total bill:
base + (units * pricePerUnit)
Use Cases:
- API calls
- SMS messages
- Cloud storage
- Video encoding minutes
Important: Usage-based plans cannot be changed mid-cycle. This prevents abuse where users could consume resources heavily then downgrade before billing.
Creating a Complete Pricing Structure
Here’s a realistic example with multiple products and plans:
use Eufaturo\Billing\Billing;
use Eufaturo\Billing\ProductPlans\Enums\Interval;
use Eufaturo\Billing\ProductPlans\Enums\ProductPlanType;
$usd = Billing::currencies()->find('USD');
// ==================
// BASIC PRODUCT
// ==================
$basic = Billing::products()->create(
name: 'Basic',
description: 'For individuals',
);
Billing::plans()->create(
product: $basic,
currency: $usd,
type: ProductPlanType::FlatRate,
name: 'Basic Monthly',
slug: 'basic-monthly',
isActive: true,
price: 900, // $9/month
interval: Interval::Monthly,
intervalCount: 1,
);
// ==================
// PRO PRODUCT
// ==================
$pro = Billing::products()->create(
name: 'Professional',
description: 'For teams',
isPopular: true,
);
// Monthly plan
Billing::plans()->create(
product: $pro,
currency: $usd,
type: ProductPlanType::FlatRate,
name: 'Pro Monthly',
slug: 'pro-monthly',
isActive: true,
price: 2900, // $29/month
interval: Interval::Monthly,
intervalCount: 1,
);
// Yearly plan (save 17%)
Billing::plans()->create(
product: $pro,
currency: $usd,
type: ProductPlanType::FlatRate,
name: 'Pro Yearly',
slug: 'pro-yearly',
isActive: true,
price: 29000, // $290/year ($24.17/month)
interval: Interval::Yearly,
intervalCount: 1,
description: 'Save 17% with annual billing',
trialInterval: Interval::Daily,
trialIntervalCount: 14,
);
// ==================
// ENTERPRISE PRODUCT
// ==================
$enterprise = Billing::products()->create(
name: 'Enterprise',
description: 'For organizations',
);
Billing::plans()->create(
product: $enterprise,
currency: $usd,
type: ProductPlanType::FlatRate,
name: 'Enterprise Yearly',
slug: 'enterprise-yearly',
isActive: true,
price: 99000, // $990/year
interval: Interval::Yearly,
intervalCount: 1,
trialInterval: Interval::Daily,
trialIntervalCount: 30,
);
Displaying Plans on Your Pricing Page
Simple Pricing Table
<div class="pricing-grid">
@foreach(Billing::products()->visible() as $product)
<div class="product-card">
<h2>{{ $product->name }}</h2>
<p>{{ $product->description }}</p>
@foreach($product->plans as $plan)
<div class="plan">
<div class="price">
${{ number_format($plan->price / 100, 0) }}
<span>/ {{ $plan->interval->value }}</span>
</div>
@if($plan->interval->value === 'yearly')
<p class="savings">
Save {{ $plan->metadata['discount'] ?? '17' }}%
</p>
@endif
@if($plan->hasTrial())
<p class="trial">
🎁 {{ $plan->trial_interval_count }}-day free trial
</p>
@endif
<button wire:click="subscribe('{{ $plan->slug }}')">
Choose Plan
</button>
</div>
@endforeach
</div>
@endforeach
</div>
Monthly/Yearly Toggle
<div x-data="{ billingCycle: 'monthly' }">
{{-- Toggle --}}
<div class="billing-toggle">
<button
@click="billingCycle = 'monthly'"
:class="{ active: billingCycle === 'monthly' }"
>
Monthly
</button>
<button
@click="billingCycle = 'yearly'"
:class="{ active: billingCycle === 'yearly' }"
>
Yearly
<span class="badge">Save 17%</span>
</button>
</div>
{{-- Pricing Cards --}}
@foreach($products as $product)
<div class="pricing-card">
<h3>{{ $product->name }}</h3>
{{-- Monthly Plan --}}
@php
$monthly = $product->plans->firstWhere('slug', "{$product->slug}-monthly");
$yearly = $product->plans->firstWhere('slug', "{$product->slug}-yearly");
@endphp
<div x-show="billingCycle === 'monthly'" x-cloak>
<div class="price">${{ number_format($monthly->price / 100, 0) }}</div>
<div class="interval">per month</div>
<button wire:click="subscribe('{{ $monthly->slug }}')">
Get Started
</button>
</div>
<div x-show="billingCycle === 'yearly'" x-cloak>
<div class="price">${{ number_format(($yearly->price / 12) / 100, 2) }}</div>
<div class="interval">per month, billed annually</div>
<p class="total">
${{ number_format($yearly->price / 100, 0) }}/year total
</p>
<button wire:click="subscribe('{{ $yearly->slug }}')">
Get Started
</button>
</div>
</div>
@endforeach
</div>
Multi-Currency Support
Offer plans in multiple currencies:
$pro = Billing::products()->find('pro');
// USD Plans
$usd = Billing::currencies()->find('USD');
Billing::plans()->create(
product: $pro,
currency: $usd,
type: ProductPlanType::FlatRate,
name: 'Pro Monthly (USD)',
slug: 'pro-monthly-usd',
isActive: true,
price: 2900, // $29.00
interval: Interval::Monthly,
intervalCount: 1,
);
// EUR Plans
$eur = Billing::currencies()->find('EUR');
Billing::plans()->create(
product: $pro,
currency: $eur,
type: ProductPlanType::FlatRate,
name: 'Pro Monthly (EUR)',
slug: 'pro-monthly-eur',
isActive: true,
price: 2700, // €27.00
interval: Interval::Monthly,
intervalCount: 1,
);
// Filter plans by user's currency
$userCurrency = $user->currency ?? 'USD';
$plans = $pro->plans()
->whereHas('currency', fn($q) => $q->where('iso_code', $userCurrency))
->get();
Managing Plans
Updating Plans
$plan = Billing::plans()->find('pro-monthly');
// Update price
$updated = Billing::plans()->update(
plan: $plan,
product: $plan->product,
currency: $plan->currency,
type: $plan->type,
name: $plan->name,
slug: $plan->slug,
isActive: true,
price: 3500, // New price: $35
interval: $plan->interval,
intervalCount: $plan->interval_count,
description: 'Price updated for 2024',
);
Grandfathering: Updating a plan doesn’t affect existing subscriptions. They keep their original pricing. This is called “grandfathering” and is a common practice in SaaS.
Hiding Plans
Make plans inactive instead of deleting:
// Hide from new signups (existing subscriptions continue)
$plan->is_active = false;
$plan->save();
// Or soft delete
Billing::plans()->delete($plan);
Restoring Deleted Plans
use Eufaturo\Billing\ProductPlans\Models\ProductPlan;
$plan = ProductPlan::withTrashed()
->where('slug', 'old-plan')
->first();
Billing::plans()->restore($plan);
Common Patterns
Grandfathering Legacy Pricing
// Create new plan with updated pricing
$newPlan = Billing::plans()->create(
product: $product,
currency: $currency,
type: ProductPlanType::FlatRate,
name: 'Pro Monthly (2024)',
slug: 'pro-monthly-2024',
isActive: true,
price: 3500, // New: $35
interval: Interval::Monthly,
intervalCount: 1,
);
// Hide old plan from new signups
$oldPlan = Billing::plans()->find('pro-monthly-2023');
$oldPlan->is_active = false;
$oldPlan->save();
// Existing customers on old plan keep $29/month pricing ✅
Seasonal Pricing
// Summer promotion
$summerPlan = Billing::plans()->create(
product: $product,
currency: $currency,
type: ProductPlanType::FlatRate,
name: 'Pro Summer Special',
slug: 'pro-summer-2024',
isActive: true,
price: 1900, // $19 (34% off)
interval: Interval::Monthly,
intervalCount: 1,
description: 'Limited time offer - ends Sept 1',
);
// Deactivate after season
$summerPlan->is_active = false;
$summerPlan->save();
Next Steps
Troubleshooting
”Plan slug already exists”
Each plan must have a unique slug:
// ❌ Duplicate slug
$plan1 = Billing::plans()->create(slug: 'pro-monthly', /* ... */);
$plan2 = Billing::plans()->create(slug: 'pro-monthly', /* ... */);
// Error!
// ✅ Unique slugs
$usdPlan = Billing::plans()->create(slug: 'pro-monthly-usd', /* ... */);
$eurPlan = Billing::plans()->create(slug: 'pro-monthly-eur', /* ... */);
Displaying Wrong Price
Always divide by 100 for display:
// ❌ Wrong
{{ $plan->price }} // Shows "2900" instead of "$29.00"
// ✅ Correct
${{ number_format($plan->price / 100, 2) }} // Shows "$29.00"
Cannot Change Usage-Based Plans
This is by design:
if (!$plan->isChangeable()) {
// Plan is usage-based - disable change button
echo "Usage-based plans cannot be changed mid-cycle";
}
See Also