--- 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')
Module: {!! config('paypalgateway.name') !!}