Files
ladill-give/app/Services/Payments/MerchantGatewayService.php
isaacclad c1b6a418ab
Deploy Ladill Give / deploy (push) Successful in 44s
Fix proxied checkout CSRF for Merchant, Give, Invoice and Events.
Public order/pay POSTs hit ladl.link first; satellite CSRF tokens cannot bind
to Link (or platform/satellite) sessions, which returned 419 and showed
"Could not start checkout." Except public checkout paths and return
access_code from owner-gateway Paystack init. Sync contained Paystack sheet.
2026-07-21 22:54:01 +00:00

89 lines
3.6 KiB
PHP

<?php
namespace App\Services\Payments;
use App\Models\PaymentGatewaySetting;
use App\Models\User;
use App\Services\Billing\PlanEntitlementService;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class MerchantGatewayService
{
public function __construct(private readonly PlanEntitlementService $entitlements) {}
public function settingFor(User|string $owner): ?PaymentGatewaySetting
{
$ownerRef = $owner instanceof User ? (string) $owner->public_id : (string) $owner;
return PaymentGatewaySetting::query()->where('owner_ref', $ownerRef)->first();
}
public function shouldUseOwnerGateway(User $owner): bool
{
return $this->entitlements->canUseCustomPaymentGateway($owner)
&& (bool) $this->settingFor($owner)?->isConfigured();
}
/** @return array{checkout_url:string,reference:string,provider:string} */
public function initialize(User $owner, int $amountMinor, string $currency, string $email, string $callbackUrl, string $reference, array $metadata = []): array
{
if (! $this->shouldUseOwnerGateway($owner)) {
throw new RuntimeException('Owner gateway is not available.');
}
$setting = $this->settingFor($owner);
$response = Http::withToken((string) $setting->secret_key)->acceptJson()->timeout(20)
->post('https://api.paystack.co/transaction/initialize', [
'email' => $email !== '' ? $email : 'pay@ladill.com',
'amount' => $amountMinor,
'currency' => strtoupper($currency ?: 'GHS'),
'reference' => $reference,
'callback_url' => $callbackUrl,
'metadata' => $metadata,
]);
if (! $response->successful() || ! ($response->json('status') ?? false) || ! $response->json('data.authorization_url')) {
throw new RuntimeException($response->json('message') ?: 'Paystack could not start checkout.');
}
$accessCode = (string) ($response->json('data.access_code') ?? '');
$checkoutUrl = (string) $response->json('data.authorization_url');
if ($accessCode === '' && $checkoutUrl !== '') {
$path = ltrim((string) (parse_url($checkoutUrl, PHP_URL_PATH) ?: ''), '/');
$maybe = explode('/', $path)[0] ?? '';
if (preg_match('/^[A-Za-z0-9_-]{6,}$/', $maybe) === 1) {
$accessCode = $maybe;
}
}
return [
'checkout_url' => $checkoutUrl,
'access_code' => $accessCode !== '' ? $accessCode : null,
'reference' => (string) ($response->json('data.reference') ?: $reference),
'provider' => PaymentGatewaySetting::PROVIDER_PAYSTACK,
];
}
/** @return array<string,mixed> */
public function verify(User|string $owner, string $reference): array
{
$setting = $this->settingFor($owner);
if (! $setting?->isConfigured()) {
throw new RuntimeException('The owner gateway used for this payment is no longer configured.');
}
$response = Http::withToken((string) $setting->secret_key)->acceptJson()->timeout(20)
->get('https://api.paystack.co/transaction/verify/'.rawurlencode($reference));
$data = (array) ($response->json('data') ?? []);
return [
'paid' => ($response->json('status') ?? false) && strtolower((string) ($data['status'] ?? '')) === 'success',
'amount_minor' => (int) ($data['amount'] ?? 0),
'reference' => (string) ($data['reference'] ?? $reference),
'provider' => PaymentGatewaySetting::PROVIDER_PAYSTACK,
'raw' => $data,
];
}
}