Skip to main content

Overview

Products are the foundation of your billing catalog. They represent the different offerings your customers can subscribe to - like “Basic”, “Pro”, or “Enterprise” plans. Each product can have multiple pricing plans, feature entitlements, and custom metadata.
Products are automatically assigned a level (1, 2, 3…) based on creation order. This creates a natural upgrade/downgrade hierarchy for your pricing tiers.

Quick Example

use Eufaturo\Billing\Billing;

// Create your first product
$basic = Billing::products()->create(
    name: 'Basic',
    description: 'Perfect for individuals just getting started',
    isVisible: true,
);

// Create a popular product
$pro = Billing::products()->create(
    name: 'Professional',
    description: 'For growing teams and businesses',
    isVisible: true,
    isPopular: true,
    isDefault: true,
);

// Create an enterprise product
$enterprise = Billing::products()->create(
    name: 'Enterprise',
    description: 'Custom solutions for large organizations',
    isVisible: true,
);

Creating a Basic Product

The simplest way to create a product:
use Eufaturo\Billing\Billing;

$product = Billing::products()->create(
    name: 'Starter Plan',
);
This creates a product with:
  • Auto-generated slug: starter-plan
  • Visible on pricing page: true
  • Auto-incremented level: 1 (or next available)
  • Not marked as popular: false
  • Not set as default: false

Customizing Products

Setting a Custom Slug

By default, slugs are auto-generated from the product name. You can override this:
$product = Billing::products()->create(
    name: 'Professional Plan',
    slug: 'pro', // Custom slug
);

// Access via slug
$foundProduct = Billing::products()->find('pro');
Use short, memorable slugs for cleaner URLs: /pricing/pro instead of /pricing/professional-plan

Adding Descriptions

Add helpful descriptions for your pricing page:
$product = Billing::products()->create(
    name: 'Business Plan',
    description: 'Everything you need to run a successful business, including priority support and advanced analytics.',
);

Visibility Control

Control which products appear on your pricing page:
// Visible product (shown on pricing page)
$currentPlan = Billing::products()->create(
    name: 'Pro 2024',
    isVisible: true,
);

// Hidden product (existing customers can keep it)
$legacyPlan = Billing::products()->create(
    name: 'Pro 2023 (Legacy)',
    isVisible: false,
);

// Get only visible products for pricing page
$visibleProducts = Billing::products()->visible();
Hidden products are perfect for legacy plans or internal testing. Existing subscriptions continue to work, but new customers can’t sign up.
Mark your most popular tier with a badge:
$product = Billing::products()->create(
    name: 'Professional',
    isPopular: true, // Shows "Most Popular" badge
    isDefault: true, // Pre-selected in UI
);

Using Metadata

Metadata lets you store custom data with products:
$product = Billing::products()->create(
    name: 'Enterprise',
    metadata: [
        'badge' => 'Best Value',
        'highlight_color' => '#3B82F6',
        'support_level' => 'dedicated',
        'max_storage_gb' => 1000,
        'custom_features' => [
            'SSO authentication',
            'Custom integrations',
            'Dedicated account manager',
        ],
    ],
);

// Access metadata in your views
$badge = $product->metadata['badge']; // "Best Value"
$color = $product->metadata['highlight_color']; // "#3B82F6"
Common Metadata Use Cases:
  • UI customization (badges, colors, icons)
  • Marketing copy variations
  • Feature flags not in the database
  • Display preferences
  • Integration settings

Product Hierarchy (Levels)

Products are automatically assigned a level based on creation order:
$basic = Billing::products()->create(name: 'Basic');
// $basic->level = 1

$pro = Billing::products()->create(name: 'Pro');
// $pro->level = 2

$enterprise = Billing::products()->create(name: 'Enterprise');
// $enterprise->level = 3
Why Levels Matter:
  • ✅ Determine if a plan change is an upgrade or downgrade
  • ✅ Calculate proration correctly
  • ✅ Display products in the correct order
  • ✅ Validate subscription changes
Levels are permanent and auto-assigned. If you need to reorder products, consider using the sort field in metadata or querying in a specific order.

Complete Product Setup

Here’s a complete example with a full product hierarchy:
use Eufaturo\Billing\Billing;

// Create Basic tier
$basic = Billing::products()->create(
    name: 'Basic',
    slug: 'basic',
    description: 'Perfect for individuals and small projects',
    isVisible: true,
    isPopular: false,
    metadata: [
        'badge' => null,
        'highlight_color' => '#6B7280',
        'support_level' => 'community',
        'recommended_for' => 'individuals',
    ],
);

// Create Pro tier (most popular)
$pro = Billing::products()->create(
    name: 'Professional',
    slug: 'pro',
    description: 'For growing teams and businesses',
    isVisible: true,
    isPopular: true, // Badge: "Most Popular"
    isDefault: true, // Pre-selected in UI
    metadata: [
        'badge' => 'Most Popular',
        'highlight_color' => '#3B82F6',
        'support_level' => 'priority',
        'recommended_for' => 'teams',
    ],
);

