laravel-shop/docs/02-stripe.md

542 lines
13 KiB
Markdown
Raw Normal View History

2025-11-21 10:49:41 +00:00
# Stripe Integration
2025-11-25 16:25:20 +00:00
## Overview
The Laravel Shop package includes Stripe integration for:
- Syncing products from Stripe to your database
- Syncing prices from Stripe
- Associating products with Stripe product IDs
- Automatic price synchronization
2025-11-21 10:49:41 +00:00
## Configuration
2025-11-25 16:25:20 +00:00
### Environment Setup
2025-11-21 10:49:41 +00:00
Add to your `.env`:
```env
SHOP_STRIPE_ENABLED=true
SHOP_STRIPE_SYNC_PRICES=true
2025-11-25 16:25:20 +00:00
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
### Config File
2025-11-21 10:49:41 +00:00
Update `config/shop.php`:
```php
'stripe' => [
'enabled' => env('SHOP_STRIPE_ENABLED', false),
'sync_prices' => env('SHOP_STRIPE_SYNC_PRICES', true),
'api_version' => '2023-10-16',
],
```
2025-11-25 16:25:20 +00:00
## Syncing Products from Stripe
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### Sync Individual Product
2025-11-21 10:49:41 +00:00
```php
2025-11-25 23:05:46 +00:00
use Blax\Shop\Services\StripeService;
2025-11-25 16:25:20 +00:00
use Stripe\Product as StripeProduct;
// Get Stripe product
$stripeProduct = StripeProduct::retrieve('prod_xxxxx');
// Sync to local database
2025-11-25 23:05:46 +00:00
$product = StripeService::syncProductDown($stripeProduct);
2025-11-25 16:25:20 +00:00
// This creates/updates a Product with:
// - stripe_product_id
// - slug (generated from name)
// - type
// - virtual flag
// - status (based on Stripe active status)
// - name (localized)
// - features (if available)
```
### Sync Product Prices
```php
// Sync all prices for a product
2025-11-25 23:05:46 +00:00
StripeService::syncProductPricesDown($product);
2025-11-25 16:25:20 +00:00
// This creates/updates ProductPrice records with:
// - stripe_price_id
// - name (from Stripe nickname)
// - type (one_time or recurring)
// - unit_amount (price in cents)
// - currency
// - billing_scheme
// - interval (for recurring)
// - interval_count (for recurring)
// - trial_period_days (for recurring)
```
## Manual Product Creation with Stripe
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### Create Product and Sync to Stripe
```php
use Blax\Shop\Models\Product;
use Stripe\Stripe;
use Stripe\Product as StripeProduct;
use Stripe\Price;
Stripe::setApiKey(config('services.stripe.secret'));
// Create local product first
2025-11-21 10:49:41 +00:00
$product = Product::create([
'slug' => 'premium-plan',
'status' => 'published',
]);
2025-11-25 16:25:20 +00:00
$product->setLocalized('name', 'Premium Plan', 'en');
$product->setLocalized('description', 'Access to all premium features', 'en');
2025-11-21 10:49:41 +00:00
// Create in Stripe
2025-11-25 16:25:20 +00:00
$stripeProduct = StripeProduct::create([
'name' => $product->getLocalized('name'),
'description' => $product->getLocalized('description'),
'metadata' => [
'product_id' => $product->id,
],
]);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Save Stripe product ID
2025-11-21 10:49:41 +00:00
$product->update([
'stripe_product_id' => $stripeProduct->id,
]);
2025-11-25 16:25:20 +00:00
// Create price in Stripe
$stripePrice = Price::create([
'product' => $stripeProduct->id,
'unit_amount' => 2999, // $29.99
'currency' => 'usd',
'recurring' => [
'interval' => 'month',
],
]);
// Create local price
ProductPrice::create([
'purchasable_type' => Product::class,
'purchasable_id' => $product->id,
'stripe_price_id' => $stripePrice->id,
'currency' => 'usd',
'unit_amount' => 2999,
'type' => 'recurring',
'interval' => 'month',
'interval_count' => 1,
'is_default' => true,
'active' => true,
]);
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## Automatic Syncing with Events
You can set up event listeners to automatically sync products to Stripe when they're created or updated.
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### Create Event Listener
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// app/Listeners/SyncProductToStripe.php
namespace App\Listeners;
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
use Blax\Shop\Events\ProductCreated;
use Stripe\Stripe;
use Stripe\Product as StripeProduct;
2025-11-21 10:49:41 +00:00
class SyncProductToStripe
{
public function handle(ProductCreated $event)
{
2025-11-25 16:25:20 +00:00
if (!config('shop.stripe.enabled')) {
return;
}
$product = $event->product;
// Skip if already has Stripe ID
if ($product->stripe_product_id) {
return;
2025-11-21 10:49:41 +00:00
}
2025-11-25 16:25:20 +00:00
Stripe::setApiKey(config('services.stripe.secret'));
$stripeProduct = StripeProduct::create([
'name' => $product->getLocalized('name') ?? $product->slug,
'description' => $product->getLocalized('description'),
'metadata' => [
'product_id' => $product->id,
'sku' => $product->sku,
],
]);
$product->update([
'stripe_product_id' => $stripeProduct->id,
]);
2025-11-21 10:49:41 +00:00
}
}
```
2025-11-25 16:25:20 +00:00
### Register Event Listener
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
```php
// app/Providers/EventServiceProvider.php
use Blax\Shop\Events\ProductCreated;
use Blax\Shop\Events\ProductUpdated;
use App\Listeners\SyncProductToStripe;
use App\Listeners\UpdateStripeProduct;
protected $listen = [
ProductCreated::class => [
SyncProductToStripe::class,
],
ProductUpdated::class => [
UpdateStripeProduct::class,
],
];
```
## Working with Stripe Prices
### One-Time Prices
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
use Stripe\Stripe;
use Stripe\Price;
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
Stripe::setApiKey(config('services.stripe.secret'));
// Create one-time price in Stripe
$stripePrice = Price::create([
'product' => $product->stripe_product_id,
'unit_amount' => 4999, // $49.99
'currency' => 'usd',
]);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Create local price
ProductPrice::create([
'purchasable_type' => Product::class,
'purchasable_id' => $product->id,
'stripe_price_id' => $stripePrice->id,
'currency' => 'usd',
'unit_amount' => 4999,
'type' => 'one_time',
'is_default' => true,
'active' => true,
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
```
### Recurring Prices
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
```php
// Monthly subscription
$stripePrice = Price::create([
'product' => $product->stripe_product_id,
'unit_amount' => 999, // $9.99/month
'currency' => 'usd',
'recurring' => [
'interval' => 'month',
'interval_count' => 1,
'trial_period_days' => 7,
],
]);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Create local price
ProductPrice::create([
'purchasable_type' => Product::class,
'purchasable_id' => $product->id,
2025-11-21 10:49:41 +00:00
'stripe_price_id' => $stripePrice->id,
2025-11-25 16:25:20 +00:00
'currency' => 'usd',
'unit_amount' => 999,
'type' => 'recurring',
'interval' => 'month',
'interval_count' => 1,
'trial_period_days' => 7,
'is_default' => true,
'active' => true,
2025-11-21 10:49:41 +00:00
]);
```
2025-11-25 16:25:20 +00:00
### Multiple Currency Prices
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
```php
// USD price
$usdPrice = Price::create([
'product' => $product->stripe_product_id,
'unit_amount' => 2999,
'currency' => 'usd',
'recurring' => ['interval' => 'month'],
]);
ProductPrice::create([
'purchasable_type' => Product::class,
'purchasable_id' => $product->id,
'stripe_price_id' => $usdPrice->id,
'currency' => 'usd',
'unit_amount' => 2999,
'type' => 'recurring',
'interval' => 'month',
'interval_count' => 1,
'is_default' => true,
'active' => true,
]);
// EUR price
$eurPrice = Price::create([
'product' => $product->stripe_product_id,
'unit_amount' => 2499,
'currency' => 'eur',
'recurring' => ['interval' => 'month'],
]);
ProductPrice::create([
'purchasable_type' => Product::class,
'purchasable_id' => $product->id,
'stripe_price_id' => $eurPrice->id,
'currency' => 'eur',
'unit_amount' => 2499,
'type' => 'recurring',
'interval' => 'month',
'interval_count' => 1,
'is_default' => false,
'active' => true,
]);
```
## Stripe Checkout Integration
### Create Checkout Session
2025-11-21 10:49:41 +00:00
```php
use Stripe\Stripe;
use Stripe\Checkout\Session;
Stripe::setApiKey(config('services.stripe.secret'));
2025-11-25 16:25:20 +00:00
Route::post('/checkout', function () {
$user = auth()->user();
$cartItems = $user->cartItems()->with('purchasable.prices')->get();
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Build line items from cart
$lineItems = $cartItems->map(function ($item) {
$price = $item->purchasable->defaultPrice()->first();
return [
'price' => $price->stripe_price_id,
'quantity' => $item->quantity,
];
})->toArray();
// Create checkout session
$session = Session::create([
'payment_method_types' => ['card'],
'line_items' => $lineItems,
'mode' => 'payment', // or 'subscription' for recurring
'success_url' => route('checkout.success') . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout.cancel'),
'customer_email' => $user->email,
'metadata' => [
'user_id' => $user->id,
'cart_id' => $user->currentCart()->id,
2025-11-21 10:49:41 +00:00
],
2025-11-25 16:25:20 +00:00
]);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
return redirect($session->url);
});
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
### Handle Successful Payment
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
Route::get('/checkout/success', function (Request $request) {
$sessionId = $request->get('session_id');
Stripe::setApiKey(config('services.stripe.secret'));
$session = Session::retrieve($sessionId);
// Verify payment succeeded
if ($session->payment_status === 'paid') {
$user = auth()->user();
// Convert cart to purchases
2025-11-28 09:24:07 +00:00
$purchases = $user->checkoutCart();
2025-11-25 16:25:20 +00:00
// Store charge ID
foreach ($purchases as $purchase) {
$purchase->update([
'status' => 'completed',
'charge_id' => $session->payment_intent,
'amount_paid' => $session->amount_total / 100,
]);
}
return view('checkout.success', compact('purchases'));
}
return redirect()->route('cart.index')
->with('error', 'Payment was not successful');
});
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## Webhook Handling
2025-11-21 10:49:41 +00:00
### Register Webhook Endpoint
```php
2025-11-25 16:25:20 +00:00
// routes/web.php
Route::post(
'/stripe/webhook',
[StripeWebhookController::class, 'handleWebhook']
)->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
### Handle Webhooks
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// app/Http/Controllers/StripeWebhookController.php
2025-11-21 10:49:41 +00:00
namespace App\Http\Controllers;
2025-11-25 16:25:20 +00:00
use Stripe\Stripe;
2025-11-21 10:49:41 +00:00
use Stripe\Webhook;
2025-11-25 16:25:20 +00:00
use Illuminate\Http\Request;
2025-11-21 10:49:41 +00:00
class StripeWebhookController extends Controller
{
2025-11-25 16:25:20 +00:00
public function handleWebhook(Request $request)
2025-11-21 10:49:41 +00:00
{
2025-11-25 16:25:20 +00:00
Stripe::setApiKey(config('services.stripe.secret'));
2025-11-21 10:49:41 +00:00
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
$webhookSecret = config('services.stripe.webhook_secret');
try {
2025-11-25 16:25:20 +00:00
$event = Webhook::constructEvent(
$payload,
$sigHeader,
$webhookSecret
);
2025-11-21 10:49:41 +00:00
} catch (\Exception $e) {
return response()->json(['error' => 'Invalid signature'], 400);
}
2025-11-25 16:25:20 +00:00
// Handle the event
2025-11-21 10:49:41 +00:00
switch ($event->type) {
case 'checkout.session.completed':
2025-11-25 16:25:20 +00:00
$this->handleCheckoutComplete($event->data->object);
2025-11-21 10:49:41 +00:00
break;
2025-11-25 16:25:20 +00:00
case 'product.created':
case 'product.updated':
$this->handleProductUpdate($event->data->object);
2025-11-21 10:49:41 +00:00
break;
2025-11-25 16:25:20 +00:00
case 'price.created':
case 'price.updated':
$this->handlePriceUpdate($event->data->object);
2025-11-21 10:49:41 +00:00
break;
}
2025-11-25 16:25:20 +00:00
return response()->json(['success' => true]);
2025-11-21 10:49:41 +00:00
}
2025-11-25 16:25:20 +00:00
protected function handleCheckoutComplete($session)
2025-11-21 10:49:41 +00:00
{
2025-11-25 16:25:20 +00:00
// Find purchase by session metadata
$userId = $session->metadata->user_id ?? null;
$cartId = $session->metadata->cart_id ?? null;
if ($userId && $cartId) {
// Mark purchases as completed
ProductPurchase::where('cart_id', $cartId)
->update([
'status' => 'completed',
'charge_id' => $session->payment_intent,
'amount_paid' => $session->amount_total / 100,
]);
2025-11-21 10:49:41 +00:00
}
}
2025-11-25 16:25:20 +00:00
protected function handleProductUpdate($stripeProduct)
2025-11-21 10:49:41 +00:00
{
2025-11-25 23:05:46 +00:00
StripeService::syncProductDown($stripeProduct);
2025-11-21 10:49:41 +00:00
}
2025-11-25 16:25:20 +00:00
protected function handlePriceUpdate($stripePrice)
2025-11-21 10:49:41 +00:00
{
2025-11-25 16:25:20 +00:00
// Update local price
$price = ProductPrice::where('stripe_price_id', $stripePrice->id)->first();
if ($price) {
$price->update([
'active' => $stripePrice->active,
'unit_amount' => $stripePrice->unit_amount,
2025-11-21 10:49:41 +00:00
]);
}
}
}
```
2025-11-25 16:25:20 +00:00
## Best Practices
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### 1. Always Use Stripe Price IDs
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
When integrating with Stripe Checkout or subscriptions, always use Stripe Price IDs:
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
```php
$price = $product->defaultPrice()->first();
$stripePriceId = $price->stripe_price_id;
```
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### 2. Keep Prices in Sync
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
Use webhooks or scheduled commands to keep prices synchronized:
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// app/Console/Commands/SyncStripePrices.php
use Stripe\Stripe;
use Stripe\Product as StripeProduct;
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
Stripe::setApiKey(config('services.stripe.secret'));
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
Product::whereNotNull('stripe_product_id')->each(function ($product) {
2025-11-25 23:05:46 +00:00
StripeService::syncProductPricesDown($product);
2025-11-25 16:25:20 +00:00
});
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
### 3. Store Stripe References
Always store Stripe IDs for traceability:
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
$product->update([
'stripe_product_id' => $stripeProduct->id,
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
$price->update([
'stripe_price_id' => $stripePrice->id,
]);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
$purchase->update([
'charge_id' => $paymentIntent->id,
]);
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
### 4. Handle Errors Gracefully
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
```php
try {
$stripeProduct = StripeProduct::create([
'name' => $product->getLocalized('name'),
]);
} catch (\Stripe\Exception\ApiErrorException $e) {
\Log::error('Stripe API error: ' . $e->getMessage());
// Handle error appropriately
}
```