--- File: D:\projects\digitalvocano\Modules\PaypalGateway\config\config.php --- 'PaypalGateway', ]; --- File: D:\projects\digitalvocano\Modules\PaypalGateway\database\seeders\PaypalGatewayDatabaseSeeder.php --- call([]); } } --- File: D:\projects\digitalvocano\Modules\PaypalGateway\Http\Controllers\Admin\PaypalConfigController.php --- route('admin.dashboard')->with('error', 'Settings helper function not found.'); } $settings = []; foreach ($this->settingKeys as $key) { $settings[$key] = setting($key); } // Default mode to sandbox if not set $settings['paypal_mode'] = $settings['paypal_mode'] ?? 'sandbox'; // Default enabled to '0' if not set if (is_null($settings['paypal_enabled'])) { $settings['paypal_enabled'] = '0'; } return view('paypalgateway::admin.config', compact('settings')); } public function update(Request $request) { if (!function_exists('setting')) { return redirect()->route('admin.dashboard')->with('error', 'Settings helper function not found.'); } $validated = $request->validate([ 'paypal_enabled' => 'nullable|boolean', 'paypal_client_id' => 'nullable|string|max:255', 'paypal_client_secret' => 'nullable|string|max:255', 'paypal_webhook_id' => 'nullable|string|max:255', 'paypal_mode' => 'required|in:sandbox,live', ]); try { // Define settings with their names, groups, and types $settingsData = [ 'paypal_enabled' => [ 'value' => $request->input('paypal_enabled', '0'), 'name' => 'Enable PayPal Gateway', 'group' => 'Payment Gateways', 'type' => 'boolean' ], 'paypal_mode' => [ 'value' => $request->input('paypal_mode', 'sandbox'), 'name' => 'PayPal Mode', 'group' => 'Payment Gateways', 'type' => 'select' ], 'paypal_client_id' => [ 'value' => $request->input('paypal_client_id'), 'name' => 'PayPal Client ID', 'group' => 'Payment Gateways', 'type' => 'text' ], 'paypal_webhook_id' => [ 'value' => $request->input('paypal_webhook_id'), 'name' => 'PayPal Webhook ID', 'group' => 'Payment Gateways', 'type' => 'text' ], ]; foreach ($settingsData as $key => $data) { Setting::setValue($key, $data['value'], $data['name'], $data['group'], $data['type']); } // Handle client secret separately: only update if a new value is provided if ($request->filled('paypal_client_secret')) { Setting::setValue('paypal_client_secret', $request->input('paypal_client_secret'), 'PayPal Client Secret', 'Payment Gateways', 'password'); } Artisan::call('cache:clear'); Artisan::call('config:clear'); return redirect()->back()->with('success', 'PayPal settings updated successfully.'); } catch (\Exception $e) { Log::error('Error updating PayPal settings: ' . $e->getMessage()); return redirect()->back()->with('error', 'Failed to update PayPal settings. Please check the logs.'); } } } --- File: D:\projects\digitalvocano\Modules\PaypalGateway\Http\Controllers\PaypalGatewayController.php --- paypalService = $paypalService; $this->creditService = $creditService; // Added $this->walletService = $walletService; // Added } public function createSubscription(Request $request, SubscriptionPlan $plan) { if (setting('paypal_enabled', '0') != '1') { return redirect()->route('subscription.plans')->with('error', 'PayPal payments are currently disabled.'); } /** @var User $user */ $user = Auth::user(); $activeSubscription = $user->subscriptions() ->whereIn('status', ['active', 'trialing', 'ACTIVE']) // PayPal uses ACTIVE, PENDING, etc. ->first(); if ($activeSubscription) { if ($activeSubscription->subscription_plan_id == $plan->id && $activeSubscription->payment_gateway == 'paypal') { return redirect()->route('subscription.plans')->with('info', 'You are already subscribed to this plan via PayPal.'); } return redirect()->route('subscription.plans')->with('error', 'You already have an active subscription. Please manage it from your profile.'); } try { $approvalLink = $this->paypalService->createPaypalSubscription($user, $plan); if ($approvalLink) { $pendingPaypalIdFromSession = session('paypal_subscription_id_pending'); Log::info("PayPal CreateSubscription: Retrieved 'paypal_subscription_id_pending' from session.", ['session_paypal_id' => $pendingPaypalIdFromSession, 'user_id' => $user->id, 'plan_id' => $plan->id]); if (empty($pendingPaypalIdFromSession)) { Log::error("PayPal CreateSubscription: 'paypal_subscription_id_pending' is empty in session. Cannot create local pending subscription.", ['user_id' => $user->id, 'plan_id' => $plan->id]); return redirect()->route('subscription.plans')->with('error', 'Failed to initiate PayPal subscription due to a session issue. Please try again.'); } // Create a local pending subscription record // The actual gateway_subscription_id will be confirmed upon execution $localPendingSubscription = null; try { $localPendingSubscription = Subscription::create([ 'user_id' => $user->id, 'subscription_plan_id' => $plan->id, 'payment_gateway' => 'paypalgateway', 'gateway_subscription_id' => $pendingPaypalIdFromSession, 'status' => 'pending_approval', 'price_at_purchase' => $plan->price, 'currency_at_purchase' => $plan->currency, ]); } catch (\Illuminate\Database\QueryException $qe) { Log::critical("PayPal CreateSubscription: DATABASE QUERY EXCEPTION during Subscription::create()", [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'attempted_paypal_id' => $pendingPaypalIdFromSession, 'error' => $qe->getMessage(), 'sql' => $qe->getSql(), 'bindings' => $qe->getBindings() ]); return redirect()->route('subscription.plans')->with('error', 'Critical error: Failed to record your pending PayPal subscription due to a database issue. Please contact support. (Ref: PENDING_DB_EX)'); } catch (\Exception $e) { // Catch any other general exception during create Log::critical("PayPal CreateSubscription: GENERAL EXCEPTION during Subscription::create()", ['user_id' => $user->id, 'plan_id' => $plan->id, 'attempted_paypal_id' => $pendingPaypalIdFromSession, 'error' => $e->getMessage()]); return redirect()->route('subscription.plans')->with('error', 'Critical error: Failed to record your pending PayPal subscription. Please contact support. (Ref: PENDING_GEN_EX)'); } if ($localPendingSubscription && $localPendingSubscription->id) { Log::info("PayPal CreateSubscription: Successfully created local pending subscription.", ['local_subscription_id' => $localPendingSubscription->id, 'gateway_subscription_id_stored' => $localPendingSubscription->gateway_subscription_id]); } else { Log::error("PayPal CreateSubscription: FAILED to create local pending subscription record in database.", ['user_id' => $user->id, 'plan_id' => $plan->id, 'attempted_paypal_id' => $pendingPaypalIdFromSession]); return redirect()->route('subscription.plans')->with('error', 'Failed to record your pending PayPal subscription. Please try again.'); } session()->forget('paypal_subscription_id_pending'); // Clean up session session()->forget('local_plan_id_pending'); return redirect($approvalLink); } return redirect()->route('subscription.plans')->with('error', 'Could not initiate PayPal subscription.'); } catch (\Exception $e) { Log::error("PayPal Create Subscription Error for user {$user->id}, plan {$plan->id}: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]); return redirect()->route('subscription.plans')->with('error', 'Could not initiate PayPal subscription: ' . $e->getMessage()); } } public function executeSubscription(Request $request) { $paypalSubscriptionId = $request->query('subscription_id'); // PayPal returns subscription_id on approval // The 'ba_token' and 'token' might also be present for older flows or different contexts. // For modern Subscriptions API, subscription_id is key. if (!$paypalSubscriptionId) { return redirect()->route('subscription.plans')->with('error', 'Invalid PayPal subscription approval.'); } try { $paypalSubscriptionDetails = $this->paypalService->getPaypalSubscriptionDetails($paypalSubscriptionId); if ($paypalSubscriptionDetails && in_array(strtoupper($paypalSubscriptionDetails->status), ['ACTIVE', 'APPROVAL_PENDING'])) { /** @var User $user */ // User might not be available directly from Auth if it's a webhook context later, // but for direct redirect, Auth::user() is fine. // For webhooks, you'd typically get user_id from metadata or the local subscription. $user = Auth::user(); // Find the pending local subscription $localSubscription = Subscription::where('gateway_subscription_id', $paypalSubscriptionId) ->where('status', 'pending_approval') ->where('user_id', $user->id) // Ensure it's for the current user ->first(); if ($localSubscription) { // Deactivate other existing subscriptions for this user $user->subscriptions() ->where('id', '!=', $localSubscription->id) ->whereIn('status', ['active', 'trialing', 'ACTIVE']) ->update(['status' => 'cancelled', 'ends_at' => now(), 'cancelled_at' => now()]); $localSubscription->status = strtolower($paypalSubscriptionDetails->status); // 'active' $localSubscription->starts_at = isset($paypalSubscriptionDetails->start_time) ? \Carbon\Carbon::parse($paypalSubscriptionDetails->start_time) : now(); $plan = $localSubscription->plan; if ($plan) { if ($plan->trial_period_days > 0 && strtoupper($paypalSubscriptionDetails->status) === 'ACTIVE') { // Or check if trial info is in $paypalSubscriptionDetails $localSubscription->trial_ends_at = $localSubscription->starts_at->copy()->addDays($plan->trial_period_days); // Determine ends_at based on trial and plan interval $billingCycleAnchor = $localSubscription->trial_ends_at; } else { $billingCycleAnchor = $localSubscription->starts_at; } $localSubscription->ends_at = $billingCycleAnchor->copy()->add($plan->interval, $plan->interval_count); // Award credits if (function_exists('setting') && setting('credits_system_enabled', '0') == '1' && $plan->credits_awarded_on_purchase > 0) { $this->creditService->awardCredits($user, $plan->credits_awarded_on_purchase, 'award_subscription_purchase', "Credits for {$plan->name} subscription", $localSubscription); } // Assign target role if (!empty($plan->target_role) && class_exists(\Spatie\Permission\Models\Role::class) && \Spatie\Permission\Models\Role::where('name', $plan->target_role)->where('guard_name', 'web')->exists()) { $user->syncRoles([$plan->target_role]); } } $localSubscription->save(); return redirect()->route('dashboard')->with('success', 'PayPal subscription successfully activated!'); } else { Log::error("PayPal Execute: Local pending subscription not found for PayPal ID: {$paypalSubscriptionId}"); return redirect()->route('subscription.plans')->with('error', 'Could not find your pending PayPal subscription record.'); } } else { Log::error("PayPal Execute: Subscription not active or details not found.", (array)$paypalSubscriptionDetails); return redirect()->route('subscription.plans')->with('error', 'PayPal subscription could not be activated.'); } } catch (\Exception $e) { Log::error("PayPal Execute Subscription Error: " . $e->getMessage(), ['paypal_subscription_id' => $paypalSubscriptionId]); return redirect()->route('subscription.plans')->with('error', 'There was an issue activating your PayPal subscription: ' . $e->getMessage()); } } public function cancelSubscriptionPage(Request $request) { // User cancelled from PayPal's approval page return redirect()->route('subscription.plans')->with('info', 'Your PayPal subscription process was cancelled.'); } public function cancelActiveSubscription(Request $request) { /** @var User $user */ $user = Auth::user(); $activeSubscription = $user->subscriptions() ->where('payment_gateway', 'paypalgateway') // Consistent key ->where('status', 'active') // Or other active-like statuses ->orderBy('created_at', 'desc') ->first(); if (!$activeSubscription) { return redirect()->back()->with('info', 'No active PayPal subscription found to cancel.'); } if ($this->paypalService->cancelSubscriptionAtGateway($activeSubscription)) { $activeSubscription->status = 'cancelled'; $activeSubscription->cancelled_at = now(); $activeSubscription->save(); return redirect()->back()->with('success', 'Your PayPal subscription has been cancelled.'); } return redirect()->back()->with('error', 'Failed to cancel PayPal subscription. Please try again or contact support.'); } // --- Wallet Deposit Methods --- public function initializeWalletDeposit(Request $request) { if (setting('paypal_enabled', '0') != '1' || setting('allow_wallet_deposits', '0') != '1') { return redirect()->route('user.wallet.deposit.form')->with('error', 'PayPal deposits are currently disabled.'); } $request->validate(['amount' => 'required|numeric|min:1']); $amount = (float) $request->input('amount'); /** @var User $user */ $user = Auth::user(); $currency = strtoupper(setting('currency_code', 'USD')); try { $response = $this->paypalService->createWalletDepositOrder($user, $amount, $currency); if ($response && $response->statusCode == 201) { // 201 Created $order = $response->result; // Store PayPal Order ID in session to verify on callback session(['paypal_wallet_deposit_order_id' => $order->id]); foreach ($order->links as $link) { if ($link->rel == 'approve') { return redirect()->away($link->href); } } } Log::error('PayPal Initialize Wallet Deposit Failed', ['response_status' => $response->statusCode ?? 'N/A', 'response_body' => $response->result ?? 'N/A']); return redirect()->route('user.wallet.deposit.form')->with('error', 'Could not initiate PayPal deposit. Please try again.'); } catch (\Exception $e) { Log::error("PayPal Initialize Wallet Deposit Error for user {$user->id}, amount {$amount}: " . $e->getMessage()); return redirect()->route('user.wallet.deposit.form')->with('error', 'An error occurred while initiating deposit with PayPal: ' . $e->getMessage()); } } public function handleWalletDepositCallback(Request $request) { $paypalOrderId = session('paypal_wallet_deposit_order_id'); $token = $request->query('token'); // This is the PayPal Order ID $payerId = $request->query('PayerID'); // PayerID is also sent /** @var User $user */ $user = Auth::user(); if (!$paypalOrderId || $paypalOrderId !== $token) { Log::error("PayPal Wallet Deposit Callback: Session Order ID mismatch or missing.", ['session_order_id' => $paypalOrderId, 'token' => $token]); return redirect()->route('user.wallet.deposit.form')->with('error', 'Invalid PayPal session for deposit.'); } try { $response = $this->paypalService->captureWalletDepositOrder($paypalOrderId); if ($response && $response->statusCode == 201 && isset($response->result->status) && $response->result->status == 'COMPLETED') { $capture = $response->result->purchase_units[0]->payments->captures[0]; $amountDeposited = (float) $capture->amount->value; $currency = $capture->amount->currency_code; // Idempotency check $existingTransaction = \App\Models\WalletTransaction::where('gateway_transaction_id', $capture->id)->first(); if ($existingTransaction) { Log::info("PayPal Wallet Deposit: Capture ID {$capture->id} already processed."); return redirect()->route('user.wallet.history')->with('success', 'Your deposit was successful.'); } $this->walletService->deposit($user, $amountDeposited, $currency, 'paypalgateway', $capture->id, "Wallet deposit via PayPal"); session()->forget('paypal_wallet_deposit_order_id'); return redirect()->route('user.wallet.history')->with('success', 'Successfully deposited ' . $currency . ' ' . number_format($amountDeposited, 2) . ' to your wallet.'); } Log::error('PayPal Wallet Deposit Capture Failed', ['response_status' => $response->statusCode ?? 'N/A', 'response_body' => $response->result ?? 'N/A']); return redirect()->route('user.wallet.deposit.form')->with('error', 'PayPal payment capture failed for deposit.'); } catch (\Exception $e) { Log::error("PayPal Wallet Deposit Callback Error: " . $e->getMessage(), ['paypal_order_id' => $paypalOrderId]); return redirect()->route('user.wallet.deposit.form')->with('error', 'An error occurred during PayPal deposit verification: ' . $e->getMessage()); } } public function handleWalletDepositCancel(Request $request) { session()->forget('paypal_wallet_deposit_order_id'); return redirect()->route('user.wallet.deposit.form')->with('info', 'Your PayPal wallet deposit was cancelled.'); } } --- File: D:\projects\digitalvocano\Modules\PaypalGateway\Http\Controllers\PaypalWebhookController.php --- paypalService = $paypalService; } public function handleWebhook(Request $request) { $rawBody = $request->getContent(); $headers = $request->headers->all(); // Get all headers as an array // **IMPORTANT: Implement robust webhook signature verification here!** // The $this->paypalService->verifyWebhookSignature() is a placeholder. // You MUST verify the signature according to PayPal's documentation. // if (!$this->paypalService->verifyWebhookSignature($headers, $rawBody)) { // Log::warning('PayPal Webhook: Invalid signature or verification failed.'); // return response()->json(['status' => 'error', 'message' => 'Signature verification failed'], 400); // } $event = json_decode($rawBody); if (!$event || !isset($event->event_type)) { Log::error('PayPal Webhook: Invalid event data received.'); return response()->json(['status' => 'error', 'message' => 'Invalid event data'], 400); } Log::info('PayPal Webhook Received:', (array) $event); $eventType = strtoupper($event->event_type); // PayPal event types are usually uppercase $resource = $event->resource ?? null; switch ($eventType) { case 'BILLING.SUBSCRIPTION.ACTIVATED': case 'BILLING.SUBSCRIPTION.UPDATED': if ($resource && isset($resource->id)) { $this->handleSubscriptionUpdate($resource); } break; case 'BILLING.SUBSCRIPTION.CANCELLED': case 'BILLING.SUBSCRIPTION.EXPIRED': case 'BILLING.SUBSCRIPTION.SUSPENDED': if ($resource && isset($resource->id)) { $this->handleSubscriptionStateChange($resource, strtolower($resource->status ?? 'cancelled')); } break; case 'PAYMENT.SALE.COMPLETED': // For recurring payments if ($resource && isset($resource->billing_agreement_id)) { // This event confirms a recurring payment. // The BILLING.SUBSCRIPTION.UPDATED event usually handles date changes. // You might want to log this or ensure the subscription is still active. $this->ensureSubscriptionActive($resource->billing_agreement_id); } break; // Add more event types as needed based on PayPal's documentation // e.g., BILLING.SUBSCRIPTION.PAYMENT.FAILED default: Log::info('PayPal Webhook: Received unhandled event type ' . $eventType); } return response()->json(['status' => 'success']); } protected function handleSubscriptionUpdate($paypalSubscriptionResource) { $localSubscription = Subscription::where('gateway_subscription_id', $paypalSubscriptionResource->id)->first(); if ($localSubscription) { $localSubscription->status = strtolower($paypalSubscriptionResource->status); // e.g., 'active' // Update start_time, agreement_details.next_billing_date etc. // $localSubscription->starts_at = \Carbon\Carbon::parse($paypalSubscriptionResource->start_time); // $localSubscription->ends_at = isset($paypalSubscriptionResource->agreement_details->next_billing_date) ? \Carbon\Carbon::parse($paypalSubscriptionResource->agreement_details->next_billing_date) : null; $localSubscription->save(); Log::info("PayPal Webhook: Updated local subscription ID {$localSubscription->id} to status {$localSubscription->status}"); } else { // Potentially create a new subscription if it's an activation and doesn't exist Log::warning("PayPal Webhook: Received update for unknown PayPal subscription ID: {$paypalSubscriptionResource->id}"); } } protected function handleSubscriptionStateChange($paypalSubscriptionResource, string $newStatus) { $localSubscription = Subscription::where('gateway_subscription_id', $paypalSubscriptionResource->id)->first(); if ($localSubscription) { $localSubscription->status = $newStatus; if (in_array($newStatus, ['cancelled', 'expired', 'suspended'])) { $localSubscription->cancelled_at = $localSubscription->cancelled_at ?? now(); } $localSubscription->save(); Log::info("PayPal Webhook: Changed local subscription ID {$localSubscription->id} to status {$newStatus}"); } } protected function ensureSubscriptionActive(string $paypalSubscriptionId) { $localSubscription = Subscription::where('gateway_subscription_id', $paypalSubscriptionId)->first(); if ($localSubscription && $localSubscription->status !== 'active') { // Potentially reactivate or log if a payment comes through for an inactive sub Log::info("PayPal Webhook: Payment received for subscription {$paypalSubscriptionId}, current local status: {$localSubscription->status}"); } } } --- File: D:\projects\digitalvocano\Modules\PaypalGateway\Providers\EventServiceProvider.php --- > */ protected $listen = []; /** * Indicates if events should be discovered. * * @var bool */ protected static $shouldDiscoverEvents = true; /** * Configure the proper event listeners for email verification. */ protected function configureEmailVerification(): void {} } --- File: D:\projects\digitalvocano\Modules\PaypalGateway\Providers\PaypalGatewayServiceProvider.php --- registerTranslations(); $this->registerConfig(); $this->registerViews(); $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations')); // Route loading is handled by the main app's RouteServiceProvider loop for modules. // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/web.php')); // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/admin.php')); // $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/api.php')); } /** * Register the service provider. * * @return void */ public function register(): void { $this->app->register(RouteServiceProvider::class); // It's good practice to register this $this->app->singleton(PaypalService::class, function ($app) { return new PaypalService(); }); } protected function registerConfig(): void { $this->publishes([ module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower . '.php'), ], 'config'); $this->mergeConfigFrom( module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower ); } public function registerViews(): void { $viewPath = resource_path('views/modules/'.$this->moduleNameLower); $sourcePath = module_path($this->moduleName, 'resources/views'); $this->publishes([$sourcePath => $viewPath], ['views', $this->moduleNameLower.'-module-views']); $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower); Blade::componentNamespace(config('modules.namespace').'\\' . $this->moduleName . '\\View\\Components', $this->moduleNameLower); } public function registerTranslations(): void { $langPath = resource_path('lang/modules/' . $this->moduleNameLower); if (is_dir($langPath)) { $this->loadTranslationsFrom($langPath, $this->moduleNameLower); $this->loadJsonTranslationsFrom($langPath); } else { $this->loadTranslationsFrom(module_path($this->moduleName, 'lang'), $this->moduleNameLower); $this->loadJsonTranslationsFrom(module_path($this->moduleName, 'lang')); } } public function provides(): array { return []; } private function getPublishableViewPaths(): array { $paths = []; foreach (config('view.paths') as $path) { if (is_dir($path.'/modules/'.$this->moduleNameLower)) { $paths[] = $path.'/modules/'.$this->moduleNameLower; } } return $paths; } } --- File: D:\projects\digitalvocano\Modules\PaypalGateway\Providers\RouteServiceProvider.php --- mapWebRoutes(); // $this->mapAdminRoutes(); // Admin routes are loaded by the main app's RouteServiceProvider $this->mapApiRoutes(); } /** * Define the "web" routes for the application. * * These routes all receive session state, CSRF protection, etc. */ protected function mapWebRoutes(): void { // The main app's RouteServiceProvider already applies the 'web' middleware. // This ensures controllers in web.php are correctly namespaced. Route::middleware('web') ->namespace($this->moduleNamespace) ->group(module_path($this->name, '/routes/web.php')); } // protected function mapAdminRoutes(): void // This method is not needed as main RSP handles admin routes // { // The main app's RouteServiceProvider handles loading admin routes, // applying 'web' and 'IsAdminMiddleware', prefixing with 'admin/paypalgateway', // naming with 'admin.paypalgateway.', and namespacing to 'Modules\PaypalGateway\Http\Controllers\Admin'. // } /** * Define the "api" routes for the application. */ protected function mapApiRoutes(): void { // The main app's RouteServiceProvider applies 'api' middleware, // 'api/paypalgateway' prefix, 'api.paypalgateway.' name prefix, // and the base 'Modules\PaypalGateway\Http\Controllers' namespace. Route::namespace($this->moduleNamespace) // Assuming API controllers are directly under Http/Controllers ->namespace($this->moduleNamespace) // Assuming API controllers are directly under Http/Controllers ->group(module_path($this->name, '/routes/api.php')); } } --- File: D:\projects\digitalvocano\Modules\PaypalGateway\resources\views\admin\form\toggle-switch.blade.php --- --- File: D:\projects\digitalvocano\Modules\PaypalGateway\resources\views\admin\partials\alerts.blade.php --- --- File: D:\projects\digitalvocano\Modules\PaypalGateway\resources\views\admin\config.blade.php --- @extends('layouts.admin') {{-- Use your admin layout --}} @section('title', 'PayPal Gateway Settings') @section('header_title', 'PayPal Gateway Configuration') @section('content')

