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.
Highlighting Popular Products
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
);
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