type !== QrCode::TYPE_PAYMENT) { throw new RuntimeException('This QR is not a payment code.'); } $amountGhs = round((float) ($data['amount'] ?? 0), 2); if ($amountGhs <= 0) { throw new RuntimeException('Enter an amount greater than zero.'); } $customerPhone = trim((string) ($data['customer_phone'] ?? '')); $qrCode->loadMissing('user'); $amountMinor = (int) round($amountGhs * 100); $reference = 'MINP-'.strtoupper(Str::random(16)); $businessName = $qrCode->content()['business_name'] ?? $qrCode->label ?? 'Payment'; $payment = MiniPayment::create([ 'qr_code_id' => $qrCode->id, 'user_id' => $qrCode->user_id, 'reference' => $reference, 'amount_minor' => $amountMinor, 'currency' => $qrCode->content()['currency'] ?? 'GHS', 'payer_name' => null, 'payer_email' => null, 'payer_phone' => $customerPhone !== '' ? $customerPhone : null, 'payer_note' => null, 'status' => MiniPayment::STATUS_PENDING, 'payment_reference' => null, ]); $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'), 'line_items' => [ [ 'name' => $businessName, 'unit_price_minor' => $amountMinor, 'quantity' => 1, ], ], 'metadata' => [ 'mini_payment_id' => $payment->id, '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, 'payment_reference' => $payOrder['reference'], 'platform_fee_minor' => $payOrder['platform_fee_minor'] ?? null, 'merchant_amount_minor' => $payOrder['merchant_amount_minor'] ?? null, ]); $checkoutUrl = (string) ($payOrder['checkout_url'] ?? ''); if ($checkoutUrl === '') { throw new RuntimeException('Could not start checkout. Please try again.'); } return [ 'payment' => $payment->fresh(), 'checkout_url' => $checkoutUrl, 'provider' => (string) ($payOrder['provider'] ?? ''), ]; } public function complete(string $paymentReference): MiniPayment { if (str_starts_with($paymentReference, 'LP-')) { return $this->completeLadillPay($paymentReference); } return $this->completeLegacy($paymentReference); } private function completeLadillPay(string $reference): MiniPayment { $payment = MiniPayment::where('payment_reference', $reference) ->where('status', MiniPayment::STATUS_PENDING) ->firstOrFail(); $payOrder = $this->pay->verify($reference); $payment->update([ 'status' => MiniPayment::STATUS_PAID, 'amount_minor' => (int) ($payOrder['amount_minor'] ?? $payment->amount_minor), 'platform_fee_minor' => (int) ($payOrder['platform_fee_minor'] ?? 0), 'merchant_amount_minor' => (int) ($payOrder['merchant_amount_minor'] ?? 0), 'pay_order_id' => $payOrder['id'] ?? $payment->pay_order_id, 'paid_at' => now(), 'metadata' => array_merge((array) $payment->metadata, ['ladill_pay' => $payOrder]), ]); $payment = $payment->fresh(['qrCode', 'merchant']); $this->notifyPayer($payment); $this->notifications->paymentReceived($payment); $this->autoWithdraw->attemptForUser($payment->merchant); return $payment; } /** Legacy MIN-* references before Ladill Pay migration. */ private function completeLegacy(string $paymentReference): MiniPayment { $payment = MiniPayment::where('payment_reference', $paymentReference) ->where('status', MiniPayment::STATUS_PENDING) ->firstOrFail(); $data = $this->paystack->verifyTransaction($paymentReference); if (($data['status'] ?? '') !== 'success') { $payment->update(['status' => MiniPayment::STATUS_FAILED]); throw new RuntimeException('Payment was not successful.'); } $paidMinor = (int) ($data['amount'] ?? $payment->amount_minor); $platformFeeMinor = (int) round($paidMinor * MiniPayment::PLATFORM_FEE_RATE); $merchantMinor = $paidMinor - $platformFeeMinor; $payment->update([ 'status' => MiniPayment::STATUS_PAID, 'amount_minor' => $paidMinor, 'platform_fee_minor' => $platformFeeMinor, 'merchant_amount_minor' => $merchantMinor, 'paid_at' => now(), 'metadata' => array_merge((array) $payment->metadata, ['paystack' => $data, 'legacy' => true]), ]); $businessName = $payment->qrCode?->content()['business_name'] ?? $payment->qrCode?->label ?? 'Payment QR'; $this->billing->credit( $payment->merchant->public_id, $merchantMinor, 'mini', 'pay', $paymentReference, $payment->id, sprintf('Payment via %s', $businessName), ); $payment = $payment->fresh(['qrCode', 'merchant']); $this->notifyPayer($payment); $this->notifications->paymentReceived($payment); $this->autoWithdraw->attemptForUser($payment->merchant); return $payment; } private function notifyPayer(MiniPayment $payment): void { if (! $payment->payer_phone) { return; } $businessName = $payment->qrCode?->content()['business_name'] ?? $payment->qrCode?->label ?? 'Payment QR'; $this->sms->send( $payment->payer_phone, sprintf( 'Payment of %s %s to %s confirmed. Ref: %s', $payment->currency, number_format($payment->amount_minor / 100, 2), $businessName, $payment->reference ) ); } }