280 lines
7.0 KiB
Markdown
280 lines
7.0 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:
|
||
|
|
- Cart status is updated to `CONVERTED`
|
||
|
|
- Cart's `converted_at` is set
|
||
|
|
- ProductPurchases are updated with:
|
||
|
|
- `status` → `COMPLETED`
|
||
|
|
- `charge_id` → Stripe Payment Intent ID
|
||
|
|
- `amount_paid` → Amount from Stripe (in dollars, converted from cents)
|
||
|
|
|
||
|
|
### 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.completed` - Updates cart to converted, updates purchases
|
||
|
|
- `checkout.session.async_payment_succeeded` - Same as completed
|
||
|
|
- `checkout.session.async_payment_failed` - Logs failure
|
||
|
|
- `charge.succeeded` - Updates purchases with charge info
|
||
|
|
- `charge.failed` - Marks purchases as `FAILED`
|
||
|
|
- `payment_intent.succeeded` - Updates purchases
|
||
|
|
- `payment_intent.payment_failed` - Marks purchases as `FAILED`
|
||
|
|
|
||
|
|
### 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 Updates
|
||
|
|
|
||
|
|
The webhook handler automatically updates ProductPurchase records with charge information if the columns exist:
|
||
|
|
|
||
|
|
- `charge_id` - Stripe Payment Intent ID
|
||
|
|
- `amount_paid` - Amount paid in dollars
|
||
|
|
|
||
|
|
These fields are automatically populated from the fillable array on the ProductPurchase model.
|
||
|
|
|
||
|
|
## 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
|