} $data * @return array{donation: GiveDonation, checkout_url: string, callback_url: string} */ public function initiateOrder(QrCode $qrCode, array $data): array { if ($qrCode->type !== QrCode::TYPE_CHURCH) { throw new RuntimeException('This QR is not a giving page.'); } if (empty($qrCode->content()['accepts_payment'])) { throw new RuntimeException('Online giving is not enabled for this page.'); } $items = $data['items'] ?? []; if (empty($items)) { throw new RuntimeException('Enter an amount to give.'); } $item = $items[0]; $amountGhs = round((float) ($item['price'] ?? 0), 2); if ($amountGhs <= 0) { throw new RuntimeException('Enter an amount greater than zero.'); } $collectionType = trim((string) ($item['name'] ?? 'Donation')); $qrCode->loadMissing('user'); $amountMinor = (int) round($amountGhs * 100); $reference = 'GIVD-'.strtoupper(Str::random(16)); $orgName = $qrCode->content()['name'] ?? $qrCode->label ?? 'Giving page'; $callbackUrl = route('qr.public.order.callback', ['shortCode' => $qrCode->short_code]); $donation = GiveDonation::create([ 'qr_code_id' => $qrCode->id, 'user_id' => $qrCode->user_id, 'reference' => $reference, 'collection_type' => $collectionType, 'amount_minor' => $amountMinor, 'currency' => $qrCode->content()['currency'] ?? 'GHS', 'payer_name' => trim((string) ($data['customer_name'] ?? '')), 'payer_email' => trim((string) ($data['customer_email'] ?? '')), 'payer_phone' => trim((string) ($data['customer_phone'] ?? '')), 'status' => GiveDonation::STATUS_PENDING, 'payment_reference' => null, ]); $payOrder = $this->pay->createCheckout([ 'merchant' => $qrCode->user->public_id, 'fee_tier' => 'donations', 'source_service' => 'give', 'source_ref' => (string) $qrCode->id, 'callback_url' => $callbackUrl, 'customer_name' => $donation->payer_name, 'customer_email' => $donation->payer_email, 'customer_phone' => $donation->payer_phone, 'line_items' => [ [ 'name' => $collectionType.' — '.$orgName, 'unit_price_minor' => $amountMinor, 'quantity' => 1, ], ], 'metadata' => [ 'give_donation_id' => $donation->id, 'give_reference' => $reference, 'qr_code_id' => $qrCode->id, 'collection_type' => $collectionType, ], ]); $donation->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 [ 'donation' => $donation->fresh(), 'checkout_url' => $checkoutUrl, 'callback_url' => $callbackUrl, ]; } public function complete(string $paymentReference): GiveDonation { if (str_starts_with($paymentReference, 'LP-')) { return $this->completeLadillPay($paymentReference); } return $this->completeLegacy($paymentReference); } private function completeLadillPay(string $reference): GiveDonation { $donation = GiveDonation::where('payment_reference', $reference) ->where('status', GiveDonation::STATUS_PENDING) ->firstOrFail(); $payOrder = $this->pay->verify($reference); $donation->update([ 'status' => GiveDonation::STATUS_PAID, 'amount_minor' => (int) ($payOrder['amount_minor'] ?? $donation->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'] ?? $donation->pay_order_id, 'paid_at' => now(), 'metadata' => array_merge((array) $donation->metadata, ['ladill_pay' => $payOrder]), ]); $this->notifyDonor($donation->fresh(['qrCode', 'merchant'])); return $donation; } private function completeLegacy(string $paymentReference): GiveDonation { $donation = GiveDonation::where('payment_reference', $paymentReference) ->where('status', GiveDonation::STATUS_PENDING) ->firstOrFail(); $data = $this->paystack->verifyTransaction($paymentReference); if (($data['status'] ?? '') !== 'success') { $donation->update(['status' => GiveDonation::STATUS_FAILED]); throw new RuntimeException('Payment was not successful.'); } $paidMinor = (int) ($data['amount'] ?? $donation->amount_minor); $platformFeeMinor = (int) round($paidMinor * GiveDonation::PLATFORM_FEE_RATE); $merchantMinor = $paidMinor - $platformFeeMinor; $donation->update([ 'status' => GiveDonation::STATUS_PAID, 'amount_minor' => $paidMinor, 'platform_fee_minor' => $platformFeeMinor, 'merchant_amount_minor' => $merchantMinor, 'paid_at' => now(), 'metadata' => array_merge((array) $donation->metadata, ['paystack' => $data, 'legacy' => true]), ]); $orgName = $donation->qrCode?->content()['name'] ?? $donation->qrCode?->label ?? 'Giving page'; $this->billing->credit( $donation->merchant->public_id, $merchantMinor, 'give', 'pay', $paymentReference, $donation->id, sprintf('Donation via %s', $orgName), ); $this->notifyDonor($donation->fresh(['qrCode', 'merchant'])); return $donation; } private function notifyDonor(GiveDonation $donation): void { if (! $donation->payer_phone) { return; } $orgName = $donation->qrCode?->content()['name'] ?? $donation->qrCode?->label ?? 'Giving page'; $this->sms->send( $donation->payer_phone, sprintf( 'Thank you! Your %s %s donation to %s is confirmed. Ref: %s', $donation->currency, number_format($donation->amount_minor / 100, 2), $orgName, $donation->reference ) ); } }