PHPackages                             particle-academy/laravel-catalog - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. particle-academy/laravel-catalog

ActiveLibrary[Payment Processing](/categories/payments)

particle-academy/laravel-catalog
================================

Laravel package for managing Stripe catalog (Products, Prices) with admin UI

v0.4(2mo ago)071MITPHPPHP ^8.2

Since Jan 2Pushed 2mo agoCompare

[ Source](https://github.com/Particle-Academy/laravel-catalog)[ Packagist](https://packagist.org/packages/particle-academy/laravel-catalog)[ RSS](/packages/particle-academy-laravel-catalog/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (5)Versions (5)Used By (0)

[![Powered by Tynn](https://camo.githubusercontent.com/db5bfff78510a1a7471a9ad18c48da044e62a4bcd1661875963411643b84303e/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d687474707325334125324625324674796e6e2e61692532466f2532467061727469636c652d61636164656d792532466c61726176656c2d636174616c6f6725324662616467652e6a736f6e)](https://tynn.ai/o/particle-academy/laravel-catalog)

Laravel Catalog Package
=======================

[](#laravel-catalog-package)

A Laravel package for managing Stripe catalog (Products and Prices) with an optional admin UI. All functionality is accessible via a facade, making it perfect for apps using their own UX.

> **Important**: Every Product must have at least one Price before it can be synced to Stripe. Plans are Products with recurring Prices - there is no separate Plan model.

Table of Contents
-----------------

[](#table-of-contents)

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Core Concepts](#core-concepts)
    - [Products, Plans, and Prices](#products-plans-and-prices)
    - [Price Requirements](#price-requirements)
- [Usage](#usage)
    - [Using the Catalog Facade](#using-the-catalog-facade)
    - [Creating Products](#creating-products)
    - [Creating Prices](#creating-prices)
    - [Working with Plans](#working-with-plans)
    - [Syncing to Stripe](#syncing-to-stripe)
    - [Creating Checkout Sessions](#creating-checkout-sessions)
- [Creating Your Own Admin UI](#creating-your-own-admin-ui)
- [Admin Interface (Published UI)](#admin-interface-published-ui)
- [Integration with FMS](#integration-with-fms)
- [Testing](#testing)
- [Common Patterns](#common-patterns)

Features
--------

[](#features)

- **Product Management**: Create, edit, and manage Stripe products with full CRUD operations
- **Price Management**: Manage recurring (subscription) and one-time prices for products
- **Plans Support**: Plans are simply Products with recurring Prices - no separate model needed
- **Stripe Sync**: Automatic or manual synchronization with Stripe's catalog
- **Facade API**: Complete programmatic access via `Catalog` facade - no UI required
- **Optional Admin UI**: Complete Livewire-based admin interface (optional, requires publishing)
- **Product Features**: Support for product features and feature configurations via FMS integration
- **Checkout Integration**: Ready-to-use Stripe Checkout session creation for subscriptions and one-time payments
- **Queue Support**: Background sync jobs for better performance
- **Event Broadcasting**: Real-time sync status updates via Laravel Broadcasting
- **Soft Deletes**: Products and Prices use soft deletes to preserve financial history
- **Metadata Support**: Flexible metadata storage for custom product configurations
- **Storefront Configuration**: Built-in support for storefront plan visibility and recommendations

Requirements
------------

[](#requirements)

- Laravel 11+ or 12+
- PHP 8.2+
- Laravel Cashier ^15.0
- Stripe PHP SDK ^13.0 or ^16.0
- Livewire 3+

Installation
------------

[](#installation)

### Step 1: Install via Composer

[](#step-1-install-via-composer)

```
composer require particle-academy/laravel-catalog
```

The package will auto-discover and register its service provider.

### Step 2: Publish Configuration (Optional)

[](#step-2-publish-configuration-optional)

```
php artisan vendor:publish --tag=catalog-config
```

This creates `config/catalog.php` where you can customize:

- Auto-sync to Stripe
- Queue connection for sync jobs
- Admin route prefix and middleware
- Broadcasting channel

### Step 3: Run Migrations

[](#step-3-run-migrations)

The package automatically loads both its own migrations and Laravel Cashier migrations (since Catalog depends on Cashier).

```
php artisan migrate
```

The package includes these migrations:

- **Cashier migrations** (auto-loaded):
    - `create_customer_columns` - Stripe customer columns on users table
    - `create_subscriptions_table` - Subscriptions table
    - `create_subscription_items_table` - Subscription items table
- **Catalog migrations** (auto-loaded):
    - `create_products_table` - Products table with Stripe sync fields
    - `create_prices_table` - Prices table for recurring and one-time pricing
    - `create_product_features_table` - Product features table
    - `create_product_feature_configs_table` - Product-feature pivot table

### Step 4: Enable UI (Optional)

[](#step-4-enable-ui-optional)

The package works without UI by default. To enable the admin UI:

1. **Set UI enabled in config** (or publish config and set `CATALOG_ENABLE_UI=true` in `.env`):

```
// config/catalog.php
'enable_ui' => env('CATALOG_ENABLE_UI', false),
```

2. **Publish views and assets**:

```
php artisan vendor:publish --tag=catalog-views
php artisan vendor:publish --tag=catalog-assets
```

3. **Register admin routes** in `routes/web.php`:

```
use LaravelCatalog\Livewire\Admin\Products\Index as ProductsIndex;

Route::prefix('ctrl')->name('admin.')->middleware(config('catalog.admin_middleware'))->group(function () {
    Route::get('/products', ProductsIndex::class)->name('products.index');
});
```

**Note**: The UI uses standard Tailwind CSS classes and does not require the custom CSS file. The UI will automatically be enabled if views are published, even without setting `enable_ui` to true.

Configuration
-------------

[](#configuration)

### Environment Variables

[](#environment-variables)

Add these to your `.env` file:

```
# Stripe Configuration (via Laravel Cashier)
STRIPE_KEY=your_stripe_key
STRIPE_SECRET=your_stripe_secret

# Catalog Package Configuration
CATALOG_AUTO_SYNC_STRIPE=false
CATALOG_QUEUE_CONNECTION=default
CATALOG_ADMIN_PREFIX=ctrl
CATALOG_ENABLE_UI=false  # Set to true to enable admin UI
```

### Configuration File

[](#configuration-file)

After publishing, edit `config/catalog.php`:

```
return [
    // Enable UI components (Livewire, views, routes)
    // UI will also be enabled automatically if views are published
    'enable_ui' => env('CATALOG_ENABLE_UI', false),

    // Auto-sync products/prices to Stripe when created/updated
    'auto_sync_stripe' => env('CATALOG_AUTO_SYNC_STRIPE', false),

    // Queue connection for sync jobs
    'queue_connection' => env('CATALOG_QUEUE_CONNECTION', 'default'),

    // Admin route prefix
    'admin_route_prefix' => env('CATALOG_ADMIN_PREFIX', 'ctrl'),

    // Admin route middleware
    'admin_middleware' => ['web', 'auth'],

    // Broadcasting channel for sync events
    'broadcast_channel' => 'admin.products',
];
```

Core Concepts
-------------

[](#core-concepts)

### Products, Plans, and Prices

[](#products-plans-and-prices)

- **Products**: Containers that hold pricing information. Can represent subscription plans, one-time purchases, or add-ons.
- **Plans**: Products with recurring Prices. There is no separate "Plan" model. A plan is a Product that:
    - Has at least one recurring Price (`type = 'recurring'`)
    - Is optionally marked for storefront display (`metadata->storefront->plan->show = true`)
- **Prices**: Define the actual pricing (amount, currency, interval). Every Product **must** have at least one Price.

### Price Requirements

[](#price-requirements)

**Critical**: Products cannot be synced to Stripe without at least one Price. Always create a Price when creating a Product:

```
// ❌ WRONG - Product without Price
$product = Product::create(['name' => 'My Product']);
Catalog::syncProduct($product); // Will fail or create incomplete Stripe product

// ✅ CORRECT - Product with Price
$product = Product::create(['name' => 'My Product']);
Price::create([
    'product_id' => $product->id,
    'unit_amount' => 2900,
    'currency' => 'USD',
    'type' => Price::TYPE_RECURRING,
    'recurring_interval' => 'month',
]);
Catalog::syncProductAndPrices($product); // Success!
```

Usage
-----

[](#usage)

### Using the Catalog Facade

[](#using-the-catalog-facade)

All catalog functionality is accessible via the `Catalog` facade, making it easy to use without the UI:

```
use LaravelCatalog\Facades\Catalog;
use LaravelCatalog\Models\Product;
use LaravelCatalog\Models\Price;

// Sync a product to Stripe (requires at least one Price)
$product = Product::with('prices')->find('product-id');
if ($product->prices->isEmpty()) {
    throw new \Exception('Product must have at least one Price before syncing.');
}
Catalog::syncProduct($product);

// Sync a price to Stripe
$price = Price::find('price-id');
Catalog::syncPrice($price);

// Sync product and all its prices (recommended)
Catalog::syncProductAndPrices($product);

// Test Stripe connection
$result = Catalog::testConnection();
// Returns: ['success' => true, 'message' => 'Connection successful']

// Create checkout session for subscription
$checkout = Catalog::subscriptionCheckout(
    owner: $user,
    price: $price, // Must be a recurring price
    successUrl: route('subscriptions.success'),
    cancelUrl: route('subscriptions.cancel'),
    metadata: ['source' => 'admin_panel'] // Optional
);

// Create checkout session for one-time payment
$checkout = Catalog::oneTimeCheckout(
    owner: $user,
    price: $price, // Must be a one-time price
    quantity: 1,
    successUrl: route('payments.success'),
    cancelUrl: route('payments.cancel'),
    metadata: [] // Optional
);

// Get checkout URLs directly (convenience methods)
$url = Catalog::getSubscriptionCheckoutUrl($user, $price, $successUrl, $cancelUrl);
$url = Catalog::getOneTimeCheckoutUrl($user, $price, 1, $successUrl, $cancelUrl);

// Access services directly if needed
Catalog::catalogService()->testConnection();
Catalog::checkoutService()->oneTimeCheckout(...);
```

### Important Notes

[](#important-notes)

- **Products Must Have Prices**: A Product cannot be synced to Stripe without at least one Price. Always create a Price when creating a Product.
- **Plans are Products**: There is no separate "Plan" model. Plans are Products with recurring Prices and storefront metadata.
- **Price Types**: Prices can be `recurring` (subscriptions) or `one_time` (one-time purchases).
- **Sync Before Checkout**: Always ensure Products/Prices are synced to Stripe before creating checkout sessions.

> For detailed explanations of Products, Plans, and Prices, see [Core Concepts](#core-concepts) above.

### Creating Products

[](#creating-products)

```
use LaravelCatalog\Models\Product;
use LaravelCatalog\Models\Price;

// Create a product (plan)
$product = Product::create([
    'name' => 'Pro Plan',
    'description' => 'Perfect for growing teams',
    'active' => true,
    'order' => 1,
    'metadata' => [
        'storefront' => [
            'plan' => [
                'show' => true,        // Show on storefront
                'recommended' => true,  // Mark as recommended
            ],
        ],
    ],
]);

// IMPORTANT: Create at least one Price for the Product
// Recurring monthly price (makes this a "plan")
$monthlyPrice = Price::create([
    'product_id' => $product->id,
    'unit_amount' => 2900, // $29.00 in cents
    'currency' => 'USD',
    'recurring_interval' => 'month',
    'recurring_interval_count' => 1,
    'type' => Price::TYPE_RECURRING,
    'active' => true,
]);

// You can add multiple prices to the same product
// Yearly price (same product, different billing interval)
$yearlyPrice = Price::create([
    'product_id' => $product->id,
    'unit_amount' => 29000, // $290.00 in cents (save $58/year)
    'currency' => 'USD',
    'recurring_interval' => 'year',
    'recurring_interval_count' => 1,
    'type' => Price::TYPE_RECURRING,
    'active' => true,
]);
```

### Creating Prices

[](#creating-prices)

```
use LaravelCatalog\Models\Price;

// Recurring monthly price (for subscription plans)
$monthlyPrice = Price::create([
    'product_id' => $product->id,
    'unit_amount' => 2900, // $29.00 in cents
    'currency' => 'USD',
    'recurring_interval' => 'month',
    'recurring_interval_count' => 1,
    'recurring_trial_period_days' => 14, // Optional trial period
    'type' => Price::TYPE_RECURRING,
    'active' => true,
]);

// Recurring yearly price
$yearlyPrice = Price::create([
    'product_id' => $product->id,
    'unit_amount' => 29000, // $290.00 in cents
    'currency' => 'USD',
    'recurring_interval' => 'year',
    'recurring_interval_count' => 1,
    'type' => Price::TYPE_RECURRING,
    'active' => true,
]);

// One-time price (for add-ons or one-time purchases)
$oneTimePrice = Price::create([
    'product_id' => $product->id,
    'unit_amount' => 9900, // $99.00 in cents
    'currency' => 'USD',
    'type' => Price::TYPE_ONE_TIME,
    'active' => true,
]);

// Using factory (recommended for tests)
$recurringPrice = Price::factory()
    ->for($product)
    ->create([
        'type' => Price::TYPE_RECURRING,
        'recurring_interval' => 'month',
    ]);

$oneTimePrice = Price::factory()
    ->for($product)
    ->oneTime()
    ->create([
        'unit_amount' => 9900,
    ]);
```

### Working with Plans

[](#working-with-plans)

Since plans are Products with recurring Prices, you can query them like this:

```
use LaravelCatalog\Models\Product;
use LaravelCatalog\Models\Price;

// Get all products that are plans (have recurring prices and are marked for storefront)
$plans = Product::whereHas('prices', function ($query) {
    $query->where('type', Price::TYPE_RECURRING);
})
->whereJsonContains('metadata->storefront->plan->show', true)
->with(['prices' => function ($query) {
    $query->where('type', Price::TYPE_RECURRING);
}])
->orderBy('order')
->get();

// Get the recommended plan
$recommendedPlan = Product::whereJsonContains('metadata->storefront->plan->recommended', true)
    ->with('prices')
    ->first();

// Check if a product is a plan
if ($product->isStorefrontPlan()) {
    // This is a plan shown on the storefront
}

// Get all recurring prices for a product (its plan options)
$planPrices = $product->prices()->where('type', Price::TYPE_RECURRING)->get();
```

### Syncing to Stripe

[](#syncing-to-stripe)

#### Manual Sync

[](#manual-sync)

```
use LaravelCatalog\Jobs\SyncProductToStripe;

// Dispatch sync job
SyncProductToStripe::dispatch($product->id);

// Or sync directly
use LaravelCatalog\Services\StripeCatalogService;

$catalogService = app(StripeCatalogService::class);
$catalogService->syncProduct($product);
```

#### Auto Sync

[](#auto-sync)

Enable auto-sync in `config/catalog.php`:

```
'auto_sync_stripe' => true,
```

Products and prices will automatically sync to Stripe when created or updated.

### Creating Checkout Sessions

[](#creating-checkout-sessions)

#### Subscription Checkout

[](#subscription-checkout)

```
use LaravelCatalog\Facades\Catalog;
use LaravelCatalog\Models\Price;
use App\Models\User;

$user = User::find(1);
$price = Price::find($priceId);

// Ensure price has been synced to Stripe first
if (!$price->stripePriceId()) {
    Catalog::syncProductAndPrices($price->product);
}

$checkout = Catalog::subscriptionCheckout(
    owner: $user,
    price: $price,
    successUrl: route('subscriptions.success'),
    cancelUrl: route('subscriptions.cancel'),
    metadata: ['source' => 'admin_panel']
);

// Redirect to Stripe Checkout
return redirect($checkout->asStripeCheckoutSession()->url);
```

#### One-Time Payment Checkout

[](#one-time-payment-checkout)

```
use LaravelCatalog\Facades\Catalog;

$checkout = Catalog::oneTimeCheckout(
    owner: $user,
    price: $oneTimePrice,
    quantity: 1,
    successUrl: route('payments.success'),
    cancelUrl: route('payments.cancel'),
);

return redirect($checkout->asStripeCheckoutSession()->url);
```

### Using Factories in Tests

[](#using-factories-in-tests)

The package includes factories that are automatically available:

```
use LaravelCatalog\Models\Product;
use LaravelCatalog\Models\Price;

// Create a product
$product = Product::factory()->create();

// Create a product with prices
$product = Product::factory()->create();
$price = Price::factory()->for($product)->create();

// Create a recurring price
$recurringPrice = Price::factory()
    ->for($product)
    ->create([
        'type' => Price::TYPE_RECURRING,
        'recurring_interval' => 'month',
    ]);

// Create a one-time price
$oneTimePrice = Price::factory()
    ->for($product)
    ->oneTime()
    ->create();
```

Creating Your Own Admin UI
--------------------------

[](#creating-your-own-admin-ui)

The package is designed to work without any UI dependencies. You can build your own admin interface using only the `Catalog` facade and Eloquent models.

### Key Principles

[](#key-principles)

1. **Use the Catalog Facade**: All Stripe operations go through `LaravelCatalog\Facades\Catalog`
2. **Products Must Have Prices**: Always create at least one Price when creating a Product
3. **Plans are Products**: Filter Products with recurring Prices to get plans
4. **Sync Before Checkout**: Ensure Products/Prices are synced to Stripe before creating checkout sessions

### Example: Custom Admin Controller

[](#example-custom-admin-controller)

```
