diff --git a/app/Models/PaymentGatewaySetting.php b/app/Models/PaymentGatewaySetting.php index 7c34791..7d378a6 100644 --- a/app/Models/PaymentGatewaySetting.php +++ b/app/Models/PaymentGatewaySetting.php @@ -39,12 +39,14 @@ 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 => $public !== '' && $secret !== '', + self::PROVIDER_HUBTEL => $public !== '' && $secret !== '', + default => false, + }; } } diff --git a/app/Services/Payments/MerchantGatewayService.php b/app/Services/Payments/MerchantGatewayService.php index dd7569b..87717f2 100644 --- a/app/Services/Payments/MerchantGatewayService.php +++ b/app/Services/Payments/MerchantGatewayService.php @@ -4,12 +4,15 @@ namespace App\Services\Payments; use App\Models\PaymentGatewaySetting; use App\Models\User; +use App\Services\Pay\PayClient; use App\Services\Pos\SubscriptionService; use Illuminate\Support\Facades\Http; use RuntimeException; class MerchantGatewayService { + public function __construct(private readonly PayClient $pay) {} + public function settingFor(User|string $owner): ?PaymentGatewaySetting { $ownerRef = $owner instanceof User ? (string) $owner->public_id : $owner; @@ -19,11 +22,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(); } /** @@ -39,15 +44,25 @@ class MerchantGatewayService string $reference, array $metadata = [], ): array { - $setting = $this->requireConfigured($owner); $currency = strtoupper($currency ?: 'GHS'); + $email = (string) config('pay.mini_checkout_email', 'pay@ladill.com'); - return match ($setting->provider) { - PaymentGatewaySetting::PROVIDER_PAYSTACK => $this->paystackInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata), - PaymentGatewaySetting::PROVIDER_FLUTTERWAVE => $this->flutterwaveInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata), - PaymentGatewaySetting::PROVIDER_HUBTEL => $this->hubtelInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata), - default => throw new RuntimeException('Unsupported payment provider.'), - }; + if (! $this->shouldUseOwnerGateway($owner)) { + return $this->ladillPayInitialize($owner, $amountMinor, $email, $callbackUrl, $reference, $metadata); + } + + $setting = $this->requireConfigured($owner); + + try { + return match ($setting->provider) { + PaymentGatewaySetting::PROVIDER_PAYSTACK => $this->paystackInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata), + PaymentGatewaySetting::PROVIDER_FLUTTERWAVE => $this->flutterwaveInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata), + PaymentGatewaySetting::PROVIDER_HUBTEL => $this->hubtelInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata), + default => throw new RuntimeException('Unsupported payment provider.'), + }; + } catch (\Throwable) { + return $this->ladillPayInitialize($owner, $amountMinor, $email, $callbackUrl, $reference, $metadata); + } } /** @@ -55,7 +70,24 @@ class MerchantGatewayService */ public function verify(User|string $owner, string $reference): array { - $setting = $this->requireConfigured($owner); + if (str_starts_with($reference, 'LP-')) { + $result = $this->pay->verify($reference); + + return [ + 'paid' => in_array(strtolower((string) ($result['status'] ?? '')), ['paid', 'success'], true), + 'amount_minor' => (int) ($result['amount_minor'] ?? 0), + 'reference' => (string) ($result['reference'] ?? $reference), + 'provider' => 'ladill_pay', + 'raw' => $result, + ]; + } + + // Verification intentionally ignores current plan state so payments + // initiated before a downgrade can still complete safely. + $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), @@ -79,6 +111,32 @@ class MerchantGatewayService return $setting; } + /** @return array{checkout_url:string,reference:string,provider:string} */ + private function ladillPayInitialize(User|string $owner, int $amountMinor, string $email, string $callbackUrl, string $reference, array $metadata): array + { + $ownerRef = $owner instanceof User ? (string) $owner->public_id : (string) $owner; + $checkout = $this->pay->createCheckout([ + 'merchant' => $ownerRef, + 'fee_tier' => 'sales', + 'source_service' => 'pos', + 'source_ref' => (string) ($metadata['pos_sale_id'] ?? $metadata['pos_payment_id'] ?? $reference), + 'callback_url' => $callbackUrl, + 'customer_email' => $email, + 'line_items' => [[ + 'name' => (string) ($metadata['title'] ?? 'POS payment'), + 'unit_price_minor' => $amountMinor, + 'quantity' => 1, + ]], + 'metadata' => $metadata, + ]); + + return [ + 'checkout_url' => (string) $checkout['checkout_url'], + 'reference' => (string) $checkout['reference'], + 'provider' => 'ladill_pay', + ]; + } + protected function ownerMayUseGateway(User|string $owner): bool { $user = $owner instanceof User diff --git a/resources/views/pos/settings.blade.php b/resources/views/pos/settings.blade.php index e079820..649b7a3 100644 --- a/resources/views/pos/settings.blade.php +++ b/resources/views/pos/settings.blade.php @@ -123,16 +123,21 @@
Payment gateway
-Connect Paystack, Flutterwave, or Hubtel. Online checkouts go 100% to you — 0% Ladill fee. Pro & Business plans only.
+Ladill Pay is recommended, selected by default, and needs no API keys. Pro and Business can optionally use an owner gateway.
+Ladill Pay — recommended default
+Zero-config card and MoMo checkout. It remains the safe fallback whenever an owner gateway is unavailable.
Gateway connected ({{ ucfirst($gateway->provider) }}).
@else -Online checkouts stay disabled until a gateway is connected.
+Ladill Pay remains active. Add valid credentials only if you want direct owner-gateway settlement.
@endif @elseYour own payment gateway is a Pro feature
-Upgrade to Pro or Business to connect Paystack, Flutterwave, or Hubtel and take card / MoMo payments with 0% Ladill fee. Cash sales stay free.
+Free uses Ladill Pay automatically. Upgrade to optionally connect Paystack, Flutterwave, or Hubtel.
View plans →