feat: implement comprehensive multi-provider payment processing system
- Add unified payment provider architecture with contract-based design - Implement 6 payment providers: Stripe, Lemon Squeezy, Polar, Oxapay, Crypto, Activation Keys - Create subscription management with lifecycle handling (create, cancel, pause, resume, update) - Add coupon system with usage tracking and trial extensions - Build Filament admin resources for payment providers, subscriptions, coupons, and trials - Implement payment orchestration service with provider registry and configuration management - Add comprehensive payment logging and webhook handling for all providers - Create customer analytics dashboard with revenue, churn, and lifetime value metrics - Add subscription migration service for provider switching - Include extensive test coverage for all payment functionality
This commit is contained in:
838
app/Services/Payments/Providers/StripeProvider.php
Normal file
838
app/Services/Payments/Providers/StripeProvider.php
Normal file
@@ -0,0 +1,838 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payments\Providers;
|
||||
|
||||
use App\Contracts\Payments\PaymentProviderContract;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Cashier\Subscription as CashierSubscription;
|
||||
use Stripe\Checkout\Session;
|
||||
use Stripe\Customer;
|
||||
use Stripe\Price;
|
||||
use Stripe\Product;
|
||||
use Stripe\Stripe;
|
||||
|
||||
class StripeProvider implements PaymentProviderContract
|
||||
{
|
||||
protected array $config;
|
||||
|
||||
protected ?string $apiKey;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user