config = array_merge([ 'api_key' => config('services.stripe.secret'), 'webhook_secret' => config('services.stripe.webhook_secret'), 'currency' => config('cashier.currency', 'USD'), 'success_url' => route('payment.success'), 'cancel_url' => route('payment.cancel'), ], $config); $this->apiKey = $this->config['api_key'] ?? null; if ($this->apiKey) { Stripe::setApiKey($this->apiKey); } } public function getName(): string { return 'stripe'; } public function isActive(): bool { return ! empty($this->apiKey) && $this->apiKey !== 'sk_test_placeholder'; } public function createSubscription(User $user, Plan $plan, array $options = []): array { try { // Create or retrieve Stripe customer $customer = $this->getOrCreateCustomer($user); // Create or retrieve Stripe product and price $priceId = $this->getOrCreatePrice($plan); // Create subscription $subscriptionBuilder = $user->newSubscription('default', $priceId); if (! empty($options['trial_days'])) { $subscriptionBuilder->trialDays($options['trial_days']); } if (! empty($options['coupon'])) { $subscriptionBuilder->withCoupon($options['coupon']); } if (! empty($options['payment_method'])) { $subscriptionBuilder->create($options['payment_method']); } else { // Create checkout session for payment method collection $sessionId = $this->createCheckoutSession($user, $plan, $options); return [ 'requires_action' => true, 'checkout_session_id' => $sessionId, 'type' => 'checkout_session', ]; } $stripeSubscription = $subscriptionBuilder->create(); return [ 'provider_subscription_id' => $stripeSubscription->stripe_id, 'status' => $stripeSubscription->stripe_status, 'current_period_start' => $stripeSubscription->created_at, 'current_period_end' => $stripeSubscription->ends_at, 'trial_ends_at' => $stripeSubscription->trial_ends_at, 'customer_id' => $stripeSubscription->stripe_id, ]; } catch (Exception $e) { Log::error('Stripe subscription creation failed', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'error' => $e->getMessage(), ]); throw $e; } } public function cancelSubscription(Subscription $subscription, string $reason = ''): bool { try { $cashierSubscription = CashierSubscription::find($subscription->stripe_id); if (! $cashierSubscription) { throw new Exception('Cashier subscription not found'); } // Cancel immediately or at period end $cashierSubscription->cancel(); return true; } catch (Exception $e) { Log::error('Stripe subscription cancellation failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function updateSubscription(Subscription $subscription, Plan $newPlan): array { try { $cashierSubscription = CashierSubscription::find($subscription->stripe_id); if (! $cashierSubscription) { throw new Exception('Cashier subscription not found'); } $newPriceId = $this->getOrCreatePrice($newPlan); // Swap to new plan $cashierSubscription->swap($newPriceId); return [ 'provider_subscription_id' => $cashierSubscription->stripe_id, 'status' => $cashierSubscription->stripe_status, 'new_price_id' => $newPriceId, ]; } catch (Exception $e) { Log::error('Stripe subscription update failed', [ 'subscription_id' => $subscription->id, 'new_plan_id' => $newPlan->id, 'error' => $e->getMessage(), ]); throw $e; } } public function pauseSubscription(Subscription $subscription): bool { try { $cashierSubscription = CashierSubscription::find($subscription->stripe_id); if (! $cashierSubscription) { throw new Exception('Cashier subscription not found'); } // Stripe doesn't have a native pause feature, so we cancel at period end $cashierSubscription->cancel(); return true; } catch (Exception $e) { Log::error('Stripe subscription pause failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function resumeSubscription(Subscription $subscription): bool { try { $cashierSubscription = CashierSubscription::find($subscription->stripe_id); if (! $cashierSubscription) { throw new Exception('Cashier subscription not found'); } // Resume cancelled subscription $cashierSubscription->resume(); return true; } catch (Exception $e) { Log::error('Stripe subscription resume failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function getSubscriptionDetails(string $providerSubscriptionId): array { try { $stripeSubscription = \Stripe\Subscription::retrieve($providerSubscriptionId); return [ 'id' => $stripeSubscription->id, 'status' => $stripeSubscription->status, 'current_period_start' => $stripeSubscription->current_period_start, 'current_period_end' => $stripeSubscription->current_period_end, 'trial_start' => $stripeSubscription->trial_start, 'trial_end' => $stripeSubscription->trial_end, 'customer' => $stripeSubscription->customer, 'items' => $stripeSubscription->items->data, 'created' => $stripeSubscription->created, 'ended_at' => $stripeSubscription->ended_at, 'canceled_at' => $stripeSubscription->canceled_at, ]; } catch (Exception $e) { Log::error('Stripe subscription details retrieval failed', [ 'subscription_id' => $providerSubscriptionId, 'error' => $e->getMessage(), ]); throw $e; } } public function createCheckoutSession(User $user, Plan $plan, array $options = []): array { try { $customer = $this->getOrCreateCustomer($user); $priceId = $this->getOrCreatePrice($plan); $sessionData = [ 'customer' => $customer->id, 'payment_method_types' => ['card'], 'line_items' => [[ 'price' => $priceId, 'quantity' => 1, ]], 'mode' => $plan->monthly_billing ? 'subscription' : 'payment', 'success_url' => $options['success_url'] ?? $this->config['success_url'], 'cancel_url' => $options['cancel_url'] ?? $this->config['cancel_url'], 'metadata' => [ 'user_id' => $user->id, 'plan_id' => $plan->id, ], ]; if ($plan->monthly_billing && ! empty($options['trial_days'])) { $sessionData['subscription_data'] = [ 'trial_period_days' => $options['trial_days'], ]; } if (! empty($options['coupon'])) { $sessionData['discounts'] = [['coupon' => $options['coupon']]]; } $session = Session::create($sessionData); return [ 'checkout_session_id' => $session->id, 'checkout_url' => $session->url, ]; } catch (Exception $e) { Log::error('Stripe checkout session creation failed', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'error' => $e->getMessage(), ]); throw $e; } } public function createCustomerPortalSession(User $user): array { try { $customer = $this->getOrCreateCustomer($user); $session = \Stripe\BillingPortal\Session::create([ 'customer' => $customer->id, 'return_url' => route('dashboard'), ]); return [ 'portal_session_id' => $session->id, 'portal_url' => $session->url, ]; } catch (Exception $e) { Log::error('Stripe customer portal session creation failed', [ 'user_id' => $user->id, 'error' => $e->getMessage(), ]); throw $e; } } public function processWebhook(Request $request): array { try { $payload = $request->getContent(); $sigHeader = $request->header('Stripe-Signature'); $event = \Stripe\Event::constructFrom( json_decode($payload, true) ); $result = [ 'event_type' => $event->type, 'processed' => false, 'data' => [], ]; switch ($event->type) { case 'invoice.payment_succeeded': $result = $this->handleInvoicePaymentSucceeded($event); break; case 'invoice.payment_failed': $result = $this->handleInvoicePaymentFailed($event); break; case 'customer.subscription.created': $result = $this->handleSubscriptionCreated($event); break; case 'customer.subscription.updated': $result = $this->handleSubscriptionUpdated($event); break; case 'customer.subscription.deleted': $result = $this->handleSubscriptionDeleted($event); break; default: Log::info('Unhandled Stripe webhook event', ['event_type' => $event->type]); } return $result; } catch (Exception $e) { Log::error('Stripe webhook processing failed', [ 'error' => $e->getMessage(), 'payload' => $request->getContent(), ]); throw $e; } } public function validateWebhook(Request $request): bool { try { $payload = $request->getContent(); $sigHeader = $request->header('Stripe-Signature'); if (! $sigHeader) { return false; } \Stripe\Webhook::constructEvent( $payload, $sigHeader, $this->config['webhook_secret'] ); return true; } catch (Exception $e) { Log::warning('Stripe webhook validation failed', [ 'error' => $e->getMessage(), ]); return false; } } public function getConfiguration(): array { return $this->config; } public function syncSubscriptionStatus(Subscription $subscription): array { return $this->getSubscriptionDetails($subscription->provider_subscription_id); } public function getPaymentMethodDetails(string $paymentMethodId): array { try { $paymentMethod = \Stripe\PaymentMethod::retrieve($paymentMethodId); return [ 'id' => $paymentMethod->id, 'type' => $paymentMethod->type, 'card' => [ 'brand' => $paymentMethod->card->brand, 'last4' => $paymentMethod->card->last4, 'exp_month' => $paymentMethod->card->exp_month, 'exp_year' => $paymentMethod->card->exp_year, ], 'created' => $paymentMethod->created, ]; } catch (Exception $e) { Log::error('Stripe payment method details retrieval failed', [ 'payment_method_id' => $paymentMethodId, 'error' => $e->getMessage(), ]); throw $e; } } public function processRefund(string $paymentIntentId, float $amount, string $reason = ''): array { try { $refund = \Stripe\Refund::create([ 'payment_intent' => $paymentIntentId, 'amount' => (int) ($amount * 100), // Convert to cents 'reason' => $reason ?: 'requested_by_customer', ]); return [ 'refund_id' => $refund->id, 'amount' => $refund->amount / 100, 'status' => $refund->status, 'created' => $refund->created, ]; } catch (Exception $e) { Log::error('Stripe refund processing failed', [ 'payment_intent_id' => $paymentIntentId, 'amount' => $amount, 'error' => $e->getMessage(), ]); throw $e; } } public function getTransactionHistory(User $user, array $filters = []): array { try { $customer = $this->getOrCreateCustomer($user); $charges = \Stripe\Charge::all([ 'customer' => $customer->id, 'limit' => $filters['limit'] ?? 100, 'starting_after' => $filters['starting_after'] ?? null, ]); $transactions = []; foreach ($charges->data as $charge) { $transactions[] = [ 'id' => $charge->id, 'amount' => $charge->amount / 100, 'currency' => $charge->currency, 'status' => $charge->status, 'created' => $charge->created, 'description' => $charge->description, 'payment_method' => $charge->payment_method, ]; } return $transactions; } catch (Exception $e) { Log::error('Stripe transaction history retrieval failed', [ 'user_id' => $user->id, 'error' => $e->getMessage(), ]); throw $e; } } public function calculateFees(float $amount): array { // Stripe fees: 2.9% + $0.30 (US), varies by country $fixedFee = 0.30; // $0.30 $percentageFee = 2.9; // 2.9% $percentageAmount = ($amount * $percentageFee) / 100; $totalFee = $fixedFee + $percentageAmount; return [ 'fixed_fee' => $fixedFee, 'percentage_fee' => $percentageAmount, 'total_fee' => $totalFee, 'net_amount' => $amount - $totalFee, ]; } public function getSupportedCurrencies(): array { return [ 'USD', 'EUR', 'GBP', 'CAD', 'AUD', 'CHF', 'SEK', 'NOK', 'DKK', 'PLN', 'CZK', 'HUF', 'RON', 'BGN', 'HRK', 'RUB', 'TRY', 'MXN', 'BRL', 'ARS', 'CLP', 'COP', 'PEN', 'UYU', 'JPY', 'SGD', 'HKD', 'INR', 'MYR', 'THB', 'PHP', 'TWD', 'KRW', 'CNY', ]; } public function supportsRecurring(): bool { return true; } public function supportsOneTime(): bool { return true; } // Helper methods protected function getOrCreateCustomer(User $user): Customer { if ($user->stripe_id) { return Customer::retrieve($user->stripe_id); } $customer = Customer::create([ 'email' => $user->email, 'name' => $user->name, 'metadata' => [ 'user_id' => $user->id, ], ]); $user->update(['stripe_id' => $customer->id]); return $customer; } protected function getOrCreatePrice(Plan $plan): string { // Check if plan already has a Stripe price ID if (! empty($plan->details['stripe_price_id'])) { return $plan->details['stripe_price_id']; } // Create product if it doesn't exist $productId = $this->getOrCreateProduct($plan); // Create price $priceData = [ 'product' => $productId, 'unit_amount' => intval($plan->price * 100), // Convert to cents 'currency' => strtolower($this->config['currency']), ]; if ($plan->monthly_billing) { $priceData['recurring'] = [ 'interval' => 'month', 'interval_count' => 1, ]; } $price = Price::create($priceData); // Update plan with new price ID $planDetails = $plan->details ?? []; $planDetails['stripe_price_id'] = $price->id; $plan->update(['details' => $planDetails]); return $price->id; } protected function getOrCreateProduct(Plan $plan): string { // Check if plan already has a Stripe product ID if (! empty($plan->details['stripe_product_id'])) { return $plan->details['stripe_product_id']; } // Create product $product = Product::create([ 'name' => $plan->name, 'description' => $plan->description, 'metadata' => [ 'plan_id' => $plan->id, ], ]); // Update plan with new product ID $planDetails = $plan->details ?? []; $planDetails['stripe_product_id'] = $product->id; $plan->update(['details' => $planDetails]); return $product->id; } // Webhook handlers protected function handleInvoicePaymentSucceeded($event): array { $invoice = $event->data->object; return [ 'event_type' => $event->type, 'processed' => true, 'data' => [ 'invoice_id' => $invoice->id, 'subscription_id' => $invoice->subscription, 'amount_paid' => $invoice->amount_paid / 100, 'status' => 'paid', ], ]; } protected function handleInvoicePaymentFailed($event): array { $invoice = $event->data->object; return [ 'event_type' => $event->type, 'processed' => true, 'data' => [ 'invoice_id' => $invoice->id, 'subscription_id' => $invoice->subscription, 'attempt_count' => $invoice->attempt_count, 'status' => 'payment_failed', ], ]; } protected function handleSubscriptionCreated($event): array { $subscription = $event->data->object; return [ 'event_type' => $event->type, 'processed' => true, 'data' => [ 'subscription_id' => $subscription->id, 'customer_id' => $subscription->customer, 'status' => $subscription->status, ], ]; } protected function handleSubscriptionUpdated($event): array { $subscription = $event->data->object; return [ 'event_type' => $event->type, 'processed' => true, 'data' => [ 'subscription_id' => $subscription->id, 'status' => $subscription->status, 'current_period_start' => $subscription->current_period_start, 'current_period_end' => $subscription->current_period_end, ], ]; } protected function handleSubscriptionDeleted($event): array { $subscription = $event->data->object; return [ 'event_type' => $event->type, 'processed' => true, 'data' => [ 'subscription_id' => $subscription->id, 'status' => 'canceled', 'ended_at' => $subscription->ended_at, ], ]; } // Additional interface methods public function getSubscriptionMetadata(Subscription $subscription): array { return $subscription->provider_data['metadata'] ?? []; } public function updateSubscriptionMetadata(Subscription $subscription, array $metadata): bool { try { $stripeSubscription = \Stripe\Subscription::retrieve($subscription->provider_subscription_id); $stripeSubscription->metadata = $metadata; $stripeSubscription->save(); return true; } catch (Exception $e) { Log::error('Failed to update subscription metadata', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); return false; } } public function startTrial(Subscription $subscription, int $trialDays): bool { try { $stripeSubscription = \Stripe\Subscription::retrieve($subscription->provider_subscription_id); $stripeSubscription->trial_end = now()->addDays($trialDays)->timestamp; $stripeSubscription->save(); return true; } catch (Exception $e) { Log::error('Failed to start trial', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); return false; } } public function applyCoupon(Subscription $subscription, string $couponCode): array { try { $stripeSubscription = \Stripe\Subscription::retrieve($subscription->provider_subscription_id); $stripeSubscription->discount = null; // Remove existing discount $stripeSubscription->save(); // Apply new coupon $stripeSubscription = \Stripe\Subscription::retrieve($subscription->provider_subscription_id); $coupon = \Stripe\Coupon::retrieve($couponCode); return [ 'coupon_id' => $coupon->id, 'amount_off' => $coupon->amount_off ?? null, 'percent_off' => $coupon->percent_off ?? null, ]; } catch (Exception $e) { Log::error('Failed to apply coupon', [ 'subscription_id' => $subscription->id, 'coupon_code' => $couponCode, 'error' => $e->getMessage(), ]); throw $e; } } public function removeCoupon(Subscription $subscription): bool { try { $stripeSubscription = \Stripe\Subscription::retrieve($subscription->provider_subscription_id); $stripeSubscription->deleteDiscount(); return true; } catch (Exception $e) { Log::error('Failed to remove coupon', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); return false; } } public function getUpcomingInvoice(Subscription $subscription): array { try { $invoice = \Stripe\Invoice::upcoming(['subscription' => $subscription->provider_subscription_id]); return [ 'amount_due' => $invoice->amount_due / 100, 'currency' => $invoice->currency, 'period_start' => $invoice->period_start, 'period_end' => $invoice->period_end, 'lines' => $invoice->lines->data, ]; } catch (Exception $e) { Log::error('Failed to get upcoming invoice', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function retryFailedPayment(Subscription $subscription): array { try { $invoice = \Stripe\Invoice::retrieve(['subscription' => $subscription->provider_subscription_id]); $invoice->pay(); return [ 'invoice_id' => $invoice->id, 'status' => $invoice->status, 'amount_paid' => $invoice->amount_paid / 100, ]; } catch (Exception $e) { Log::error('Failed to retry payment', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function canModifySubscription(Subscription $subscription): bool { try { $stripeSubscription = \Stripe\Subscription::retrieve($subscription->provider_subscription_id); return in_array($stripeSubscription->status, ['active', 'trialing']); } catch (Exception $e) { return false; } } public function getCancellationTerms(Subscription $subscription): array { return [ 'immediate_cancellation' => true, 'refund_policy' => 'pro_rata', 'cancellation_effective' => 'immediately', 'billing_cycle_proration' => true, ]; } public function exportSubscriptionData(Subscription $subscription): array { return [ 'provider' => 'stripe', 'provider_subscription_id' => $subscription->provider_subscription_id, 'stripe_id' => $subscription->stripe_id, 'status' => $subscription->stripe_status, 'data' => $subscription->provider_data, ]; } public function importSubscriptionData(User $user, array $subscriptionData): array { // This would be used for migrating subscriptions to Stripe // Implementation depends on specific requirements throw new Exception('Import to Stripe not implemented'); } }