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 @@
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
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, + ]); + } +}