--- File: D:\projects\digitalvocano\Modules\PaynowGateway\app\Http\Controllers\PaynowGatewayController.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\PaynowGateway\app\Providers\PaynowGatewayServiceProvider.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\PaynowGateway\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\PaynowGateway\config\config.php --- 'PaynowGateway', ]; --- File: D:\projects\digitalvocano\Modules\PaynowGateway\database\seeders\PaynowGatewayDatabaseSeeder.php --- 'paynowgateway_enabled', 'value' => '0', // Default to disabled 'name' => 'Enable Paynow Gateway', 'group' => 'Payment Gateways', 'type' => 'checkbox', 'description' => 'Enable or disable the Paynow payment gateway.' ], [ 'key' => 'paynow_integration_id', 'value' => '', 'name' => 'Paynow Integration ID', 'group' => 'Payment Gateways', 'type' => 'text', 'description' => 'Your Paynow Integration ID provided by Paynow.' ], [ 'key' => 'paynow_integration_key', 'value' => '', 'name' => 'Paynow Integration Key', 'group' => 'Payment Gateways', 'type' => 'password', 'description' => 'Your Paynow Integration Key provided by Paynow.' ], [ 'key' => 'paynow_mode', 'value' => 'test', 'name' => 'Paynow Mode', 'group' => 'Payment Gateways', 'type' => 'select', // View should render options: {"test": "Test/Sandbox", "live": "Live"} 'description' => 'Set Paynow to Test/Sandbox or Live mode.' ], [ 'key' => 'paynow_webhook_secret', 'value' => '', 'name' => 'Paynow Webhook Secret (for hash verification)', 'group' => 'Payment Gateways', 'type' => 'password', 'description' => 'The secret key used to verify incoming Paynow webhooks.' ], ]; foreach ($settings as $settingData) { // Check if the setting already exists to prevent overriding user-configured values $existingSetting = Setting::where('key', $settingData['key'])->first(); if (!$existingSetting) { Setting::create([ 'key' => $settingData['key'], 'value' => $settingData['value'], 'name' => $settingData['name'], 'group' => $settingData['group'], 'type' => $settingData['type'], 'description' => $settingData['description'] ?? null, ]); } } } } --- File: D:\projects\digitalvocano\Modules\PaynowGateway\Http\Controllers\Admin\PaynowConfigController.php --- validate([ 'paynow_enabled' => 'nullable|boolean', 'paynow_integration_id' => 'nullable|string|max:255', 'paynow_integration_key' => 'nullable|string|max:255', 'paynow_mode' => 'required|in:test,live', // Adjust values as per Paynow 'paynow_webhook_secret' => 'nullable|string|max:255', ]); try { Setting::setValue('paynow_enabled', $request->input('paynow_enabled', '0'), 'Enable Paynow Gateway', 'Payment Gateways', 'boolean'); Setting::setValue('paynow_integration_id', $request->input('paynow_integration_id'), 'Paynow Integration ID', 'Payment Gateways', 'text'); Setting::setValue('paynow_integration_key', $request->input('paynow_integration_key'), 'Paynow Integration Key', 'Payment Gateways', 'password'); Setting::setValue('paynow_mode', $request->input('paynow_mode', 'test'), 'Paynow Mode', 'Payment Gateways', 'select'); Setting::setValue('paynow_webhook_secret', $request->input('paynow_webhook_secret'), 'Paynow Webhook Secret', 'Payment Gateways', 'password'); return redirect()->route('admin.paynowgateway.settings.edit') ->with('success', 'Paynow settings updated successfully.'); } catch (\Exception $e) { Log::error('Error updating Paynow settings: ' . $e->getMessage()); return redirect()->route('admin.paynowgateway.settings.edit') ->with('error', 'Failed to update Paynow settings. Please try again.'); } } } --- File: D:\projects\digitalvocano\Modules\PaynowGateway\Http\Controllers\PaynowGatewayController.php --- paynowService = $paynowService; $this->creditService = $creditService; $this->walletService = $walletService; } public function initializeSubscriptionPayment(Request $request, SubscriptionPlan $plan) { if (setting('paynowgateway_enabled', '0') != '1') { return redirect()->route('subscription.plans')->with('error', 'Paynow payments are currently disabled.'); } /** @var User $user */ $user = Auth::user(); $subscriptionReference = 'SUB-' . Str::uuid()->toString(); // Unique reference for this payment attempt try { // Create a pending local subscription record $pendingSubscription = Subscription::create([ 'user_id' => $user->id, 'subscription_plan_id' => $plan->id, 'payment_gateway' => 'paynow_gateway', 'gateway_transaction_id' => $subscriptionReference, // Store our unique reference 'status' => 'pending_payment', 'price_at_purchase' => $plan->price, 'currency_at_purchase' => $plan->currency, // Use plan's currency // gateway_poll_url will be set after successful initiation ]); $paymentDetails = $this->paynowService->initiateSubscriptionPayment( $user, $plan, $pendingSubscription->id, $subscriptionReference ); if ($paymentDetails && $paymentDetails['status'] === 'success' && isset($paymentDetails['redirect_url'])) { // Update local subscription with Paynow's poll_url and potentially their reference $pendingSubscription->gateway_poll_url = $paymentDetails['poll_url']; // $pendingSubscription->gateway_transaction_id = $paymentDetails['paynow_reference']; // Or keep $subscriptionReference $pendingSubscription->save(); return redirect()->away($paymentDetails['redirect_url']); } // If initiation failed at the service level $pendingSubscription->update(['status' => 'failed', 'notes' => 'Paynow initiation failed.']); Log::error('Paynow initialize subscription payment failed at controller.', [ 'service_response' => $paymentDetails, 'user_id' => $user->id, 'plan_id' => $plan->id ]); return redirect()->route('subscription.plans')->with('error', 'Could not initiate Paynow payment. Please try again.'); } catch (\Exception $e) { if (isset($pendingSubscription) && $pendingSubscription->exists) { $pendingSubscription->update(['status' => 'failed', 'notes' => 'Error during Paynow initiation: ' . $e->getMessage()]); } Log::error("Paynow Initialize Subscription Payment Error for user {$user->id}, plan {$plan->id}: " . $e->getMessage()); return redirect()->route('subscription.plans')->with('error', 'An error occurred: ' . $e->getMessage()); } } public function handleSubscriptionCallback(Request $request) { /** @var User $user */ $user = Auth::user(); $localSubscriptionId = $request->query('subscription_id'); $transactionReference = $request->query('ref'); // This is our $subscriptionReference Log::info('Paynow Subscription Callback Received:', $request->all()); if (!$localSubscriptionId || !$transactionReference) { Log::error('Paynow Subscription Callback: Missing subscription_id or ref in callback.', $request->all()); return redirect()->route('subscription.plans')->with('error', 'Invalid callback data. Please contact support.'); } $subscription = Subscription::where('id', $localSubscriptionId) ->where('gateway_transaction_id', $transactionReference) ->where('status', 'pending_payment') ->first(); if (!$subscription) { Log::warning('Paynow Subscription Callback: Local subscription not found, not pending, or reference mismatch.', [ 'local_subscription_id' => $localSubscriptionId, 'transaction_reference' => $transactionReference, 'user_id' => $user->id, ]); // Check if it was already processed $alreadyProcessedSub = Subscription::find($localSubscriptionId); if ($alreadyProcessedSub && $alreadyProcessedSub->status !== 'pending_payment') { return redirect()->route('dashboard')->with('info', 'This subscription payment has already been processed.'); } return redirect()->route('subscription.plans')->with('error', 'Subscription record issue. Please contact support.'); } if (empty($subscription->gateway_poll_url)) { Log::error('Paynow Subscription Callback: Poll URL missing for subscription.', ['sub_id' => $subscription->id]); $subscription->update(['status' => 'failed', 'notes' => 'Callback received but Poll URL was missing.']); return redirect()->route('subscription.plans')->with('error', 'Cannot verify payment: Poll URL missing. Contact support.'); } try { $verificationResult = $this->paynowService->verifyPaymentStatus($subscription->gateway_poll_url); Log::info('Paynow Subscription Callback - Verification Result:', $verificationResult); if ($verificationResult && $verificationResult['paid'] === true) { // Cancel other active/trialing subscriptions for the user $user->subscriptions()->where('id', '!=', $subscription->id)->whereIn('status', ['active', 'trialing'])->update(['status' => 'cancelled', 'ends_at' => now(), 'cancelled_at' => now()]); $plan = $subscription->plan; $subscription->status = $plan->trial_period_days > 0 ? 'trialing' : 'active'; $subscription->starts_at = now(); $subscription->trial_ends_at = $plan->trial_period_days > 0 ? now()->addDays($plan->trial_period_days) : null; $subscription->ends_at = now()->add($plan->interval, $plan->interval_count); $subscription->gateway_transaction_id = $verificationResult['paynow_reference'] ?? $subscription->gateway_transaction_id; // Update to Paynow's ref if available $subscription->notes = 'Payment successful via Paynow. Status: ' . $verificationResult['status']; $subscription->save(); 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", $subscription); } if (!empty($plan->target_role) && class_exists(Role::class) && Role::where('name', $plan->target_role)->where('guard_name', 'web')->exists()) { $user->syncRoles([$plan->target_role]); } return redirect()->route('dashboard')->with('success', 'Subscription successfully activated via Paynow!'); } else { // Payment not confirmed as paid by Paynow $subscription->status = 'failed'; $subscription->notes = 'Paynow payment verification failed or payment not completed. Status: ' . ($verificationResult['status'] ?? 'unknown') . '. Message: ' . ($verificationResult['message'] ?? ''); $subscription->save(); Log::error('Paynow subscription verification indicated failure or pending.', [ 'verification_data' => $verificationResult, 'request_data' => $request->all(), 'subscription_id' => $subscription->id ]); return redirect()->route('subscription.plans')->with('error', $verificationResult['message'] ?? 'Paynow payment verification failed or payment not completed.'); } } catch (\Exception $e) { Log::error("Paynow Subscription Callback Exception: " . $e->getMessage(), [ 'request_data' => $request->all(), 'subscription_id' => $subscription->id ?? null, // Use null coalescing as $subscription might not be set if first() fails ]); if (isset($subscription) && $subscription->exists) { // Check if $subscription was successfully fetched and exists $subscription->status = 'failed'; $subscription->notes = 'Error during Paynow callback processing: ' . $e->getMessage(); $subscription->save(); } return redirect()->route('subscription.plans')->with('error', 'An error occurred during payment verification.'); } } public function handleSubscriptionCancel(Request $request) { // Optionally, find and update the pending subscription to 'cancelled' if a reference is passed back $transactionReference = $request->query('ref'); if ($transactionReference) { $subscription = Subscription::where('gateway_transaction_id', $transactionReference) ->where('status', 'pending_payment') ->first(); if ($subscription) { $subscription->update(['status' => 'cancelled', 'notes' => 'User cancelled payment process on Paynow.']); } } return redirect()->route('subscription.plans')->with('info', 'Paynow subscription process was cancelled.'); } // --- Wallet Deposit Methods --- public function initializeWalletDeposit(Request $request) { if (setting('paynowgateway_enabled', '0') != '1' || setting('allow_wallet_deposits', '0') != '1') { return redirect()->route('user.wallet.deposit.form')->with('error', 'Paynow deposits are currently disabled.'); } $minDeposit = setting('wallet_min_deposit_amount', 1); $validated = $request->validate(['amount' => "required|numeric|min:{$minDeposit}"]); $amount = (float) $validated['amount']; /** @var User $user */ $user = Auth::user(); $currency = strtoupper(setting('wallet_default_currency', 'USD')); $transactionReference = 'WLT-' . Str::uuid()->toString(); try { $paymentDetails = $this->paynowService->initiateOneTimePayment( $user, $amount, $currency, "Wallet Deposit for user {$user->email}", $transactionReference ); if ($paymentDetails && $paymentDetails['status'] === 'success' && isset($paymentDetails['redirect_url'])) { // Store necessary details in session for callback verification session([ 'paynow_wallet_reference' => $transactionReference, 'paynow_wallet_poll_url' => $paymentDetails['poll_url'], 'paynow_wallet_amount' => $amount, 'paynow_wallet_currency' => $currency, ]); return redirect()->away($paymentDetails['redirect_url']); } Log::error('Paynow initialize wallet deposit failed at controller.', [ 'service_response' => $paymentDetails, 'user_id' => $user->id, 'amount' => $amount ]); return redirect()->route('user.wallet.deposit.form')->with('error', 'Could not initiate Paynow deposit.'); } catch (\Exception $e) { Log::error("Paynow Initialize Wallet Deposit Error for user {$user->id}, amount {$amount}: " . $e->getMessage()); return redirect()->route('user.wallet.deposit.form')->with('error', 'An error occurred: ' . $e->getMessage()); } } public function handleWalletDepositCallback(Request $request) { /** @var User $user */ $user = Auth::user(); $transactionReference = $request->query('ref'); // This is our $transactionReference // Retrieve details from session $sessionReference = session('paynow_wallet_reference'); $pollUrl = session('paynow_wallet_poll_url'); $amount = session('paynow_wallet_amount'); $currency = session('paynow_wallet_currency'); Log::info('Paynow Wallet Deposit Callback Received:', $request->all()); if (!$transactionReference || $transactionReference !== $sessionReference || !$pollUrl || !$amount) { Log::error('Paynow Wallet Callback: Invalid or missing session data or reference mismatch.', [ 'request_ref' => $transactionReference, 'session_ref' => $sessionReference, 'poll_url_present' => !empty($pollUrl), 'user_id' => $user->id, ]); $this->clearWalletSessionData(); return redirect()->route('user.wallet.deposit.form')->with('error', 'Invalid deposit session or callback. Please try again.'); } try { $verificationResult = $this->paynowService->verifyPaymentStatus($pollUrl); Log::info('Paynow Wallet Deposit Callback - Verification Result:', $verificationResult); if ($verificationResult && $verificationResult['paid'] === true) { // Optional: Verify amount if Paynow returns it in verification $paidAmount = (float) ($verificationResult['amount'] ?? $amount); // Use session amount if not in verification if ($paidAmount < $amount) { // Check if paid amount is less than expected Log::warning('Paynow Wallet Deposit: Amount mismatch.', [ 'expected' => $amount, 'paid' => $paidAmount, 'user_id' => $user->id, 'ref' => $transactionReference ]); // Decide how to handle amount mismatch, e.g., log and proceed, or error out // For now, we'll proceed with the session amount if Paynow confirms 'paid' } $this->walletService->deposit( $user, $amount, // Use the originally intended amount $currency, 'paynow_gateway', $verificationResult['paynow_reference'] ?? $transactionReference, // Use Paynow's ref if available "Wallet deposit via Paynow. Ref: {$transactionReference}" ); $this->clearWalletSessionData(); return redirect()->route('user.wallet.index')->with('success', 'Wallet deposit successful via Paynow!'); } else { Log::error('Paynow wallet deposit verification indicated failure or pending.', [ 'verification_data' => $verificationResult, 'user_id' => $user->id, 'ref' => $transactionReference ]); $this->clearWalletSessionData(); return redirect()->route('user.wallet.deposit.form')->with('error', $verificationResult['message'] ?? 'Paynow deposit verification failed or payment not completed.'); } } catch (\Exception $e) { Log::error("Paynow Wallet Deposit Callback Exception: " . $e->getMessage(), [ 'request_data' => $request->all(), 'user_id' => $user->id, 'ref' => $transactionReference ]); $this->clearWalletSessionData(); return redirect()->route('user.wallet.deposit.form')->with('error', 'An error occurred during deposit verification.'); } } private function clearWalletSessionData() { session()->forget([ 'paynow_wallet_reference', 'paynow_wallet_poll_url', 'paynow_wallet_amount', 'paynow_wallet_currency' ]); } public function handleWalletDepositCancel(Request $request) { $this->clearWalletSessionData(); return redirect()->route('user.wallet.deposit.form')->with('info', 'Paynow wallet deposit was cancelled.'); } } --- File: D:\projects\digitalvocano\Modules\PaynowGateway\Http\Controllers\PaynowWebhookController.php --- paynowService = $paynowService; $this->walletService = $walletService; $this->creditService = $creditService; } public function handleWebhook(Request $request) { $payload = $request->all(); // Paynow typically POSTs form data for webhooks Log::info('Paynow Webhook Received (raw):', $payload); $webhookProcessingResult = $this->paynowService->processWebhook($payload); if (!$webhookProcessingResult['verified']) { Log::warning('Paynow Webhook: Verification failed (e.g., invalid hash).', [ 'payload' => $payload, 'result' => $webhookProcessingResult ]); return response()->json(['status' => 'error', 'message' => $webhookProcessingResult['message'] ?? 'Webhook verification failed'], 400); } Log::info('Paynow Webhook Processed Data:', $webhookProcessingResult); $transactionReference = $webhookProcessingResult['external_reference']; $paynowReference = $webhookProcessingResult['paynow_reference']; $status = strtolower($webhookProcessingResult['status']); // Ensure lowercase for consistent comparison $pollUrl = $webhookProcessingResult['poll_url']; if (!$transactionReference) { Log::error('Paynow Webhook: Missing transaction reference in processed data.'); return response()->json(['status' => 'error', 'message' => 'Missing reference'], 400); } if (Str::startsWith($transactionReference, 'SUB-')) { $this->handleSubscriptionWebhook($transactionReference, $paynowReference, $status, $pollUrl, $webhookProcessingResult); } elseif (Str::startsWith($transactionReference, 'WLT-')) { $this->handleWalletWebhook($transactionReference, $paynowReference, $status, $pollUrl, $webhookProcessingResult); } else { Log::info("Paynow Webhook: Unhandled transaction type for reference '{$transactionReference}'"); } return response()->json(['status' => 'success', 'message' => 'Webhook received'], 200); } protected function handleSubscriptionWebhook($localRef, $paynowRef, $eventStatus, $pollUrl, $webhookData) { $subscription = Subscription::where('gateway_transaction_id', $localRef) ->orderBy('created_at', 'desc') // In case of retries or multiple attempts with same ref ->first(); if (!$subscription) { Log::warning("Paynow Webhook: Subscription not found for local reference '{$localRef}' or Paynow ref '{$paynowRef}'."); return; } // Idempotency check: If already active and webhook says 'paid', likely already processed. if (($subscription->status === 'active' || $subscription->status === 'trialing') && $eventStatus === 'paid') { Log::info("Paynow Webhook: Subscription {$subscription->id} already active/trialing. Ignoring 'paid' event for local ref '{$localRef}'."); // Optionally update poll_url or paynow_reference if they differ and are useful if ($pollUrl && $subscription->gateway_poll_url !== $pollUrl) { $subscription->gateway_poll_url = $pollUrl; } if ($paynowRef && $subscription->gateway_transaction_id !== $paynowRef && $subscription->gateway_transaction_id === $localRef) { // If we stored localRef initially, update to Paynow's actual transaction ID if preferred // $subscription->gateway_transaction_id = $paynowRef; } if ($subscription->isDirty()) { $subscription->save(); } return; } Log::info("Paynow Webhook: Processing subscription {$subscription->id} for event '{$eventStatus}'. Local ref '{$localRef}'."); $originalStatus = $subscription->status; switch ($eventStatus) { case 'paid': // This is a common success status from Paynow case 'delivered': // Another possible success status if ($originalStatus !== 'active' && $originalStatus !== 'trialing') { $user = $subscription->user; // Cancel other active/trialing subscriptions for the user $user->subscriptions()->where('id', '!=', $subscription->id)->whereIn('status', ['active', 'trialing']) ->update(['status' => 'cancelled', 'ends_at' => now(), 'cancelled_at' => now()]); $plan = $subscription->plan; $subscription->status = $plan->trial_period_days > 0 ? 'trialing' : 'active'; $subscription->starts_at = $subscription->starts_at ?? now(); // Keep existing if already set (e.g. by callback) $subscription->trial_ends_at = $plan->trial_period_days > 0 ? ($subscription->trial_ends_at ?? now()->addDays($plan->trial_period_days)) : null; $subscription->ends_at = $subscription->ends_at ?? now()->add($plan->interval, $plan->interval_count); if (function_exists('setting') && setting('credits_system_enabled', '0') == '1' && $plan->credits_awarded_on_purchase > 0) { // Consider adding an idempotency check for credit awarding if this webhook might re-trigger $this->creditService->awardCredits($user, $plan->credits_awarded_on_purchase, 'award_subscription_purchase_webhook', "Credits for {$plan->name} (Webhook)", $subscription); } 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]); } Log::info("Paynow Webhook: Subscription {$subscription->id} activated/updated via webhook."); } break; case 'cancelled': // User cancelled on Paynow, or merchant cancelled case 'failed': // Payment failed case 'disputed': // Payment disputed if ($originalStatus !== 'cancelled' && $originalStatus !== 'failed') { $subscription->status = ($eventStatus === 'cancelled' || $eventStatus === 'disputed') ? 'cancelled' : 'failed'; if (!$subscription->ends_at || $subscription->ends_at->isFuture()) { $subscription->ends_at = now(); } $subscription->cancelled_at = $subscription->cancelled_at ?? now(); Log::info("Paynow Webhook: Subscription {$subscription->id} status updated to '{$eventStatus}' via webhook."); } break; default: Log::info("Paynow Webhook: Unhandled subscription event status '{$eventStatus}' for subscription {$subscription->id}. Local ref '{$localRef}'."); } $subscription->gateway_poll_url = $pollUrl ?? $subscription->gateway_poll_url; // Update to Paynow's reference if it's different and we were using our local one // $subscription->gateway_transaction_id = $paynowRef ?? $localRef; $subscription->notes = ($subscription->notes ? $subscription->notes . " | " : "") . "Webhook: Status '{$eventStatus}'. PaynowRef: {$paynowRef}. LocalRef: {$localRef}."; if ($subscription->isDirty()) { $subscription->save(); } } protected function handleWalletWebhook($localRef, $paynowRef, $eventStatus, $pollUrl, $webhookData) { Log::info("Paynow Webhook: Received wallet event '{$eventStatus}' for local reference '{$localRef}'. PaynowRef: '{$paynowRef}'."); // Wallet deposits are primarily handled by the synchronous callback for immediate user feedback. // Webhooks here serve as a reconciliation mechanism or handle cases where the user's browser // was closed before the callback was fully processed by your application. // Example: If you have a `wallet_transactions` table: // 1. Find the transaction by $localRef (and potentially $user_id if not globally unique). // 2. If its status is 'pending' and $eventStatus is 'paid' or 'delivered': // - Mark your local transaction as 'completed'. // - Credit the user's wallet using $this->walletService->deposit(...). // Ensure this action is idempotent (doesn't credit twice if webhook is resent). // You might check if a wallet credit with $paynowRef already exists. // 3. If its status is 'pending' and $eventStatus is 'failed'/'cancelled': // - Mark your local transaction as 'failed'. // For now, this is a placeholder for more robust wallet webhook handling. // You would need a separate table to track individual wallet deposit attempts to make this robust. if ($eventStatus === 'paid' || $eventStatus === 'delivered') { // Potentially find user by email if $localRef doesn't directly link to a user or pending transaction // $user = User::where('email', $webhookData['raw_data']['authemail'] ?? null)->first(); // if ($user) { // $amount = (float) ($webhookData['amount'] ?? 0); // $currency = setting('wallet_default_currency', 'USD'); // Assuming default // // Check if this deposit was already processed via callback // // This requires a way to uniquely identify the deposit attempt. // Log::info("Paynow Webhook: Potential wallet deposit success for {$localRef}. Amount: {$amount}. Implement robust handling."); // } } } } --- File: D:\projects\digitalvocano\Modules\PaynowGateway\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\PaynowGateway\Providers\PaynowGatewayServiceProvider.php --- registerConfig(); $this->registerViews(); $this->loadMigrationsFrom(module_path($this->name, 'database/migrations')); $this->loadRoutesFrom(module_path($this->name, 'routes/web.php')); $this->loadRoutesFrom(module_path($this->name, 'routes/admin.php')); // $this->loadRoutesFrom(module_path($this->name, 'routes/api.php')); // If you have API routes } /** * Register the service provider. */ public function register(): void { // Register your PaynowService $this->app->singleton(\Modules\PaynowGateway\Services\PaynowService::class, function ($app) { return new \Modules\PaynowGateway\Services\PaynowService(); }); // You might register an EventServiceProvider if you have module-specific events/listeners // $this->app->register(EventServiceProvider::class); } /** * Register config. */ protected function registerConfig(): void { $this->publishes([ module_path($this->name, 'config/config.php') => config_path($this->nameLower . '.php'), ], 'config'); $this->mergeConfigFrom( module_path($this->name, 'config/config.php'), $this->nameLower ); } /** * 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); } private function getPublishableViewPaths(): array // Helper from your AuthorizeNetGatewayServiceProvider { // This logic might need adjustment based on your main app's view configuration // For simplicity, assuming it works as in your other modules. return [resource_path('views/modules/' . $this->nameLower)]; } } --- File: D:\projects\digitalvocano\Modules\PaynowGateway\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\PaynowGateway\resources\views\admin\config.blade.php --- @extends('layouts.admin') @section('title', 'Paynow Gateway Settings') @section('header_title', 'Paynow Gateway Settings') @section('content')
Price: {{ $plan->currency }} {{ number_format($plan->price, 2) }} / {{ $plan->interval }}
@if (session('error'))You will be redirected to Paynow to complete your payment.
{{-- If Paynow requires a form to be submitted to their endpoint, you would build that form here with the necessary hidden fields populated by $paymentDetails from your controller. Example: --}}If you are not redirected automatically, please click here.
You will be redirected to Paynow to complete your deposit.
If you are not redirected automatically, please click here.
Module: {!! config('paynowgateway.name') !!}