Donation checkout via Ladill Pay (9% fee), church/giving QR pages, SSO, and platform scan forwarding. Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
5.8 KiB
PHP
172 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Billing;
|
|
|
|
use App\Models\PlatformSetting;
|
|
use Illuminate\Http\Client\Response;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use RuntimeException;
|
|
|
|
class PaystackService
|
|
{
|
|
public function publicKey(): string
|
|
{
|
|
return (string) $this->settingValue('paystack_public_key', config('services.paystack.public_key', ''));
|
|
}
|
|
|
|
public function initializeTransaction(array $payload): array
|
|
{
|
|
return $this->initializeTransactionWithCredentials($payload);
|
|
}
|
|
|
|
public function initializeTransactionWithCredentials(array $payload, ?string $secretKey = null, ?string $baseUrl = null): array
|
|
{
|
|
$response = $this->request(secretKey: $secretKey, baseUrl: $baseUrl)
|
|
->post('/transaction/initialize', $payload);
|
|
|
|
return $this->extractData($response, 'Failed to initialize Paystack transaction.');
|
|
}
|
|
|
|
public function verifyTransaction(string $reference): array
|
|
{
|
|
return $this->verifyTransactionWithCredentials($reference);
|
|
}
|
|
|
|
public function verifyTransactionWithCredentials(string $reference, ?string $secretKey = null, ?string $baseUrl = null): array
|
|
{
|
|
$response = $this->request(secretKey: $secretKey, baseUrl: $baseUrl)
|
|
->get('/transaction/verify/' . urlencode($reference));
|
|
|
|
return $this->extractData($response, 'Failed to verify Paystack transaction.');
|
|
}
|
|
|
|
public function verifyWebhookSignature(string $rawBody, ?string $signature): bool
|
|
{
|
|
$secret = (string) $this->settingValue('paystack_webhook_secret', config('services.paystack.webhook_secret'));
|
|
if ($secret === '') {
|
|
$secret = (string) $this->settingValue('paystack_secret_key', config('services.paystack.secret_key'));
|
|
}
|
|
if ($secret === '' || !$signature) {
|
|
return false;
|
|
}
|
|
|
|
$computed = hash_hmac('sha512', $rawBody, $secret);
|
|
return hash_equals($computed, $signature);
|
|
}
|
|
|
|
protected function request(?string $secretKey = null, ?string $baseUrl = null)
|
|
{
|
|
$resolvedBaseUrl = rtrim((string) ($baseUrl ?: $this->settingValue('paystack_base_url', config('services.paystack.base_url', 'https://api.paystack.co'))), '/');
|
|
$secret = $this->normalizeSecretKey(
|
|
(string) ($secretKey ?: $this->settingValue('paystack_secret_key', config('services.paystack.secret_key')))
|
|
);
|
|
|
|
return Http::baseUrl($resolvedBaseUrl)
|
|
->withToken($secret)
|
|
->acceptJson()
|
|
->asJson();
|
|
}
|
|
|
|
protected function settingsConnection(): string
|
|
{
|
|
return (string) config('billing.platform_settings_connection', 'platform');
|
|
}
|
|
|
|
protected function settingValue(string $key, mixed $fallback = null): mixed
|
|
{
|
|
try {
|
|
$connection = $this->settingsConnection();
|
|
|
|
if (! Schema::connection($connection)->hasTable('platform_settings')) {
|
|
return $fallback;
|
|
}
|
|
|
|
$setting = PlatformSetting::query()
|
|
->where('key', $key)
|
|
->where('is_active', true)
|
|
->first();
|
|
|
|
return $setting ? ($setting->value['value'] ?? $fallback) : $fallback;
|
|
} catch (\Throwable) {
|
|
return $fallback;
|
|
}
|
|
}
|
|
|
|
protected function extractData(Response $response, string $message): array
|
|
{
|
|
if ($response->failed()) {
|
|
throw new RuntimeException($message);
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (!is_array($json) || !($json['status'] ?? false)) {
|
|
throw new RuntimeException((string) ($json['message'] ?? $message));
|
|
}
|
|
|
|
$data = $json['data'] ?? null;
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
/**
|
|
* List supported banks / mobile money providers.
|
|
* @param string $country e.g. 'ghana', 'nigeria'
|
|
* @param string $type 'nuban', 'mobile_money', or '' for all
|
|
*/
|
|
public function listBanks(string $country = 'ghana', string $type = '', ?string $currency = null): array
|
|
{
|
|
$query = ['perPage' => 200];
|
|
if ($currency !== null && $currency !== '') {
|
|
$query['currency'] = strtoupper($currency);
|
|
} else {
|
|
$query['country'] = $country;
|
|
}
|
|
if ($type !== '') {
|
|
$query['type'] = $type;
|
|
}
|
|
$response = $this->request()->get('/bank', $query);
|
|
$json = $response->json();
|
|
return is_array($json['data'] ?? null) ? $json['data'] : [];
|
|
}
|
|
|
|
/**
|
|
* Create a transfer recipient (bank account or mobile money).
|
|
* @param array{type: string, name: string, account_number: string, bank_code: string, currency?: string} $payload
|
|
*/
|
|
public function createTransferRecipient(array $payload): array
|
|
{
|
|
$response = $this->request()->post('/transferrecipient', $payload);
|
|
return $this->extractData($response, 'Failed to create transfer recipient.');
|
|
}
|
|
|
|
/**
|
|
* Initiate a transfer to a recipient.
|
|
* @param array{source: string, amount: int, recipient: string, reason?: string, reference?: string} $payload
|
|
*/
|
|
public function initiateTransfer(array $payload): array
|
|
{
|
|
$response = $this->request()->post('/transfer', $payload);
|
|
return $this->extractData($response, 'Failed to initiate transfer.');
|
|
}
|
|
|
|
/**
|
|
* Verify a transfer by reference.
|
|
*/
|
|
public function verifyTransfer(string $reference): array
|
|
{
|
|
$response = $this->request()->get('/transfer/verify/' . urlencode($reference));
|
|
return $this->extractData($response, 'Failed to verify transfer.');
|
|
}
|
|
|
|
protected function normalizeSecretKey(string $secret): string
|
|
{
|
|
$normalized = trim($secret);
|
|
|
|
if (str_starts_with(strtolower($normalized), 'bearer ')) {
|
|
$normalized = trim(substr($normalized, 7));
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
}
|