HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/app/Http/Controllers/Customer/BillingController.php
<?php

namespace App\Http\Controllers\Customer;

use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\CampaignRecipient;
use App\Models\Coupon;
use App\Models\ListSubscriber;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\WebhookEvent;
use App\Models\Setting;
use App\Services\Billing\FlutterwavePaymentService;
use App\Services\Billing\ManualPaymentService;
use App\Services\Billing\PayPalPaymentService;
use App\Services\Billing\PaymentProviderInterface;
use App\Services\Billing\RazorpayPaymentService;
use App\Services\Billing\StripePaymentService;
use App\Services\Billing\UddoktaPayPaymentService;
use App\Services\Billing\UsageService;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Stripe\StripeClient;

class BillingController extends Controller
{
    public function __construct(
        private readonly UsageService $usageService,
    ) {
    }

    public function index(Request $request)
    {
        $customer = $request->user('customer');
        $subscription = $customer->subscriptions()
            ->whereIn('status', ['active', 'trialing'])
            ->latest()
            ->first();

        $pendingSubscription = $customer->subscriptions()
            ->where('status', 'pending')
            ->latest()
            ->first();

        if ($subscription && $customer->stripe_customer_id) {
            try {
                $stripe = app(StripeClient::class);

                $stripeSubscriptionId = null;

                if ($subscription->stripe_checkout_session_id) {
                    $session = $stripe->checkout->sessions->retrieve($subscription->stripe_checkout_session_id, [
                        'expand' => ['subscription'],
                    ]);

                    if (isset($session->subscription) && is_string($session->subscription)) {
                        $stripeSubscriptionId = $session->subscription;
                    } elseif (isset($session->subscription->id) && is_string($session->subscription->id)) {
                        $stripeSubscriptionId = $session->subscription->id;
                    }
                }

                if (!$stripeSubscriptionId && $subscription->stripe_subscription_id) {
                    $stripeSubscriptionId = $subscription->stripe_subscription_id;
                }

                if ($stripeSubscriptionId) {
                    $stripeSubscription = $stripe->subscriptions->retrieve($stripeSubscriptionId, []);

                    $status = match ((string) ($stripeSubscription->status ?? '')) {
                        'trialing' => 'trialing',
                        'active' => 'active',
                        'past_due' => 'past_due',
                        'canceled' => 'cancelled',
                        'unpaid' => 'suspended',
                        default => 'pending',
                    };

                    $stripePriceId = $stripeSubscription->items->data[0]->price->id ?? null;

                    $planDbId = $subscription->plan_db_id;
                    if (isset($stripeSubscription->metadata->plan_id) && is_numeric($stripeSubscription->metadata->plan_id)) {
                        $planDbId = (int) $stripeSubscription->metadata->plan_id;
                    }
                    if (!$planDbId && $stripePriceId) {
                        $planDbId = Plan::query()->where('stripe_price_id', $stripePriceId)->value('id');
                    }

                    $subscription->update([
                        'status' => $status,
                        'stripe_subscription_id' => $stripeSubscriptionId,
                        'stripe_price_id' => $stripePriceId ?? $subscription->stripe_price_id,
                        'plan_id' => $stripePriceId ?? $subscription->plan_id,
                        'plan_db_id' => $planDbId ?: $subscription->plan_db_id,
                        'period_start' => isset($stripeSubscription->current_period_start) ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start) : $subscription->period_start,
                        'period_end' => isset($stripeSubscription->current_period_end) ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end) : $subscription->period_end,
                        'trial_ends_at' => isset($stripeSubscription->trial_end) ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end) : $subscription->trial_ends_at,
                        'cancel_at_period_end' => (bool) ($stripeSubscription->cancel_at_period_end ?? $subscription->cancel_at_period_end),
                        'auto_renew' => !((bool) ($stripeSubscription->cancel_at_period_end ?? $subscription->cancel_at_period_end)),
                        'last_payment_status' => (string) ($stripeSubscription->status ?? $subscription->last_payment_status),
                    ]);
                }
            } catch (\Throwable $e) {
                // Ignore Stripe refresh errors and fall back to local status
            }
        }
        $plans = Plan::with('customerGroup')->where('is_active', true)->where('is_public', true)->orderBy('price')->get();
        $usage = $this->usageService->getUsage($customer);

        $monthStart = now()->startOfMonth();
        $monthEnd = now()->endOfMonth()->endOfDay();

        $usage['emails_sent_this_month'] = CampaignRecipient::query()
            ->whereNotNull('sent_at')
            ->whereBetween('sent_at', [$monthStart, $monthEnd])
            ->whereIn('status', ['sent', 'opened', 'clicked'])
            ->whereHas('campaign', function ($q) use ($customer) {
                $q->where('customer_id', $customer->id);
            })
            ->count();

        $usage['subscribers_count'] = ListSubscriber::query()
            ->whereHas('list', function ($q) use ($customer) {
                $q->where('customer_id', $customer->id);
            })
            ->where('status', 'confirmed')
            ->count();

        $usage['campaigns_count'] = Campaign::query()
            ->where('customer_id', $customer->id)
            ->count();

        $customerName = $customer->full_name ?? trim(($customer->first_name ?? '') . ' ' . ($customer->last_name ?? ''));

        $currentPlan = null;
        if ($subscription?->plan_db_id) {
            $currentPlan = Plan::with('customerGroup')->find($subscription->plan_db_id);
        }

        $usageLimits = [
            'emails_sent_this_month' => $customer->groupLimit('sending_quota.monthly_quota')
                ?? (int) ($currentPlan?->customerGroup?->limit('sending_quota.monthly_quota', 0) ?? 0),
            'subscribers_count' => $customer->groupLimit('lists.limits.max_subscribers')
                ?? (int) ($currentPlan?->customerGroup?->limit('lists.limits.max_subscribers', 0) ?? 0),
            'campaigns_count' => $customer->groupLimit('campaigns.limits.max_campaigns')
                ?? (int) ($currentPlan?->customerGroup?->limit('campaigns.limits.max_campaigns', 0) ?? 0),
        ];

        $portalUrl = null;
        try {
            $paymentProvider = app(PaymentProviderInterface::class);
            $portalUrl = $paymentProvider->createCustomerPortal($customer, [
                'return_url' => route('customer.billing.index'),
            ]);
        } catch (\Throwable $e) {
            $portalUrl = null;
        }

        $invoiceEvents = collect();
        if ($customer->stripe_customer_id) {
            $invoiceEvents = WebhookEvent::query()
                ->where('provider', 'stripe')
                ->where('type', 'like', 'invoice.%')
                ->where('payload->customer', $customer->stripe_customer_id)
                ->orderByDesc('processed_at')
                ->orderByDesc('created_at')
                ->limit(10)
                ->get();
        }

        $stripeInvoices = collect();
        if ($customer->stripe_customer_id && $invoiceEvents->count() === 0) {
            try {
                $stripe = app(StripeClient::class);
                $invoices = $stripe->invoices->all([
                    'customer' => $customer->stripe_customer_id,
                    'limit' => 10,
                ]);

                $stripeInvoices = collect($invoices->data ?? []);
            } catch (\Throwable $e) {
                $stripeInvoices = collect();
            }
        }

        $latestInvoiceUrl = null;
        if ($invoiceEvents->count() > 0) {
            $latestInvoicePayload = $invoiceEvents->first()?->payload;
            $latestInvoiceUrl = data_get($latestInvoicePayload, 'hosted_invoice_url')
                ?: data_get($latestInvoicePayload, 'invoice_pdf');
        } elseif ($stripeInvoices->count() > 0) {
            $latestInvoice = $stripeInvoices->first();
            $latestInvoiceUrl = data_get($latestInvoice, 'hosted_invoice_url')
                ?: data_get($latestInvoice, 'invoice_pdf');
        }

        $paymentMethodCard = null;
        if ($customer->stripe_customer_id) {
            try {
                $stripe = app(StripeClient::class);
                $stripeCustomer = $stripe->customers->retrieve($customer->stripe_customer_id, []);
                $defaultPaymentMethodId = data_get($stripeCustomer, 'invoice_settings.default_payment_method');

                $paymentMethod = null;
                if (is_string($defaultPaymentMethodId) && trim($defaultPaymentMethodId) !== '') {
                    $paymentMethod = $stripe->paymentMethods->retrieve($defaultPaymentMethodId, []);
                } else {
                    $paymentMethods = $stripe->paymentMethods->all([
                        'customer' => $customer->stripe_customer_id,
                        'type' => 'card',
                        'limit' => 1,
                    ]);
                    $paymentMethod = $paymentMethods->data[0] ?? null;
                }

                if ($paymentMethod && isset($paymentMethod->card)) {
                    $paymentMethodCard = [
                        'brand' => (string) ($paymentMethod->card->brand ?? ''),
                        'last4' => (string) ($paymentMethod->card->last4 ?? ''),
                        'exp_month' => (int) ($paymentMethod->card->exp_month ?? 0),
                        'exp_year' => (int) ($paymentMethod->card->exp_year ?? 0),
                    ];
                }
            } catch (\Throwable $e) {
                $paymentMethodCard = null;
            }
        }

        return view('customer.billing.index', compact('subscription', 'pendingSubscription', 'plans', 'usage', 'usageLimits', 'currentPlan', 'portalUrl', 'invoiceEvents', 'stripeInvoices', 'latestInvoiceUrl', 'paymentMethodCard', 'customerName'));
    }

    public function showCheckout(Request $request, Plan $plan)
    {
        $customer = $request->user('customer');
        $plan->load('customerGroup');

        $planCurrency = strtoupper((string) ($plan->currency ?? 'USD'));
        if (!preg_match('/^[A-Z]{3}$/', $planCurrency)) {
            $planCurrency = 'USD';
        }

        $planValue = is_numeric($plan->price ?? null) ? (float) $plan->price : 0.0;

        $this->flashMetaPixelEvent($request, 'InitiateCheckout', [
            'value' => $planValue,
            'currency' => $planCurrency,
            'content_name' => (string) $plan->name,
            'content_category' => 'subscription',
        ]);

        $enabled = $this->enabledProviderKeys();

        $legacyProvider = 'stripe';
        $defaultProvider = 'stripe';
        try {
            $legacyProvider = Setting::get('billing_provider', config('billing.provider', 'stripe'));
            $defaultProvider = Setting::get('billing_default_provider', $legacyProvider);
        } catch (\Throwable $e) {
            // Ignore DB/settings failures; use fallback.
        }

        if (!is_string($defaultProvider) || $defaultProvider === '' || !in_array($defaultProvider, $enabled, true)) {
            $defaultProvider = (string) ($enabled[0] ?? 'stripe');
        }

        $selectedProvider = $request->query('provider');
        if (is_string($selectedProvider)) {
            $selectedProvider = trim($selectedProvider);
        }

        if (!is_string($selectedProvider) || $selectedProvider === '' || !in_array($selectedProvider, $enabled, true)) {
            $selectedProvider = $defaultProvider;
        }

        $couponCode = $request->query('coupon_code');
        if (is_string($couponCode)) {
            $couponCode = trim($couponCode);
        }

        return view('customer.billing.checkout', [
            'customer' => $customer,
            'plan' => $plan,
            'enabledProviders' => $enabled,
            'defaultProvider' => $defaultProvider,
            'selectedProvider' => $selectedProvider,
            'couponCode' => is_string($couponCode) ? $couponCode : '',
        ]);
    }

    public function checkout(Request $request, Plan $plan)
    {
        $customer = $request->user('customer');
        $plan->load('customerGroup');

        $requestedProvider = $request->input('provider');
        if (is_string($requestedProvider)) {
            $requestedProvider = trim($requestedProvider);
        }

        $providerKey = is_string($requestedProvider) && $requestedProvider !== ''
            ? $requestedProvider
            : $this->resolveBillingProviderKey();

        $enabledProviderKeys = $this->enabledProviderKeys();
        if (!in_array($providerKey, $enabledProviderKeys, true)) {
            return back()->with('error', 'Selected payment method is not available.');
        }

        $redirectUrl = match ($providerKey) {
            'flutterwave' => route('billing.flutterwave.callback'),
            'uddoktapay' => route('billing.uddoktapay.callback'),
            'razorpay' => route('billing.razorpay.callback'),
            'paypal' => route('billing.paypal.callback'),
            default => null,
        };

        $validated = $request->validate([
            'coupon_code' => ['nullable', 'string', 'max:64'],
        ]);

        $promotionCodeId = null;
        $couponCode = trim((string) ($validated['coupon_code'] ?? ''));
        if ($couponCode !== '') {
            $coupon = Coupon::where('code', strtoupper($couponCode))->first();
            if (!$coupon || !$coupon->isUsable()) {
                return back()->with('error', 'Invalid or expired coupon code.');
            }

            if ($providerKey !== 'stripe') {
                return back()->with('error', 'Coupon codes are only supported for Stripe checkout.');
            }

            if (!$coupon->stripe_promotion_code_id) {
                return back()->with('error', 'Coupon is not available for checkout.');
            }

            $promotionCodeId = $coupon->stripe_promotion_code_id;
        }

        $limits = [
            'emails_sent_this_month' => $plan->customerGroup?->limit('sending_quota.monthly_quota', 0),
            'subscribers_count' => $plan->customerGroup?->limit('lists.limits.max_subscribers', 0),
            'campaigns_count' => $plan->customerGroup?->limit('campaigns.limits.max_campaigns', 0),
        ];

        $subscription = Subscription::create([
            'customer_id' => $customer->id,
            'plan_id' => $providerKey === 'stripe' ? ($plan->stripe_price_id ?? (string) $plan->id) : (string) $plan->id,
            'plan_db_id' => $plan->id,
            'plan_name' => $plan->name,
            'status' => 'pending',
            'billing_cycle' => $plan->billing_cycle,
            'price' => $plan->price,
            'currency' => $plan->currency,
            'starts_at' => now(),
            'trial_ends_at' => $plan->trial_days ? now()->addDays($plan->trial_days) : null,
            'limits' => array_filter($limits, fn ($v) => $v !== null),
            'features' => $plan->customerGroup?->permissions,
            'payment_gateway' => $providerKey === 'stripe' ? 'stripe_checkout' : $providerKey,
            'provider' => $providerKey,
            'stripe_price_id' => $providerKey === 'stripe' ? $plan->stripe_price_id : null,
        ]);

        $successUrl = route('customer.billing.success', ['provider' => $providerKey]);
        if ($providerKey === 'stripe') {
            $successUrl .= (str_contains($successUrl, '?') ? '&' : '?') . 'session_id={CHECKOUT_SESSION_ID}';
        }

        $cancelUrl = route('customer.billing.checkout.show', ['plan' => $plan, 'provider' => $providerKey]);

        try {
            $paymentProvider = $this->resolvePaymentProvider($providerKey);
            $url = $paymentProvider->createCheckoutSession($customer, $plan, [
                'success_url' => $successUrl,
                'cancel_url' => $cancelUrl,
                'redirect_url' => $redirectUrl,
                'promotion_code' => $promotionCodeId,
                'local_subscription_id' => $subscription->id,
            ]);
        } catch (\Throwable $e) {
            $subscription->delete();
            return back()->with('error', $e->getMessage());
        }

        if ($providerKey === 'manual') {
            return redirect()->to($url);
        }

        return redirect()->away($url);
    }

    public function success(Request $request)
    {
        $customer = $request->user('customer');

        $provider = $request->query('provider');
        $provider = is_string($provider) ? trim($provider) : '';

        $sessionId = $request->query('session_id');
        $sessionId = is_string($sessionId) ? trim($sessionId) : '';

        $orderId = $request->query('order_id');
        $orderId = is_string($orderId) ? trim($orderId) : '';

        $invoiceId = $request->query('invoice_id');
        $invoiceId = is_string($invoiceId) ? trim($invoiceId) : '';

        $subscription = null;

        if ($sessionId !== '') {
            $subscription = $customer->subscriptions()
                ->where('stripe_checkout_session_id', $sessionId)
                ->latest()
                ->first();
        }

        if (!$subscription && $orderId !== '') {
            $subscription = $customer->subscriptions()
                ->where('provider', 'paypal')
                ->where('stripe_checkout_session_id', $orderId)
                ->latest()
                ->first();
        }

        if (!$subscription && $invoiceId !== '') {
            $subscription = $customer->subscriptions()
                ->where('provider', 'uddoktapay')
                ->where('stripe_checkout_session_id', $invoiceId)
                ->latest()
                ->first();

            if (!$subscription) {
                try {
                    $subscription = app(UddoktaPayPaymentService::class)->applyVerifiedPaymentByInvoice($invoiceId);
                } catch (\Throwable $e) {
                    $subscription = null;
                }
            }
        }

        if (!$subscription) {
            $subscription = $customer->subscriptions()
                ->whereIn('status', ['pending', 'active', 'trialing'])
                ->latest()
                ->first();
        }

        if ($subscription) {
            $purchaseCurrency = strtoupper((string) ($subscription->currency ?? 'USD'));
            if (!preg_match('/^[A-Z]{3}$/', $purchaseCurrency)) {
                $purchaseCurrency = 'USD';
            }

            $purchaseValue = is_numeric($subscription->price ?? null) ? (float) $subscription->price : 0.0;

            $this->flashMetaPixelEvent($request, 'Purchase', [
                'value' => $purchaseValue,
                'currency' => $purchaseCurrency,
                'content_name' => (string) ($subscription->plan_name ?: ($subscription->plan?->name ?? 'Subscription')),
                'content_category' => 'subscription',
                'content_type' => 'product',
            ]);
        }

        return view('customer.billing.success', [
            'customer' => $customer,
            'provider' => $provider,
            'subscription' => $subscription,
        ]);
    }

    private function resolveBillingProviderKey(): string
    {
        $provider = config('billing.provider', 'stripe');

        try {
            $legacyProvider = Setting::get('billing_provider', $provider);
            $defaultProvider = Setting::get('billing_default_provider', $legacyProvider);
            $billingProviders = Setting::get('billing_providers');

            if (!is_array($billingProviders)) {
                $provider = $defaultProvider;
            } else {
                $provider = $defaultProvider;
                if (!(bool) data_get($billingProviders, $provider . '.enabled', false)) {
                    $provider = collect(['stripe', 'uddoktapay', 'paypal', 'razorpay', 'paystack', 'flutterwave', 'manual'])
                        ->first(fn ($key) => (bool) data_get($billingProviders, $key . '.enabled', false))
                        ?? $legacyProvider;
                }
            }
        } catch (\Throwable $e) {
            // Ignore DB/settings failures; use config fallback.
        }

        return is_string($provider) && $provider !== '' ? $provider : 'stripe';
    }

    private function enabledProviderKeys(): array
    {
        $billingProviders = [];

        try {
            $billingProviders = Setting::get('billing_providers');
        } catch (\Throwable $e) {
            $billingProviders = [];
        }

        if (!is_array($billingProviders)) {
            $billingProviders = [];
        }

        $supported = ['stripe', 'uddoktapay', 'paypal', 'razorpay', 'flutterwave', 'manual'];

        $enabled = collect($supported)
            ->filter(fn ($key) => (bool) data_get($billingProviders, $key . '.enabled', false))
            ->values()
            ->all();

        return empty($enabled) ? ['stripe'] : $enabled;
    }

    private function flashMetaPixelEvent(Request $request, string $event, array $payload = []): void
    {
        $events = $request->session()->get('meta_pixel_events', []);
        if (!is_array($events)) {
            $events = [];
        }

        $events[] = [
            'event' => trim($event),
            'payload' => $payload,
        ];

        $request->session()->flash('meta_pixel_events', $events);
    }

    private function resolvePaymentProvider(string $providerKey): PaymentProviderInterface
    {
        return match ($providerKey) {
            'uddoktapay' => app(UddoktaPayPaymentService::class),
            'paypal' => app(PayPalPaymentService::class),
            'razorpay' => app(RazorpayPaymentService::class),
            'flutterwave' => app(FlutterwavePaymentService::class),
            'manual' => app(ManualPaymentService::class),
            default => new StripePaymentService(app(StripeClient::class)),
        };
    }

    public function destroyInvoiceEvent(Request $request, WebhookEvent $event): RedirectResponse
    {
        $customer = $request->user('customer');

        if (!$customer->stripe_customer_id) {
            return back()->with('error', 'Billing history is not available.');
        }

        $belongsToCustomer = $event->provider === 'stripe'
            && is_string($event->type)
            && str_starts_with($event->type, 'invoice.')
            && data_get($event->payload, 'customer') === $customer->stripe_customer_id;

        abort_unless($belongsToCustomer, 403);

        $event->delete();

        return back()->with('success', 'Invoice entry deleted.');
    }
}