PayPal Settings

@include('admin.partials.alerts') {{-- For success/error messages --}}
@csrf @method('PUT')
@php $id = 'paypal_enabled'; $name = 'paypal_enabled'; $label = 'Enable PayPal Gateway'; $value = '1'; $checked = old('paypal_enabled', $settings['paypal_enabled'] ?? '0') == '1'; $helpText = 'Toggle this to activate or deactivate the PayPal payment gateway.'; @endphp @if($helpText)

{{ $helpText }}

@endif @error($name)

{{ $message }}

@enderror
@error('paypal_mode')

{{ $message }}

@enderror
@error('paypal_client_id')

{{ $message }}

@enderror

Enter a new secret to update it. Leave blank to keep the existing one.

@error('paypal_client_secret')

{{ $message }}

@enderror

Get this from your PayPal Developer Dashboard after creating a webhook.

@error('paypal_webhook_id')

{{ $message }}

@enderror
@endsection --- File: D:\projects\digitalvocano\Modules\PaypalGateway\resources\views\components\layouts\master.blade.php --- PaypalGateway Module - {{ config('app.name', 'Laravel') }} {{-- Vite CSS --}} {{-- {{ module_vite('build-paypalgateway', 'resources/assets/sass/app.scss') }} --}} {{ $slot }} {{-- Vite JS --}} {{-- {{ module_vite('build-paypalgateway', 'resources/assets/js/app.js') }} --}} --- File: D:\projects\digitalvocano\Modules\PaypalGateway\resources\views\index.blade.php ---

