laravel-shop/docs/02-stripe.md

7.8 KiB

Stripe Integration

Configuration

Enable Stripe

Add to your .env:

SHOP_STRIPE_ENABLED=true
SHOP_STRIPE_SYNC_PRICES=true
STRIPE_KEY=your_stripe_key
STRIPE_SECRET=your_stripe_secret

Update config/shop.php:

'stripe' => [
    'enabled' => env('SHOP_STRIPE_ENABLED', false),
    'sync_prices' => env('SHOP_STRIPE_SYNC_PRICES', true),
    'api_version' => '2023-10-16',
],

Creating Products in Stripe

Manual Stripe Product Creation

use App\Services\StripeService;

$product = Product::create([
    'slug' => 'premium-plan',
    'price' => 29.99,
    'status' => 'published',
]);

// Create in Stripe
$stripeProduct = StripeService::createProduct($product);

// Store Stripe product ID
$product->update([
    'stripe_product_id' => $stripeProduct->id,
]);

Automatic Sync

If you have event listeners set up, products can be automatically synced to Stripe:

use Blax\Shop\Events\ProductCreated;
use App\Listeners\SyncProductToStripe;

// In EventServiceProvider
protected $listen = [
    ProductCreated::class => [
        SyncProductToStripe::class,
    ],
];

// Listener implementation
class SyncProductToStripe
{
    public function handle(ProductCreated $event)
    {
        if (config('shop.stripe.enabled')) {
            $stripeProduct = StripeService::createProduct($event->product);
            
            $event->product->update([
                'stripe_product_id' => $stripeProduct->id,
            ]);
        }
    }
}

Syncing Prices to Stripe

Create Stripe Prices

use App\Services\StripeService;
use Blax\Shop\Models\ProductPrice;

// Sync default price
StripeService::syncProductPricesDown($product);

// Create additional currency prices
$eurPrice = ProductPrice::create([
    'product_id' => $product->id,
    'currency' => 'EUR',
    'price' => 24.99,
]);

// Create corresponding Stripe price
$stripePrice = StripeService::createPrice($product, $eurPrice);

$eurPrice->update([
    'stripe_price_id' => $stripePrice->id,
]);

Creating Checkout Sessions

One-time Payment

use Stripe\Stripe;
use Stripe\Checkout\Session;

Stripe::setApiKey(config('services.stripe.secret'));

$product = Product::find($productId);

$session = Session::create([
    'payment_method_types' => ['card'],
    'line_items' => [[
        'price_data' => [
            'currency' => 'usd',
            'product_data' => [
                'name' => $product->getLocalized('name'),
                'description' => $product->getLocalized('short_description'),
            ],
            'unit_amount' => $product->getCurrentPrice() * 100, // Convert to cents
        ],
        'quantity' => 1,
    ]],
    'mode' => 'payment',
    'success_url' => route('checkout.success') . '?session_id={CHECKOUT_SESSION_ID}',
    'cancel_url' => route('checkout.cancel'),
    'metadata' => [
        'product_id' => $product->id,
    ],
]);

return redirect($session->url);

Using Stripe Price IDs

// If you have synced prices
$priceId = $product->prices()
    ->where('currency', 'USD')
    ->where('is_default', true)
    ->first()
    ->stripe_price_id;

$session = Session::create([
    'payment_method_types' => ['card'],
    'line_items' => [[
        'price' => $priceId,
        'quantity' => 1,
    ]],
    'mode' => 'payment',
    'success_url' => route('checkout.success') . '?session_id={CHECKOUT_SESSION_ID}',
    'cancel_url' => route('checkout.cancel'),
]);

Handling Webhooks

Register Webhook Endpoint

// routes/api.php
use App\Http\Controllers\StripeWebhookController;

Route::post('/stripe/webhook', [StripeWebhookController::class, 'handle']);

Webhook Controller

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Stripe\Webhook;
use Blax\Shop\Models\Product;

class StripeWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $payload = $request->getContent();
        $sigHeader = $request->header('Stripe-Signature');
        $webhookSecret = config('services.stripe.webhook_secret');

        try {
            $event = Webhook::constructEvent($payload, $sigHeader, $webhookSecret);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Invalid signature'], 400);
        }

        switch ($event->type) {
            case 'checkout.session.completed':
                $this->handleCheckoutCompleted($event->data->object);
                break;
                
            case 'payment_intent.succeeded':
                $this->handlePaymentSucceeded($event->data->object);
                break;
                
            case 'charge.refunded':
                $this->handleRefund($event->data->object);
                break;
        }

        return response()->json(['status' => 'success']);
    }

    protected function handleCheckoutCompleted($session)
    {
        $productId = $session->metadata->product_id ?? null;
        
        if (!$productId) {
            return;
        }

        $product = Product::find($productId);
        
        if (!$product) {
            return;
        }

        // Decrease stock
        $quantity = $session->metadata->quantity ?? 1;
        $product->decreaseStock($quantity);

        // Create purchase record
        $purchase = $product->purchases()->create([
            'purchasable_type' => get_class(auth()->user()),
            'purchasable_id' => $session->customer ?? $session->client_reference_id,
            'quantity' => $quantity,
            'status' => 'completed',
            'meta' => [
                'stripe_session_id' => $session->id,
                'stripe_payment_intent' => $session->payment_intent,
            ],
        ]);

        // Trigger product actions
        $product->callActions('purchased', $purchase, [
            'stripe_session' => $session,
        ]);
    }

    protected function handlePaymentSucceeded($paymentIntent)
    {
        // Handle successful payment
    }

    protected function handleRefund($charge)
    {
        // Handle refund
        $metadata = $charge->metadata;
        $productId = $metadata->product_id ?? null;

        if ($productId) {
            $product = Product::find($productId);
            $quantity = $metadata->quantity ?? 1;
            
            $product->increaseStock($quantity);
            
            // Trigger refund actions
            $product->callActions('refunded', null, [
                'stripe_charge' => $charge,
            ]);
        }
    }
}

Configure Webhook Secret

Add to .env:

STRIPE_WEBHOOK_SECRET=whsec_...

Get your webhook secret from Stripe Dashboard → Developers → Webhooks.

Multi-Currency Support

Create Prices for Multiple Currencies

$product = Product::create([
    'slug' => 'premium-plan',
    'price' => 29.99, // USD base price
]);

// USD (default)
ProductPrice::create([
    'product_id' => $product->id,
    'currency' => 'USD',
    'price' => 29.99,
    'is_default' => true,
]);

// EUR
ProductPrice::create([
    'product_id' => $product->id,
    'currency' => 'EUR',
    'price' => 24.99,
]);

// GBP
ProductPrice::create([
    'product_id' => $product->id,
    'currency' => 'GBP',
    'price' => 21.99,
]);

// Sync all to Stripe
StripeService::syncProductPricesDown($product);

Checkout with Currency Selection

$currency = $request->input('currency', 'USD');

$price = $product->prices()
    ->where('currency', $currency)
    ->first();

$session = Session::create([
    'payment_method_types' => ['card'],
    'line_items' => [[
        'price' => $price->stripe_price_id,
        'quantity' => 1,
    ]],
    'mode' => 'payment',
    'success_url' => route('checkout.success'),
    'cancel_url' => route('checkout.cancel'),
]);

Testing

Use Stripe Test Mode

STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...

Test Card Numbers