From 1bcbdbfdb113231b89339b283eb26b2f3e2c35e5 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Tue, 21 Jul 2026 19:47:10 +0000 Subject: [PATCH] Make Events payment routing plan-aware. Co-authored-by: Cursor --- app/Http/Controllers/Qr/AccountController.php | 11 ++- app/Models/PaymentGatewaySetting.php | 11 +-- .../Events/EventRegistrationService.php | 69 +++++++++++++------ .../Payments/MerchantGatewayService.php | 17 +++-- resources/views/qr/account/settings.blade.php | 26 ++++++- tests/Feature/EventsProTest.php | 22 ++++++ 6 files changed, 119 insertions(+), 37 deletions(-) diff --git a/app/Http/Controllers/Qr/AccountController.php b/app/Http/Controllers/Qr/AccountController.php index 65d949d..2cb8baf 100644 --- a/app/Http/Controllers/Qr/AccountController.php +++ b/app/Http/Controllers/Qr/AccountController.php @@ -174,12 +174,17 @@ class AccountController extends Controller ); $provider = (string) ($data['gateway_provider'] ?? ''); - if ($provider !== '' && $this->subscriptions->canUsePaymentGateway($account)) { + if ($this->subscriptions->canUsePaymentGateway($account)) { $existing = PaymentGatewaySetting::query()->firstOrNew([ 'owner_ref' => $account->public_id, ]); - $existing->provider = $provider; - $existing->is_active = $request->boolean('gateway_is_active', true); + if ($provider === '' && ! $existing->exists) { + return redirect()->route('account.settings')->with('success', 'Settings saved.'); + } + if ($provider !== '') { + $existing->provider = $provider; + } + $existing->is_active = $provider !== '' && $request->boolean('gateway_is_active'); if (filled($data['gateway_public_key'] ?? null)) { $existing->public_key = $data['gateway_public_key']; } diff --git a/app/Models/PaymentGatewaySetting.php b/app/Models/PaymentGatewaySetting.php index 7c34791..72e0d40 100644 --- a/app/Models/PaymentGatewaySetting.php +++ b/app/Models/PaymentGatewaySetting.php @@ -39,12 +39,13 @@ class PaymentGatewaySetting extends Model return false; } + $public = trim((string) $this->public_key); $secret = trim((string) $this->secret_key); - return $secret !== '' && in_array($this->provider, [ - self::PROVIDER_PAYSTACK, - self::PROVIDER_FLUTTERWAVE, - self::PROVIDER_HUBTEL, - ], true); + return match ($this->provider) { + self::PROVIDER_PAYSTACK => str_starts_with($public, 'pk_') && str_starts_with($secret, 'sk_'), + self::PROVIDER_FLUTTERWAVE, self::PROVIDER_HUBTEL => $public !== '' && $secret !== '', + default => false, + }; } } diff --git a/app/Services/Events/EventRegistrationService.php b/app/Services/Events/EventRegistrationService.php index 2e80d7c..b50216d 100644 --- a/app/Services/Events/EventRegistrationService.php +++ b/app/Services/Events/EventRegistrationService.php @@ -133,30 +133,54 @@ class EventRegistrationService $feeTier = $mode === 'contributions' ? 'donations' : 'sales'; $callbackUrl = LadillLink::path($qrCode->short_code, 'register/callback'); - $payOrder = $this->pay->createCheckout([ - 'merchant' => $qrCode->user->public_id, - 'fee_tier' => $feeTier, - 'source_service' => 'events', - 'source_ref' => (string) $qrCode->id, - 'callback_url' => $callbackUrl, - 'customer_name' => $name, - 'customer_email' => $email, - 'customer_phone' => $registration->attendee_phone, - 'line_items' => [ - [ + $usesOwnerGateway = $this->gateway->shouldUseOwnerGateway($qrCode->user); + $payOrder = null; + if ($usesOwnerGateway) { + try { + $payOrder = $this->gateway->initializeCheckout( + $qrCode->user, + $amountMinor, + (string) ($content['currency'] ?? 'GHS'), + (string) config('pay.mini_checkout_email', 'pay@ladill.com'), + $callbackUrl, + 'EVTP-'.strtoupper(Str::random(16)), + [ + 'title' => $lineName, + 'registration_id' => $registration->id, + 'registration_reference' => $registration->reference, + 'qr_code_id' => $qrCode->id, + 'mode' => $mode, + 'attendee_email' => $email, + ], + ); + } catch (\Throwable) { + $usesOwnerGateway = false; + } + } + if (! $usesOwnerGateway) { + $payOrder = $this->pay->createCheckout([ + 'merchant' => $qrCode->user->public_id, + 'fee_tier' => $feeTier, + 'source_service' => 'events', + 'source_ref' => (string) $qrCode->id, + 'callback_url' => $callbackUrl, + 'customer_name' => $name, + 'customer_email' => $email, + 'customer_phone' => $registration->attendee_phone, + 'line_items' => [[ 'name' => $lineName, 'unit_price_minor' => $amountMinor, 'quantity' => 1, + ]], + 'metadata' => [ + 'registration_id' => $registration->id, + 'registration_reference' => $registration->reference, + 'qr_code_id' => $qrCode->id, + 'mode' => $mode, + 'attendee_email' => $email, ], - ], - 'metadata' => [ - 'registration_id' => $registration->id, - 'registration_reference' => $registration->reference, - 'qr_code_id' => $qrCode->id, - 'mode' => $mode, - 'attendee_email' => $email, - ], - ]); + ]); + } $checkoutUrl = (string) ($payOrder['checkout_url'] ?? ''); if ($checkoutUrl === '') { @@ -164,10 +188,11 @@ class EventRegistrationService } $registration->update([ - 'pay_order_id' => $payOrder['id'] ?? null, + 'pay_order_id' => $usesOwnerGateway ? null : ($payOrder['id'] ?? null), 'payment_reference' => $payOrder['reference'], 'metadata' => array_merge((array) $registration->metadata, [ - 'ladill_pay_init' => [ + ($usesOwnerGateway ? 'merchant_gateway_init' : 'ladill_pay_init') => [ + 'provider' => $payOrder['provider'] ?? 'ladill_pay', 'platform_fee_minor' => $payOrder['platform_fee_minor'] ?? null, 'merchant_amount_minor' => $payOrder['merchant_amount_minor'] ?? null, ], diff --git a/app/Services/Payments/MerchantGatewayService.php b/app/Services/Payments/MerchantGatewayService.php index 14da68a..38633bc 100644 --- a/app/Services/Payments/MerchantGatewayService.php +++ b/app/Services/Payments/MerchantGatewayService.php @@ -19,11 +19,13 @@ class MerchantGatewayService public function isConfigured(User|string $owner): bool { - if (! $this->ownerMayUseGateway($owner)) { - return false; - } + return $this->shouldUseOwnerGateway($owner); + } - return (bool) $this->settingFor($owner)?->isConfigured(); + public function shouldUseOwnerGateway(User|string $owner): bool + { + return $this->ownerMayUseGateway($owner) + && (bool) $this->settingFor($owner)?->isConfigured(); } /** @@ -55,7 +57,12 @@ class MerchantGatewayService */ public function verify(User|string $owner, string $reference): array { - $setting = $this->requireConfigured($owner); + // Do not re-check the current plan here: an owner-gateway payment + // initiated before a downgrade must remain verifiable. + $setting = $this->settingFor($owner); + if (! $setting?->isConfigured()) { + throw new RuntimeException('The payment gateway used for this transaction is no longer configured.'); + } return match ($setting->provider) { PaymentGatewaySetting::PROVIDER_PAYSTACK => $this->paystackVerify($setting, $reference), diff --git a/resources/views/qr/account/settings.blade.php b/resources/views/qr/account/settings.blade.php index c4d89a0..ce327e0 100644 --- a/resources/views/qr/account/settings.blade.php +++ b/resources/views/qr/account/settings.blade.php @@ -272,9 +272,31 @@

Online payments

-

Ticket and contribution payments are processed by Ladill Pay (Paystack). Funds settle to your Ladill wallet — no API keys required.

+

Ladill Pay is the recommended default and needs no API keys. Pro and Business may optionally select an owner gateway.

-

Ladill Pay is active for paid tickets and contributions on all plans.

+

Ladill Pay is active on all plans and remains the automatic fallback.

+ @if ($canUsePaymentGateway ?? false) + +
+ + +
+ + +

Incomplete or invalid credentials automatically fall back to Ladill Pay.

+ @else +

Free uses Ladill Pay only. Upgrade to Pro to optionally connect your own gateway.

+ @endif