// Create Enterprise tier
$enterprise = Billing::products()->create(
    name: 'Enterprise',
    slug: 'enterprise',
    description: 'Custom solutions for large organizations',
    isVisible: true,
    isPopular: false,
    metadata: [
        'badge' => 'Custom',
        'highlight_color' => '#8B5CF6',
        'support_level' => 'dedicated',
        'contact_sales' => true,
        'recommended_for' => 'organizations',
    ],
);

Displaying Products

On Your Pricing Page

// Get all visible products
$products = Billing::products()->visible();

// Pass to your view
return view('pricing', [
    'products' => $products,
]);
{{-- resources/views/pricing.blade.php --}}
<div class="pricing-grid">
    @foreach($products as $product)
        <div class="pricing-card" style="border-color: {{ $product->metadata['highlight_color'] ?? '#gray' }}">
            {{-- Popular badge --}}
            @if($product->is_popular)
                <span class="badge">{{ $product->metadata['badge'] }}</span>
            @endif

            <h3>{{ $product->name }}</h3>
            <p>{{ $product->description }}</p>

            {{-- Display plans for this product --}}
            @foreach($product->plans as $plan)
                <div class="plan">
                    <span class="price">{{ $plan->price / 100 }}</span>
                    <span class="interval">/ {{ $plan->interval->value }}</span>
                </div>
            @endforeach

            <button
                class="subscribe-btn {{ $product->is_default ? 'primary' : 'secondary' }}"
                wire:click="subscribe('{{ $product->slug }}')"
            >
                Choose {{ $product->name }}
            </button>
        </div>
    @endforeach
</div>

In Your Admin Panel

// Get all products (including hidden)
$allProducts = Billing::products()->all();

// Get with deleted products
$withDeleted = Billing::products()->withTrashed();

// Find by slug
$product = Billing::products()->find('pro');

Updating Products

You can update products after creation:
$product = Billing::products()->find('pro');

$updated = Billing::products()->update(
    product: $product,
    name: 'Professional (Updated)',
    description: 'New and improved description',
    isPopular: true,
    metadata: [
        'badge' => 'Best Value',
        'new_feature' => 'AI Assistant included',
    ],
);
Updating a product doesn’t affect existing subscriptions. They keep their original plan details. Only new subscriptions will see the changes.

Deleting Products

Products use soft deletes for safety:
$product = Billing::products()->find('old-plan');

// Soft delete (sets deleted_at timestamp)
Billing::products()->delete($product);

// Product is hidden but data is preserved
expect($product->trashed())->toBeTrue();

// Restore if needed
Billing::products()->restore($product);
What Happens When You Delete a Product:
  • ❌ Product is hidden from visible() and all() queries
  • ✅ Existing subscriptions continue to work
  • ✅ All plans and relationships are preserved
  • ✅ Can be restored at any time
Deleted products are perfect for retiring old pricing tiers while keeping existing customer subscriptions active.

Next Steps

Now that you’ve created products, you’ll want to:

Common Patterns

Create a Complete Product Catalog

use Eufaturo\Billing\Billing;

class SeedProductCatalog
{
    public function run(): void
    {
        $products = [
            [
                'name' => 'Starter',
                'description' => 'For individuals',
                'metadata' => ['recommended_users' => '1'],
            ],
            [
                'name' => 'Business',
                'description' => 'For teams',
                'popular' => true,
                'default' => true,
                'metadata' => ['recommended_users' => '2-10'],
            ],
            [
                'name' => 'Enterprise',
                'description' => 'For organizations',
                'metadata' => ['recommended_users' => '10+', 'contact_sales' => true],
            ],
        ];

        foreach ($products as $data) {
            Billing::products()->create(
                name: $data['name'],
                description: $data['description'],
                isPopular: $data['popular'] ?? false,
                isDefault: $data['default'] ?? false,
                metadata: $data['metadata'],
            );
        }
    }
}

Migrate Legacy Products to Hidden

use Eufaturo\Billing\Products\Models\Product;

// Hide all legacy products
Product::where('name', 'LIKE', '%2023%')
    ->update(['is_visible' => false]);

// Or soft delete them
$legacyProducts = Product::where('name', 'LIKE', '%2023%')->get();
foreach ($legacyProducts as $product) {
    Billing::products()->delete($product);
}

Clone a Product

$original = Billing::products()->find('pro-2023');

$new = Billing::products()->create(
    name: 'Pro 2024',
    slug: 'pro-2024',
    description: $original->description,
    isVisible: true,
    isPopular: $original->is_popular,
    metadata: $original->metadata->toArray(),
);

Troubleshooting

Slug Already Exists

// ❌ This will fail if slug already exists
try {
    $product = Billing::products()->create(
        name: 'Pro Plan',
        slug: 'pro', // Already exists
    );
} catch (\Exception $e) {
    // Handle duplicate slug
}

// ✅ Use unique slugs
$product = Billing::products()->create(
    name: 'Pro Plan',
    slug: 'pro-2024', // Unique slug
);

Product Not Found

$product = Billing::products()->find('non-existent');

if (!$product) {
    // Handle not found
    abort(404, 'Product not found');
}

// Or use firstOrFail
use Eufaturo\Billing\Products\Models\Product;
$product = Product::where('slug', 'pro')->firstOrFail();

See Also