Files
ladill-mini/app/Services/Mini/MiniPaymentService.php
T
isaaccladandCursor 5cdc0a046d
Deploy Ladill Mini / deploy (push) Successful in 1m23s
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 <cursoragent@cursor.com>
2026-07-21 14:55:14 +00:00

209 lines
7.3 KiB
PHP

<?php
namespace App\Services\Mini;
use App\Models\MiniPayment;
use App\Models\QrCode;
use App\Services\Billing\BillingClient;
use App\Services\Billing\PaystackService;
use App\Services\Billing\SmsService;
use App\Services\Notifications\MiniNotificationService;
use App\Services\Pay\PayClient;
use App\Support\LadillLink;
use Illuminate\Support\Str;
use RuntimeException;
class MiniPaymentService
{
public function __construct(
private PayClient $pay,
private PaystackService $paystack,
private BillingClient $billing,
private SmsService $sms,
private MiniNotificationService $notifications,
private AutoWithdrawService $autoWithdraw,
) {}
/**
* @param array{amount: float, customer_phone?: string} $data
* @return array{payment: MiniPayment, checkout_url: string, provider: string}
*/
public function initiate(QrCode $qrCode, array $data): array
{
if ($qrCode->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
)
);
}
}