all(); $ipnMessage = $input['ipn_track_id'] ?? null; $verification = $this->verifyIPN($request); if (!$verification) { Log::warning('PayPal IPN verification failed', ['ipn_track_id' => $ipnMessage]); return response('Invalid IPN', 400); } $eventType = $input['payment_status'] ?? null; $customId = $input['custom'] ?? null; if (!$eventType || !$customId) { Log::warning('Missing event type or custom ID in PayPal IPN', ['input' => $input]); return response('Invalid data', 400); } if ($eventType !== 'Completed') { Log::info('Non-completed PayPal event ignored', ['event' => $eventType, 'ipn_track_id' => $ipnMessage]); return response('OK', 200); } try { $metadata = json_decode($customId, true); if (!$metadata || !isset($metadata['tenant_id'], $metadata['package_id'])) { throw new Exception('Invalid metadata'); } $tenant = Tenant::find($metadata['tenant_id']); $package = Package::find($metadata['package_id']); if (!$tenant || !$package) { throw new Exception('Tenant or package not found'); } // Idempotent: Check if already processed $existingPurchase = PackagePurchase::where('tenant_id', $tenant->id) ->where('package_id', $package->id) ->where('provider_id', 'paypal') ->where('purchased_at', '>=', now()->subDay()) // Recent to avoid duplicates ->first(); if ($existingPurchase) { Log::info('PayPal purchase already processed', ['purchase_id' => $existingPurchase->id]); return response('OK', 200); } // Activate package TenantPackage::updateOrCreate( [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ], [ 'active' => true, 'purchased_at' => now(), 'expires_at' => now()->addYear(), ] ); // Log purchase PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => 'paypal', 'price' => $package->price, 'type' => $package->type, 'purchased_at' => now(), 'refunded' => false, ]); $tenant->update(['subscription_status' => 'active']); Log::info('PayPal purchase processed successfully', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'ipn_track_id' => $ipnMessage, ]); return response('OK', 200); } catch (Exception $e) { Log::error('PayPal webhook processing error: ' . $e->getMessage(), [ 'input' => $input, 'ipn_track_id' => $ipnMessage, ]); return response('Error', 500); } } private function verifyIPN(Request $request) { $rawBody = $request->getContent(); $params = $request->all(); // For sandbox, post to PayPal verify endpoint $verifyParams = array_merge($params, ['cmd' => '_notify-validate']); $response = file_get_contents('https://ipnpb.paypal.com/cgi-bin/webscr', false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', 'content' => http_build_query($verifyParams), ], ])); if ($response === false) { Log::error('PayPal IPN verification request failed'); return false; } return trim($response) === 'VERIFIED'; } }