From cf6dda57939ec9db2d20407a18232ef187393efb Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 8 Jun 2026 05:20:09 +0000 Subject: [PATCH] Route Mini payments through Ladill Pay API. Checkout and settlement now go via platform Pay instead of direct Paystack + Billing credit, with legacy MIN-* support retained. Co-authored-by: Cursor --- .env.example | 3 + DEPLOY.md | 1 + app/Models/MiniPayment.php | 1 + app/Services/Mini/MiniPaymentService.php | 112 +++++++++++++----- app/Services/Pay/PayClient.php | 50 ++++++++ config/pay.php | 6 + ...dd_pay_order_id_to_mini_payments_table.php | 22 ++++ 7 files changed, 165 insertions(+), 30 deletions(-) create mode 100644 app/Services/Pay/PayClient.php create mode 100644 config/pay.php create mode 100644 database/migrations/2026_06_08_130000_add_pay_order_id_to_mini_payments_table.php diff --git a/.env.example b/.env.example index cf6e8b4..12a2e43 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,9 @@ LADILL_SSO_CLIENT_SECRET= BILLING_API_URL=https://ladill.com/api/billing BILLING_API_KEY_MINI= +PAY_API_URL=https://ladill.com/api/pay +PAY_API_KEY_MINI= + IDENTITY_API_URL=https://ladill.com/api IDENTITY_API_KEY_MINI= diff --git a/DEPLOY.md b/DEPLOY.md index c372554..2842ea2 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -23,6 +23,7 @@ main app DB user. |---|---| | `LADILL_SSO_CLIENT_ID` / `LADILL_SSO_CLIENT_SECRET` | `passport:client` on platform | | `BILLING_API_KEY_MINI` | platform `.env` + this app | +| `PAY_API_KEY_MINI` | platform `.env` (`PAY_API_KEY_MINI`) + this app — Ladill Pay checkout | | `IDENTITY_API_KEY_MINI` | platform `.env` + this app | | `PLATFORM_DB_*` | read `ladilldb.platform_settings` (Paystack keys from admin → Billing) | diff --git a/app/Models/MiniPayment.php b/app/Models/MiniPayment.php index 5938f89..166e14e 100644 --- a/app/Models/MiniPayment.php +++ b/app/Models/MiniPayment.php @@ -15,6 +15,7 @@ class MiniPayment extends Model public const PLATFORM_FEE_RATE = 0.05; protected $fillable = [ + 'pay_order_id', 'qr_code_id', 'user_id', 'reference', diff --git a/app/Services/Mini/MiniPaymentService.php b/app/Services/Mini/MiniPaymentService.php index cbf02b7..320986c 100644 --- a/app/Services/Mini/MiniPaymentService.php +++ b/app/Services/Mini/MiniPaymentService.php @@ -7,12 +7,14 @@ use App\Models\QrCode; use App\Services\Billing\BillingClient; use App\Services\Billing\PaystackService; use App\Services\Billing\SmsService; +use App\Services\Pay\PayClient; use Illuminate\Support\Str; use RuntimeException; class MiniPaymentService { public function __construct( + private PayClient $pay, private PaystackService $paystack, private BillingClient $billing, private SmsService $sms, @@ -36,7 +38,7 @@ class MiniPaymentService $qrCode->loadMissing('user'); $amountMinor = (int) round($amountGhs * 100); $reference = 'MINP-'.strtoupper(Str::random(16)); - $payRef = 'MIN-'.strtoupper(Str::random(16)); + $businessName = $qrCode->content()['business_name'] ?? $qrCode->label ?? 'Payment'; $payment = MiniPayment::create([ 'qr_code_id' => $qrCode->id, @@ -49,38 +51,81 @@ class MiniPaymentService 'payer_phone' => null, 'payer_note' => null, 'status' => MiniPayment::STATUS_PENDING, - 'payment_reference' => $payRef, + 'payment_reference' => null, ]); - $paystackData = $this->paystack->initializeTransaction([ - 'email' => (string) config('services.paystack.checkout_email', 'pay@ladill.com'), - 'amount' => $amountMinor, - 'currency' => $payment->currency, - 'reference' => $payRef, + $payOrder = $this->pay->createCheckout([ + 'merchant' => $qrCode->user->public_id, + 'fee_tier' => 'payments', + 'source_service' => 'mini', + 'source_ref' => (string) $qrCode->id, 'callback_url' => route('qr.public.payment.callback', ['shortCode' => $qrCode->short_code]), + 'line_items' => [ + [ + 'name' => $businessName, + 'unit_price_minor' => $amountMinor, + 'quantity' => 1, + ], + ], 'metadata' => [ 'mini_payment_id' => $payment->id, + 'mini_reference' => $reference, 'qr_code_id' => $qrCode->id, - 'merchant_id' => $qrCode->user_id, ], ]); - $checkoutUrl = $paystackData['authorization_url'] - ?? (isset($paystackData['access_code']) && $paystackData['access_code'] !== '' - ? 'https://checkout.paystack.com/'.$paystackData['access_code'] - : ''); + $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 Paystack checkout. Please try again.'); + throw new RuntimeException('Could not start checkout. Please try again.'); } return [ - 'payment' => $payment, + 'payment' => $payment->fresh(), 'checkout_url' => $checkoutUrl, ]; } 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]), + ]); + + $this->notifyPayer($payment->fresh(['qrCode', '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) @@ -103,14 +148,12 @@ class MiniPaymentService 'platform_fee_minor' => $platformFeeMinor, 'merchant_amount_minor' => $merchantMinor, 'paid_at' => now(), - 'metadata' => array_merge((array) $payment->metadata, ['paystack' => $data]), + 'metadata' => array_merge((array) $payment->metadata, ['paystack' => $data, 'legacy' => true]), ]); - $merchant = $payment->merchant; $businessName = $payment->qrCode?->content()['business_name'] ?? $payment->qrCode?->label ?? 'Payment QR'; - $this->billing->credit( - $merchant->public_id, + $payment->merchant->public_id, $merchantMinor, 'mini', 'pay', @@ -119,19 +162,28 @@ class MiniPaymentService sprintf('Payment via %s', $businessName), ); - if ($payment->payer_phone) { - $this->sms->send( - $payment->payer_phone, - sprintf( - 'Payment of %s %s to %s confirmed. Ref: %s', - $payment->currency, - number_format($paidMinor / 100, 2), - $businessName, - $payment->reference - ) - ); + $this->notifyPayer($payment->fresh(['qrCode', 'merchant'])); + + return $payment; + } + + private function notifyPayer(MiniPayment $payment): void + { + if (! $payment->payer_phone) { + return; } - return $payment->fresh(['qrCode', 'merchant']); + $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 + ) + ); } } diff --git a/app/Services/Pay/PayClient.php b/app/Services/Pay/PayClient.php new file mode 100644 index 0000000..19c94b5 --- /dev/null +++ b/app/Services/Pay/PayClient.php @@ -0,0 +1,50 @@ + $payload */ + public function createCheckout(array $payload): array + { + $res = Http::withToken($this->token())->acceptJson()->timeout(15)->post($this->base().'/checkouts', $payload); + $res->throw(); + + return (array) $res->json(); + } + + public function verify(string $reference): array + { + $res = Http::withToken($this->token())->acceptJson()->timeout(15)->post($this->base().'/checkouts/verify', [ + 'reference' => $reference, + ]); + $res->throw(); + + return (array) $res->json(); + } + + public function show(string $reference): array + { + $res = Http::withToken($this->token())->acceptJson()->timeout(10)->get($this->base().'/orders/'.$reference); + $res->throw(); + + return (array) $res->json(); + } +} diff --git a/config/pay.php b/config/pay.php new file mode 100644 index 0000000..c0abb0e --- /dev/null +++ b/config/pay.php @@ -0,0 +1,6 @@ + env('PAY_API_URL', 'https://ladill.com/api/pay'), + 'api_key' => env('PAY_API_KEY_MINI'), +]; diff --git a/database/migrations/2026_06_08_130000_add_pay_order_id_to_mini_payments_table.php b/database/migrations/2026_06_08_130000_add_pay_order_id_to_mini_payments_table.php new file mode 100644 index 0000000..13696bb --- /dev/null +++ b/database/migrations/2026_06_08_130000_add_pay_order_id_to_mini_payments_table.php @@ -0,0 +1,22 @@ +unsignedBigInteger('pay_order_id')->nullable()->after('id'); + }); + } + + public function down(): void + { + Schema::table('mini_payments', function (Blueprint $table) { + $table->dropColumn('pay_order_id'); + }); + } +};