Deploy Ladill Mini / deploy (push) Successful in 1m23s
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>
209 lines
7.3 KiB
PHP
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
|
|
)
|
|
);
|
|
}
|
|
}
|