laravel-shop/docs/04-stripe-checkout.md

317 lines
8.5 KiB
Markdown

# 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:
```env
SHOP_STRIPE_ENABLED=true
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
```
### Configure Services
In your `config/services.php`:
```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
```php
$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
```bash
curl -X POST https://your-domain.com/api/shop/stripe/checkout/cart-uuid-here \
-H "Authorization: Bearer YOUR_TOKEN"
```
### Example Response
```json
{
"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_at` is set
- Order is created from the cart (if not already exists)
- Payment is recorded on the order
- ProductPurchases are created with:
- `status``COMPLETED`
- `charge_id` → Stripe Payment Intent ID
- `amount` and `amount_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 payment
- `checkout.session.async_payment_succeeded` - Same as completed
- `checkout.session.async_payment_failed` - Marks order as failed if exists
- `checkout.session.expired` - Adds note to order
**Charge Events:**
- `charge.succeeded` - Updates purchases with charge info, records payment on order
- `charge.failed` - Marks purchases as `FAILED`, adds note to order
- `charge.refunded` - Records refund on order
- `charge.dispute.created` - Puts order on hold, adds dispute note
- `charge.dispute.closed` - Updates order based on dispute outcome
**Payment Intent Events:**
- `payment_intent.succeeded` - Records payment on order
- `payment_intent.payment_failed` - Adds failure note to order
- `payment_intent.canceled` - Adds cancellation note
**Refund Events:**
- `refund.created` - Records refund on order
- `refund.updated` - Updates refund information
**Invoice Events** (for subscriptions):
- `invoice.payment_succeeded` - Handles subscription payments
- `invoice.payment_failed` - Handles failed subscription payments
### Configuring Webhook in Stripe
1. Go to Stripe Dashboard → Developers → Webhooks
2. Click "Add endpoint"
3. Enter your webhook URL: `https://your-domain.com/api/shop/stripe/webhook`
4. Select events to listen to (or select "receive all events")
5. Copy the signing secret and add it to your `.env` as `STRIPE_WEBHOOK_SECRET`
## Route Customization
### Disabling Stripe Routes
The Stripe routes are automatically registered if:
- `config('shop.stripe.enabled')` is `true`
- 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
```php
// 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 ID
- `amount` - Amount in cents
- `amount_paid` - Amount paid in cents
- `status` - Updated to COMPLETED, FAILED, or REFUNDED based on event
### Order Creation and Updates
When a checkout session is completed:
1. Cart is marked as CONVERTED
2. Order is created from cart (if doesn't exist) via `Order::createFromCart($cart)`
3. Payment is recorded on order via `$order->recordPayment($amount, $reference, 'stripe', 'stripe')`
4. Order status is updated to PROCESSING when payment is successful
5. OrderNote records are created for payment events
These fields are automatically populated:
- `payment_reference` - Stripe Payment Intent ID
- `payment_method` - 'stripe'
- `payment_provider` - 'stripe'
- `amount_paid` - Amount paid in cents
- `paid_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:
```json
{
"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:
```json
{
"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:
```php
$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
```php
// 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:
```php
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
1. **Always verify webhook signatures** - Set `STRIPE_WEBHOOK_SECRET` in production
2. **Use HTTPS** - Stripe requires HTTPS for webhooks
3. **Validate cart ownership** - Ensure users can only checkout their own carts
4. **Test mode first** - Use Stripe test keys during development
5. **Monitor webhooks** - Check Stripe Dashboard for webhook delivery issues