From 5cdc0a046d365db1bd8f1efc2115cfa170cb07ca Mon Sep 17 00:00:00 2001 From: isaacclad Date: Tue, 21 Jul 2026 14:55:14 +0000 Subject: [PATCH] Use Paystack/Ladill Pay on Mini payment landing. Drop the required MTN MoMo number field and MoMo copy so public payment QRs open Paystack checkout via Ladill Pay like the other restored storefronts. Co-authored-by: Cursor --- .../Controllers/Public/PaymentController.php | 2 +- app/Services/Mini/MiniPaymentService.php | 16 +-- resources/js/app.js | 11 +- .../views/mini/payment-qrs/index.blade.php | 2 +- .../views/public/qr/payment-landing.blade.php | 28 +---- tests/Feature/MiniPaymentCheckoutTest.php | 104 ++++++++++++++++++ 6 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 tests/Feature/MiniPaymentCheckoutTest.php diff --git a/app/Http/Controllers/Public/PaymentController.php b/app/Http/Controllers/Public/PaymentController.php index 311bea5..8877098 100644 --- a/app/Http/Controllers/Public/PaymentController.php +++ b/app/Http/Controllers/Public/PaymentController.php @@ -27,7 +27,7 @@ class PaymentController extends Controller $validated = $request->validate([ 'amount' => 'required|numeric|min:0.01', - 'customer_phone' => 'required|string|max:32', + 'customer_phone' => 'nullable|string|max:32', ]); try { diff --git a/app/Services/Mini/MiniPaymentService.php b/app/Services/Mini/MiniPaymentService.php index a0fa4a8..f6b30d3 100644 --- a/app/Services/Mini/MiniPaymentService.php +++ b/app/Services/Mini/MiniPaymentService.php @@ -40,9 +40,6 @@ class MiniPaymentService } $customerPhone = trim((string) ($data['customer_phone'] ?? '')); - if ($customerPhone === '') { - throw new RuntimeException('Enter your MoMo number to pay.'); - } $qrCode->loadMissing('user'); $amountMinor = (int) round($amountGhs * 100); @@ -57,19 +54,18 @@ class MiniPaymentService 'currency' => $qrCode->content()['currency'] ?? 'GHS', 'payer_name' => null, 'payer_email' => null, - 'payer_phone' => $customerPhone, + 'payer_phone' => $customerPhone !== '' ? $customerPhone : null, 'payer_note' => null, 'status' => MiniPayment::STATUS_PENDING, 'payment_reference' => null, ]); - $payOrder = $this->pay->createCheckout([ + $checkout = [ 'merchant' => $qrCode->user->public_id, 'fee_tier' => 'payments', 'source_service' => 'mini', 'source_ref' => (string) $qrCode->id, 'callback_url' => LadillLink::path($qrCode->short_code, 'pay/callback'), - 'customer_phone' => $customerPhone, 'line_items' => [ [ 'name' => $businessName, @@ -82,7 +78,13 @@ class MiniPaymentService 'mini_reference' => $reference, 'qr_code_id' => $qrCode->id, ], - ]); + ]; + + if ($customerPhone !== '') { + $checkout['customer_phone'] = $customerPhone; + } + + $payOrder = $this->pay->createCheckout($checkout); $payment->update([ 'pay_order_id' => $payOrder['id'] ?? null, diff --git a/resources/js/app.js b/resources/js/app.js index db90b0c..dea4856 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -160,7 +160,6 @@ function mobileKeyboardBottomOffset() { Alpine.data('miniPaymentLanding', (config = {}) => ({ amount: config.amount ?? '', - phone: config.phone ?? '', loading: false, errorMsg: config.errorMsg ?? '', showSheet: false, @@ -216,12 +215,6 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ return; } - const phone = String(this.phone || '').trim(); - if (!phone) { - this.errorMsg = 'Enter your MoMo number to pay.'; - return; - } - this.errorMsg = ''; this.loading = true; @@ -237,7 +230,7 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ 'X-CSRF-TOKEN': config.csrf, 'X-Requested-With': 'XMLHttpRequest', }, - body: JSON.stringify({ amount: value, customer_phone: phone }), + body: JSON.stringify({ amount: value }), signal: controller.signal, }); const data = await res.json().catch(() => ({})); @@ -253,7 +246,7 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ return; } - // MoMo waiting page must be full-page — never trap it in the Paystack iframe + // Legacy MoMo waiting page must be full-page — never trap it in the Paystack iframe // (cross-origin iframe is blank / looks like "Pay did nothing"). const useSheet = data.provider !== 'mtn_momo' && window.innerWidth < 768; if (useSheet) { diff --git a/resources/views/mini/payment-qrs/index.blade.php b/resources/views/mini/payment-qrs/index.blade.php index f2adfc4..6aef6df 100644 --- a/resources/views/mini/payment-qrs/index.blade.php +++ b/resources/views/mini/payment-qrs/index.blade.php @@ -22,7 +22,7 @@
- MoMo · Card · Ladill Pay + Card · MoMo · Ladill Pay

Your payment QRs

diff --git a/resources/views/public/qr/payment-landing.blade.php b/resources/views/public/qr/payment-landing.blade.php index f4b7af7..68b4c69 100644 --- a/resources/views/public/qr/payment-landing.blade.php +++ b/resources/views/public/qr/payment-landing.blade.php @@ -22,7 +22,6 @@ payUrl: @js($payUrl), csrf: @js($csrf), amount: @js(old('amount')), - phone: @js(old('customer_phone')), errorMsg: @js(session('error')), })"> @@ -64,12 +63,6 @@ placeholder="0.00" value="{{ old('amount') }}">

