Files
ladill-merchant/app/Services/Billing/PaystackService.php
T
isaaccladandCursor f718b9cfbf Initial Ladill Merchant app with Gitea deploy pipeline.
Shop, menu, and booking storefronts with OIDC SSO, Pay checkout, and merchant.ladill.com deployment workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 23:05:21 +00:00

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;
}
}