--- File: D:\projects\digitalvocano\Modules\PaystackGateway\app\Http\Controllers\Admin\PaystackConfigController.php --- route('admin.dashboard')->with('error', 'Settings helper function not found.'); } $settings = []; foreach ($this->settingKeys as $key) { $settings[$key] = setting($key); } $settings['paystack_mode'] = $settings['paystack_mode'] ?? 'test'; // Default to test return view('paystackgateway::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([ 'paystack_enabled' => 'nullable|boolean', 'paystack_public_key' => 'nullable|string|max:255', 'paystack_secret_key' => 'nullable|string|max:255', 'paystack_mode' => 'required|in:test,live', ]); try { foreach ($this->settingKeys as $key) { $valueToStore = null; if ($key === 'paystack_enabled') { $valueToStore = $request->has('paystack_enabled') ? '1' : '0'; } elseif ($request->filled($key)) { $valueToStore = $request->input($key); } Setting::updateOrCreate(['key' => $key], ['value' => $valueToStore]); } Artisan::call('cache:clear'); Artisan::call('config:clear'); return redirect()->back()->with('success', 'Paystack settings updated successfully.'); } catch (\Exception $e) { Log::error('Error updating Paystack settings: ' . $e->getMessage()); return redirect()->back()->with('error', 'Failed to update Paystack settings. Please check the logs.'); } } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\app\Http\Controllers\PaystackGatewayController.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\PaystackGateway\app\Providers\PaystackGatewayServiceProvider.php --- registerCommands(); $this->registerCommandSchedules(); $this->registerTranslations(); $this->registerConfig(); $this->registerViews(); $this->loadMigrationsFrom(module_path($this->name, 'database/migrations')); } /** * Register the service provider. */ public function register(): void { $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } /** * Register commands in the format of Command::class */ protected function registerCommands(): void { // $this->commands([]); } /** * Register command Schedules. */ protected function registerCommandSchedules(): void { // $this->app->booted(function () { // $schedule = $this->app->make(Schedule::class); // $schedule->command('inspire')->hourly(); // }); } /** * Register translations. */ public function registerTranslations(): void { $langPath = resource_path('lang/modules/'.$this->nameLower); if (is_dir($langPath)) { $this->loadTranslationsFrom($langPath, $this->nameLower); $this->loadJsonTranslationsFrom($langPath); } else { $this->loadTranslationsFrom(module_path($this->name, 'lang'), $this->nameLower); $this->loadJsonTranslationsFrom(module_path($this->name, 'lang')); } } /** * Register config. */ protected function registerConfig(): void { $configPath = module_path($this->name, config('modules.paths.generator.config.path')); if (is_dir($configPath)) { $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); foreach ($iterator as $file) { if ($file->isFile() && $file->getExtension() === 'php') { $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); $segments = explode('.', $this->nameLower.'.'.$config_key); // Remove duplicated adjacent segments $normalized = []; foreach ($segments as $segment) { if (end($normalized) !== $segment) { $normalized[] = $segment; } } $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); $this->publishes([$file->getPathname() => config_path($config)], 'config'); $this->merge_config_from($file->getPathname(), $key); } } } } /** * Merge config from the given path recursively. */ protected function merge_config_from(string $path, string $key): void { $existing = config($key, []); $module_config = require $path; config([$key => array_replace_recursive($existing, $module_config)]); } /** * Register views. */ public function registerViews(): void { $viewPath = resource_path('views/modules/'.$this->nameLower); $sourcePath = module_path($this->name, 'resources/views'); $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); } /** * Get the services provided by the provider. */ public function provides(): array { return []; } private function getPublishableViewPaths(): array { $paths = []; foreach (config('view.paths') as $path) { if (is_dir($path.'/modules/'.$this->nameLower)) { $paths[] = $path.'/modules/'.$this->nameLower; } } return $paths; } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\app\Providers\RouteServiceProvider.php --- mapApiRoutes(); $this->mapWebRoutes(); } /** * Define the "web" routes for the application. * * These routes all receive session state, CSRF protection, etc. */ protected function mapWebRoutes(): void { Route::middleware('web')->group(module_path($this->name, '/routes/web.php')); } /** * Define the "api" routes for the application. * * These routes are typically stateless. */ protected function mapApiRoutes(): void { Route::middleware('api')->prefix('api')->name('api.')->group(module_path($this->name, '/routes/api.php')); } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\config\config.php --- 'PaystackGateway', ]; --- File: D:\projects\digitalvocano\Modules\PaystackGateway\database\seeders\PaystackGatewayDatabaseSeeder.php --- call([]); } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\Http\Controllers\Admin\PaystackConfigController.php --- route('admin.dashboard')->with('error', 'Settings helper function not found.'); } $settings = []; foreach ($this->settingKeys as $key) { $settings[$key] = setting($key); } $settings['paystack_mode'] = $settings['paystack_mode'] ?? 'test'; // Default to test // Default enabled to '0' if not set if (is_null($settings['paystack_enabled'])) { $settings['paystack_enabled'] = '0'; } return view('paystackgateway::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([ 'paystack_enabled' => 'nullable|boolean', 'paystack_public_key' => 'nullable|string|max:255', 'paystack_secret_key' => 'nullable|string|max:255', 'paystack_mode' => 'required|in:test,live', ]); try { // Define settings with their names, groups, and types $settingsData = [ 'paystack_enabled' => [ 'value' => $request->input('paystack_enabled', '0'), 'name' => 'Enable Paystack Gateway', 'group' => 'Payment Gateways', 'type' => 'boolean' ], 'paystack_mode' => [ 'value' => $request->input('paystack_mode', 'test'), 'name' => 'Paystack Mode', 'group' => 'Payment Gateways', 'type' => 'select' ], 'paystack_public_key' => [ 'value' => $request->input('paystack_public_key'), 'name' => 'Paystack Public Key', 'group' => 'Payment Gateways', 'type' => 'text' ], // Secret key is handled separately ]; foreach ($settingsData as $key => $data) { Setting::setValue($key, $data['value'], $data['name'], $data['group'], $data['type']); } // Handle secret key: only update if a new value is provided if ($request->filled('paystack_secret_key')) { Setting::setValue('paystack_secret_key', $request->input('paystack_secret_key'), 'Paystack Secret Key', 'Payment Gateways', 'password'); } Artisan::call('cache:clear'); Artisan::call('config:clear'); return redirect()->back()->with('success', 'Paystack settings updated successfully.'); } catch (\Exception $e) { Log::error('Error updating Paystack settings: ' . $e->getMessage()); return redirect()->back()->with('error', 'Failed to update Paystack settings. Please check the logs.'); } } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\Http\Controllers\PaystackGatewayController.php --- paystackService = $paystackService; $this->creditService = $creditService; $this->walletService = $walletService; // Added } public function initializePayment(Request $request, SubscriptionPlan $plan) { // --- Test with a Direct Eloquent Query --- $slugFromRoute = $request->route('subscriptionPlan'); // Get the raw slug or resolved model if (is_object($slugFromRoute) && $slugFromRoute instanceof SubscriptionPlan) { // If RMB resolved it (even to an empty model), get the slug it tried to use $slugValueToQuery = $slugFromRoute->slug ?? $request->segment(count($request->segments())); } else { $slugValueToQuery = (string) $slugFromRoute; } $directPlan = \App\Models\SubscriptionPlan::where('slug', $slugValueToQuery)->first(); Log::debug("Paystack Initialize: Direct Eloquent query for slug '{$slugValueToQuery}'", [ 'found_plan_id_direct_query' => $directPlan ? $directPlan->id : 'NOT FOUND BY DIRECT QUERY', 'direct_plan_attributes' => $directPlan ? $directPlan->getAttributes() : null, ]); // --- End Test with a Direct Eloquent Query --- // Log the incoming plan object immediately to see what Route Model Binding resolved if (!$plan->exists) { // Check if the model instance is "empty" (not found by RMB) Log::error("Paystack Initialize: Route Model Binding failed to find a SubscriptionPlan for the provided slug.", [ 'route_param_slug_from_request' => $request->route('subscriptionPlan'), 'plan_object_class_on_failure' => get_class($plan) // Log the class of the $plan object ]); // Optionally, redirect or abort if plan not found, as further processing will fail. return redirect()->route('subscription.plans')->with('error', 'The selected subscription plan could not be found.'); } Log::debug("Paystack Initialize: Entered initializePayment method.", [ 'route_param_slug' => $request->route('subscriptionPlan') instanceof SubscriptionPlan ? $request->route('subscriptionPlan')->slug : $request->route('subscriptionPlan'), // Get the raw slug from route 'plan_id_resolved' => $plan->id ?? 'PLAN ID IS NULL', 'plan_slug_resolved' => $plan->slug ?? 'PLAN SLUG IS NULL', 'plan_name_resolved' => $plan->name ?? 'PLAN NAME IS NULL', 'plan_price_resolved' => $plan->price ?? 'PLAN PRICE IS NULL', 'plan_currency_resolved' => $plan->currency ?? 'PLAN CURRENCY IS NULL', 'is_plan_model_empty' => $plan->exists === false, // Check if it's an empty model instance 'plan_object_attributes' => $plan ? $plan->getAttributes() : null ]); if (setting('paystack_enabled', '0') != '1') { return redirect()->route('subscription.plans')->with('error', 'Paystack payments are currently disabled.'); } /** @var User $user */ $user = Auth::user(); // Check for existing active subscription (similar to PayPal) $activeSubscription = $user->currentSubscription(); if ($activeSubscription) { if ($activeSubscription->subscription_plan_id == $plan->id && $activeSubscription->payment_gateway == 'paystackgateway') { return redirect()->route('subscription.plans')->with('info', 'You are already subscribed to this plan via Paystack.'); } elseif ($activeSubscription->subscription_plan_id != $plan->id) { // User has an active subscription to a DIFFERENT plan. Allow them to proceed. // Inform them that their current plan will be cancelled if the new subscription is successful. session()->flash('info_plan_switch', "You are switching from '{$activeSubscription->plan->name}' to '{$plan->name}'. Your current plan will be cancelled upon successful activation of the new plan."); } } try { // $amountInKobo = $plan->price * 100; // Paystack service will handle amount based on plan // Create a pending subscription record $pendingSubscription = null; $subscriptionDataToCreate = [ 'user_id' => $user->id, 'subscription_plan_id' => $plan->id, 'payment_gateway' => 'paystackgateway', 'gateway_transaction_id' => 'TEMP_PAYSTACK_INIT-' . Str::random(12) . '-' . time(), // Temporary initial reference 'status' => 'pending_payment', 'price_at_purchase' => $plan->price, 'currency_at_purchase' => $plan->currency, ]; Log::debug("Paystack Initialize: Attempting to create Subscription with data:", $subscriptionDataToCreate); try { $pendingSubscription = Subscription::create($subscriptionDataToCreate); } catch (\Illuminate\Database\QueryException $qe) { Log::critical("Paystack Initialize: DATABASE QUERY EXCEPTION during Subscription::create()", [ 'temp_reference_attempted' => $subscriptionDataToCreate['gateway_transaction_id'], 'user_id' => $user->id, 'plan_id' => $plan->id, 'error' => $qe->getMessage(), 'sql' => $qe->getSql(), 'bindings' => $qe->getBindings(), 'trace' => method_exists(Str::class, 'limit') ? Str::limit($qe->getTraceAsString(), 1000) : substr($qe->getTraceAsString(), 0, 1000) ]); return redirect()->route('subscription.plans')->with('error', 'Critical error: Failed to initialize your subscription record due to a database issue. Please contact support. (Ref: INIT_DB_EX)'); } catch (\Exception $e) { Log::critical("Paystack Initialize: GENERAL EXCEPTION during Subscription::create()", [ 'temp_reference_attempted' => $subscriptionDataToCreate['gateway_transaction_id'], 'user_id' => $user->id, 'plan_id' => $plan->id, 'error' => $e->getMessage(), 'trace' => method_exists(Str::class, 'limit') ? Str::limit($e->getTraceAsString(), 1000) : substr($e->getTraceAsString(), 0, 1000) ]); return redirect()->route('subscription.plans')->with('error', 'Critical error: Failed to initialize your subscription record. Please contact support. (Ref: INIT_GEN_EX)'); } if (!$pendingSubscription || !$pendingSubscription->id) { Log::error("Paystack Initialize: FAILED to create pending subscription record (Subscription::create() returned null or invalid object).", [ 'temp_reference_attempted' => $subscriptionDataToCreate['gateway_transaction_id'], 'user_id' => $user->id, 'plan_id' => $plan->id, 'data_attempted' => $subscriptionDataToCreate ]); return redirect()->route('subscription.plans')->with('error', 'Failed to initialize your subscription record. Please try again or contact support. (Ref: POST_CREATE_NULL)'); } else { $createdSubscriptionId = $pendingSubscription->id; // Store ID for re-fetch Log::info("Paystack Initialize: Successfully CREATED pending subscription record in DB.", [ 'id' => $pendingSubscription->id, 'initial_temp_reference_stored' => $pendingSubscription->gateway_transaction_id, 'user_id' => $pendingSubscription->user_id, 'plan_id' => $pendingSubscription->subscription_plan_id, 'status_stored' => $pendingSubscription->status ]); } // Call the service method for initializing a SUBSCRIPTION transaction, // passing the ID of our local pending subscription. $paymentDetails = $this->paystackService->initializeSubscriptionTransaction($user, $plan, $pendingSubscription->id); if ($paymentDetails && !empty($paymentDetails['authorization_url']) && !empty($paymentDetails['reference'])) { // Update the local subscription with the actual reference from Paystack $pendingSubscription->gateway_transaction_id = $paymentDetails['reference']; if ($pendingSubscription->save()) { Log::info("Paystack Initialize: Successfully updated local subscription with Paystack's reference.", [ 'local_subscription_id' => $pendingSubscription->id, 'paystack_reference' => $paymentDetails['reference'] ]); } else { Log::error("Paystack Initialize: FAILED to SAVE Paystack's reference to local subscription.", [ 'local_subscription_id' => $pendingSubscription->id, 'attempted_paystack_reference' => $paymentDetails['reference'] ]); $pendingSubscription->update(['status' => 'failed']); // Mark as failed return redirect()->route('subscription.plans')->with('error', 'Failed to record payment reference. Please contact support.'); } return redirect()->away($paymentDetails['authorization_url']); } Log::error('Paystack initialize subscription transaction failed or missing data from service.', ['response_from_service' => $paymentDetails, 'user_id' => $user->id, 'plan_id' => $plan->id]); $pendingSubscription->update(['status' => 'failed']); // Mark as failed return redirect()->route('subscription.plans')->with('error', 'Could not initiate Paystack payment. Please try again.'); } catch (\Exception $e) { Log::error("Paystack Initialize Payment Error for user {$user->id}, plan {$plan->id}: " . $e->getMessage()); return redirect()->route('subscription.plans')->with('error', 'An error occurred while initiating payment: ' . $e->getMessage()); } } public function handleCallback(Request $request) { $reference = $request->query('reference'); if (!$reference) { return redirect()->route('subscription.plans')->with('error', 'Invalid Paystack callback. Reference missing.'); } try { $verificationData = $this->paystackService->verifyTransaction($reference); if ($verificationData && isset($verificationData['status']) && $verificationData['status'] === true && isset($verificationData['data']['status']) && $verificationData['data']['status'] === 'success') { $paystackData = $verificationData['data']; $metadata = $paystackData['metadata'] ?? []; $localSubscriptionIdFromMeta = $metadata['subscription_id'] ?? null; $referenceFromQuery = $request->query('reference'); // This is the $reference used for verification $subscription = null; if ($localSubscriptionIdFromMeta) { $subscription = Subscription::find($localSubscriptionIdFromMeta); } // Diagnostic: Check if ANY subscription exists with this reference, regardless of status if (!$subscription && $referenceFromQuery) { $anySubscriptionWithReference = Subscription::where('gateway_transaction_id', $referenceFromQuery) ->where('payment_gateway', 'paystackgateway') ->first(); if ($anySubscriptionWithReference) { Log::info("Paystack Callback: A subscription (ID: {$anySubscriptionWithReference->id}) was found with reference '{$referenceFromQuery}', but its status is '{$anySubscriptionWithReference->status}'. Expected 'pending_payment'."); } else { Log::info("Paystack Callback: NO subscription record found at all with reference '{$referenceFromQuery}'."); } } // If not found by metadata ID, or if metadata ID was not present, try finding by the reference // This assumes the reference was stored in gateway_transaction_id during initialization if (!$subscription && $referenceFromQuery) { Log::info("Paystack Callback: Subscription not found by metadata ID '{$localSubscriptionIdFromMeta}'. Attempting lookup by reference '{$referenceFromQuery}'."); $subscription = Subscription::where('gateway_transaction_id', $referenceFromQuery) ->where('payment_gateway', 'paystackgateway') // Ensure it's a Paystack sub ->where('status', 'pending_payment') // Specifically look for pending ->first(); } if ($subscription && $subscription->status === 'pending_payment') { // Ensure it's the correct pending subscription // Deactivate any other existing subscriptions for this user $subscription->user->subscriptions() ->where('id', '!=', $subscription->id) ->whereIn('status', ['active', 'trialing']) ->update(['status' => 'cancelled', 'ends_at' => now(), 'cancelled_at' => now()]); $subscription->updateFromPaystackData($paystackData); // Implement this method in Subscription model // Award credits (similar to SubscriptionController) $plan = $subscription->plan; if (function_exists('setting') && setting('credits_system_enabled', '0') == '1' && $plan->credits_awarded_on_purchase > 0) { $this->creditService->awardCredits($subscription->user, $plan->credits_awarded_on_purchase, 'award_subscription_purchase', "Credits for {$plan->name} subscription", $subscription); } // Assign target role if defined 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()) { $subscription->user->syncRoles([$plan->target_role]); } return redirect()->route('dashboard')->with('success', 'Subscription successfully activated via Paystack!'); } Log::warning('Paystack Callback: Local subscription not found or not in a pending state.', [ 'reference' => $referenceFromQuery, 'local_id_from_meta' => $localSubscriptionIdFromMeta, 'subscription_found_id' => $subscription ? $subscription->id : null, 'status_if_found' => $subscription ? $subscription->status : 'N/A' ]); return redirect()->route('subscription.plans')->with('error', 'Subscription record mismatch or already processed. Please contact support.'); } Log::error('Paystack transaction verification failed or payment not successful.', ['reference' => $reference, 'response' => $verificationData]); return redirect()->route('subscription.plans')->with('error', $verificationData['message'] ?? 'Paystack payment verification failed.'); } catch (\Exception $e) { Log::error("Paystack Callback Error for reference {$reference}: " . $e->getMessage()); return redirect()->route('subscription.plans')->with('error', 'An error occurred during payment verification: ' . $e->getMessage()); } } // --- Wallet Deposit Methods --- public function initializeWalletDeposit(Request $request) { if (setting('paystack_enabled', '0') != '1' || setting('allow_wallet_deposits', '0') != '1') { return redirect()->route('user.wallet.deposit.form')->with('error', 'Paystack deposits are currently disabled.'); } $request->validate(['amount' => 'required|numeric|min:1']); $amount = (float) $request->input('amount'); /** @var User $user */ $user = Auth::user(); try { $reference = 'DEPOSIT-' . Str::random(8) . '-' . time(); $amountInKobo = $amount * 100; // Paystack expects amount in kobo $currency = setting('currency_code', 'NGN'); // Paystack often defaults to NGN // Optionally create a pending WalletTransaction record here if needed for tracking before redirect $callbackUrl = route('wallet.paystack.depositCallback'); $metadata = [ 'user_id' => $user->id, 'deposit_amount' => $amount, 'currency' => $currency, 'type' => 'wallet_deposit', 'custom_fields' => [ ['display_name' => "Wallet Deposit", 'variable_name' => "transaction_type", 'value' => "Wallet Top-up"] ] ]; $paymentData = $this->paystackService->initializeTransaction($amountInKobo, $user->email, $reference, $callbackUrl, $metadata); if ($paymentData && isset($paymentData['status']) && $paymentData['status'] === true && isset($paymentData['data']['authorization_url'])) { return redirect()->away($paymentData['data']['authorization_url']); } Log::error('Paystack initialize wallet deposit failed.', ['response' => $paymentData, 'user_id' => $user->id, 'amount' => $amount]); return redirect()->route('user.wallet.deposit.form')->with('error', $paymentData['message'] ?? 'Could not initiate Paystack deposit. Please try again.'); } catch (\Exception $e) { Log::error("Paystack 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: ' . $e->getMessage()); } } public function handleWalletDepositCallback(Request $request) { $reference = $request->query('reference'); if (!$reference) { return redirect()->route('user.wallet.deposit.form')->with('error', 'Invalid Paystack callback for deposit. Reference missing.'); } try { $verificationData = $this->paystackService->verifyTransaction($reference); if ($verificationData && isset($verificationData['status']) && $verificationData['status'] === true && isset($verificationData['data']['status']) && $verificationData['data']['status'] === 'success') { $paystackData = $verificationData['data']; $amountDeposited = $paystackData['amount'] / 100; // Amount is in Kobo $currency = $paystackData['currency'] ?? 'NGN'; $userIdFromMeta = $paystackData['metadata']['user_id'] ?? null; if (!$userIdFromMeta) { Log::error('Paystack Wallet Deposit Callback: User ID missing from metadata.', ['reference' => $reference]); return redirect()->route('user.wallet.deposit.form')->with('error', 'User identification failed for deposit.'); } $user = User::find($userIdFromMeta); if (!$user) { Log::error('Paystack Wallet Deposit Callback: User not found for ID from metadata.', ['reference' => $reference, 'user_id_from_meta' => $userIdFromMeta]); return redirect()->route('user.wallet.deposit.form')->with('error', 'User account not found for deposit.'); } // User successfully identified from metadata $this->walletService->deposit($user, $amountDeposited, $currency, 'paystackgateway', $reference, "Wallet deposit via Paystack"); return redirect()->route('user.wallet.history')->with('success', 'Successfully deposited ' . $currency . ' ' . number_format($amountDeposited, 2) . ' to your wallet.'); } Log::error('Paystack wallet deposit verification failed or payment not successful.', ['reference' => $reference, 'response' => $verificationData]); return redirect()->route('user.wallet.deposit.form')->with('error', $verificationData['message'] ?? 'Paystack payment verification failed for deposit.'); } catch (\Exception $e) { Log::error("Paystack Wallet Deposit Callback Error for reference {$reference}: " . $e->getMessage()); return redirect()->route('user.wallet.deposit.form')->with('error', 'An error occurred during deposit verification: ' . $e->getMessage()); } } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\Http\Controllers\PaystackWebhookController.php --- paystackService = $paystackService; } public function handleWebhook(Request $request) { // 1. Verify the webhook signature $signature = $request->header('x-paystack-signature'); $rawBody = $request->getContent(); if (!$this->paystackService->secretKey || !$signature) { Log::warning('Paystack Webhook: Missing secret key or signature for verification.'); return response()->json(['status' => 'error', 'message' => 'Configuration error or missing signature'], 400); } $calculatedSignature = hash_hmac('sha512', $rawBody, $this->paystackService->secretKey); if (!hash_equals($calculatedSignature, $signature)) { Log::warning('Paystack Webhook: Invalid signature.', [ 'received_signature' => $signature, 'calculated_signature' => $calculatedSignature, 'ip_address' => $request->ip(), ]); return response()->json(['status' => 'error', 'message' => 'Signature verification failed'], 401); } // 2. Process the event $event = json_decode($rawBody); if (!$event || !isset($event->event)) { Log::error('Paystack Webhook: Invalid event data received or missing event type.'); return response()->json(['status' => 'error', 'message' => 'Invalid event data'], 400); } Log::info('Paystack Webhook Received:', ['event_type' => $event->event, 'data' => (array)($event->data ?? [])]); $eventType = $event->event; $data = $event->data ?? null; switch ($eventType) { case 'charge.success': // This is a crucial event for recurring payments or successful one-time charges. if ($data && isset($data->reference)) { // If it's related to a subscription, the metadata might contain subscription_id or plan_code. // You might need to fetch the local subscription using $data->reference or metadata. // For now, let's assume it might be a subscription payment. $this->handleSuccessfulCharge($data); } break; case 'subscription.create': // A new subscription is created on Paystack case 'subscription.enable': // A previously disabled subscription is re-enabled // You might want to update your local subscription status if it was, for example, 'pending' or 'disabled'. // $data->subscription_code, $data->customer->email, $data->plan->plan_code break; case 'subscription.disable': // Subscription is disabled (e.g., due to payment failure or manual action) // Update your local subscription status to 'cancelled' or 'suspended'. // $data->subscription_code break; // Add more Paystack event types as needed: // - invoice.create // - invoice.update // - invoice.payment_failed // - transfer.success, transfer.failed (if using Paystack transfers) default: Log::info('Paystack Webhook: Received unhandled event type: ' . $eventType); } return response()->json(['status' => 'success'], 200); } protected function handleSuccessfulCharge($data) { // Example: Find subscription by reference (if you store Paystack's transaction reference) // $subscription = Subscription::where('gateway_transaction_id', $data->reference)->first(); // if ($subscription && $subscription->status !== 'active') { // $subscription->status = 'active'; // // Potentially update ends_at based on the new payment // $subscription->save(); // Log::info("Paystack Webhook: Subscription ID {$subscription->id} marked active via charge.success for reference {$data->reference}."); // } Log::info("Paystack Webhook: Successful charge processed for reference: {$data->reference}", (array)$data); // If this charge is for a subscription renewal, you'd update the subscription's `ends_at` date. } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\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\PaystackGateway\Providers\PaystackGatewayServiceProvider.php --- registerConfig(); $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations')); $this->registerViews(); // Uncomment and implement this $this->registerTranslations(); // Uncomment and implement if you have translations // 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')); } // In Modules\PaystackGateway\Providers\PaystackGatewayServiceProvider.php public function register(): void { $this->app->register(EventServiceProvider::class); // Register the module's EventServiceProvider $this->app->register(RouteServiceProvider::class); // Register the module's RouteServiceProvider $this->app->singleton(PaystackService::class, function ($app) { return new PaystackService(); }); } 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\PaystackGateway\Providers\RouteServiceProvider.php --- mapWebRoutes(); // $this->mapAdminRoutes(); // Admin routes are loaded by the main app's RouteServiceProvider by convention $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/paystackgateway', // naming with 'admin.paystackgateway.', and namespacing to 'Modules\PaystackGateway\Http\Controllers\Admin'. // } /** * Define the "api" routes for the application. */ protected function mapApiRoutes(): void { // The main app's RouteServiceProvider applies 'api' middleware, // 'api/paystackgateway' prefix, 'api.paystackgateway.' name prefix, // and the base 'Modules\PaystackGateway\Http\Controllers' namespace. Route::namespace($this->moduleNamespace) // Assuming API controllers are directly under Http/Controllers ->group(module_path($this->name, 'routes/api.php')); } } --- File: D:\projects\digitalvocano\Modules\PaystackGateway\resources\views\admin\config.blade.php --- @extends('layouts.admin') @section('title', 'Paystack Gateway Settings') @section('header_title', 'Paystack Gateway Settings') @section('content')
Module: {!! config('paystackgateway.name') !!}