8.5 KiB
Stripe Checkout Integration
This document describes the Stripe Checkout integration for the Laravel Shop package.
Configuration
Enable Stripe
Add the following to your .env file:
SHOP_STRIPE_ENABLED=true
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
Configure Services
In your config/services.php:
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
Price Configuration
All products and product prices must have a stripe_price_id before they can be used in Stripe Checkout.
Setting Stripe Price ID
$product = Product::find($id);
$price = $product->defaultPrice()->first();
$price->update(['stripe_price_id' => 'price_...']);
Creating a Checkout Session
API Endpoint
POST /api/shop/stripe/checkout/{cartId}
Example Request
curl -X POST https://your-domain.com/api/shop/stripe/checkout/cart-uuid-here \
-H "Authorization: Bearer YOUR_TOKEN"
Example Response
{
"session_id": "cs_test_...",
"url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
Redirect the user to the url to complete payment.
Handling Success/Cancel
Success URL
GET /api/shop/stripe/success?session_id={SESSION_ID}&cart_id={CART_ID}
When payment is successful (handled via webhook):
- Cart status is updated to
CONVERTED - Cart's
converted_atis set - Order is created from the cart (if not already exists)
- Payment is recorded on the order
- ProductPurchases are created with:
status→COMPLETEDcharge_id→ Stripe Payment Intent IDamountandamount_paid→ Amount from Stripe (in cents)
- Order status changes to
PROCESSING
Cancel URL
GET /api/shop/stripe/cancel?cart_id={CART_ID}
When payment is cancelled, the cart remains in ACTIVE status and the user can try again.
Webhook Handler
Webhook URL
POST /api/shop/stripe/webhook
Supported Events
The webhook handler processes the following Stripe events:
Checkout Session Events:
checkout.session.completed- Converts cart, creates order if needed, records paymentcheckout.session.async_payment_succeeded- Same as completedcheckout.session.async_payment_failed- Marks order as failed if existscheckout.session.expired- Adds note to order
Charge Events:
charge.succeeded- Updates purchases with charge info, records payment on ordercharge.failed- Marks purchases asFAILED, adds note to ordercharge.refunded- Records refund on ordercharge.dispute.created- Puts order on hold, adds dispute notecharge.dispute.closed- Updates order based on dispute outcome
Payment Intent Events:
payment_intent.succeeded- Records payment on orderpayment_intent.payment_failed- Adds failure note to orderpayment_intent.canceled- Adds cancellation note
Refund Events:
refund.created- Records refund on orderrefund.updated- Updates refund information
Invoice Events (for subscriptions):
invoice.payment_succeeded- Handles subscription paymentsinvoice.payment_failed- Handles failed subscription payments
Configuring Webhook in Stripe
- Go to Stripe Dashboard → Developers → Webhooks
- Click "Add endpoint"
- Enter your webhook URL:
https://your-domain.com/api/shop/stripe/webhook - Select events to listen to (or select "receive all events")
- Copy the signing secret and add it to your
.envasSTRIPE_WEBHOOK_SECRET
Route Customization
Disabling Stripe Routes
The Stripe routes are automatically registered if:
config('shop.stripe.enabled')istrue- Routes haven't already been defined in your Laravel app
You can manually define routes in your application's route files to override the default behavior.
Custom Routes Example
// routes/web.php or routes/api.php
use Blax\Shop\Http\Controllers\StripeCheckoutController;
use Blax\Shop\Http\Controllers\StripeWebhookController;
Route::post('custom/stripe/checkout/{cartId}', [StripeCheckoutController::class, 'createCheckoutSession'])
->name('shop.stripe.checkout');
Route::get('custom/stripe/success', [StripeCheckoutController::class, 'success'])
->name('shop.stripe.success');
Route::get('custom/stripe/cancel', [StripeCheckoutController::class, 'cancel'])
->name('shop.stripe.cancel');
Route::post('custom/stripe/webhook', [StripeWebhookController::class, 'handleWebhook'])
->name('shop.stripe.webhook');
ProductPurchase and Order Updates
The webhook handler automatically updates ProductPurchase records and creates/updates Order records:
Purchase Updates
charge_id- Stripe Payment Intent IDamount- Amount in centsamount_paid- Amount paid in centsstatus- Updated to COMPLETED, FAILED, or REFUNDED based on event
Order Creation and Updates
When a checkout session is completed:
- Cart is marked as CONVERTED
- Order is created from cart (if doesn't exist) via
Order::createFromCart($cart) - Payment is recorded on order via
$order->recordPayment($amount, $reference, 'stripe', 'stripe') - Order status is updated to PROCESSING when payment is successful
- OrderNote records are created for payment events
These fields are automatically populated:
payment_reference- Stripe Payment Intent IDpayment_method- 'stripe'payment_provider- 'stripe'amount_paid- Amount paid in centspaid_at- Timestamp when payment was received
Error Handling
Missing Stripe Price ID
If a cart item doesn't have a stripe_price_id, the checkout session creation will fail with:
{
"error": "Item 'Product Name' is missing a Stripe price ID"
}
Stripe API Errors
All Stripe API errors are caught and logged. The response will include:
{
"error": "Failed to create checkout session: [error message]"
}
Pool Products with MayBePoolProduct Trait
Pool-related methods have been moved to the MayBePoolProduct trait to keep the Product model cleaner.
Using Pool Methods
All pool methods work the same way, they're just now in a trait:
$pool = Product::find($poolId);
// Check if pool
$pool->isPool(); // returns bool
// Get available quantity
$pool->getAvailableQuantity($from, $until);
// Get pool max quantity
$pool->getPoolMaxQuantity($from, $until);
// Claim pool stock
$pool->claimPoolStock($quantity, $reference, $from, $until);
// Release pool stock
$pool->releasePoolStock($reference);
// Pricing methods
$pool->getLowestPoolPrice();
$pool->getHighestPoolPrice();
$pool->getPoolPriceRange();
$pool->setPoolPricingStrategy('lowest'); // 'lowest', 'highest', 'average'
// Validation
$pool->validatePoolConfiguration();
// Availability methods
$pool->getPoolAvailabilityCalendar($start, $end, $quantity);
$pool->getSingleItemsAvailability($from, $until);
$pool->isPoolAvailable($from, $until, $quantity);
$pool->getPoolAvailablePeriods($start, $end, $quantity, $minDays);
Benefits of Trait
- Cleaner Product model
- Pool functionality can be used by other models in the future
- Better separation of concerns
- Easier testing and maintenance
Example Usage Flow
// 1. Create a product with Stripe price
$product = Product::create([...]);
$price = ProductPrice::create([
'purchasable_id' => $product->id,
'purchasable_type' => Product::class,
'stripe_price_id' => 'price_1234567890',
'unit_amount' => 2000, // $20.00 in cents
'is_default' => true,
]);
// 2. Add to cart
$cart = auth()->user()->currentCart();
$cart->addToCart($product, 1);
// 3. Create Stripe checkout session
$response = Http::post('/api/shop/stripe/checkout/' . $cart->id);
$checkoutUrl = $response->json('url');
// 4. Redirect user to Stripe
return redirect($checkoutUrl);
// 5. Stripe redirects back to success URL
// 6. Webhook processes payment
// 7. Cart is converted, purchases are completed
Testing
Mock Stripe in your tests:
use Stripe\Stripe;
use Stripe\Checkout\Session as StripeSession;
// Mock Stripe session creation
Stripe::setApiKey('sk_test_fake');
StripeSession::create([...]); // Use test mode
Security Considerations
- Always verify webhook signatures - Set
STRIPE_WEBHOOK_SECRETin production - Use HTTPS - Stripe requires HTTPS for webhooks
- Validate cart ownership - Ensure users can only checkout their own carts
- Test mode first - Use Stripe test keys during development
- Monitor webhooks - Check Stripe Dashboard for webhook delivery issues