paypal = $paypal; } /** * Get current subscription status */ public function status(Request $request): JsonResponse { $user = $request->user(); $subscription = $user->subscriptions()->active()->with('plan')->first(); $currentPlan = $user->currentPlan(); return response()->json([ 'success' => true, 'data' => [ 'has_subscription' => $subscription !== null, 'on_trial' => $subscription?->isOnTrial() ?? false, 'trial_ends_at' => $subscription?->trial_ends_at, 'days_until_trial_ends' => $subscription?->days_until_trial_ends, 'current_period_end' => $subscription?->current_period_end, 'status' => $subscription?->status, 'status_label' => $subscription?->status_label, 'canceled_at' => $subscription?->canceled_at, 'on_grace_period' => $subscription?->onGracePeriod() ?? false, 'plan' => $currentPlan ? [ 'id' => $currentPlan->id, 'slug' => $currentPlan->slug, 'name' => $currentPlan->name, 'price' => $currentPlan->price, 'formatted_price' => $currentPlan->formatted_price, 'billing_period' => $currentPlan->billing_period, 'is_free' => $currentPlan->is_free, 'features' => $currentPlan->features, 'limits' => $currentPlan->limits, ] : null, ], ]); } /** * Create a subscription checkout */ public function subscribe(Request $request): JsonResponse { $request->validate([ 'plan_slug' => 'required|string|exists:plans,slug', ]); $user = $request->user(); $plan = Plan::where('slug', $request->plan_slug)->where('is_active', true)->first(); if (!$plan) { return response()->json([ 'success' => false, 'message' => 'Plan not found or inactive', ], 404); } // Check if plan is free if ($plan->is_free) { return $this->subscribeFree($user, $plan); } // Check if PayPal is configured if (!$this->paypal->isConfigured()) { return response()->json([ 'success' => false, 'message' => 'Payment gateway not configured', ], 500); } // Check if plan has PayPal plan ID if (!$plan->paypal_plan_id) { return response()->json([ 'success' => false, 'message' => 'Plan not configured for payments yet', ], 500); } // Create PayPal subscription $frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com'); $returnUrl = "{$frontendUrl}/billing?success=true&plan={$plan->slug}"; $cancelUrl = "{$frontendUrl}/pricing?canceled=true"; $paypalSubscription = $this->paypal->createSubscription($plan, $returnUrl, $cancelUrl); if (!$paypalSubscription) { return response()->json([ 'success' => false, 'message' => 'Failed to create subscription', ], 500); } // Find approve link $approveUrl = collect($paypalSubscription['links'] ?? []) ->firstWhere('rel', 'approve')['href'] ?? null; if (!$approveUrl) { return response()->json([ 'success' => false, 'message' => 'No approval URL received', ], 500); } // Create pending subscription in our DB $subscription = Subscription::create([ 'user_id' => $user->id, 'plan_id' => $plan->id, 'status' => Subscription::STATUS_TRIALING, 'paypal_subscription_id' => $paypalSubscription['id'], 'paypal_status' => $paypalSubscription['status'], 'paypal_data' => $paypalSubscription, 'price_paid' => $plan->price, 'currency' => $plan->currency, ]); return response()->json([ 'success' => true, 'data' => [ 'subscription_id' => $subscription->id, 'paypal_subscription_id' => $paypalSubscription['id'], 'approve_url' => $approveUrl, ], ]); } /** * Subscribe to free plan */ private function subscribeFree($user, Plan $plan): JsonResponse { // Cancel any existing subscriptions $user->subscriptions()->active()->update([ 'status' => Subscription::STATUS_CANCELED, 'canceled_at' => now(), 'ends_at' => now(), 'cancel_reason' => 'Downgraded to free plan', ]); // Create free subscription $subscription = Subscription::createForUser($user, $plan); return response()->json([ 'success' => true, 'message' => 'Subscribed to free plan', 'data' => [ 'subscription_id' => $subscription->id, 'status' => $subscription->status, ], ]); } /** * Confirm subscription after PayPal approval */ public function confirm(Request $request): JsonResponse { $request->validate([ 'subscription_id' => 'required|string', ]); $user = $request->user(); $subscription = Subscription::where('paypal_subscription_id', $request->subscription_id) ->where('user_id', $user->id) ->first(); if (!$subscription) { return response()->json([ 'success' => false, 'message' => 'Subscription not found', ], 404); } // Get subscription details from PayPal $paypalData = $this->paypal->getSubscription($request->subscription_id); if (!$paypalData) { return response()->json([ 'success' => false, 'message' => 'Failed to verify subscription', ], 500); } // Update subscription based on PayPal status $this->updateSubscriptionFromPayPal($subscription, $paypalData); // Cancel other active subscriptions $user->subscriptions() ->where('id', '!=', $subscription->id) ->active() ->update([ 'status' => Subscription::STATUS_CANCELED, 'canceled_at' => now(), 'ends_at' => now(), 'cancel_reason' => 'Replaced by new subscription', ]); // Create invoice for the subscription if ($subscription->isActive() && !$subscription->plan->is_free) { Invoice::createForSubscription( $subscription, Invoice::REASON_SUBSCRIPTION_CREATE, "{$subscription->plan->name} - Nueva suscripción" )->markAsPaid($paypalData['id'] ?? null); } return response()->json([ 'success' => true, 'message' => 'Subscription confirmed', 'data' => [ 'status' => $subscription->status, 'status_label' => $subscription->status_label, 'plan' => $subscription->plan->name, ], ]); } /** * Cancel subscription */ public function cancel(Request $request): JsonResponse { $request->validate([ 'reason' => 'nullable|string|max:500', 'immediately' => 'nullable|boolean', ]); $user = $request->user(); $subscription = $user->subscriptions()->active()->first(); if (!$subscription) { return response()->json([ 'success' => false, 'message' => 'No active subscription found', ], 404); } // If it's a paid plan, cancel on PayPal if ($subscription->paypal_subscription_id && !$subscription->plan->is_free) { $canceled = $this->paypal->cancelSubscription( $subscription->paypal_subscription_id, $request->reason ?? 'User requested cancellation' ); if (!$canceled) { return response()->json([ 'success' => false, 'message' => 'Failed to cancel subscription on PayPal', ], 500); } } // Cancel in our DB $subscription->cancel( $request->reason, $request->boolean('immediately', false) ); return response()->json([ 'success' => true, 'message' => $request->boolean('immediately') ? 'Subscription canceled immediately' : 'Subscription will be canceled at period end', 'data' => [ 'status' => $subscription->status, 'ends_at' => $subscription->ends_at, ], ]); } /** * Get invoices */ public function invoices(Request $request): JsonResponse { $user = $request->user(); $invoices = $user->invoices() ->with('subscription.plan') ->recent() ->paginate(20); return response()->json([ 'success' => true, 'data' => [ 'invoices' => $invoices->map(function ($invoice) { return [ 'id' => $invoice->id, 'number' => $invoice->number, 'status' => $invoice->status, 'status_label' => $invoice->status_label, 'status_color' => $invoice->status_color, 'description' => $invoice->description, 'subtotal' => $invoice->subtotal, 'tax' => $invoice->tax, 'tax_percent' => $invoice->tax_percent, 'total' => $invoice->total, 'formatted_total' => $invoice->formatted_total, 'currency' => $invoice->currency, 'billing_reason' => $invoice->billing_reason, 'billing_reason_label' => $invoice->billing_reason_label, 'paid_at' => $invoice->paid_at, 'created_at' => $invoice->created_at, 'plan_name' => $invoice->subscription?->plan?->name, ]; }), 'pagination' => [ 'current_page' => $invoices->currentPage(), 'last_page' => $invoices->lastPage(), 'per_page' => $invoices->perPage(), 'total' => $invoices->total(), ], ], ]); } /** * Get PayPal client ID for frontend */ public function paypalConfig(): JsonResponse { return response()->json([ 'success' => true, 'data' => [ 'client_id' => $this->paypal->getClientId(), 'sandbox' => $this->paypal->isSandbox(), ], ]); } /** * PayPal webhook handler */ public function webhook(Request $request): JsonResponse { $webhookId = config('services.paypal.webhook_id'); // Verify webhook signature (skip in sandbox for testing) if (!$this->paypal->isSandbox() && $webhookId) { $verified = $this->paypal->verifyWebhookSignature( $request->headers->all(), $request->getContent(), $webhookId ); if (!$verified) { Log::warning('PayPal webhook signature verification failed'); return response()->json(['status' => 'signature_invalid'], 400); } } $event = $request->all(); $eventType = $event['event_type'] ?? ''; $resource = $event['resource'] ?? []; Log::info('PayPal webhook received', ['event_type' => $eventType]); switch ($eventType) { case 'BILLING.SUBSCRIPTION.ACTIVATED': $this->handleSubscriptionActivated($resource); break; case 'BILLING.SUBSCRIPTION.CANCELLED': $this->handleSubscriptionCancelled($resource); break; case 'BILLING.SUBSCRIPTION.EXPIRED': $this->handleSubscriptionExpired($resource); break; case 'BILLING.SUBSCRIPTION.SUSPENDED': $this->handleSubscriptionSuspended($resource); break; case 'PAYMENT.SALE.COMPLETED': $this->handlePaymentCompleted($resource); break; case 'PAYMENT.SALE.DENIED': case 'PAYMENT.SALE.REFUNDED': $this->handlePaymentFailed($resource); break; default: Log::info('Unhandled PayPal webhook event', ['event_type' => $eventType]); } return response()->json(['status' => 'ok']); } // ==================== WEBHOOK HANDLERS ==================== private function handleSubscriptionActivated(array $resource): void { $subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first(); if (!$subscription) return; $this->updateSubscriptionFromPayPal($subscription, $resource); Log::info('Subscription activated', ['subscription_id' => $subscription->id]); } private function handleSubscriptionCancelled(array $resource): void { $subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first(); if (!$subscription) return; $subscription->update([ 'status' => Subscription::STATUS_CANCELED, 'canceled_at' => now(), 'paypal_status' => $resource['status'] ?? 'CANCELLED', 'paypal_data' => $resource, ]); Log::info('Subscription cancelled via webhook', ['subscription_id' => $subscription->id]); } private function handleSubscriptionExpired(array $resource): void { $subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first(); if (!$subscription) return; $subscription->markAsExpired(); $subscription->update([ 'paypal_status' => $resource['status'] ?? 'EXPIRED', 'paypal_data' => $resource, ]); Log::info('Subscription expired via webhook', ['subscription_id' => $subscription->id]); } private function handleSubscriptionSuspended(array $resource): void { $subscription = Subscription::where('paypal_subscription_id', $resource['id'])->first(); if (!$subscription) return; $subscription->update([ 'status' => Subscription::STATUS_PAST_DUE, 'paypal_status' => $resource['status'] ?? 'SUSPENDED', 'paypal_data' => $resource, ]); Log::info('Subscription suspended via webhook', ['subscription_id' => $subscription->id]); } private function handlePaymentCompleted(array $resource): void { $billingAgreementId = $resource['billing_agreement_id'] ?? null; if (!$billingAgreementId) return; $subscription = Subscription::where('paypal_subscription_id', $billingAgreementId)->first(); if (!$subscription) return; // Create invoice for recurring payment $invoice = Invoice::createForSubscription( $subscription, Invoice::REASON_SUBSCRIPTION_CYCLE, "{$subscription->plan->name} - " . now()->format('F Y') ); $invoice->update([ 'paypal_payment_id' => $resource['id'] ?? null, 'paypal_data' => $resource, ]); $invoice->markAsPaid($resource['id'] ?? null); // Renew subscription period $subscription->renew(); Log::info('Payment completed, subscription renewed', [ 'subscription_id' => $subscription->id, 'invoice_id' => $invoice->id, ]); } private function handlePaymentFailed(array $resource): void { $billingAgreementId = $resource['billing_agreement_id'] ?? null; if (!$billingAgreementId) return; $subscription = Subscription::where('paypal_subscription_id', $billingAgreementId)->first(); if (!$subscription) return; $subscription->update([ 'status' => Subscription::STATUS_PAST_DUE, ]); Log::warning('Payment failed', ['subscription_id' => $subscription->id]); } // ==================== HELPERS ==================== private function updateSubscriptionFromPayPal(Subscription $subscription, array $paypalData): void { $status = $paypalData['status'] ?? ''; $subscription->paypal_status = $status; $subscription->paypal_data = $paypalData; switch ($status) { case 'ACTIVE': $subscription->status = Subscription::STATUS_ACTIVE; if (isset($paypalData['billing_info']['next_billing_time'])) { $subscription->current_period_end = Carbon::parse($paypalData['billing_info']['next_billing_time']); } if (isset($paypalData['billing_info']['last_payment']['time'])) { $subscription->current_period_start = Carbon::parse($paypalData['billing_info']['last_payment']['time']); } break; case 'APPROVAL_PENDING': // Keep as trialing until approved break; case 'APPROVED': // Subscription approved but not yet active if ($subscription->plan->has_trial) { $subscription->startTrial($subscription->plan->trial_days); } else { $subscription->activate(); } break; case 'SUSPENDED': $subscription->status = Subscription::STATUS_PAST_DUE; break; case 'CANCELLED': $subscription->status = Subscription::STATUS_CANCELED; $subscription->canceled_at = now(); break; case 'EXPIRED': $subscription->status = Subscription::STATUS_EXPIRED; break; } $subscription->save(); } }