Skip to main content

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:
  1. A Product - The product these plans belong to
  2. 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