Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
php: [8.3]
php: [8.4]

steps:
- name: Checkout code
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest ]
php: [ 8.3 ]
os: [ubuntu-latest]
php: [8.4]
# services:
# redis:
# image: redis
Expand Down
136 changes: 76 additions & 60 deletions app/Http/Controllers/CartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
use App\Models\Plugin;
use App\Models\PluginBundle;
use App\Services\CartService;
use App\Services\StripeConnectService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Laravel\Cashier\Cashier;

class CartController extends Controller
{
public function __construct(
protected CartService $cartService,
protected StripeConnectService $stripeConnectService
protected CartService $cartService
) {}

public function show(Request $request): View
Expand Down Expand Up @@ -217,6 +216,8 @@ public function checkout(Request $request): RedirectResponse
try {
$session = $this->createMultiItemCheckoutSession($cart, $user);

$cart->update(['stripe_checkout_session_id' => $session->id]);

return redirect($session->url);
} catch (\Exception $e) {
Log::error('Cart checkout failed', [
Expand All @@ -241,11 +242,7 @@ public function success(Request $request): View|RedirectResponse
->with('error', 'Invalid checkout session. Please try again.');
}

$user = Auth::user();

// Clear the cart - the webhook will create the licenses
$cart = $this->cartService->getCart($user);
$cart->clear();
// Cart will be marked as completed by the webhook after licenses are created

return view('cart.success', [
'sessionId' => $sessionId,
Expand All @@ -256,9 +253,27 @@ public function status(Request $request, string $sessionId): JsonResponse
{
$user = Auth::user();

// Check if licenses exist for this checkout session
// Retrieve the checkout session to get the invoice ID
try {
$session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);
$invoiceId = $session->invoice;
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => 'Unable to verify purchase status.',
], 400);
}

if (! $invoiceId) {
return response()->json([
'status' => 'pending',
'message' => 'Processing your purchase...',
]);
}

// Check if licenses exist for this invoice
$licenses = $user->pluginLicenses()
->where('stripe_checkout_session_id', $sessionId)
->where('stripe_invoice_id', $invoiceId)
->with('plugin')
->get();

Expand All @@ -274,6 +289,7 @@ public function status(Request $request, string $sessionId): JsonResponse
'message' => 'Purchase complete!',
'licenses' => $licenses->map(fn ($license) => [
'id' => $license->id,
'plugin_id' => $license->plugin->id,
'plugin_name' => $license->plugin->name,
'plugin_slug' => $license->plugin->slug,
]),
Expand All @@ -296,20 +312,10 @@ public function count(Request $request): JsonResponse

protected function createMultiItemCheckoutSession($cart, $user): \Stripe\Checkout\Session
{
$stripe = new \Stripe\StripeClient(config('cashier.secret'));

// Eager load items with plugins and bundles to avoid any stale data issues
$cart->load('items.plugin', 'items.pluginBundle.plugins');

$lineItems = [];
$metadata = [
'cart_id' => $cart->id,
'user_id' => $user->id,
'plugin_ids' => [],
'price_ids' => [],
'bundle_ids' => [],
'bundle_plugin_ids' => [],
];

Log::info('Creating multi-item checkout session', [
'cart_id' => $cart->id,
Expand All @@ -321,14 +327,6 @@ protected function createMultiItemCheckoutSession($cart, $user): \Stripe\Checkou
if ($item->isBundle()) {
$bundle = $item->pluginBundle;

Log::info('Adding bundle to checkout session', [
'cart_id' => $cart->id,
'cart_item_id' => $item->id,
'bundle_id' => $bundle->id,
'bundle_name' => $bundle->name,
'plugin_count' => $bundle->plugins->count(),
]);

$pluginNames = $bundle->plugins->pluck('name')->take(3)->implode(', ');
if ($bundle->plugins->count() > 3) {
$pluginNames .= ' and '.($bundle->plugins->count() - 3).' more';
Expand All @@ -345,20 +343,9 @@ protected function createMultiItemCheckoutSession($cart, $user): \Stripe\Checkou
],
'quantity' => 1,
];

$metadata['bundle_ids'][] = $bundle->id;
$metadata['bundle_plugin_ids'][$bundle->id] = $bundle->plugins->pluck('id')->implode(':');
} else {
$plugin = $item->plugin;

Log::info('Adding plugin to checkout session', [
'cart_id' => $cart->id,
'cart_item_id' => $item->id,
'plugin_id' => $plugin->id,
'plugin_name' => $plugin->name,
'price_id' => $item->plugin_price_id,
]);

$lineItems[] = [
'price_data' => [
'currency' => strtolower($item->currency),
Expand All @@ -370,32 +357,18 @@ protected function createMultiItemCheckoutSession($cart, $user): \Stripe\Checkou
],
'quantity' => 1,
];

$metadata['plugin_ids'][] = $plugin->id;
$metadata['price_ids'][] = $item->plugin_price_id;
}
}

// Encode arrays for Stripe metadata (must be strings)
$metadata['cart_id'] = (string) $metadata['cart_id'];
$metadata['user_id'] = (string) $metadata['user_id'];
$metadata['plugin_ids'] = implode(',', $metadata['plugin_ids']);
$metadata['price_ids'] = implode(',', $metadata['price_ids']);
$metadata['bundle_ids'] = implode(',', $metadata['bundle_ids']);
$metadata['bundle_plugin_ids'] = json_encode($metadata['bundle_plugin_ids']);
// Ensure the user has a valid Stripe customer ID
$this->ensureValidStripeCustomer($user);

Log::info('Checkout session metadata prepared', [
'cart_id' => $cart->id,
'metadata' => $metadata,
'line_items_count' => count($lineItems),
]);

// Ensure the user has a Stripe customer ID (required for Stripe Accounts V2)
if (! $user->stripe_id) {
$user->createAsStripeCustomer();
}
// Metadata only needs cart_id - we'll look up items from the cart
$metadata = [
'cart_id' => (string) $cart->id,
];

return $stripe->checkout->sessions->create([
$session = Cashier::stripe()->checkout->sessions->create([
'mode' => 'payment',
'line_items' => $lineItems,
'success_url' => route('cart.success').'?session_id={CHECKOUT_SESSION_ID}',
Expand All @@ -414,8 +387,51 @@ protected function createMultiItemCheckoutSession($cart, $user): \Stripe\Checkou
'invoice_data' => [
'description' => 'NativePHP Plugin Purchase',
'footer' => 'Thank you for your purchase!',
'metadata' => $metadata,
],
],
]);

// Store the Stripe checkout session ID on the cart
$cart->update(['stripe_checkout_session_id' => $session->id]);

Log::info('Checkout session created', [
'cart_id' => $cart->id,
'session_id' => $session->id,
]);

return $session;
}

/**
* Ensure the user has a valid Stripe customer ID.
* Creates a new customer if none exists or if the existing one is invalid.
*/
protected function ensureValidStripeCustomer($user): void
{
if (! $user->stripe_id) {
$user->createAsStripeCustomer();

return;
}

// Verify the customer exists in Stripe
try {
Cashier::stripe()->customers->retrieve($user->stripe_id);
} catch (\Stripe\Exception\InvalidRequestException $e) {
// Customer doesn't exist in Stripe, create a new one
if (str_contains($e->getMessage(), 'No such customer')) {
Log::warning('Stripe customer not found, creating new customer', [
'user_id' => $user->id,
'old_stripe_id' => $user->stripe_id,
]);

$user->stripe_id = null;
$user->save();
$user->createAsStripeCustomer();
} else {
throw $e;
}
}
}
}
Loading
Loading