From 836aa83f41455d8ba6d87e936c9d9da3c96ed696 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Wed, 15 Jul 2026 21:04:09 +0000 Subject: [PATCH] Gate payment gateway behind Pro and Business plans. Free accounts keep cash sales only; connecting a gateway and card/MoMo checkout require a paid plan, with settings upsell copy for free users. --- .../Controllers/Pos/SettingsController.php | 7 ++ .../Payments/MerchantGatewayService.php | 22 ++++++ app/Services/Pos/SubscriptionService.php | 6 ++ config/pos.php | 2 +- resources/views/pos/settings.blade.php | 70 +++++++++++-------- tests/Feature/PosProTest.php | 58 +++++++++++++++ 6 files changed, 133 insertions(+), 32 deletions(-) diff --git a/app/Http/Controllers/Pos/SettingsController.php b/app/Http/Controllers/Pos/SettingsController.php index aa2ad3f..61e1ae1 100644 --- a/app/Http/Controllers/Pos/SettingsController.php +++ b/app/Http/Controllers/Pos/SettingsController.php @@ -53,6 +53,7 @@ class SettingsController extends Controller 'tables' => $this->scopeToLocation($request, PosTable::owned($owner)) ->orderBy('area')->orderBy('position')->orderBy('label')->get(), 'gateway' => $this->gateway->settingFor($account), + 'canUsePaymentGateway' => $this->subscriptions->canUsePaymentGateway($account), ]); } @@ -144,6 +145,7 @@ class SettingsController extends Controller /** * Upsert merchant gateway credentials for the acting account. * Blank secret fields keep the previously stored encrypted values. + * Free plan cannot connect or change gateway credentials. */ protected function persistGateway(Request $request, User $user): void { @@ -160,6 +162,11 @@ class SettingsController extends Controller return; } + if (! $this->subscriptions->canUsePaymentGateway($user)) { + // Ignore gateway fields on free plan (other settings may still save). + return; + } + $attributes = [ 'provider' => $provider !== '' ? $provider : $existing->provider, 'is_active' => $request->boolean('gateway_is_active'), diff --git a/app/Services/Payments/MerchantGatewayService.php b/app/Services/Payments/MerchantGatewayService.php index f41c599..dd7569b 100644 --- a/app/Services/Payments/MerchantGatewayService.php +++ b/app/Services/Payments/MerchantGatewayService.php @@ -4,6 +4,7 @@ namespace App\Services\Payments; use App\Models\PaymentGatewaySetting; use App\Models\User; +use App\Services\Pos\SubscriptionService; use Illuminate\Support\Facades\Http; use RuntimeException; @@ -18,6 +19,10 @@ class MerchantGatewayService public function isConfigured(User|string $owner): bool { + if (! $this->ownerMayUseGateway($owner)) { + return false; + } + return (bool) $this->settingFor($owner)?->isConfigured(); } @@ -62,6 +67,10 @@ class MerchantGatewayService protected function requireConfigured(User|string $owner): PaymentGatewaySetting { + if (! $this->ownerMayUseGateway($owner)) { + throw new RuntimeException('Connecting your own payment gateway requires a paid plan. Upgrade to Pro or Business.'); + } + $setting = $this->settingFor($owner); if (! $setting?->isConfigured()) { throw new RuntimeException('Connect Paystack, Flutterwave, or Hubtel in Settings before accepting online payments.'); @@ -70,6 +79,19 @@ class MerchantGatewayService return $setting; } + protected function ownerMayUseGateway(User|string $owner): bool + { + $user = $owner instanceof User + ? $owner + : User::query()->where('public_id', (string) $owner)->first(); + + if (! $user) { + return false; + } + + return app(SubscriptionService::class)->canUsePaymentGateway($user); + } + /** * @param array $metadata * @return array{checkout_url: string, reference: string, provider: string} diff --git a/app/Services/Pos/SubscriptionService.php b/app/Services/Pos/SubscriptionService.php index b13d019..215c66a 100644 --- a/app/Services/Pos/SubscriptionService.php +++ b/app/Services/Pos/SubscriptionService.php @@ -69,6 +69,12 @@ class SubscriptionService return $this->hasPaidPlan($user); } + /** Own payment gateway (Paystack / Flutterwave / Hubtel) requires Pro or Business. */ + public function canUsePaymentGateway(User $user): bool + { + return $this->hasPaidPlan($user); + } + public function productCount(string $ownerRef, bool $restaurant): int { if ($restaurant) { diff --git a/config/pos.php b/config/pos.php index 4653a7d..5fddda8 100644 --- a/config/pos.php +++ b/config/pos.php @@ -46,7 +46,7 @@ return [ 'upgrade_banner' => [ 'title' => 'Unlock POS Pro or Business', - 'description' => 'Unlimited registers & catalog, stock tracking & ecosystem sync — from GHS 790/mo.', + 'description' => 'Your own payment gateway, unlimited catalog, multi-branch & team — from GHS 790/mo.', 'route' => 'pos.pro.index', ], ]; diff --git a/resources/views/pos/settings.blade.php b/resources/views/pos/settings.blade.php index 153236c..ee7295a 100644 --- a/resources/views/pos/settings.blade.php +++ b/resources/views/pos/settings.blade.php @@ -123,43 +123,51 @@

Payment gateway

-

Connect Paystack, Flutterwave, or Hubtel. Online checkouts go 100% to you — 0% Ladill fee.

+

Connect Paystack, Flutterwave, or Hubtel. Online checkouts go 100% to you — 0% Ladill fee. Pro & Business plans only.

-
- - -
-
+ @if ($canUsePaymentGateway ?? false)
- - + + +
+
+
+ + +
+
+ + +
- - + +
-
-
- - -
- - @if ($gateway?->isConfigured()) -

Gateway connected ({{ ucfirst($gateway->provider) }}).

+ + @if ($gateway?->isConfigured()) +

Gateway connected ({{ ucfirst($gateway->provider) }}).

+ @else +

Online checkouts stay disabled until a gateway is connected.

+ @endif @else -

Online checkouts stay disabled until a gateway is connected.

+
+

Your 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.

+ View plans → +
@endif
diff --git a/tests/Feature/PosProTest.php b/tests/Feature/PosProTest.php index a003dd3..7d83509 100644 --- a/tests/Feature/PosProTest.php +++ b/tests/Feature/PosProTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\Http\Middleware\EnsurePlatformSession; use App\Models\Pos\ProSubscription; use App\Models\User; use App\Services\Pos\SubscriptionService; @@ -16,6 +17,7 @@ class PosProTest extends TestCase protected function setUp(): void { parent::setUp(); + $this->withoutMiddleware(EnsurePlatformSession::class); config([ 'billing.api_url' => 'https://ladill.com/api/billing', 'billing.api_key' => 'pos-billing-key', @@ -49,6 +51,62 @@ class PosProTest extends TestCase $this->assertFalse(app(SubscriptionService::class)->canUseRestaurantMode($this->user())); } + public function test_free_user_cannot_use_payment_gateway(): void + { + $user = $this->user(); + $svc = app(SubscriptionService::class); + + $this->assertFalse($svc->canUsePaymentGateway($user)); + $this->assertFalse(app(\App\Services\Payments\MerchantGatewayService::class)->isConfigured($user)); + } + + public function test_pro_user_can_use_payment_gateway_when_configured(): void + { + Http::fake(['*/debit' => Http::response(['id' => 1], 201)]); + $user = $this->user(); + $svc = app(SubscriptionService::class); + $svc->subscribe($user); + + \App\Models\PaymentGatewaySetting::create([ + 'owner_ref' => $user->public_id, + 'provider' => \App\Models\PaymentGatewaySetting::PROVIDER_PAYSTACK, + 'public_key' => 'pk_test', + 'secret_key' => 'sk_test', + 'is_active' => true, + ]); + + $this->assertTrue($svc->canUsePaymentGateway($user)); + $this->assertTrue(app(\App\Services\Payments\MerchantGatewayService::class)->isConfigured($user)); + } + + public function test_free_user_settings_ignores_gateway_credentials(): void + { + $user = $this->user(); + \App\Models\PosLocation::create([ + 'owner_ref' => $user->public_id, + 'name' => 'Main', + 'currency' => 'GHS', + ]); + + $this->actingAs($user) + ->put(route('pos.settings.update'), [ + 'name' => 'Main', + 'currency' => 'GHS', + 'service_style' => 'retail', + 'printer_paper_mm' => 80, + 'gateway_provider' => \App\Models\PaymentGatewaySetting::PROVIDER_PAYSTACK, + 'gateway_public_key' => 'pk_free_blocked', + 'gateway_secret_key' => 'sk_free_blocked', + 'gateway_is_active' => '1', + ]) + ->assertRedirect() + ->assertSessionHas('success'); + + $this->assertNull( + \App\Models\PaymentGatewaySetting::query()->where('owner_ref', $user->public_id)->first() + ); + } + public function test_subscribe_enterprise_charges_wallet_and_activates(): void { Http::fake(['*/debit' => Http::response(['id' => 1], 201)]);