- - - -

Pay with MTN MoMo. Powered by Ladill Pay

+

Secured by Paystack. Powered by Ladill Pay

@@ -113,12 +106,6 @@ placeholder="0.00" value="{{ old('amount') }}"> - - - -

Pay with MTN MoMo. Powered by Ladill Pay

+

Secured by Paystack. Powered by Ladill Pay

@@ -146,7 +133,6 @@ // Re-register so Pay works even when Blade deploys ahead of an old Vite bundle. Alpine.data('miniPaymentLanding', (config = {}) => ({ amount: config.amount ?? '', - phone: config.phone ?? '', loading: false, errorMsg: config.errorMsg ?? '', showSheet: false, @@ -178,11 +164,6 @@ this.errorMsg = 'Enter an amount greater than zero.'; return; } - const phone = String(this.phone || '').trim(); - if (!phone) { - this.errorMsg = 'Enter your MoMo number to pay.'; - return; - } this.errorMsg = ''; this.loading = true; const controller = new AbortController(); @@ -196,7 +177,7 @@ 'X-CSRF-TOKEN': config.csrf, 'X-Requested-With': 'XMLHttpRequest', }, - body: JSON.stringify({ amount: value, customer_phone: phone }), + body: JSON.stringify({ amount: value }), signal: controller.signal, }); const data = await res.json().catch(() => ({})); @@ -211,8 +192,7 @@ this.errorMsg = 'Could not start payment. Please try again.'; return; } - // MoMo waiting page must be full-page — never trap it in the Paystack iframe - // (cross-origin iframe is blank / "does nothing"). + // Legacy MoMo waiting page must be full-page — never trap it in the Paystack iframe. const useSheet = data.provider !== 'mtn_momo' && window.innerWidth < 768; if (useSheet) { this.checkoutUrl = data.checkout_url; diff --git a/tests/Feature/MiniPaymentCheckoutTest.php b/tests/Feature/MiniPaymentCheckoutTest.php new file mode 100644 index 0000000..47925dc --- /dev/null +++ b/tests/Feature/MiniPaymentCheckoutTest.php @@ -0,0 +1,104 @@ + (string) Str::uuid(), + 'name' => 'Vendor', + 'email' => 'vendor@example.com', + ]); + + $qr = QrCode::query()->create([ + 'user_id' => $user->id, + 'short_code' => 'payland01', + 'type' => QrCode::TYPE_PAYMENT, + 'label' => 'Till', + 'payload' => [ + 'content' => [ + 'business_name' => 'Accra Kiosk', + 'currency' => 'GHS', + ], + 'style' => [], + ], + 'is_active' => true, + ]); + + $this->withHeader(\App\Support\LadillLink::INTERNAL_HEADER, '1') + ->get('/q/'.$qr->short_code) + ->assertOk() + ->assertSee('Secured by Paystack. Powered by Ladill Pay', false) + ->assertDontSee('MTN MoMo number', false) + ->assertDontSee('Pay with MTN MoMo', false); + } + + public function test_pay_initiation_does_not_require_phone_and_returns_paystack_checkout(): void + { + $user = User::create([ + 'public_id' => (string) Str::uuid(), + 'name' => 'Vendor', + 'email' => 'vendor@example.com', + ]); + + $qr = QrCode::query()->create([ + 'user_id' => $user->id, + 'short_code' => 'payinit01', + 'type' => QrCode::TYPE_PAYMENT, + 'label' => 'Till', + 'payload' => [ + 'content' => [ + 'business_name' => 'Accra Kiosk', + 'currency' => 'GHS', + ], + 'style' => [], + ], + 'is_active' => true, + ]); + + $pay = Mockery::mock(PayClient::class); + $pay->shouldReceive('createCheckout') + ->once() + ->with(Mockery::on(function (array $payload): bool { + return ($payload['source_service'] ?? '') === 'mini' + && ! array_key_exists('customer_phone', $payload) + && ($payload['line_items'][0]['unit_price_minor'] ?? 0) === 2500; + })) + ->andReturn([ + 'id' => 99, + 'reference' => 'LP-TESTMINIPAY001', + 'checkout_url' => 'https://checkout.paystack.com/mini-test', + 'provider' => 'paystack', + 'platform_fee_minor' => 38, + 'merchant_amount_minor' => 2462, + ]); + $this->app->instance(PayClient::class, $pay); + + $this->withHeader(\App\Support\LadillLink::INTERNAL_HEADER, '1') + ->postJson('/q/'.$qr->short_code.'/pay', [ + 'amount' => 25, + ]) + ->assertOk() + ->assertJsonPath('checkout_url', 'https://checkout.paystack.com/mini-test') + ->assertJsonPath('provider', 'paystack'); + + $this->assertDatabaseHas('mini_payments', [ + 'qr_code_id' => $qr->id, + 'amount_minor' => 2500, + 'payment_reference' => 'LP-TESTMINIPAY001', + 'payer_phone' => null, + ]); + } +}