132 lines
4.4 KiB
PHP
132 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Log;
|
|
use PayPal\PayPalHttp\Client;
|
|
use PayPal\Checkout\Orders\OrdersGetRequest;
|
|
use App\Models\TenantPackage;
|
|
use App\Models\PackagePurchase;
|
|
use App\Models\Package;
|
|
use App\Models\Tenant;
|
|
use Exception;
|
|
|
|
class PayPalWebhookController extends Controller
|
|
{
|
|
public function handle(Request $request)
|
|
{
|
|
$input = $request->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';
|
|
}
|
|
}
|