Hello World

Module: {!! config('paypalgateway.name') !!}

--- File: D:\projects\digitalvocano\Modules\PaypalGateway\routes\admin.php --- name('settings.edit'); Route::put('settings', [PaypalConfigController::class, 'update'])->name('settings.update'); --- File: D:\projects\digitalvocano\Modules\PaypalGateway\routes\api.php --- prefix('v1')->group(function () { Route::apiResource('paypalgateways', PaypalGatewayController::class)->names('paypalgateway'); }); --- File: D:\projects\digitalvocano\Modules\PaypalGateway\routes\web.php --- name('webhooks.paypal.handle'); Route::middleware(['web', 'auth']) // User must be authenticated ->prefix('subscribe/paypal') ->name('subscription.paypal.') ->group(function () { // The {subscriptionPlan:slug} will use route model binding Route::post('create-agreement/{subscriptionPlan:slug}', [PaypalSubscriptionController::class, 'createSubscription'])->name('create.subscription'); Route::get('execute-agreement', [PaypalSubscriptionController::class, 'executeSubscription'])->name('execute.subscription'); // Success Route::get('cancel-agreement', [PaypalSubscriptionController::class, 'cancelSubscriptionPage'])->name('cancel.subscription.page'); // User initiated cancel from PayPal Route::post('cancel-active', [PaypalSubscriptionController::class, 'cancelActiveSubscription'])->name('cancel.active'); // User cancels from app }); Route::middleware(['web', 'auth']) ->prefix('wallet/paypal') ->name('wallet.paypal.') ->group(function () { // The WalletController redirects to this route with amount in POST/GET Route::match(['get', 'post'], 'initialize-deposit', [PaypalSubscriptionController::class, 'initializeWalletDeposit'])->name('initializeDeposit'); Route::get('deposit-callback', [PaypalSubscriptionController::class, 'handleWalletDepositCallback'])->name('depositCallback'); Route::get('deposit-cancel', [PaypalSubscriptionController::class, 'handleWalletDepositCancel'])->name('depositCancel'); }); --- File: D:\projects\digitalvocano\Modules\PaypalGateway\Services\PaypalService.php --- clientId = setting('paypal_client_id'); $this->clientSecret = setting('paypal_client_secret'); $this->mode = setting('paypal_mode', 'sandbox'); $this->webhookId = setting('paypal_webhook_id'); if ($this->clientId && $this->clientSecret) { Log::info("PayPalService initialized with Mode: {$this->mode}"); $environment = ($this->mode === 'live') ? new ProductionEnvironment($this->clientId, $this->clientSecret) : new SandboxEnvironment($this->clientId, $this->clientSecret); $this->client = new PayPalHttpClient($environment); } else { Log::error('PayPalService not fully configured. Client ID or Secret might be missing.'); } } protected function ensureClientIsAvailable() { if (!$this->client) { Log::error('PayPal client is not configured. Client ID or Secret might be missing.'); throw new \Exception('PayPal client is not configured. Please set credentials in admin settings.'); } } public function isConfigured(): bool { return !empty($this->clientId) && !empty($this->clientSecret) && $this->client !== null; } /** * Create a PayPal subscription. * * @param User $user * @param SubscriptionPlan $localPlan * @return string|null The approval link or null on failure * @throws \Exception */ public function createPaypalSubscription(User $user, SubscriptionPlan $localPlan): ?string { $this->ensureClientIsAvailable(); if (empty($localPlan->paypal_plan_id)) { Log::error("PayPal Plan ID is missing for local plan ID: {$localPlan->id} ({$localPlan->name})"); throw new \Exception("PayPal Plan ID is not configured for the '{$localPlan->name}' subscription plan."); } // Manually construct the request for creating a subscription // Endpoint: /v1/billing/subscriptions $request = new HttpRequest("/v1/billing/subscriptions", "POST"); $request->headers["Content-Type"] = "application/json"; $request->headers["Prefer"] = "return=representation"; // To get the full resource representation back // Set the request body $request->body = [ 'plan_id' => $localPlan->paypal_plan_id, 'subscriber' => [ 'name' => [ // More robust name splitting if first_name/last_name are not separate attributes 'given_name' => $user->first_name ?? (explode(' ', $user->name, 2)[0] ?? $user->name), 'surname' => $user->last_name ?? (explode(' ', $user->name, 2)[1] ?? ''), // Use empty string if no surname part ], 'email_address' => $user->email, ], 'application_context' => [ 'brand_name' => config('app.name'), 'locale' => 'en-US', 'shipping_preference' => 'NO_SHIPPING', 'user_action' => 'SUBSCRIBE_NOW', 'return_url' => route('subscription.paypal.execute.subscription'), 'cancel_url' => route('subscription.paypal.cancel.subscription.page'), ], ]; try { $response = $this->client->execute($request); if ($response->statusCode == 201 && isset($response->result->id)) { // 201 Created foreach ($response->result->links as $link) { if ($link->rel === 'approve') { session(['paypal_subscription_id_pending' => $response->result->id]); session(['local_plan_id_pending' => $localPlan->id]); // Optional: if needed on return return $link->href; } } } Log::error("PayPal Subscription Create: No approval link found or unexpected status.", (array)$response); } catch (HttpException $e) { $debugId = $e->getHeaders()['PayPal-Debug-Id'] ?? 'N/A'; $errorDetails = json_decode($e->getMessage(), true); // Attempt to get structured error Log::error("PayPal Subscription Create Error - Debug ID: {$debugId}", [ 'message' => $errorDetails['message'] ?? $e->getMessage(), 'details' => $errorDetails['details'] ?? $errorDetails ?? $e->getMessage(), 'statusCode' => $e->statusCode ]); throw new \Exception("PayPal API Error during subscription creation: " . ($errorDetails['message'] ?? $e->getMessage())); } return null; } /** * Get details of a PayPal subscription. * * @param string $paypalSubscriptionId * @return \stdClass|null * @throws \Exception */ public function getPaypalSubscriptionDetails(string $paypalSubscriptionId): ?\stdClass { $this->ensureClientIsAvailable(); // Manually construct the request for getting subscription details // Endpoint: /v1/billing/subscriptions/{id} $request = new HttpRequest("/v1/billing/subscriptions/" . $paypalSubscriptionId, "GET"); $request->headers["Content-Type"] = "application/json"; // $request = new SubscriptionsGetRequest($paypalSubscriptionId); // If the class existed try { $response = $this->client->execute($request); return $response->result; } catch (HttpException $e) { $debugId = $e->getHeaders()['PayPal-Debug-Id'] ?? 'N/A'; $errorDetails = json_decode($e->getMessage(), true); Log::error("PayPal Get Subscription Details Error for ID {$paypalSubscriptionId} - Debug ID: {$debugId}", [ 'message' => $errorDetails['message'] ?? $e->getMessage(), 'details' => $errorDetails['details'] ?? $errorDetails ?? $e->getMessage(), 'statusCode' => $e->statusCode ]); return null; } } /** * Cancel a PayPal subscription at the gateway. * * @param Subscription $localSubscription * @return bool * @throws \Exception */ public function cancelSubscriptionAtGateway(Subscription $localSubscription): bool { $this->ensureClientIsAvailable(); if (!$localSubscription->gateway_subscription_id) { Log::error("Cannot cancel PayPal subscription for local subscription ID {$localSubscription->id}: missing gateway_subscription_id."); return false; } // Manually construct the request for cancelling a subscription // Endpoint: /v1/billing/subscriptions/{id}/cancel $request = new HttpRequest("/v1/billing/subscriptions/" . $localSubscription->gateway_subscription_id . "/cancel", "POST"); $request->headers["Content-Type"] = "application/json"; $request->body = ['reason' => 'Subscription cancelled by user from application.']; // $request = new SubscriptionsCancelRequest($localSubscription->gateway_subscription_id); // If the class existed try { $response = $this->client->execute($request); // PayPal returns 204 No Content on success if ($response->statusCode == 204) { Log::info("PayPal subscription {$localSubscription->gateway_subscription_id} cancelled successfully at gateway."); return true; } Log::warning("PayPal subscription cancellation for {$localSubscription->gateway_subscription_id} returned status: {$response->statusCode}"); return false; } catch (HttpException $e) { $debugId = $e->getHeaders()['PayPal-Debug-Id'] ?? 'N/A'; $errorDetails = json_decode($e->getMessage(), true); Log::error("Error cancelling PayPal subscription {$localSubscription->gateway_subscription_id} - Debug ID: {$debugId}", [ 'message' => $errorDetails['message'] ?? $e->getMessage(), 'details' => $errorDetails['details'] ?? $errorDetails ?? $e->getMessage(), 'statusCode' => $e->statusCode ]); return false; } } /** * Create a PayPal order for a one-time wallet deposit. * * @param \App\Models\User $user * @param float $amount * @param string $currency * @return \PayPalHttp\HttpResponse|null * @throws \PayPalHttp\IOException * @throws \Exception */ public function createWalletDepositOrder(\App\Models\User $user, float $amount, string $currency = 'USD') { $this->ensureClientIsAvailable(); $request = new OrdersCreateRequest(); $request->prefer('return=representation'); $request->body = [ 'intent' => 'CAPTURE', 'application_context' => [ 'return_url' => route('wallet.paypal.depositCallback'), 'cancel_url' => route('wallet.paypal.depositCancel'), 'brand_name' => config('app.name'), 'user_action' => 'PAY_NOW', ], 'purchase_units' => [[ 'description' => 'Wallet Deposit for ' . config('app.name'), 'amount' => [ 'currency_code' => strtoupper($currency), 'value' => number_format($amount, 2, '.', ''), ], 'custom_id' => 'DEPOSIT-' . $user->id . '-' . time(), // Custom ID for tracking ]], ]; try { return $this->client->execute($request); } catch (HttpException $e) { $debugId = $e->getHeaders()['PayPal-Debug-Id'] ?? 'N/A'; $errorDetails = json_decode($e->getMessage(), true); Log::error('PayPal Create Wallet Deposit Order Error - Debug ID: ' . $debugId, [ 'message' => $errorDetails['message'] ?? $e->getMessage(), 'details' => $errorDetails['details'] ?? $errorDetails ?? $e->getMessage(), 'statusCode' => $e->statusCode ]); throw $e; } } /** * Capture a PayPal order for a wallet deposit. * * @param string $paypalOrderId * @return \PayPalHttp\HttpResponse|null * @throws \PayPalHttp\IOException * @throws \Exception */ public function captureWalletDepositOrder(string $paypalOrderId) { $this->ensureClientIsAvailable(); $request = new OrdersCaptureRequest($paypalOrderId); $request->prefer('return=representation'); try { return $this->client->execute($request); } catch (HttpException $e) { $debugId = $e->getHeaders()['PayPal-Debug-Id'] ?? 'N/A'; $errorDetails = json_decode($e->getMessage(), true); Log::error('PayPal Capture Wallet Deposit Order Error for Order ID ' . $paypalOrderId . ' - Debug ID: ' . $debugId, [ 'message' => $errorDetails['message'] ?? $e->getMessage(), 'details' => $errorDetails['details'] ?? $errorDetails ?? $e->getMessage(), 'statusCode' => $e->statusCode ]); throw $e; } } /** * Verify PayPal Webhook Signature. * * @param array $headers All request headers * @param string $rawBody The raw request body * @return bool * @throws \Exception */ public function verifyWebhookSignature(array $headers, string $rawBody): bool { $this->ensureClientIsAvailable(); if (!$this->webhookId) { Log::error('PayPal Webhook ID is not configured. Cannot verify signature.'); return false; } // Extract necessary headers. Ensure they are lowercase as Laravel might convert them. $transmissionId = $headers['paypal-transmission-id'][0] ?? null; $transmissionTime = $headers['paypal-transmission-time'][0] ?? null; $certUrl = $headers['paypal-cert-url'][0] ?? null; $authAlgo = $headers['paypal-auth-algo'][0] ?? null; $transmissionSig = $headers['paypal-transmission-sig'][0] ?? null; if (!$transmissionId || !$transmissionTime || !$certUrl || !$authAlgo || !$transmissionSig) { Log::error('PayPal Webhook: Missing required headers for signature verification.'); return false; } // IMPORTANT: The PayPal PHP SDK v2 does not include a helper method for webhook signature verification. // You MUST implement this by making an API call to PayPal's /v1/notifications/verify-webhook-signature endpoint. // Refer to PayPal documentation: https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature_post $payload = [ 'auth_algo' => $authAlgo, 'cert_url' => $certUrl, 'transmission_id' => $transmissionId, 'transmission_sig' => $transmissionSig, 'transmission_time' => $transmissionTime, 'webhook_id' => $this->webhookId, 'webhook_event' => json_decode($rawBody) // PayPal expects the event body as a JSON object ]; // You'll need to use an HTTP client (like Guzzle, or Laravel's Http facade if available globally) // to make a POST request to the PayPal API endpoint for verification. // Example using a conceptual HTTP client: /* try { $environment = ($this->mode === 'live') ? new ProductionEnvironment($this->clientId, $this->clientSecret) : new SandboxEnvironment($this->clientId, $this->clientSecret); $verifyUrl = $environment->baseUrl() . '/v1/notifications/verify-webhook-signature'; // Construct the full URL // $httpClient = new \GuzzleHttp\Client(); // Or your preferred HTTP client // $response = $httpClient->post($verifyUrl, [ // 'headers' => ['Content-Type' => 'application/json', 'Authorization' => 'Bearer YOUR_ACCESS_TOKEN'], // You need an access token // 'json' => $payload // ]); // $responseData = json_decode($response->getBody()->getContents(), true); // return isset($responseData['verification_status']) && $responseData['verification_status'] === 'SUCCESS'; } catch (\Exception $e) { Log::error('PayPal Webhook: Error during signature verification API call.', ['error' => $e->getMessage()]); return false; } */ Log::critical('PayPal Webhook Signature Verification: CRITICAL - Full cryptographic verification via API call is NOT YET IMPLEMENTED. This is a major security risk in production. The current check is INSECURE.'); // Returning false by default to highlight that this needs proper implementation. // For development, you might temporarily return true AFTER understanding the risks. return false; // MUST BE REPLACED WITH ACTUAL VERIFICATION } }