From 23848fb6346a95bb570bca3d62e520e2018ac15b Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sat, 2 Aug 2025 18:35:42 +0100 Subject: [PATCH 01/30] Rewrote and restructure files for SRP --- config/paystack.php | 51 ++ src/Client/PaystackClient.php | 144 ++++++ src/Paystack.php | 740 +++------------------------ src/PaystackServiceProvider.php | 111 +++- src/Services/BankService.php | 40 ++ src/Services/CustomerService.php | 45 ++ src/Services/PageService.php | 46 ++ src/Services/PlanService.php | 45 ++ src/Services/SubAccountService.php | 45 ++ src/Services/SubscriptionService.php | 57 +++ src/Services/TransactionService.php | 51 ++ 11 files changed, 685 insertions(+), 690 deletions(-) create mode 100644 config/paystack.php create mode 100644 src/Client/PaystackClient.php create mode 100644 src/Services/BankService.php create mode 100644 src/Services/CustomerService.php create mode 100644 src/Services/PageService.php create mode 100644 src/Services/PlanService.php create mode 100644 src/Services/SubAccountService.php create mode 100644 src/Services/SubscriptionService.php create mode 100644 src/Services/TransactionService.php diff --git a/config/paystack.php b/config/paystack.php new file mode 100644 index 0000000..cd6dc9d --- /dev/null +++ b/config/paystack.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + + /** + * Public Key From Paystack Dashboard + * + */ + 'publicKey' => env('PAYSTACK_PUBLIC_KEY'), + + /** + * Secret Key From Paystack Dashboard + * + */ + 'secretKey' => env('PAYSTACK_SECRET_KEY'), + + /** + * Paystack Payment URL + * + */ + 'paymentUrl' => env('PAYSTACK_PAYMENT_URL'), + + /** + * Optional email address of the merchant + * + */ + 'merchantEmail' => env('MERCHANT_EMAIL'), + + /* + |-------------------------------------------------------------------------- + | Enable Package Routes - Feature + |-------------------------------------------------------------------------- + | + | This option controls whether the Paystack package should automatically + | load its built-in web routes. You may disable this if you prefer + | to define your own routes or extend the functionality manually. + | + | Default: false + | + */ + 'enable_routes' => false, +]; diff --git a/src/Client/PaystackClient.php b/src/Client/PaystackClient.php new file mode 100644 index 0000000..fda941a --- /dev/null +++ b/src/Client/PaystackClient.php @@ -0,0 +1,144 @@ +baseUrl = $baseUrl ?: config('paystack.paymentUrl', 'https://api.paystack.co'); + $this->secretKey = $secretKey ?: config('paystack.secretKey'); + } + + /** + * Get a configured HTTP client for sending requests to Paystack. + * + * @internal + * @return \Illuminate\Http\Client\PendingRequest + */ + protected function client(): \Illuminate\Http\Client\PendingRequest + { + return Http::retry(3, 150) + ->withHeaders([ + 'Authorization' => 'Bearer ' . $this->secretKey, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ]) + ->withOptions([ + 'http_errors' => false, + 'version' => 1.1, + 'verify' => true, // Set it to false for local debug + ]); + } + + /** + * Handle API response errors. + * + * @internal + * @param Response $response + * @return Response + * + * @throws PaystackRequestException + */ + protected function handleErrors(Response $response): Response + { + if (! $response->successful()) { + $message = $response->json('message') ?? 'Paystack request failed.'; + // \Log::error('Paystack API error', ['response' => $response->body()]); + throw new PaystackRequestException($message, $response); + } + + return $response; + } + + /** + * Make an HTTP request to Paystack. + * + * @internal + * @param string $method + * @param string $endpoint + * @param array $data + * @return Response + * + * @throws PaystackRequestException + */ + protected function request(string $method, string $endpoint, array $data = []): Response + { + $url = $this->baseUrl . '/' . ltrim($endpoint, '/'); + $response = $this->client()->{$method}($url, $data); + + return $this->handleErrors($response); + } + + /** + * Send a GET request to a Paystack API endpoint. + * + * @param string $endpoint + * @return Response + * + * @throws PaystackRequestException + */ + public function get(string $endpoint): Response + { + return $this->request('get', $endpoint); + } + + /** + * Send a POST request to a Paystack API endpoint. + * + * @param string $endpoint + * @param array $data + * @return Response + * + * @throws PaystackRequestException + */ + public function post(string $endpoint, array $data): Response + { + return $this->request('post', $endpoint, $data); + } + + /** + * Send a PUT request to a Paystack API endpoint. + * + * @param string $endpoint + * @param array $data + * @return Response + * + * @throws PaystackRequestException + */ + public function put(string $endpoint, array $data): Response + { + return $this->request('put', $endpoint, $data); + } + + /** + * Send a DELETE request to a Paystack API endpoint. + * + * @param string $endpoint + * @return Response + * + * @throws PaystackRequestException + */ + public function delete(string $endpoint): Response + { + return $this->request('delete', $endpoint); + } +} diff --git a/src/Paystack.php b/src/Paystack.php index f4b91dc..b542642 100644 --- a/src/Paystack.php +++ b/src/Paystack.php @@ -11,719 +11,113 @@ namespace Unicodeveloper\Paystack; -use GuzzleHttp\Client; -use Illuminate\Support\Facades\Config; -use Unicodeveloper\Paystack\Exceptions\IsNullException; -use Unicodeveloper\Paystack\Exceptions\PaymentVerificationFailedException; - +use Unicodeveloper\Paystack\Services\TransactionService; +use Unicodeveloper\Paystack\Services\CustomerService; +use Unicodeveloper\Paystack\Services\PlanService; +use Unicodeveloper\Paystack\Services\SubscriptionService; +use Unicodeveloper\Paystack\Services\PageService; +use Unicodeveloper\Paystack\Services\SubAccountService; +use Unicodeveloper\Paystack\Services\BankService; +use Unicodeveloper\Paystack\Client\PaystackClient; +use Unicodeveloper\Paystack\Support\TransRef; + +/** + * Paystack Service Container + * + * Provides access to Paystack services like Transaction, Customer, Plan, etc. +*/ class Paystack { - /** - * Transaction Verification Successful - */ - const VS = 'Verification successful'; - - /** - * Invalid Transaction reference - */ - const ITF = "Invalid transaction reference"; - - /** - * Issue Secret Key from your Paystack Dashboard - * @var string - */ - protected $secretKey; - - /** - * Instance of Client - * @var Client - */ - protected $client; - - /** - * Response from requests made to Paystack - * @var mixed - */ - protected $response; - - /** - * Paystack API base Url - * @var string - */ - protected $baseUrl; - - /** - * Authorization Url - Paystack payment page - * @var string - */ - protected $authorizationUrl; - - public function __construct() - { - $this->setKey(); - $this->setBaseUrl(); - $this->setRequestOptions(); - } - - /** - * Get Base Url from Paystack config file - */ - public function setBaseUrl() - { - $this->baseUrl = Config::get('paystack.paymentUrl'); - } - - /** - * Get secret key from Paystack config file - */ - public function setKey() - { - $this->secretKey = Config::get('paystack.secretKey'); - } - - /** - * Set options for making the Client request - */ - private function setRequestOptions() - { - $authBearer = 'Bearer ' . $this->secretKey; - - $this->client = new Client( - [ - 'base_uri' => $this->baseUrl, - 'headers' => [ - 'Authorization' => $authBearer, - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' - ] - ] - ); - } - - - /** - - * Initiate a payment request to Paystack - * Included the option to pass the payload to this method for situations - * when the payload is built on the fly (not passed to the controller from a view) - * @return Paystack - */ - - public function makePaymentRequest($data = null) - { - if ($data == null) { - - $quantity = intval(request()->quantity ?? 1); - - $data = array_filter([ - "amount" => intval(request()->amount) * $quantity, - "reference" => request()->reference, - "email" => request()->email, - "channels" => request()->channels, - "plan" => request()->plan, - "first_name" => request()->first_name, - "last_name" => request()->last_name, - "callback_url" => request()->callback_url, - "currency" => (request()->currency != "" ? request()->currency : "NGN"), - - /* - Paystack allows for transactions to be split into a subaccount - - The following lines trap the subaccount ID - as well as the ammount to charge the subaccount (if overriden in the form) - both values need to be entered within hidden input fields - */ - "subaccount" => request()->subaccount, - "transaction_charge" => request()->transaction_charge, - - /** - * Paystack allows for transaction to be split into multi accounts(subaccounts) - * The following lines trap the split ID handling the split - * More details here: https://paystack.com/docs/payments/multi-split-payments/#using-transaction-splits-with-payments - */ - "split_code" => request()->split_code, - - /** - * Paystack allows transaction to be split into multi account(subaccounts) on the fly without predefined split - * form need an input field: - * array must be set up as: - * $split = [ - * "type" => "percentage", - * "currency" => "KES", - * "subaccounts" => [ - * { "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 10 }, - * { "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 30 }, - * ], - * "bearer_type" => "all", - * "main_account_share" => 70, - * ] - * More details here: https://paystack.com/docs/payments/multi-split-payments/#dynamic-splits - */ - "split" => request()->split, - /* - * to allow use of metadata on Paystack dashboard and a means to return additional data back to redirect url - * form need an input field: - * array must be set up as: - * $array = [ 'custom_fields' => [ - * ['display_name' => "Cart Id", "variable_name" => "cart_id", "value" => "2"], - * ['display_name' => "Sex", "variable_name" => "sex", "value" => "female"], - * . - * . - * . - * ] - * ] - */ - 'metadata' => request()->metadata - ]); - } - - $this->setHttpResponse('/transaction/initialize', 'POST', $data); - - return $this; - } - - - /** - * @param string $relativeUrl - * @param string $method - * @param array $body - * @return Paystack - * @throws IsNullException - */ - private function setHttpResponse($relativeUrl, $method, $body = []) - { - if (is_null($method)) { - throw new IsNullException("Empty method not allowed"); - } - - $this->response = $this->client->{strtolower($method)}( - $this->baseUrl . $relativeUrl, - ["body" => json_encode($body)] - ); - - return $this; - } - - /** - * Get the authorization url from the callback response - * @return Paystack - */ - public function getAuthorizationUrl($data = null) - { - $this->makePaymentRequest($data); - - $this->url = $this->getResponse()['data']['authorization_url']; - - return $this; - } - - /** - * Get the authorization callback response - * In situations where Laravel serves as an backend for a detached UI, the api cannot redirect - * and might need to take different actions based on the success or not of the transaction - * @return array - */ - public function getAuthorizationResponse($data) - { - $this->makePaymentRequest($data); - - $this->url = $this->getResponse()['data']['authorization_url']; - - return $this->getResponse(); - } - - /** - * Hit Paystack Gateway to Verify that the transaction is valid - */ - private function verifyTransactionAtGateway($transaction_id = null) - { - $transactionRef = $transaction_id ?? request()->query('trxref'); - - $relativeUrl = "/transaction/verify/{$transactionRef}"; - - $this->response = $this->client->get($this->baseUrl . $relativeUrl, []); - } - - /** - * True or false condition whether the transaction is verified - * @return boolean - */ - public function isTransactionVerificationValid($transaction_id = null) - { - $this->verifyTransactionAtGateway($transaction_id); - - $result = $this->getResponse()['message']; - - switch ($result) { - case self::VS: - $validate = true; - break; - case self::ITF: - $validate = false; - break; - default: - $validate = false; - break; - } - - return $validate; - } - - /** - * Get Payment details if the transaction was verified successfully - * @return json - * @throws PaymentVerificationFailedException - */ - public function getPaymentData() - { - if ($this->isTransactionVerificationValid()) { - return $this->getResponse(); - } else { - throw new PaymentVerificationFailedException("Invalid Transaction Reference"); - } - } - - /** - * Fluent method to redirect to Paystack Payment Page - */ - public function redirectNow() - { - return redirect($this->url); - } - - /** - * Get Access code from transaction callback respose - * @return string - */ - public function getAccessCode() - { - return $this->getResponse()['data']['access_code']; - } - - /** - * Generate a Unique Transaction Reference - * @return string - */ - public function genTranxRef() - { - return TransRef::getHashedToken(); - } - - /** - * Get all the customers that have made transactions on your platform - * @return array - */ - public function getAllCustomers() - { - $this->setRequestOptions(); - - return $this->setHttpResponse("/customer", 'GET', [])->getData(); - } - - /** - * Get all the plans that you have on Paystack - * @return array - */ - public function getAllPlans() - { - $this->setRequestOptions(); - - return $this->setHttpResponse("/plan", 'GET', [])->getData(); - } + protected PaystackClient $client; /** - * Get all the transactions that have happened overtime - * @return array - */ - public function getAllTransactions() - { - $this->setRequestOptions(); - - return $this->setHttpResponse("/transaction", 'GET', [])->getData(); - } - - /** - * Get the whole response from a get operation - * @return array - */ - private function getResponse() - { - return json_decode($this->response->getBody(), true); - } - - /** - * Get the data response from a get operation - * @return array - */ - private function getData() - { - return $this->getResponse()['data']; - } - - /** - * Create a plan - */ - public function createPlan() - { - $data = [ - "name" => request()->name, - "description" => request()->desc, - "amount" => intval(request()->amount), - "interval" => request()->interval, - "send_invoices" => request()->send_invoices, - "send_sms" => request()->send_sms, - "currency" => request()->currency, - ]; - - $this->setRequestOptions(); - - return $this->setHttpResponse("/plan", 'POST', $data)->getResponse(); - } - - /** - * Fetch any plan based on its plan id or code - * @param $plan_code - * @return array - */ - public function fetchPlan($plan_code) - { - $this->setRequestOptions(); - return $this->setHttpResponse('/plan/' . $plan_code, 'GET', [])->getResponse(); - } - - /** - * Update any plan's details based on its id or code - * @param $plan_code - * @return array - */ - public function updatePlan($plan_code) - { - $data = [ - "name" => request()->name, - "description" => request()->desc, - "amount" => intval(request()->amount), - "interval" => request()->interval, - "send_invoices" => request()->send_invoices, - "send_sms" => request()->send_sms, - "currency" => request()->currency, - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/plan/' . $plan_code, 'PUT', $data)->getResponse(); - } - - /** - * Create a customer - */ - public function createCustomer($data = null) - { - if ($data == null) { - - $data = [ - "email" => request()->email, - "first_name" => request()->fname, - "last_name" => request()->lname, - "phone" => request()->phone, - "metadata" => request()->additional_info /* key => value pairs array */ - - ]; - } - - $this->setRequestOptions(); - return $this->setHttpResponse('/customer', 'POST', $data)->getResponse(); - } - - /** - * Fetch a customer based on id or code - * @param $customer_id - * @return array - */ - public function fetchCustomer($customer_id) - { - $this->setRequestOptions(); - return $this->setHttpResponse('/customer/' . $customer_id, 'GET', [])->getResponse(); - } - - /** - * Update a customer's details based on their id or code - * @param $customer_id - * @return array - */ - public function updateCustomer($customer_id) - { - $data = [ - "email" => request()->email, - "first_name" => request()->fname, - "last_name" => request()->lname, - "phone" => request()->phone, - "metadata" => request()->additional_info /* key => value pairs array */ - - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/customer/' . $customer_id, 'PUT', $data)->getResponse(); - } - - /** - * Export transactions in .CSV - * @return array - */ - public function exportTransactions() - { - $data = [ - "from" => request()->from, - "to" => request()->to, - 'settled' => request()->settled - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/transaction/export', 'GET', $data)->getResponse(); - } - - /** - * Create a subscription to a plan from a customer. - */ - public function createSubscription() - { - $data = [ - "customer" => request()->customer, //Customer email or code - "plan" => request()->plan, - "authorization" => request()->authorization_code - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/subscription', 'POST', $data)->getResponse(); - } - - /** - * Get all the subscriptions made on Paystack. + * Create a new Paystack instance. * - * @return array - */ - public function getAllSubscriptions() + * @param PaystackClient $client + */ + public function __construct(PaystackClient $client) { - $this->setRequestOptions(); - - return $this->setHttpResponse("/subscription", 'GET', [])->getData(); + $this->client = $client; } /** - * Get customer subscriptions + * Get the TransactionService instance. * - * @param integer $customer_id - * @return array - */ - public function getCustomerSubscriptions($customer_id) + * @return TransactionService + */ + public function transaction(): TransactionService { - $this->setRequestOptions(); - - return $this->setHttpResponse('/subscription?customer=' . $customer_id, 'GET', [])->getData(); + return new TransactionService($this->client); } /** - * Get plan subscriptions + * Get the CustomerService instance. * - * @param integer $plan_id - * @return array - */ - public function getPlanSubscriptions($plan_id) - { - $this->setRequestOptions(); - - return $this->setHttpResponse('/subscription?plan=' . $plan_id, 'GET', [])->getData(); - } - - /** - * Enable a subscription using the subscription code and token - * @return array - */ - public function enableSubscription() - { - $data = [ - "code" => request()->code, - "token" => request()->token, - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/subscription/enable', 'POST', $data)->getResponse(); - } - - /** - * Disable a subscription using the subscription code and token - * @return array - */ - public function disableSubscription() - { - $data = [ - "code" => request()->code, - "token" => request()->token, - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/subscription/disable', 'POST', $data)->getResponse(); - } - - /** - * Fetch details about a certain subscription - * @param mixed $subscription_id - * @return array - */ - public function fetchSubscription($subscription_id) - { - $this->setRequestOptions(); - return $this->setHttpResponse('/subscription/' . $subscription_id, 'GET', [])->getResponse(); - } - - /** - * Create pages you can share with users using the returned slug - */ - public function createPage() - { - $data = [ - "name" => request()->name, - "description" => request()->description, - "amount" => request()->amount - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/page', 'POST', $data)->getResponse(); - } - - /** - * Fetches all the pages the merchant has - * @return array - */ - public function getAllPages() + * @return CustomerService + */ + public function customer(): CustomerService { - $this->setRequestOptions(); - return $this->setHttpResponse('/page', 'GET', [])->getResponse(); + return new CustomerService($this->client); } /** - * Fetch details about a certain page using its id or slug - * @param mixed $page_id - * @return array - */ - public function fetchPage($page_id) - { - $this->setRequestOptions(); - return $this->setHttpResponse('/page/' . $page_id, 'GET', [])->getResponse(); - } - - /** - * Update the details about a particular page - * @param $page_id - * @return array - */ - public function updatePage($page_id) - { - $data = [ - "name" => request()->name, - "description" => request()->description, - "amount" => request()->amount - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/page/' . $page_id, 'PUT', $data)->getResponse(); - } - - /** - * Creates a subaccount to be used for split payments . Required params are business_name , settlement_bank , account_number , percentage_charge + * Get the PlanService instance. * - * @return array - */ - - public function createSubAccount() + * @return PlanService + */ + public function plan(): PlanService { - $data = [ - "business_name" => request()->business_name, - "settlement_bank" => request()->settlement_bank, - "account_number" => request()->account_number, - "percentage_charge" => request()->percentage_charge, - "primary_contact_email" => request()->primary_contact_email, - "primary_contact_name" => request()->primary_contact_name, - "primary_contact_phone" => request()->primary_contact_phone, - "metadata" => request()->metadata, - 'settlement_schedule' => request()->settlement_schedule - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse('/subaccount', 'POST', array_filter($data))->getResponse(); + return new PlanService($this->client); } /** - * Fetches details of a subaccount - * @param subaccount code - * @return array - */ - public function fetchSubAccount($subaccount_code) + * Get the SubscriptionService instance. + * + * @return SubscriptionService + */ + public function subscription(): SubscriptionService { - - $this->setRequestOptions(); - return $this->setHttpResponse("/subaccount/{$subaccount_code}", "GET", [])->getResponse(); + return new SubscriptionService($this->client); } /** - * Lists all the subaccounts associated with the account - * @param $per_page - Specifies how many records to retrieve per page , $page - SPecifies exactly what page to retrieve - * @return array - */ - public function listSubAccounts($per_page, $page) + * Get the PageService instance. + * + * @return PageService + */ + public function page(): PageService { - - $this->setRequestOptions(); - return $this->setHttpResponse("/subaccount/?perPage=" . (int) $per_page . "&page=" . (int) $page, "GET")->getResponse(); + return new PageService($this->client); } - /** - * Updates a subaccount to be used for split payments . Required params are business_name , settlement_bank , account_number , percentage_charge - * @param subaccount code - * @return array - */ - - public function updateSubAccount($subaccount_code) + * Get the SubAccountService instance. + * + * @return SubAccountService + */ + public function subAccount(): SubAccountService { - $data = [ - "business_name" => request()->business_name, - "settlement_bank" => request()->settlement_bank, - "account_number" => request()->account_number, - "percentage_charge" => request()->percentage_charge, - "description" => request()->description, - "primary_contact_email" => request()->primary_contact_email, - "primary_contact_name" => request()->primary_contact_name, - "primary_contact_phone" => request()->primary_contact_phone, - "metadata" => request()->metadata, - 'settlement_schedule' => request()->settlement_schedule - ]; - - $this->setRequestOptions(); - return $this->setHttpResponse("/subaccount/{$subaccount_code}", "PUT", array_filter($data))->getResponse(); + return new SubAccountService($this->client); } - /** - * Get a list of all supported banks and their properties - * @param $country - The country from which to obtain the list of supported banks, $per_page - Specifies how many records to retrieve per page , - * $use_cursor - Flag to enable cursor pagination on the endpoint - * @return array - */ - public function getBanks(?string $country, int $per_page = 50, bool $use_cursor = false) + * Get the BankService instance. + * + * @return BankService + */ + public function bank(): BankService { - if (!$country) - $country = request()->country ?? 'nigeria'; - - $this->setRequestOptions(); - return $this->setHttpResponse("/bank/?country=" . $country . "&use_cursor=" . $use_cursor . "&perPage=" . (int) $per_page, "GET")->getResponse(); + return new BankService($this->client); } /** - * Confirm an account belongs to the right customer - * @param $account_number - Account Number, $bank_code - You can get the list of bank codes by calling the List Banks endpoint - * @return array - */ - public function confirmAccount(string $account_number, string $bank_code) + * Generate a unique transaction reference. + * + * @return string + */ + public function transRef(): string { - - $this->setRequestOptions(); - return $this->setHttpResponse("/bank/resolve/?account_number=" . $account_number . "&bank_code=" . $bank_code, "GET")->getResponse(); + return TransRef::generate(); } } + \ No newline at end of file diff --git a/src/PaystackServiceProvider.php b/src/PaystackServiceProvider.php index ec2cc54..35622c5 100644 --- a/src/PaystackServiceProvider.php +++ b/src/PaystackServiceProvider.php @@ -12,47 +12,124 @@ namespace Unicodeveloper\Paystack; use Illuminate\Support\ServiceProvider; +use Unicodeveloper\Paystack\Client\PaystackClient; +use Unicodeveloper\Paystack\Services\BankService; +use Unicodeveloper\Paystack\Services\CustomerService; +use Unicodeveloper\Paystack\Services\PageService; +use Unicodeveloper\Paystack\Services\PlanService; +use Unicodeveloper\Paystack\Services\SubAccountService; +use Unicodeveloper\Paystack\Services\SubscriptionService; +use Unicodeveloper\Paystack\Services\TransactionService; class PaystackServiceProvider extends ServiceProvider { - - /* - * Indicates if loading of the provider is deferred. - * - * @var bool - */ - protected $defer = false; - /** * Publishes all the config file this package needs to function */ public function boot() { - $config = realpath(__DIR__.'/../resources/config/paystack.php'); - - $this->publishes([ - $config => config_path('paystack.php') - ]); + $this->bootConfig(); + $this->bootViews(); + $this->bootRoutes(); } /** - * Register the application services. + * Register the Paystack service and merge package configuration. + * + * This method binds the main Paystack class into the service container + * and merges the package's config file with the application's config. + * + * @return void */ public function register() { - $this->app->bind('laravel-paystack', function () { + $this->mergeConfigFrom(__DIR__ . '/../config/paystack.php', 'paystack'); + + $this->app->singleton(PaystackClient::class, function () { + return new PaystackClient(config('paystack.secretKey')); + }); - return new Paystack; + $this->app->singleton(TransactionService::class, fn($app) => new TransactionService($app->make(PaystackClient::class))); + $this->app->singleton(CustomerService::class, fn($app) => new CustomerService($app->make(PaystackClient::class))); + $this->app->singleton(PlanService::class, fn($app) => new PlanService($app->make(PaystackClient::class))); + $this->app->singleton(SubscriptionService::class, fn($app) => new SubscriptionService($app->make(PaystackClient::class))); + $this->app->singleton(PageService::class, fn($app) => new PageService($app->make(PaystackClient::class))); + $this->app->singleton(SubAccountService::class, fn($app) => new SubAccountService($app->make(PaystackClient::class))); + $this->app->singleton(BankService::class, fn($app) => new BankService($app->make(PaystackClient::class))); + $this->app->singleton(Paystack::class, function ($app) { + return new Paystack($app->make(PaystackClient::class)); }); + + // Bind the alias needed by the Facade + $this->app->alias(Paystack::class, 'laravel-paystack'); } + /** * Get the services provided by the provider * @return array */ public function provides() { - return ['laravel-paystack']; + return [ + PaystackClient::class, + TransactionService::class, + CustomerService::class, + PlanService::class, + SubscriptionService::class, + PageService::class, + SubAccountService::class, + BankService::class, + Paystack::class, + ]; + } + + /** + * Publish the Paystack configuration file to the application's config directory. + * + * This allows users to override the default package configuration + * by running: php artisan vendor:publish --tag=paystack-config + * + * @return void + */ + protected function bootConfig() + { + $this->publishes([ + __DIR__.'/../config/paystack.php' => config_path('paystack.php'), + ], 'config'); + } + + /** + * Load and optionally publish the Paystack views to the application's resources. + * + * This loads the views using the 'paystack::' namespace and allows users to + * customize them by running: php artisan vendor:publish --tag=paystack-views + * + * @return void + */ + protected function bootViews() + { + $this->loadViewsFrom(__DIR__.'/../resources/views', 'paystack'); + + $this->publishes([ + __DIR__.'/../resources/views' => resource_path('views/vendor/paystack'), + ], 'paystack-views'); } + + /** + * Load Paystack package routes. + * + * This method registers the routes defined in the package so + * they are available in the host Laravel application. + * + * @return void + */ + protected function bootRoutes() + { + if (config('paystack.enable_routes', true)) { + $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); + } + } + } diff --git a/src/Services/BankService.php b/src/Services/BankService.php new file mode 100644 index 0000000..8be2c94 --- /dev/null +++ b/src/Services/BankService.php @@ -0,0 +1,40 @@ +client = $client; + } + + protected function handle(callable $callback): array + { + try { + return $callback(); + } catch (PaystackRequestException $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => null, + ]; + } + } + + public function list(): array + { + return $this->handle(fn() => $this->client->get('bank')->json()); + } + + public function resolveAccount(array $data): array + { + return $this->handle(fn() => $this->client->get('bank/resolve', $data)->json()); + } +} diff --git a/src/Services/CustomerService.php b/src/Services/CustomerService.php new file mode 100644 index 0000000..10d744d --- /dev/null +++ b/src/Services/CustomerService.php @@ -0,0 +1,45 @@ +client = $client; + } + + protected function handle(callable $callback): array + { + try { + return $callback(); + } catch (PaystackRequestException $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => null, + ]; + } + } + + public function create(array $data): array + { + return $this->handle(fn() => $this->client->post('customer', $data)->json()); + } + + public function fetch(string $customerCode): array + { + return $this->handle(fn() => $this->client->get("customer/{$customerCode}")->json()); + } + + public function list(int $perPage = 50, int $page = 1): array + { + return $this->handle(fn() => $this->client->get("customer?perPage={$perPage}&page={$page}")->json()); + } +} \ No newline at end of file diff --git a/src/Services/PageService.php b/src/Services/PageService.php new file mode 100644 index 0000000..5064428 --- /dev/null +++ b/src/Services/PageService.php @@ -0,0 +1,46 @@ +client = $client; + } + + protected function handle(callable $callback): array + { + try { + return $callback(); + } catch (PaystackRequestException $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => null, + ]; + } + } + + public function create(array $data): array + { + return $this->handle(fn() => $this->client->post('page', $data)->json()); + + } + + public function fetch(string $pageId): array + { + return $this->handle(fn() => $this->client->get("page/{$pageId}")->json()); + } + + public function list(): array + { + return $this->handle(fn() => $this->client->get("page")->json()); + } +} diff --git a/src/Services/PlanService.php b/src/Services/PlanService.php new file mode 100644 index 0000000..08d4d2c --- /dev/null +++ b/src/Services/PlanService.php @@ -0,0 +1,45 @@ +client = $client; + } + + protected function handle(callable $callback): array + { + try { + return $callback(); + } catch (PaystackRequestException $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => null, + ]; + } + } + + public function create(array $data): array + { + return $this->handle(fn() => $this->client->post('plan', $data)->json()); + } + + public function fetch(string $planCode): array + { + return $this->handle(fn() => $this->client->get("plan/{$planCode}")->json()); + } + + public function list(int $perPage = 50, int $page = 1): array + { + return $this->handle(fn() => $this->client->get("plan?perPage={$perPage}&page={$page}")->json()); + } +} \ No newline at end of file diff --git a/src/Services/SubAccountService.php b/src/Services/SubAccountService.php new file mode 100644 index 0000000..d5c24ea --- /dev/null +++ b/src/Services/SubAccountService.php @@ -0,0 +1,45 @@ +client = $client; + } + + protected function handle(callable $callback): array + { + try { + return $callback(); + } catch (PaystackRequestException $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => null, + ]; + } + } + + public function create(array $data): array + { + return $this->handle(fn() => $this->client->post('subaccount', $data)->json()); + } + + public function fetch(string $subaccountCode): array + { + return $this->handle(fn() => $this->client->get("subaccount/{$subaccountCode}")->json()); + } + + public function list(): array + { + return $this->handle(fn() => $this->client->get("subaccount")->json()); + } +} diff --git a/src/Services/SubscriptionService.php b/src/Services/SubscriptionService.php new file mode 100644 index 0000000..f46f4f9 --- /dev/null +++ b/src/Services/SubscriptionService.php @@ -0,0 +1,57 @@ +client = $client; + } + + protected function handle(callable $callback): array + { + try { + return $callback(); + } catch (PaystackRequestException $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => null, + ]; + } + } + + + public function create(array $data): array + { + return $this->handle(fn() => $this->client->post('subscription', $data)->json()); + } + + public function disable(array $data): array + { + return $this->handle(fn() => $this->client->post('subscription/disable', $data)->json()); + } + + public function enable(array $data): array + { + return $this->handle(fn() => $this->client->post('subscription/enable', $data)->json()); + } + + public function fetch(string $subscriptionCode): array + { + return $this->handle(fn() => $this->client->get("subscription/{$subscriptionCode}")->json()); + } + + public function list(array $params = []): array + { + return $this->handle(fn() => $this->client->get('subscription', $params)->json()); + } + +} diff --git a/src/Services/TransactionService.php b/src/Services/TransactionService.php new file mode 100644 index 0000000..171ba4b --- /dev/null +++ b/src/Services/TransactionService.php @@ -0,0 +1,51 @@ +client = $client; + } + + protected function handle(callable $callback): array + { + try { + return $callback(); + } catch (PaystackRequestException $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => null, + ]; + } + } + + public function initialize(array $payload): array + { + return $this->handle(fn() => $this->client->post('transaction/initialize', $payload)->json()); + } + + public function verify(string $reference): array + { + return $this->handle(fn() => $this->client->get("transaction/verify/{$reference}")->json()); + } + + public function list(int $perPage = 50, int $page = 1): array + { + return $this->handle(fn() => $this->client->get("transaction?perPage={$perPage}&page={$page}")->json()); + } + + public function fetch(int|string $id): array + { + return $this->handle(fn() => $this->client->get("transaction/{$id}")->json()); + } +} From 4d5849d96d1bcbc36d3d364119b5bb4a28c76dde Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sat, 2 Aug 2025 18:44:42 +0100 Subject: [PATCH 02/30] Add Unit test --- .github/pull_request_template.md | 42 +++++++++ .gitignore | 53 ++++++++++- src/Support/TransRef.php | 12 +++ tests/TestCase.php | 72 +++++++++++++++ tests/Unit/CustomerServiceTest.php | 55 ++++++++++++ tests/Unit/PageServiceTest.php | 87 +++++++++++++++++++ tests/Unit/PlanServiceTest.php | 54 ++++++++++++ tests/Unit/SubscriptionServiceTest.php | 67 ++++++++++++++ tests/Unit/TransactionServiceTest.php | 116 +++++++++++++++++++++++++ 9 files changed, 554 insertions(+), 4 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 src/Support/TransRef.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/CustomerServiceTest.php create mode 100644 tests/Unit/PageServiceTest.php create mode 100644 tests/Unit/PlanServiceTest.php create mode 100644 tests/Unit/SubscriptionServiceTest.php create mode 100644 tests/Unit/TransactionServiceTest.php diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9fcc1d4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,42 @@ +# ๐Ÿš€ Pull Request + +## Summary + + +Refactor to enforce Single Responsibility Principle (SRP) in `PaystackClient`. Extracted responsibilities into dedicated service classes. + +## ๐Ÿ” Related Issues + + +Fixes # + +## โœ… Changes Made + +- [x] Extracted HTTP logic into `PaystackClient` +- [x] Created `TransactionService`, `CustomerService`, etc. +- [x] Applied PSR-4 autoloading +- [x] Added phpDocumentor support +- [x] Improved test structure + +## ๐Ÿ’ก Motivation + + +Improving maintainability, testability, and code organization of the SDK. + +## ๐Ÿงช Tests + + +- Existing unit tests pass โœ… +- Added new tests for each service class + +## ๐Ÿ“‹ Checklist + +- [ ] Code builds without errors +- [ ] Tests pass locally +- [ ] Linted with `php-cs-fixer` or similar +- [ ] Documentation updated (if applicable) +- [ ] New classes follow SRP and SOLID + +--- + +> _Please review this PR and leave feedback if needed._ diff --git a/.gitignore b/.gitignore index 08a1747..d8be81f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,50 @@ -build -vendor -.DS_Store +# Laravel +/vendor +/node_modules +/public/storage +/storage/*.key +.env +.env.*.backup +.phpunit.result.cache +Homestead.yaml +Homestead.json +/.vagrant +/storage/logs/*.log +/storage/framework/cache/data/* +/storage/framework/sessions/* +/storage/framework/testing/* +/storage/framework/views/*.php +/public/hot +/public/storage +/storage/debugbar +npm-debug.log +yarn-error.log + +# Composer composer.lock -.idea \ No newline at end of file + +# IDEs +.idea/ +*.sublime-project +*.sublime-workspace +.vscode/ + +# OS files +.DS_Store +Thumbs.db + +# PHPStorm specific +/.phpstorm.meta.php + +# phpDocumentor output +/output/ + +# Coverage/Test +coverage/ +clover.xml +.junit.xml +build/ +teamcity.txt +.env.testing +.phpunit.cache +.phpunit.result.cache diff --git a/src/Support/TransRef.php b/src/Support/TransRef.php new file mode 100644 index 0000000..080c7c4 --- /dev/null +++ b/src/Support/TransRef.php @@ -0,0 +1,12 @@ + + */ + protected function getPackageProviders($app): array + { + return [ + PaystackServiceProvider::class, + ]; + } + + /** + * Define environment setup. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function getEnvironmentSetUp($app): void + { + parent::getEnvironmentSetUp($app); + + if (file_exists(__DIR__ . '/../.env.testing')) { + Dotenv::createImmutable(__DIR__ . '/../', '.env.testing')->load(); + } + + // Set Paystack config from env.testing + $app['config']->set('paystack.secretKey', $_ENV['PAYSTACK_SECRET_KEY'] ?? env('PAYSTACK_SECRET_KEY')); + $app['config']->set('paystack.publicKey', $_ENV['PAYSTACK_PUBLIC_KEY'] ?? env('PAYSTACK_PUBLIC_KEY')); + $app['config']->set('paystack.paymentUrl', $_ENV['PAYSTACK_PAYMENT_URL'] ?? 'https://api.paystack.co'); + } + + + /** + * Register package aliases (facades). + * + * @param \Illuminate\Foundation\Application $app + * @return array + */ + protected function getPackageAliases($app) + { + return [ + 'Paystack' => Paystack::class, + ]; + } +} diff --git a/tests/Unit/CustomerServiceTest.php b/tests/Unit/CustomerServiceTest.php new file mode 100644 index 0000000..7736fa5 --- /dev/null +++ b/tests/Unit/CustomerServiceTest.php @@ -0,0 +1,55 @@ + Http::response(['status' => true, 'data' => ['email' => 'test@example.com']]) + ]); + + $client = new PaystackClient(); + $service = new CustomerService($client); + $response = $service->create(['email' => 'test@example.com']); + + $this->assertTrue($response['status']); + $this->assertEquals('test@example.com', $response['data']['email']); + } + + public function testListCustomers() + { + Http::fake([ + 'https://api.paystack.co/customer*' => Http::response(['status' => true, 'data' => [['email' => 'test@example.com']]]) + ]); + + $client = new PaystackClient(); + $service = new CustomerService($client); + $response = $service->list(); + + $this->assertTrue($response['status']); + $this->assertIsArray($response['data']); + } + + public function testFetchCustomer() + { + $id = 12345; + Http::fake([ + "https://api.paystack.co/customer/{$id}" => Http::response(['status' => true, 'data' => ['id' => $id]]) + ]); + + $client = new PaystackClient(); + $service = new CustomerService($client); + $response = $service->fetch($id); + + $this->assertTrue($response['status']); + $this->assertEquals($id, $response['data']['id']); + } +} diff --git a/tests/Unit/PageServiceTest.php b/tests/Unit/PageServiceTest.php new file mode 100644 index 0000000..374a118 --- /dev/null +++ b/tests/Unit/PageServiceTest.php @@ -0,0 +1,87 @@ + 'Test Page', + 'description' => 'Test Description', + 'amount' => 5000, + ]; + + Http::fake([ + 'https://api.paystack.co/page' => Http::response([ + 'status' => true, + 'data' => [ + 'id' => 'pg123', + 'name' => 'Test Page', + 'description' => 'A test page', + 'amount' => 5000, + ], + 'message' => 'Page created successfully', + ], 200) + ]); + + $client = new PaystackClient(); + $service = new PageService($client); + + $response = $service->create($payload); + + $this->assertTrue($response['status']); + $this->assertIsArray($response['data']); + $this->assertEquals('Page created successfully', $response['message']); + } + + public function testFetchPaymentPage() + { + $pageId = 'abc123'; + + Http::fake([ + "https://api.paystack.co/page/{$pageId}" => Http::response([ + 'status' => true, + 'data' => [ + 'id' => $pageId, + 'name' => 'Test Page', + ], + ], 200) + ]); + + $client = new PaystackClient(); + $service = new PageService($client); + + $response = $service->fetch($pageId); + + $this->assertTrue($response['status']); + $this->assertEquals($pageId, $response['data']['id']); + } + + public function testListPaymentPages() + { + Http::fake([ + 'https://api.paystack.co/page*' => Http::response([ + 'status' => true, + 'data' => [ + ['id' => 1, 'name' => 'Page 1'], + ['id' => 2, 'name' => 'Page 2'], + ], + ], 200) + ]); + + $client = new PaystackClient(); + $service = new PageService($client); + + $response = $service->list(); + + $this->assertTrue($response['status']); + $this->assertCount(2, $response['data']); + } + +} diff --git a/tests/Unit/PlanServiceTest.php b/tests/Unit/PlanServiceTest.php new file mode 100644 index 0000000..d945602 --- /dev/null +++ b/tests/Unit/PlanServiceTest.php @@ -0,0 +1,54 @@ + Http::response(['status' => true, 'data' => ['name' => 'Basic Plan']]) + ]); + + $client = new PaystackClient(); + $service = new PlanService($client); + $response = $service->create(['name' => 'Basic Plan']); + + $this->assertTrue($response['status']); + $this->assertEquals('Basic Plan', $response['data']['name']); + } + + public function testListPlans() + { + Http::fake([ + 'https://api.paystack.co/plan*' => Http::response(['status' => true, 'data' => [['name' => 'Basic Plan']]]) + ]); + + $client = new PaystackClient(); + $service = new PlanService($client); + $response = $service->list(); + + $this->assertTrue($response['status']); + $this->assertIsArray($response['data']); + } + + public function testFetchPlan() + { + $id = 67890; + Http::fake([ + "https://api.paystack.co/plan/{$id}" => Http::response(['status' => true, 'data' => ['id' => $id]]) + ]); + + $client = new PaystackClient(); + $service = new PlanService($client); + $response = $service->fetch($id); + + $this->assertTrue($response['status']); + $this->assertEquals($id, $response['data']['id']); + } +} diff --git a/tests/Unit/SubscriptionServiceTest.php b/tests/Unit/SubscriptionServiceTest.php new file mode 100644 index 0000000..7f7b769 --- /dev/null +++ b/tests/Unit/SubscriptionServiceTest.php @@ -0,0 +1,67 @@ + Http::response(['status' => true, 'data' => ['email' => 'user@example.com']]) + ]); + + $response = Paystack::subscription()->create( + [ + 'email' => 'user@example.com' + ] + ); + + $this->assertTrue($response['status']); + $this->assertEquals('user@example.com', $response['data']['email']); + } + + public function testDisableSubscription() + { + Http::fake([ + 'https://api.paystack.co/subscription/disable' => Http::response(['status' => true]) + ]); + + $response = Paystack::subscription()->disable(['code' => 'SUB123']); + + $this->assertTrue($response['status']); + } + + public function testEnableSubscription() + { + Http::fake([ + 'https://api.paystack.co/subscription/enable' => Http::response(['status' => true]) + ]); + + $client = new PaystackClient(); + $service = new SubscriptionService($client); + $response = $service->enable(['code' => 'SUB123']); + + $this->assertTrue($response['status']); + } + + public function testFetchSubscription() + { + $code = 'SUB123'; + Http::fake([ + "https://api.paystack.co/subscription/{$code}" => Http::response(['status' => true, 'data' => ['subscription_code' => $code]]) + ]); + + // $client = new PaystackClient(); + // $service = new SubscriptionService($client); + $response = paystack()->subscription()->fetch($code); + + $this->assertTrue($response['status']); + $this->assertEquals($code, $response['data']['subscription_code']); + } +} diff --git a/tests/Unit/TransactionServiceTest.php b/tests/Unit/TransactionServiceTest.php new file mode 100644 index 0000000..7943f82 --- /dev/null +++ b/tests/Unit/TransactionServiceTest.php @@ -0,0 +1,116 @@ +toString(); + + Http::fake([ + 'https://api.paystack.co/transaction*' => Http::response([ + 'status' => true, + 'data' => [ + 'authorization_url' => 'https://paystack.com/pay/test', + 'reference' => $mockReference + ] + ]) + ]); + + // Facade registered in TestCase + $response = Paystack::transaction()->initialize([ + 'email' => 'test@example.com', + 'amount' => 10000 + ]); + + $this->assertTrue($response['status']); + $this->assertEquals('https://paystack.com/pay/test', $response['data']['authorization_url']); + } + + public function testVerifyTransaction() + { + $reference = 'txn_ref_123'; + + Http::fake([ + "https://api.paystack.co/transaction/verify/{$reference}" => Http::response([ + 'status' => true, + 'message' => 'Verification successful', + ]) + ]); + + $client = new PaystackClient('', ''); + $service = new TransactionService($client); + $response = $service->verify($reference); + + $this->assertTrue($response['status']); + $this->assertEquals('Verification successful', $response['message']); + } + + public function testFetchTransaction() + { + $id = 7890; + + Http::fake([ + "https://api.paystack.co/transaction/{$id}" => Http::response([ + 'status' => true, + 'data' => [ + 'id' => $id + ], + ]) + ]); + + $client = new PaystackClient(); + $service = new TransactionService($client); + $response = $service->fetch($id); + + $this->assertTrue($response['status']); + $this->assertEquals($id, $response['data']['id']); + } + + public function testListPaginatedTransactions() + { + $perPage = 3; + $page = 1; + + + Http::fake([ + "https://api.paystack.co/transaction*" => Http::response([ + 'status' => true, + 'data' => [ + ['id' => 101, 'amount' => 15000], + ['id' => 102, 'amount' => 25000], + ['id' => 103, 'amount' => 35000], + ] + ]) + ]); + + $client = new PaystackClient(); + $service = new TransactionService($client); + $response = $service->list($perPage, $page); + + $this->assertTrue($response['status']); + $this->assertIsArray($response['data']); + $this->assertCount(3, $response['data']); + $this->assertEquals(15000, $response['data'][0]['amount']); + $this->assertEquals(25000, $response['data'][1]['amount']); + } + + public function testGenerateTransactionReference() + { + $ref = Paystack::transRef(); + + // print_r($ref); + $this->assertIsString($ref); + $this->assertStringStartsWith('TXN_', $ref); + $this->assertGreaterThan(10, strlen($ref)); + } + +} From a7cdd3e08eb99f275abed802f9c1604397d5d153 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sat, 2 Aug 2025 18:46:17 +0100 Subject: [PATCH 03/30] Remove irrelevant files --- resources/config/paystack.php | 38 ------------------------ tests/HelpersTest.php | 33 --------------------- tests/PaystackTest.php | 56 ----------------------------------- 3 files changed, 127 deletions(-) delete mode 100644 resources/config/paystack.php delete mode 100644 tests/HelpersTest.php delete mode 100644 tests/PaystackTest.php diff --git a/resources/config/paystack.php b/resources/config/paystack.php deleted file mode 100644 index e6d0d29..0000000 --- a/resources/config/paystack.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -return [ - - /** - * Public Key From Paystack Dashboard - * - */ - 'publicKey' => getenv('PAYSTACK_PUBLIC_KEY'), - - /** - * Secret Key From Paystack Dashboard - * - */ - 'secretKey' => getenv('PAYSTACK_SECRET_KEY'), - - /** - * Paystack Payment URL - * - */ - 'paymentUrl' => getenv('PAYSTACK_PAYMENT_URL'), - - /** - * Optional email address of the merchant - * - */ - 'merchantEmail' => getenv('MERCHANT_EMAIL'), - -]; diff --git a/tests/HelpersTest.php b/tests/HelpersTest.php deleted file mode 100644 index 7bb3da9..0000000 --- a/tests/HelpersTest.php +++ /dev/null @@ -1,33 +0,0 @@ -paystack = m::mock('Unicodeveloper\Paystack\Paystack'); - $this->mock = m::mock('GuzzleHttp\Client'); - } - - public function tearDown(): void - { - m::close(); - } - - /** - * Tests that helper returns - * - * @test - * @return void - */ - function it_returns_instance_of_paystack () { - - $this->assertInstanceOf("Unicodeveloper\Paystack\Paystack", $this->paystack); - } -} \ No newline at end of file diff --git a/tests/PaystackTest.php b/tests/PaystackTest.php deleted file mode 100644 index cabd082..0000000 --- a/tests/PaystackTest.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Unicodeveloper\Paystack\Test; - -use Mockery as m; -use GuzzleHttp\Client; -use PHPUnit\Framework\TestCase; -use Unicodeveloper\Paystack\Paystack; -use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Facade as Facade; - -class PaystackTest extends TestCase -{ - protected $paystack; - - public function setUp(): void - { - $this->paystack = m::mock('Unicodeveloper\Paystack\Paystack'); - $this->mock = m::mock('GuzzleHttp\Client'); - } - - public function tearDown(): void - { - m::close(); - } - - public function testAllCustomersAreReturned() - { - $array = $this->paystack->shouldReceive('getAllCustomers')->andReturn(['prosper']); - - $this->assertEquals('array', gettype(array($array))); - } - - public function testAllTransactionsAreReturned() - { - $array = $this->paystack->shouldReceive('getAllTransactions')->andReturn(['transactions']); - - $this->assertEquals('array', gettype(array($array))); - } - - public function testAllPlansAreReturned() - { - $array = $this->paystack->shouldReceive('getAllPlans')->andReturn(['intermediate-plan']); - - $this->assertEquals('array', gettype(array($array))); - } -} From 30c843baa3604831091e9e803675243724235f07 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 04:33:05 +0100 Subject: [PATCH 04/30] Ignore PHPUnit result cache file --- .phpunit.result.cache | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .phpunit.result.cache diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index 5c6751f..0000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":[],"times":{"Unicodeveloper\\Paystack\\Test\\HelpersTest::it_returns_instance_of_paystack":0.213,"Unicodeveloper\\Paystack\\Test\\PaystackTest::testAllCustomersAreReturned":0.089,"Unicodeveloper\\Paystack\\Test\\PaystackTest::testAllTransactionsAreReturned":0.001,"Unicodeveloper\\Paystack\\Test\\PaystackTest::testAllPlansAreReturned":0.001}} \ No newline at end of file From 7678df3038adc1b1f8f1f0f3dd27880b53f85576 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 04:37:37 +0100 Subject: [PATCH 05/30] Modify ignore, setups file, and tests --- .env.testing.example | 6 ++++ .gitignore | 2 ++ composer.json | 24 ++++++++++++---- phpunit.xml.dist | 59 ++++++++++++++++++++++------------------ src/Facades/Paystack.php | 25 +++++++++-------- tests/TestCase.php | 1 - 6 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 .env.testing.example diff --git a/.env.testing.example b/.env.testing.example new file mode 100644 index 0000000..7770425 --- /dev/null +++ b/.env.testing.example @@ -0,0 +1,6 @@ +# Rename this file to .env.testing and suppliy necessary value for integration testing +PAYSTACK_PUBLIC_KEY= +PAYSTACK_SECRET_KEY= +PAYSTACK_PAYMENT_URL=https://api.paystack.co +PAYSTACK_CALLBACK_URL= +MERCHANT_EMAIL='' diff --git a/.gitignore b/.gitignore index d8be81f..3999575 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ teamcity.txt .env.testing .phpunit.cache .phpunit.result.cache +.phpunit.result.cache +phpunit.xml.dist.bak diff --git a/composer.json b/composer.json index e67e985..3bd0681 100644 --- a/composer.json +++ b/composer.json @@ -26,16 +26,25 @@ } ], "minimum-stability": "stable", + "prefer-stable": true, "require": { - "php": "^7.2|^8.0|^8.1", - "illuminate/support": "~6|~7|~8|~9|^10.0|^11.0", - "guzzlehttp/guzzle": "~6|~7|~8|~9" + "php": "^7.2|^8.0|^8.1|^8.2", + "illuminate/support": "~6|~7|~8|~9|^10.0|^11.0|^12.0", + "illuminate/events": "~6|~7|~8|~9|^10.0|^11.0|^12.0", + "illuminate/http": "~6|~7|~8|~9|^10.0|^11.0|^12.0", + "illuminate/routing": "~6|~7|~8|~9|^10.0|^11.0|^12.0", + "illuminate/view": "~6|~7|~8|~9|^10.0|^11.0|^12.0", + "illuminate/database": "~6|~7|~8|~9|^10.0|^11.0|^12.0" }, "require-dev": { + "icanhazstring/composer-unused": "^0.9.4", + "laravel/legacy-factories": "^1.4", + "mockery/mockery": "^1.3", + "orchestra/testbench": "^8.36", + "php-coveralls/php-coveralls": "^2.0", "phpunit/phpunit": "^8.4|^9.0|^10.5", "scrutinizer/ocular": "~1.1", - "php-coveralls/php-coveralls": "^2.0", - "mockery/mockery": "^1.3" + "vlucas/phpdotenv": "^5.6" }, "autoload": { "files": [ @@ -47,6 +56,8 @@ }, "autoload-dev": { "psr-4": { + "Unicodeveloper\\Paystack\\Database\\Factories\\": "database/factories/", + "Unicodeveloper\\Paystack\\Database\\Seeders\\": "database/seeders/", "Unicodeveloper\\Paystack\\Test\\": "tests" } }, @@ -62,5 +73,8 @@ "Paystack": "Unicodeveloper\\Paystack\\Facades\\Paystack" } } + }, + "config": { + "sort-packages": true } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2abdbb2..46265b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,29 +1,36 @@ - - - - tests - - - - - src/ - - - - - - - - + + + + + ./tests/Unit + + + ./tests/Integration + + + + + + + + + + src/ + + diff --git a/src/Facades/Paystack.php b/src/Facades/Paystack.php index 8805f92..95e5074 100644 --- a/src/Facades/Paystack.php +++ b/src/Facades/Paystack.php @@ -1,25 +1,26 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Unicodeveloper\Paystack\Facades; use Illuminate\Support\Facades\Facade; +/** + * @see \Unicodeveloper\Paystack\Paystack + * + * @method static \Unicodeveloper\Paystack\Services\TransactionService transaction() + * @method static \Unicodeveloper\Paystack\Services\CustomerService customer() + * @method static \Unicodeveloper\Paystack\Services\SubscriptionService subscription() + * @method static \Unicodeveloper\Paystack\Services\PlanService plan() + * @method static \Unicodeveloper\Paystack\Services\TransferService transfer() + * @method static \Unicodeveloper\Paystack\Services\TransferRecipientService transferRecipient() + * @method static \Unicodeveloper\Paystack\Services\PaymentPageService paymentPage() + */ class Paystack extends Facade { /** - * Get the registered name of the component - * @return string + * Get the registered name of the component. */ - protected static function getFacadeAccessor() + protected static function getFacadeAccessor(): string { return 'laravel-paystack'; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 3e6afa0..c59da63 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,7 +3,6 @@ namespace Unicodeveloper\Paystack\Test; use Dotenv\Dotenv; -use GuzzleHttp\Client; use Orchestra\Testbench\TestCase as BaseTestCase; use Unicodeveloper\Paystack\Facades\Paystack; use Unicodeveloper\Paystack\PaystackServiceProvider; From c71577a9e97aec80598509fc8e5a249271e4a0a5 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 04:38:47 +0100 Subject: [PATCH 06/30] Add request exception --- src/Exceptions/PaystackRequestException.php | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Exceptions/PaystackRequestException.php diff --git a/src/Exceptions/PaystackRequestException.php b/src/Exceptions/PaystackRequestException.php new file mode 100644 index 0000000..4694da0 --- /dev/null +++ b/src/Exceptions/PaystackRequestException.php @@ -0,0 +1,22 @@ +response = $response; + } + + public function getResponse(): ?Response + { + return $this->response; + } +} From 839ecaad8e3df0931db8cec67a002da46ca6c378 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 06:28:06 +0100 Subject: [PATCH 07/30] Add installation setup file --- setup.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 setup.sh diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..7ded010 --- /dev/null +++ b/setup.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +echo "Starting setup for Laravel-paystack Package..." + +# Exit on any error +set -e + +# Install dependencies +echo "Installing Composer dependencies..." +composer install + +# Copy .env.testing if it doesnโ€™t exist +if [ ! -f .env.testing ]; then + echo "Copying .env.testing.example to .env.testing" + cp .env.testing.example .env.testing +fi + +# Migrate PHPUnit configuration if needed +echo "Migrating PHPUnit configuration..." +vendor/bin/phpunit --migrate-configuration + +echo "โœ… Setup complete!" From aa2040defb79749311a0902f785b2963eb07fe42 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 06:30:39 +0100 Subject: [PATCH 08/30] Add Docblock --- src/Exceptions/PaystackRequestException.php | 24 +++++ src/Paystack.php | 1 + src/PaystackServiceProvider.php | 4 +- src/Services/BankService.php | 49 ++++++++- src/Services/CustomerService.php | 61 ++++++++++- src/Services/PageService.php | 57 +++++++++- src/Services/PlanService.php | 60 ++++++++++- src/Services/SubAccountService.php | 57 +++++++++- src/Services/SubscriptionService.php | 109 ++++++++++++++++++-- src/Services/TransactionService.php | 70 ++++++++++++- src/Support/TransRef.php | 15 ++- src/Support/helpers.php | 19 +++- 12 files changed, 501 insertions(+), 25 deletions(-) diff --git a/src/Exceptions/PaystackRequestException.php b/src/Exceptions/PaystackRequestException.php index 4694da0..3eaee00 100644 --- a/src/Exceptions/PaystackRequestException.php +++ b/src/Exceptions/PaystackRequestException.php @@ -5,16 +5,40 @@ use Exception; use Illuminate\Http\Client\Response; +/** + * Class PaystackRequestException + * + * Exception thrown when a Paystack API request fails. + * Optionally stores the original HTTP response for further debugging. + * + * @package Unicodeveloper\Paystack\Exceptions +*/ class PaystackRequestException extends Exception { + /** + * The HTTP client response from the failed request (if available). + * + * @var Response|null + */ protected ?Response $response; + /** + * Create a new PaystackRequestException instance. + * + * @param string $message The error message. + * @param Response|null $response The original HTTP response from the failed request (optional). + */ public function __construct(string $message, ?Response $response = null) { parent::__construct($message); $this->response = $response; } + /** + * Get the HTTP client response from the failed request, if available. + * + * @return Response|null + */ public function getResponse(): ?Response { return $this->response; diff --git a/src/Paystack.php b/src/Paystack.php index b542642..b25be76 100644 --- a/src/Paystack.php +++ b/src/Paystack.php @@ -25,6 +25,7 @@ * Paystack Service Container * * Provides access to Paystack services like Transaction, Customer, Plan, etc. + * @package Unicodeveloper\Paystack */ class Paystack { diff --git a/src/PaystackServiceProvider.php b/src/PaystackServiceProvider.php index 35622c5..64c0b68 100644 --- a/src/PaystackServiceProvider.php +++ b/src/PaystackServiceProvider.php @@ -29,8 +29,8 @@ class PaystackServiceProvider extends ServiceProvider public function boot() { $this->bootConfig(); - $this->bootViews(); - $this->bootRoutes(); + // $this->bootViews(); // TODO + // $this->bootRoutes(); // TODO } /** diff --git a/src/Services/BankService.php b/src/Services/BankService.php index 8be2c94..898a175 100644 --- a/src/Services/BankService.php +++ b/src/Services/BankService.php @@ -1,20 +1,48 @@ client = $client; } + /** + * Wrap API calls with error handling. + * + * @internal This method is not part of the public API and may change without notice. + * + * @param callable $callback + * @return array { + * @type bool $status Whether the request was successful. + * @type string $message Message or error information. + * @type mixed $data The response data or null on failure. + * } + */ protected function handle(callable $callback): array { try { @@ -28,11 +56,30 @@ protected function handle(callable $callback): array } } + /** + * Retrieve a list of available banks from Paystack. + * + * @return array The response from Paystack API. + */ public function list(): array { return $this->handle(fn() => $this->client->get('bank')->json()); } + /** + * Resolve a bank account number and bank code to retrieve account details. + * + * Example payload: + * ```php + * [ + * 'account_number' => '1234567890', + * 'bank_code' => '058' + * ] + * ``` + * + * @param array $data An associative array containing `account_number` and `bank_code`. + * @return array The response from Paystack API. + */ public function resolveAccount(array $data): array { return $this->handle(fn() => $this->client->get('bank/resolve', $data)->json()); diff --git a/src/Services/CustomerService.php b/src/Services/CustomerService.php index 10d744d..d500667 100644 --- a/src/Services/CustomerService.php +++ b/src/Services/CustomerService.php @@ -1,20 +1,48 @@ client = $client; } + /** + * Wrap API calls with error handling. + * + * @internal This method is not part of the public API and may change without notice. + * + * @param callable $callback + * @return array { + * @type bool $status Whether the request was successful. + * @type string $message Message or error information. + * @type mixed $data The response data or null on failure. + * } + */ protected function handle(callable $callback): array { try { @@ -28,18 +56,47 @@ protected function handle(callable $callback): array } } + /** + * Create a new customer on Paystack. + * + * Example payload: + * ```php + * [ + * 'email' => 'customer@example.com', + * 'first_name' => 'John', + * 'last_name' => 'Doe', + * 'phone' => '08012345678' + * ] + * ``` + * + * @param array $data Customer data for creation. + * @return array The response from Paystack API. + */ public function create(array $data): array { return $this->handle(fn() => $this->client->post('customer', $data)->json()); } + /** + * Fetch a customer by their customer code. + * + * @param string $customerCode The unique code identifying the customer. + * @return array The response from Paystack API. + */ public function fetch(string $customerCode): array { return $this->handle(fn() => $this->client->get("customer/{$customerCode}")->json()); } + /** + * Retrieve a paginated list of customers. + * + * @param int $perPage Number of customers per page (default: 50). + * @param int $page Page number to retrieve (default: 1). + * @return array The response from Paystack API. + */ public function list(int $perPage = 50, int $page = 1): array { return $this->handle(fn() => $this->client->get("customer?perPage={$perPage}&page={$page}")->json()); } -} \ No newline at end of file +} diff --git a/src/Services/PageService.php b/src/Services/PageService.php index 5064428..57d524d 100644 --- a/src/Services/PageService.php +++ b/src/Services/PageService.php @@ -1,20 +1,48 @@ client = $client; } + /** + * Wrap API calls with error handling. + * + * @internal This method is not part of the public API and may change without notice. + * + * @param callable $callback + * @return array { + * @type bool $status Whether the request was successful. + * @type string $message Message or error information. + * @type mixed $data The response data or null on failure. + * } + */ protected function handle(callable $callback): array { try { @@ -28,17 +56,42 @@ protected function handle(callable $callback): array } } + /** + * Create a new Payment Page on Paystack. + * + * Example payload: + * ```php + * [ + * 'name' => 'Special Offer', + * 'description' => 'Limited time only', + * 'amount' => 500000 // in kobo + * ] + * ``` + * + * @param array $data Data required to create the page. + * @return array The response from Paystack API. + */ public function create(array $data): array { return $this->handle(fn() => $this->client->post('page', $data)->json()); - } + /** + * Fetch a single Payment Page by its ID or slug. + * + * @param string $pageId The page ID or slug. + * @return array The response from Paystack API. + */ public function fetch(string $pageId): array { return $this->handle(fn() => $this->client->get("page/{$pageId}")->json()); } + /** + * Retrieve a list of all Payment Pages. + * + * @return array The response from Paystack API. + */ public function list(): array { return $this->handle(fn() => $this->client->get("page")->json()); diff --git a/src/Services/PlanService.php b/src/Services/PlanService.php index 08d4d2c..bbcfa82 100644 --- a/src/Services/PlanService.php +++ b/src/Services/PlanService.php @@ -1,20 +1,48 @@ client = $client; } + /** + * Wrap API calls with error handling. + * + * @internal This method is not part of the public API and may change without notice. + * + * @param callable $callback + * @return array { + * @type bool $status Whether the request was successful. + * @type string $message Message or error information. + * @type mixed $data The response data or null on failure. + * } + */ protected function handle(callable $callback): array { try { @@ -28,18 +56,46 @@ protected function handle(callable $callback): array } } + /** + * Create a new subscription plan on Paystack. + * + * Example payload: + * ```php + * [ + * 'name' => 'Monthly Gold Plan', + * 'amount' => 100000, // in kobo + * 'interval' => 'monthly', + * ] + * ``` + * + * @param array $data The data required to create the plan. + * @return array The response from Paystack API. + */ public function create(array $data): array { return $this->handle(fn() => $this->client->post('plan', $data)->json()); } + /** + * Fetch a subscription plan by its code. + * + * @param string $planCode The plan code from Paystack. + * @return array The response from Paystack API. + */ public function fetch(string $planCode): array { return $this->handle(fn() => $this->client->get("plan/{$planCode}")->json()); } + /** + * List all subscription plans with pagination. + * + * @param int $perPage Number of plans per page. + * @param int $page The current page number. + * @return array The response from Paystack API. + */ public function list(int $perPage = 50, int $page = 1): array { return $this->handle(fn() => $this->client->get("plan?perPage={$perPage}&page={$page}")->json()); } -} \ No newline at end of file +} diff --git a/src/Services/SubAccountService.php b/src/Services/SubAccountService.php index d5c24ea..11786d4 100644 --- a/src/Services/SubAccountService.php +++ b/src/Services/SubAccountService.php @@ -1,20 +1,48 @@ client = $client; } + /** + * Wrap API calls with exception handling. + * + * @internal This method is not part of the public API and may change without notice. + * + * @param callable $callback + * @return array { + * @type bool $status Whether the request was successful. + * @type string $message Error or success message. + * @type mixed $data Response data or null if failed. + * } + */ protected function handle(callable $callback): array { try { @@ -28,16 +56,43 @@ protected function handle(callable $callback): array } } + /** + * Create a new subaccount on Paystack. + * + * Example payload: + * ```php + * [ + * 'business_name' => 'My Business', + * 'settlement_bank' => 'Access Bank', + * 'account_number' => '1234567890', + * 'percentage_charge' => 10.5 + * ] + * ``` + * + * @param array $data Data for creating the subaccount. + * @return array The response from Paystack API. + */ public function create(array $data): array { return $this->handle(fn() => $this->client->post('subaccount', $data)->json()); } + /** + * Fetch a specific subaccount by its code. + * + * @param string $subaccountCode The unique code of the subaccount. + * @return array The response from Paystack API. + */ public function fetch(string $subaccountCode): array { return $this->handle(fn() => $this->client->get("subaccount/{$subaccountCode}")->json()); } + /** + * List all subaccounts. + * + * @return array The response from Paystack API. + */ public function list(): array { return $this->handle(fn() => $this->client->get("subaccount")->json()); diff --git a/src/Services/SubscriptionService.php b/src/Services/SubscriptionService.php index f46f4f9..7c6e64f 100644 --- a/src/Services/SubscriptionService.php +++ b/src/Services/SubscriptionService.php @@ -1,20 +1,48 @@ client = $client; } + /** + * Wrap API calls with exception handling. + * + * @internal This method is not part of the public API and may change without notice. + * + * @param callable $callback + * @return array { + * @type bool $status Whether the request was successful. + * @type string $message Error or success message. + * @type mixed $data Response data or null if failed. + * } + */ protected function handle(callable $callback): array { try { @@ -28,30 +56,93 @@ protected function handle(callable $callback): array } } - - public function create(array $data): array + /** + * Create a subscription between a customer and a plan. + * + * Example payload: + * ```php + * [ + * 'customer' => 'CUS_xxxxxxx', + * 'plan' => 'PLN_xxxxxxx', + * 'authorization' => 'AUTH_xxxxxxx' // Optional if email token is used + * ] + * ``` + * + * @param array $payload Subscription creation data. + * @return array The response from Paystack API. + */ + public function create(array $payload): array { - return $this->handle(fn() => $this->client->post('subscription', $data)->json()); + return $this->handle(fn() => $this->client->post('subscription', $payload)->json()); } - public function disable(array $data): array + /** + * Disable a subscription by customer code or email token. + * + * Example payload: + * ```php + * [ + * 'code' => 'SUB_xxxxxxx', + * 'token' => 'email_token_xxxxxxx' + * ] + * ``` + * + * @param array $payload Disable payload. + * @return array The response from Paystack API. + */ + public function disable(array $payload): array { - return $this->handle(fn() => $this->client->post('subscription/disable', $data)->json()); + return $this->handle(fn() => $this->client->post('subscription/disable', $payload)->json()); } - public function enable(array $data): array + /** + * Enable a subscription using code and token. + * + * Example payload: + * ```php + * [ + * 'code' => 'SUB_xxxxxxx', + * 'token' => 'email_token_xxxxxxx' + * ] + * ``` + * + * @param array $payload Enable payload. + * @return array The response from Paystack API. + */ + public function enable(array $payload): array { - return $this->handle(fn() => $this->client->post('subscription/enable', $data)->json()); + return $this->handle(fn() => $this->client->post('subscription/enable', $payload)->json()); } + /** + * Fetch details of a specific subscription by code. + * + * @param string $subscriptionCode The subscription code. + * @return array The response from Paystack API. + */ public function fetch(string $subscriptionCode): array { return $this->handle(fn() => $this->client->get("subscription/{$subscriptionCode}")->json()); } + /** + * List all subscriptions or filter them. + * + * Example filters: + * ```php + * [ + * 'customer' => 'CUS_xxxxxxx', + * 'plan' => 'PLN_xxxxxxx', + * 'perPage' => 50, + * 'page' => 1 + * ] + * ``` + * + * @param array $params Optional query parameters. + * @return array The response from Paystack API. + */ public function list(array $params = []): array { return $this->handle(fn() => $this->client->get('subscription', $params)->json()); } - } diff --git a/src/Services/TransactionService.php b/src/Services/TransactionService.php index 171ba4b..128ad41 100644 --- a/src/Services/TransactionService.php +++ b/src/Services/TransactionService.php @@ -5,17 +5,47 @@ use Illuminate\Support\Facades\Request; use Unicodeveloper\Paystack\Client\PaystackClient; use Unicodeveloper\Paystack\Exceptions\PaystackRequestException; -// use Unicodeveloper\Paystack\Paystack; +/** + * Class TransactionService + * + * Handles transaction-related operations with the Paystack API. + * + * @package Unicodeveloper\Paystack\Services +*/ class TransactionService { + /** + * Paystack HTTP client instance. + * + * @var \Unicodeveloper\Paystack\Client\PaystackClient + */ protected PaystackClient $client; + /** + * TransactionService constructor. + * + * @param \Unicodeveloper\Paystack\Client\PaystackClient $client + */ public function __construct(PaystackClient $client) { $this->client = $client; } + /** + * Handle exceptions gracefully and format the result. + * + * @internal This method is not part of the public API and may change without notice. + * + * @internal This method is not part of the public API and may change without notice. + * + * @param callable $callback + * @return array { + * @type bool $status Indicates success or failure. + * @type string $message Descriptive message or error. + * @type mixed $data The API response data or null on failure. + * } + */ protected function handle(callable $callback): array { try { @@ -29,21 +59,59 @@ protected function handle(callable $callback): array } } + /** + * Initialize a transaction. + * + * Required payload fields: + * - email: Customer's email address. + * - amount: Amount in kobo (10000 = โ‚ฆ100). + * + * Example: + * ```php + * [ + * 'email' => 'user@example.com', + * 'amount' => 10000, + * 'callback_url' => 'https://yourapp.com/callback' + * ] + * ``` + * + * @param array $payload Transaction initialization data. + * @return array Paystack response including authorization URL. + */ public function initialize(array $payload): array { return $this->handle(fn() => $this->client->post('transaction/initialize', $payload)->json()); } + /** + * Verify a transaction by reference code. + * + * @param string $reference The transaction reference. + * @return array Paystack verification result. + */ public function verify(string $reference): array { return $this->handle(fn() => $this->client->get("transaction/verify/{$reference}")->json()); } + /** + * List transactions with pagination support. + * + * @param int $perPage Number of results per page. + * @param int $page Current page number. + * @return array Paginated list of transactions. + */ public function list(int $perPage = 50, int $page = 1): array { return $this->handle(fn() => $this->client->get("transaction?perPage={$perPage}&page={$page}")->json()); } + /** + * Fetch a single transaction by its ID or reference. + * + * @param int|string $id Transaction ID or reference string. + * @return array Transaction details. + */ public function fetch(int|string $id): array { return $this->handle(fn() => $this->client->get("transaction/{$id}")->json()); diff --git a/src/Support/TransRef.php b/src/Support/TransRef.php index 080c7c4..2b8f8fe 100644 --- a/src/Support/TransRef.php +++ b/src/Support/TransRef.php @@ -2,11 +2,24 @@ namespace Unicodeveloper\Paystack\Support; +/** + * Class TransRef + * + * Generates a unique transaction reference string. + * + * @package Unicodeveloper\Paystack\Support +*/ class TransRef { + /** + * Generate a unique transaction reference. + * + * Format: TXN__ + * + * @return string + */ public static function generate(): string { - // return 'TXN_' . strtoupper(uniqid(bin2hex(random_bytes(4)), true)); return 'TXN_' . uniqid() . '_' . bin2hex(random_bytes(4)); } } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index c0a0eaf..1d96be4 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -1,9 +1,20 @@ getAuthorizationUrl(); + * ``` + * + * @return \Unicodeveloper\Paystack\Paystack +*/ +if (! function_exists("paystack")) { function paystack() { - return app()->make('laravel-paystack'); } -} \ No newline at end of file +} From f1b4b6c11063b68d53e8aa1abb7d5705954da804 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 06:31:29 +0100 Subject: [PATCH 09/30] Add what changed --- CHANGELOG.md | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73df5fb..6a17033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,50 @@ All Notable changes to `laravel-paystack` will be documented in this file -## 2015-11-04 -- Initial release +The format is inspired by [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). + +--- + +## [Unreleased] + +### Added +- Created `PaystackClient` class to abstract Guzzle HTTP requests. +- Introduced service classes: `TransactionService`, `CustomerService`, `PlanService`, etc., each handling a specific Paystack domain. +- Added unit test, and integration test for `TransactionService` using real API keys (with backup mocking planned). +- Improved exception handling in Paystack services +- Created `.phpunit.result.cache` for PHPUnit test caching. +- Added docblocks to service classes and methods for improved IDE support and maintainability. +- Added `setup.sh` to automate initial project setup + + +### Changed +- Refactored `Paystack.php` to delegate to service classes, following the Single Responsibility Principle (SRP). +- Composer autoload structure improved for better PSR-4 compliance. +- Improved test folder and autoload mappings. +- Improved `CHANGELOG.md` for release history tracking. +- Improved folder structuring eg `resources/config/paystack.php` to `config/paystack.php` + +### Fixed +- Removed deprecated test code + +### Removed +- Removed unused service logic from the core `Paystack` class. +- Cleaned up old/unused config entries. + +--- ## 2020-05-23 + +### Added - Support for Laravel 7 - Support for splitting payments into subaccounts - Support for more than one currency. Now you can use USD! - Support for multiple quantities -- Support for helpers \ No newline at end of file +- Support for helpers + +--- + +## 2015-11-04 + +### Added +- Initial release From 002aa30c966310a63f9a7fc50f81def8ce870e62 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 06:31:50 +0100 Subject: [PATCH 10/30] Modify ignore file --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3999575..d8be81f 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,3 @@ teamcity.txt .env.testing .phpunit.cache .phpunit.result.cache -.phpunit.result.cache -phpunit.xml.dist.bak From c345323d8a6cceb26bf9c156d6785e3a1c906357 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 06:34:40 +0100 Subject: [PATCH 11/30] Add Integration test --- tests/Integration/TransactionServiceTest.php | 111 +++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/Integration/TransactionServiceTest.php diff --git a/tests/Integration/TransactionServiceTest.php b/tests/Integration/TransactionServiceTest.php new file mode 100644 index 0000000..d100589 --- /dev/null +++ b/tests/Integration/TransactionServiceTest.php @@ -0,0 +1,111 @@ +secretKey = config('paystack.secretKey'); + $this->publicKey = config('paystack.publicKey'); + $this->paymentUrl = config('paystack.paymentUrl'); + + // $client = new PaystackClient(secretKey: $this->secretKey, baseUrl: $this->paymentUrl); + // $this->transaction = new TransactionService($client); + // dump($this->secretKey); + + $this->transaction = $this->app->make(TransactionService::class); + + // Example: Log or use secret key from config + + } + + + + public function testInitializeTransactionWithRealApi() + { + if (! str_starts_with($this->baseUrl, 'http')) { + throw new \InvalidArgumentException("Invalid Paystack base URL: {$this->baseUrl}"); + } + + $reference = Str::uuid()->toString(); + // dd($this->paymentUrl, $this->secretKey, $this->publicKey,); + + $response = $this->transaction->initialize([ + 'email' => 'customer@example.com', + 'amount' => 5000, // in kobo + 'reference' => $reference, + 'callback_url' => 'https://example.com/callback' + ]); + + $this->assertIsArray($response); + $this->assertTrue($response['status']); + $this->assertArrayHasKey('authorization_url', $response['data']); + $this->assertArrayHasKey('reference', $response['data']); + } + + public function testVerifyTransactionWithRealApi() + { + $reference = Str::uuid()->toString(); + + $initResponse = $this->transaction->initialize([ + 'email' => 'verify@example.com', + 'amount' => 10000, + 'reference' => $reference, + 'callback_url' => 'https://example.com/callback' + ]); + + $this->assertTrue($initResponse['status']); + + // Simulate verifying the same reference + $verifyResponse = $this->transaction->verify($reference); + + $this->assertIsArray($verifyResponse); + $this->assertTrue($verifyResponse['status']); + $this->assertEquals($reference, $verifyResponse['data']['reference']); + } + + public function testListTransactions() + { + $response = $this->transaction->list(perPage: 10, page: 1); + + // dd(gettype($response)); + + $this->assertIsArray($response); + $this->assertTrue($response['status']); + $this->assertArrayHasKey('data', $response); + $this->assertIsArray($response['data']); + } + + public function testFetchTransactionById() + { + $listResponse = $this->transaction->list(perPage: 1); + + $this->assertTrue($listResponse['status']); + $transactions = $listResponse['data']; + + if (count($transactions) > 0) { + $id = $transactions[0]['id']; + + $fetchResponse = $this->transaction->fetch($id); + + $this->assertTrue($fetchResponse['status']); + $this->assertEquals($id, $fetchResponse['data']['id']); + } else { + $this->markTestSkipped('No transactions available to fetch.'); + } + } + +} From c81b915b025ffd09d6f2722c3b7a4bf77a6393ea Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 06:44:17 +0100 Subject: [PATCH 12/30] Modify helper file --- src/Support/helpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 1d96be4..8b7ccdf 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -8,7 +8,7 @@ * Example: * ```php * $paystack = paystack(); - * $response = $paystack->getAuthorizationUrl(); + * $response = $paystack->transaction()->initialize($payload); * ``` * * @return \Unicodeveloper\Paystack\Paystack From 2b94a1bfd6e5b6d2ee0d8efea3d51071516d3c60 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 09:53:35 +0100 Subject: [PATCH 13/30] Install php-cs-fixer --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3bd0681..7f7bcf6 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "illuminate/database": "~6|~7|~8|~9|^10.0|^11.0|^12.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.85", "icanhazstring/composer-unused": "^0.9.4", "laravel/legacy-factories": "^1.4", "mockery/mockery": "^1.3", @@ -62,7 +63,9 @@ } }, "scripts": { - "test": "vendor/bin/phpunit" + "test": "vendor/bin/phpunit", + "unused": "vendor/bin/composer-unused", + "php-fix": "vendor/bin/php-cs-fixer fix src" }, "extra": { "laravel": { From d8e0cb36fb3a94d35cd2f2b1d3848ba42aa092f5 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 09:58:11 +0100 Subject: [PATCH 14/30] Enforce PSR compliance --- src/Client/PaystackClient.php | 16 ++++++++-------- src/Exceptions/IsNullException.php | 1 - src/Paystack.php | 1 - src/PaystackServiceProvider.php | 14 +++++++------- src/Services/BankService.php | 8 ++++---- src/Services/CustomerService.php | 8 ++++---- src/Services/PageService.php | 8 ++++---- src/Services/PlanService.php | 8 ++++---- src/Services/SubAccountService.php | 8 ++++---- src/Services/SubscriptionService.php | 12 ++++++------ src/Services/TransactionService.php | 12 ++++++------ src/Support/helpers.php | 3 ++- 12 files changed, 49 insertions(+), 50 deletions(-) diff --git a/src/Client/PaystackClient.php b/src/Client/PaystackClient.php index fda941a..d3ba139 100644 --- a/src/Client/PaystackClient.php +++ b/src/Client/PaystackClient.php @@ -25,7 +25,7 @@ class PaystackClient public function __construct(?string $secretKey = '', ?string $baseUrl = '') { $this->baseUrl = $baseUrl ?: config('paystack.paymentUrl', 'https://api.paystack.co'); - $this->secretKey = $secretKey ?: config('paystack.secretKey'); + $this->secretKey = $secretKey ?: config('paystack.secretKey'); } /** @@ -129,13 +129,13 @@ public function put(string $endpoint, array $data): Response return $this->request('put', $endpoint, $data); } - /** - * Send a DELETE request to a Paystack API endpoint. - * - * @param string $endpoint - * @return Response - * - * @throws PaystackRequestException + /** + * Send a DELETE request to a Paystack API endpoint. + * + * @param string $endpoint + * @return Response + * + * @throws PaystackRequestException */ public function delete(string $endpoint): Response { diff --git a/src/Exceptions/IsNullException.php b/src/Exceptions/IsNullException.php index d90777e..6111f8e 100644 --- a/src/Exceptions/IsNullException.php +++ b/src/Exceptions/IsNullException.php @@ -15,5 +15,4 @@ class IsNullException extends Exception { - } diff --git a/src/Paystack.php b/src/Paystack.php index b25be76..6cb8c06 100644 --- a/src/Paystack.php +++ b/src/Paystack.php @@ -121,4 +121,3 @@ public function transRef(): string return TransRef::generate(); } } - \ No newline at end of file diff --git a/src/PaystackServiceProvider.php b/src/PaystackServiceProvider.php index 64c0b68..60d15c9 100644 --- a/src/PaystackServiceProvider.php +++ b/src/PaystackServiceProvider.php @@ -49,13 +49,13 @@ public function register() return new PaystackClient(config('paystack.secretKey')); }); - $this->app->singleton(TransactionService::class, fn($app) => new TransactionService($app->make(PaystackClient::class))); - $this->app->singleton(CustomerService::class, fn($app) => new CustomerService($app->make(PaystackClient::class))); - $this->app->singleton(PlanService::class, fn($app) => new PlanService($app->make(PaystackClient::class))); - $this->app->singleton(SubscriptionService::class, fn($app) => new SubscriptionService($app->make(PaystackClient::class))); - $this->app->singleton(PageService::class, fn($app) => new PageService($app->make(PaystackClient::class))); - $this->app->singleton(SubAccountService::class, fn($app) => new SubAccountService($app->make(PaystackClient::class))); - $this->app->singleton(BankService::class, fn($app) => new BankService($app->make(PaystackClient::class))); + $this->app->singleton(TransactionService::class, fn ($app) => new TransactionService($app->make(PaystackClient::class))); + $this->app->singleton(CustomerService::class, fn ($app) => new CustomerService($app->make(PaystackClient::class))); + $this->app->singleton(PlanService::class, fn ($app) => new PlanService($app->make(PaystackClient::class))); + $this->app->singleton(SubscriptionService::class, fn ($app) => new SubscriptionService($app->make(PaystackClient::class))); + $this->app->singleton(PageService::class, fn ($app) => new PageService($app->make(PaystackClient::class))); + $this->app->singleton(SubAccountService::class, fn ($app) => new SubAccountService($app->make(PaystackClient::class))); + $this->app->singleton(BankService::class, fn ($app) => new BankService($app->make(PaystackClient::class))); $this->app->singleton(Paystack::class, function ($app) { return new Paystack($app->make(PaystackClient::class)); diff --git a/src/Services/BankService.php b/src/Services/BankService.php index 898a175..f92300c 100644 --- a/src/Services/BankService.php +++ b/src/Services/BankService.php @@ -20,7 +20,7 @@ class BankService * @var \Unicodeveloper\Paystack\Client\PaystackClient */ protected PaystackClient $client; - + /** * BankService constructor. * @@ -35,7 +35,7 @@ public function __construct(PaystackClient $client) * Wrap API calls with error handling. * * @internal This method is not part of the public API and may change without notice. - * + * * @param callable $callback * @return array { * @type bool $status Whether the request was successful. @@ -63,7 +63,7 @@ protected function handle(callable $callback): array */ public function list(): array { - return $this->handle(fn() => $this->client->get('bank')->json()); + return $this->handle(fn () => $this->client->get('bank')->json()); } /** @@ -82,6 +82,6 @@ public function list(): array */ public function resolveAccount(array $data): array { - return $this->handle(fn() => $this->client->get('bank/resolve', $data)->json()); + return $this->handle(fn () => $this->client->get('bank/resolve', $data)->json()); } } diff --git a/src/Services/CustomerService.php b/src/Services/CustomerService.php index d500667..70dfc01 100644 --- a/src/Services/CustomerService.php +++ b/src/Services/CustomerService.php @@ -35,7 +35,7 @@ public function __construct(PaystackClient $client) * Wrap API calls with error handling. * * @internal This method is not part of the public API and may change without notice. - * + * * @param callable $callback * @return array { * @type bool $status Whether the request was successful. @@ -74,7 +74,7 @@ protected function handle(callable $callback): array */ public function create(array $data): array { - return $this->handle(fn() => $this->client->post('customer', $data)->json()); + return $this->handle(fn () => $this->client->post('customer', $data)->json()); } /** @@ -85,7 +85,7 @@ public function create(array $data): array */ public function fetch(string $customerCode): array { - return $this->handle(fn() => $this->client->get("customer/{$customerCode}")->json()); + return $this->handle(fn () => $this->client->get("customer/{$customerCode}")->json()); } /** @@ -97,6 +97,6 @@ public function fetch(string $customerCode): array */ public function list(int $perPage = 50, int $page = 1): array { - return $this->handle(fn() => $this->client->get("customer?perPage={$perPage}&page={$page}")->json()); + return $this->handle(fn () => $this->client->get("customer?perPage={$perPage}&page={$page}")->json()); } } diff --git a/src/Services/PageService.php b/src/Services/PageService.php index 57d524d..d25b3b4 100644 --- a/src/Services/PageService.php +++ b/src/Services/PageService.php @@ -35,7 +35,7 @@ public function __construct(PaystackClient $client) * Wrap API calls with error handling. * * @internal This method is not part of the public API and may change without notice. - * + * * @param callable $callback * @return array { * @type bool $status Whether the request was successful. @@ -73,7 +73,7 @@ protected function handle(callable $callback): array */ public function create(array $data): array { - return $this->handle(fn() => $this->client->post('page', $data)->json()); + return $this->handle(fn () => $this->client->post('page', $data)->json()); } /** @@ -84,7 +84,7 @@ public function create(array $data): array */ public function fetch(string $pageId): array { - return $this->handle(fn() => $this->client->get("page/{$pageId}")->json()); + return $this->handle(fn () => $this->client->get("page/{$pageId}")->json()); } /** @@ -94,6 +94,6 @@ public function fetch(string $pageId): array */ public function list(): array { - return $this->handle(fn() => $this->client->get("page")->json()); + return $this->handle(fn () => $this->client->get("page")->json()); } } diff --git a/src/Services/PlanService.php b/src/Services/PlanService.php index bbcfa82..c35c3f7 100644 --- a/src/Services/PlanService.php +++ b/src/Services/PlanService.php @@ -35,7 +35,7 @@ public function __construct(PaystackClient $client) * Wrap API calls with error handling. * * @internal This method is not part of the public API and may change without notice. - * + * * @param callable $callback * @return array { * @type bool $status Whether the request was successful. @@ -73,7 +73,7 @@ protected function handle(callable $callback): array */ public function create(array $data): array { - return $this->handle(fn() => $this->client->post('plan', $data)->json()); + return $this->handle(fn () => $this->client->post('plan', $data)->json()); } /** @@ -84,7 +84,7 @@ public function create(array $data): array */ public function fetch(string $planCode): array { - return $this->handle(fn() => $this->client->get("plan/{$planCode}")->json()); + return $this->handle(fn () => $this->client->get("plan/{$planCode}")->json()); } /** @@ -96,6 +96,6 @@ public function fetch(string $planCode): array */ public function list(int $perPage = 50, int $page = 1): array { - return $this->handle(fn() => $this->client->get("plan?perPage={$perPage}&page={$page}")->json()); + return $this->handle(fn () => $this->client->get("plan?perPage={$perPage}&page={$page}")->json()); } } diff --git a/src/Services/SubAccountService.php b/src/Services/SubAccountService.php index 11786d4..4dc8457 100644 --- a/src/Services/SubAccountService.php +++ b/src/Services/SubAccountService.php @@ -35,7 +35,7 @@ public function __construct(PaystackClient $client) * Wrap API calls with exception handling. * * @internal This method is not part of the public API and may change without notice. - * + * * @param callable $callback * @return array { * @type bool $status Whether the request was successful. @@ -74,7 +74,7 @@ protected function handle(callable $callback): array */ public function create(array $data): array { - return $this->handle(fn() => $this->client->post('subaccount', $data)->json()); + return $this->handle(fn () => $this->client->post('subaccount', $data)->json()); } /** @@ -85,7 +85,7 @@ public function create(array $data): array */ public function fetch(string $subaccountCode): array { - return $this->handle(fn() => $this->client->get("subaccount/{$subaccountCode}")->json()); + return $this->handle(fn () => $this->client->get("subaccount/{$subaccountCode}")->json()); } /** @@ -95,6 +95,6 @@ public function fetch(string $subaccountCode): array */ public function list(): array { - return $this->handle(fn() => $this->client->get("subaccount")->json()); + return $this->handle(fn () => $this->client->get("subaccount")->json()); } } diff --git a/src/Services/SubscriptionService.php b/src/Services/SubscriptionService.php index 7c6e64f..e24f315 100644 --- a/src/Services/SubscriptionService.php +++ b/src/Services/SubscriptionService.php @@ -35,7 +35,7 @@ public function __construct(PaystackClient $client) * Wrap API calls with exception handling. * * @internal This method is not part of the public API and may change without notice. - * + * * @param callable $callback * @return array { * @type bool $status Whether the request was successful. @@ -73,7 +73,7 @@ protected function handle(callable $callback): array */ public function create(array $payload): array { - return $this->handle(fn() => $this->client->post('subscription', $payload)->json()); + return $this->handle(fn () => $this->client->post('subscription', $payload)->json()); } /** @@ -92,7 +92,7 @@ public function create(array $payload): array */ public function disable(array $payload): array { - return $this->handle(fn() => $this->client->post('subscription/disable', $payload)->json()); + return $this->handle(fn () => $this->client->post('subscription/disable', $payload)->json()); } /** @@ -111,7 +111,7 @@ public function disable(array $payload): array */ public function enable(array $payload): array { - return $this->handle(fn() => $this->client->post('subscription/enable', $payload)->json()); + return $this->handle(fn () => $this->client->post('subscription/enable', $payload)->json()); } /** @@ -122,7 +122,7 @@ public function enable(array $payload): array */ public function fetch(string $subscriptionCode): array { - return $this->handle(fn() => $this->client->get("subscription/{$subscriptionCode}")->json()); + return $this->handle(fn () => $this->client->get("subscription/{$subscriptionCode}")->json()); } /** @@ -143,6 +143,6 @@ public function fetch(string $subscriptionCode): array */ public function list(array $params = []): array { - return $this->handle(fn() => $this->client->get('subscription', $params)->json()); + return $this->handle(fn () => $this->client->get('subscription', $params)->json()); } } diff --git a/src/Services/TransactionService.php b/src/Services/TransactionService.php index 128ad41..fdd553f 100644 --- a/src/Services/TransactionService.php +++ b/src/Services/TransactionService.php @@ -36,9 +36,9 @@ public function __construct(PaystackClient $client) * Handle exceptions gracefully and format the result. * * @internal This method is not part of the public API and may change without notice. - * + * * @internal This method is not part of the public API and may change without notice. - * + * * @param callable $callback * @return array { * @type bool $status Indicates success or failure. @@ -80,7 +80,7 @@ protected function handle(callable $callback): array */ public function initialize(array $payload): array { - return $this->handle(fn() => $this->client->post('transaction/initialize', $payload)->json()); + return $this->handle(fn () => $this->client->post('transaction/initialize', $payload)->json()); } /** @@ -91,7 +91,7 @@ public function initialize(array $payload): array */ public function verify(string $reference): array { - return $this->handle(fn() => $this->client->get("transaction/verify/{$reference}")->json()); + return $this->handle(fn () => $this->client->get("transaction/verify/{$reference}")->json()); } /** @@ -103,7 +103,7 @@ public function verify(string $reference): array */ public function list(int $perPage = 50, int $page = 1): array { - return $this->handle(fn() => $this->client->get("transaction?perPage={$perPage}&page={$page}")->json()); + return $this->handle(fn () => $this->client->get("transaction?perPage={$perPage}&page={$page}")->json()); } /** @@ -114,6 +114,6 @@ public function list(int $perPage = 50, int $page = 1): array */ public function fetch(int|string $id): array { - return $this->handle(fn() => $this->client->get("transaction/{$id}")->json()); + return $this->handle(fn () => $this->client->get("transaction/{$id}")->json()); } } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 8b7ccdf..e4b1f39 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -14,7 +14,8 @@ * @return \Unicodeveloper\Paystack\Paystack */ if (! function_exists("paystack")) { - function paystack() { + function paystack() + { return app()->make('laravel-paystack'); } } From 84d8204436f2a3fe2c8f6d59c7f6d220987c2a48 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 10:06:54 +0100 Subject: [PATCH 15/30] Modify setup file --- setup.sh | 9 +++++ src/TransRef.php | 93 ------------------------------------------------ 2 files changed, 9 insertions(+), 93 deletions(-) delete mode 100644 src/TransRef.php diff --git a/setup.sh b/setup.sh index 7ded010..a966283 100644 --- a/setup.sh +++ b/setup.sh @@ -5,6 +5,13 @@ echo "Starting setup for Laravel-paystack Package..." # Exit on any error set -e +# Ensure composer is available +if ! command -v composer &> /dev/null +then + echo "Composer not found. Please install Composer." + exit +fi + # Install dependencies echo "Installing Composer dependencies..." composer install @@ -13,6 +20,8 @@ composer install if [ ! -f .env.testing ]; then echo "Copying .env.testing.example to .env.testing" cp .env.testing.example .env.testing +else + echo ".env.testing already exists, skipped." fi # Migrate PHPUnit configuration if needed diff --git a/src/TransRef.php b/src/TransRef.php deleted file mode 100644 index 7a166c3..0000000 --- a/src/TransRef.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * Source http://stackoverflow.com/a/13733588/179104 - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Unicodeveloper\Paystack; - -class TransRef -{ - /** - * Get the pool to use based on the type of prefix hash - * @param string $type - * @return string - */ - private static function getPool($type = 'alnum') - { - switch ($type) { - case 'alnum': - $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - break; - case 'alpha': - $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - break; - case 'hexdec': - $pool = '0123456789abcdef'; - break; - case 'numeric': - $pool = '0123456789'; - break; - case 'nozero': - $pool = '123456789'; - break; - case 'distinct': - $pool = '2345679ACDEFHJKLMNPRSTUVWXYZ'; - break; - default: - $pool = (string) $type; - break; - } - - return $pool; - } - - /** - * Generate a random secure crypt figure - * @param integer $min - * @param integer $max - * @return integer - */ - private static function secureCrypt($min, $max) - { - $range = $max - $min; - - if ($range < 0) { - return $min; // not so random... - } - - $log = log($range, 2); - $bytes = (int) ($log / 8) + 1; // length in bytes - $bits = (int) $log + 1; // length in bits - $filter = (int) (1 << $bits) - 1; // set all lower bits to 1 - do { - $rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes))); - $rnd = $rnd & $filter; // discard irrelevant bits - } while ($rnd >= $range); - - return $min + $rnd; - } - - /** - * Finally, generate a hashed token - * @param integer $length - * @return string - */ - public static function getHashedToken($length = 25) - { - $token = ""; - $max = strlen(static::getPool()); - for ($i = 0; $i < $length; $i++) { - $token .= static::getPool()[static::secureCrypt(0, $max)]; - } - - return $token; - } -} From a87c159e69fe88b9d015256c097c1c1f7df27c89 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Sun, 3 Aug 2025 10:07:30 +0100 Subject: [PATCH 16/30] Fix for PSR Compliance --- src/Exceptions/PaymentVerificationFailedException.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Exceptions/PaymentVerificationFailedException.php b/src/Exceptions/PaymentVerificationFailedException.php index 7886880..f084306 100644 --- a/src/Exceptions/PaymentVerificationFailedException.php +++ b/src/Exceptions/PaymentVerificationFailedException.php @@ -15,5 +15,4 @@ class PaymentVerificationFailedException extends Exception { - } From 17a6fa654dba442f1f9c9f0cd6af75972ee44965 Mon Sep 17 00:00:00 2001 From: Bello-ibrahm Date: Sun, 3 Aug 2025 13:37:15 +0100 Subject: [PATCH 17/30] Modify gitignore file --- .gitignore | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.gitignore b/.gitignore index d8be81f..e329814 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,19 @@ teamcity.txt .env.testing .phpunit.cache .phpunit.result.cache +phpunit.xml.dist.bak +.php-cs-fixer.cache + +laravel-paystack-app/ +myNote.md +routes/ +resources/ +Documentation.md +database/ +src/Http/ +src/Models/ +tests/Feature/ +src/Exceptions/TransactionDashboardException.php +src/TransRef.php +tests/TestCaseBackup.php + From 08813b064c93dd5af6a72b5b7db6e10cc604b939 Mon Sep 17 00:00:00 2001 From: Bello-ibrahm Date: Sun, 3 Aug 2025 15:28:45 +0100 Subject: [PATCH 18/30] Modify PR template --- .github/pull_request_template.md | 55 +++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9fcc1d4..61aeb02 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,38 +5,69 @@ Refactor to enforce Single Responsibility Principle (SRP) in `PaystackClient`. Extracted responsibilities into dedicated service classes. +--- + ## ๐Ÿ” Related Issues Fixes # +--- + ## โœ… Changes Made - [x] Extracted HTTP logic into `PaystackClient` - [x] Created `TransactionService`, `CustomerService`, etc. -- [x] Applied PSR-4 autoloading -- [x] Added phpDocumentor support -- [x] Improved test structure +- [x] Applied PSR-4 autoloading structure +- [x] Added docblocks for IDE +- [x] Improved test folder and structure + +--- ## ๐Ÿ’ก Motivation -Improving maintainability, testability, and code organization of the SDK. +Improving maintainability, testability, and adherence to clean architecture (SRP/SOLID principles). + +--- ## ๐Ÿงช Tests -- Existing unit tests pass โœ… -- Added new tests for each service class +- [x] Unit tests for all new service classes +- [x] Integration tests using live API keys +- [x] Fallbacks to HTTP fake/mocks planned + +--- ## ๐Ÿ“‹ Checklist -- [ ] Code builds without errors -- [ ] Tests pass locally -- [ ] Linted with `php-cs-fixer` or similar -- [ ] Documentation updated (if applicable) -- [ ] New classes follow SRP and SOLID +- [x] Code builds without errors +- [x] All PHPUnit tests pass +- [x] Linted with `php-cs-fixer` or similar +- [x] Documentation updated (if applicable) +- [x] SRP principles respected across all services +- [x] PR title and description are clear --- -> _Please review this PR and leave feedback if needed._ +## ๐Ÿ“ธ Screenshots (UI-related changes only) + + + +--- + +## โ— Breaking Changes? + +- [ ] Yes +- [x] No + + + +--- + +## ๐Ÿ’ฌ Additional Notes + + + +> _Please review this PR and provide feedback if needed._ From 898771cdc4ba5f7a98cb3ae32ef2e491ec974b20 Mon Sep 17 00:00:00 2001 From: Bello-ibrahm Date: Sun, 3 Aug 2025 15:41:58 +0100 Subject: [PATCH 19/30] Add document file --- doc/README.md | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 doc/README.md diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..fa164e6 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,205 @@ +# Laravel Paystack SDK + +> A Laravel SDK for integrating with the [Paystack API](https://paystack.com/docs/api/), providing a clean and expressive way to interact with transactions, customers, plans, subscriptions, and more. + +> ๐Ÿ“ฆ Supports Laravel 9, 10, 11, and 12. Built with PHP 8.2+ features. + +--- + +## ๐Ÿš€ Features + +- Simple Laravel-style service and facade structure +- Fully modular service classes (e.g., `TransactionService`, `CustomerService`, etc.) +- Simple and expressive API: `$paystack->transaction()->initialize([...])` +- Strong type declarations and IDE-friendly docblocks +- Auto-generated transaction references with `transRef()` +- Built-in error handling and retry logic +- PSR-4 compliant and fully testable (unit & integration) + +--- + +## ๐Ÿ“ฆ Installation + +Install via Composer: + +```bash +composer require unicodeveloper/laravel-paystack +``` +> Laravel auto-discovers the package. No manual registration needed. + +--- + +## โš™๏ธ Configuration +Publish the config file: +```bash +php artisan vendor:publish --provider="Unicodeveloper\Paystack\PaystackServiceProvider" +``` +Update your `.env`: +``` +PAYSTACK_PUBLIC_KEY=pk_test_xxxxx +PAYSTACK_SECRET_KEY=sk_test_xxxxx +PAYSTACK_PAYMENT_URL=https://api.paystack.co +PAYSTACK_CALLBACK_URL=https://example.com/payment/callback +MERCHANT_EMAIL='example@example.com' +``` +--- + +## ๐Ÿงช Usage +### Transaction +``` +use Paystack; + +$response = Paystack::transaction()->initialize([ + 'email' => 'user@example.com', + 'amount' => 200000, // Amount in kobo +]); + +$authorizationUrl = $response['data']['authorization_url']; +``` +### Customer +``` +$customer = Paystack::customer()->create([ + 'email' => 'john@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe' +]); +``` +### Plan +``` +$plan = Paystack::plan()->create([ + 'name' => 'Premium Monthly', + 'amount' => 5000 * 100, + 'interval' => 'monthly' +]); +``` +### Controller Example +``` +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email', + 'amount' => 'required|numeric|min:100', + 'description' => 'nullable|string', + ]); + + $reference = Paystack::transRef(); + + $payload = [ + 'email' => $request->email, + 'amount' => $request->amount * 100, + 'reference' => $reference, + 'callback_url' => route('payment.callback'), + 'metadata' => [ + 'custom_fields' => [ + [ + 'display_name' => 'Name', + 'variable_name' => 'name', + 'value' => $request->name + ], + [ + 'display_name' => 'Description', + 'variable_name' => 'description', + 'value' => $request->description + ] + ] + ] + ]; + + try { + $response = Paystack::transaction()->initialize($payload); + $transAuthURL = $response['data']['authorization_url']; + + return redirect($transAuthURL); + } catch (\Exception $e) { + Log::error('Paystack Error', ['message' => $e->getMessage()]); + return back()->with('error', 'Failed to initiate payment.'); + } + } + + /** + * Handle the callback from Paystack after payment. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse + */ + public function handleGatewayCallback(Request $request) + { + $reference = $request->query('reference'); + + try { + $response = Paystack::transaction()->verify($reference); + $data = $response['data']; + + // Here, you could store the payment record, send a receipt email, etc. + return view('payments.success', ['payment' => $data]); + } catch (\Exception $e) { + Log::error('Verification Error', ['message' => $e->getMessage()]); + return redirect('/payment')->with('error', 'Payment verification failed.'); + } + } +} +``` +> All methods return Laravel HTTP responses with decoded JSON. + +### Route Example +``` +Route::get('/payment', [App\Http\Controllers\PaymentController::class, 'index'])->name('payment.form'); +Route::post('/checkout', [App\Http\Controllers\PaymentController::class, 'redirectToGateway'])->name('checkout.process'); +Route::get('/payment/callback', [App\Http\Controllers\PaymentController::class, 'handleGatewayCallback'])->name('payment.callback'); +``` + + +| Service | Description | +| ---------------- | ------------------------------- | +| `transaction()` | Handle payment transactions | +| `customer()` | Manage customer records | +| `plan()` | Create and manage payment plans | +| `subscription()` | Handle recurring subscriptions | +| `transfer()` | Initiate and manage transfers | +| `bank()` | Retrieve bank lists | + +--- +## โœ… Testing +Run tests: +``` +composer test +``` +> Integration tests assume PAYSTACK_SECRET_KEY is set in .env.testing. + +--- + + From a7bf8a927a691d41735925108fe60322845edcb5 Mon Sep 17 00:00:00 2001 From: Bello-ibrahm Date: Sun, 3 Aug 2025 16:12:57 +0100 Subject: [PATCH 20/30] Implement Built-in retry logic for the PaystackClient --- .env.testing.example | 4 +++ .github/pull_request_template.md | 2 +- CHANGELOG.md | 45 +++++++++++++++++++------------- config/paystack.php | 6 +++++ src/Client/PaystackClient.php | 5 +++- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/.env.testing.example b/.env.testing.example index 7770425..3eb4155 100644 --- a/.env.testing.example +++ b/.env.testing.example @@ -4,3 +4,7 @@ PAYSTACK_SECRET_KEY= PAYSTACK_PAYMENT_URL=https://api.paystack.co PAYSTACK_CALLBACK_URL= MERCHANT_EMAIL='' + +# Optional retry settings +PAYSTACK_RETRY_ATTEMPTS=3 +PAYSTACK_RETRY_DELAY=150 \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 61aeb02..3728374 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,7 +21,7 @@ Fixes # - [x] Applied PSR-4 autoloading structure - [x] Added docblocks for IDE - [x] Improved test folder and structure - +- [x] Configurable retry logic to `PaystackClient` via `retry_attempts` and `retry_delay` in `config/paystack.php`. --- ## ๐Ÿ’ก Motivation diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a17033..ce780f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,28 +9,37 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com) and thi ## [Unreleased] ### Added -- Created `PaystackClient` class to abstract Guzzle HTTP requests. -- Introduced service classes: `TransactionService`, `CustomerService`, `PlanService`, etc., each handling a specific Paystack domain. -- Added unit test, and integration test for `TransactionService` using real API keys (with backup mocking planned). -- Improved exception handling in Paystack services -- Created `.phpunit.result.cache` for PHPUnit test caching. -- Added docblocks to service classes and methods for improved IDE support and maintainability. -- Added `setup.sh` to automate initial project setup - +- Introduced `PaystackClient` class to abstract Guzzle HTTP requests. +- Configurable retry logic to `PaystackClient` via `retry_attempts` and `retry_delay` in `config/paystack.php`. +- Added service classes for clear separation of concerns: + - `TransactionService` + - `CustomerService` + - `PlanService` + - `SubscriptionService` + - `PageService` + - `SubAccountService` + - `BankService` +- Implemented `TransRef` helper for generating transaction references (`Paystack::transRef()`). +- Wrote unit and integration tests for services using PHPUnit. +- Added `setup.sh` for bootstrapping the package setup. +- Added docblocks for all service classes and methods to improve developer experience. ### Changed -- Refactored `Paystack.php` to delegate to service classes, following the Single Responsibility Principle (SRP). -- Composer autoload structure improved for better PSR-4 compliance. -- Improved test folder and autoload mappings. -- Improved `CHANGELOG.md` for release history tracking. -- Improved folder structuring eg `resources/config/paystack.php` to `config/paystack.php` - -### Fixed -- Removed deprecated test code +- Refactored core `Paystack.php` class to follow SRP and delegate responsibilities to dedicated service classes. +- Centralized API logic through `PaystackClient` to improve testability and HTTP abstraction. +- Improved autoload structure for PSR-4 compliance. +- Restructured folders (`resources/config/paystack.php` โ†’ `config/paystack.php`). +- Enhanced PHPUnit configuration and improved test folder layout. +- Enhanced exception handling and removed reliance on global helpers like `request()` or `config()` in services. ### Removed -- Removed unused service logic from the core `Paystack` class. -- Cleaned up old/unused config entries. +- Deprecated or unused logic from the core `Paystack` class. +- Obsolete configuration entries. +- Old and redundant test code. + +### Fixed +- Resolved PSR-4 autoload warnings for tests. +- Fixed XML validation issue in `phpunit.xml` caused by invalid `` structure. --- diff --git a/config/paystack.php b/config/paystack.php index cd6dc9d..0fbf392 100644 --- a/config/paystack.php +++ b/config/paystack.php @@ -35,6 +35,12 @@ */ 'merchantEmail' => env('MERCHANT_EMAIL'), + // Maximum retry attempts for HTTP client (default: 3) + 'retry_attempts' => env('PAYSTACK_RETRY_ATTEMPTS', 3), + + // Delay (ms) between retry attempts (default: 150) + 'retry_delay' => env('PAYSTACK_RETRY_DELAY', 150), + /* |-------------------------------------------------------------------------- | Enable Package Routes - Feature diff --git a/src/Client/PaystackClient.php b/src/Client/PaystackClient.php index d3ba139..28d38c3 100644 --- a/src/Client/PaystackClient.php +++ b/src/Client/PaystackClient.php @@ -36,7 +36,10 @@ public function __construct(?string $secretKey = '', ?string $baseUrl = '') */ protected function client(): \Illuminate\Http\Client\PendingRequest { - return Http::retry(3, 150) + return Http::retry( + config('paystack.retry_attempts', 3), + config('paystack.retry_delay', 150) + ) ->withHeaders([ 'Authorization' => 'Bearer ' . $this->secretKey, 'Accept' => 'application/json', From 299d1da55ac9fed628e3ccfa9e31c14971cbeb44 Mon Sep 17 00:00:00 2001 From: Bello-ibrahm Date: Sun, 3 Aug 2025 16:13:38 +0100 Subject: [PATCH 21/30] Add README.md doc --- doc/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/README.md b/doc/README.md index fa164e6..4b14b63 100644 --- a/doc/README.md +++ b/doc/README.md @@ -15,6 +15,7 @@ - Auto-generated transaction references with `transRef()` - Built-in error handling and retry logic - PSR-4 compliant and fully testable (unit & integration) +- Built-in retry logic (configurable with PAYSTACK_RETRY_ATTEMPTS and PAYSTACK_RETRY_DELAY) --- @@ -41,6 +42,10 @@ PAYSTACK_SECRET_KEY=sk_test_xxxxx PAYSTACK_PAYMENT_URL=https://api.paystack.co PAYSTACK_CALLBACK_URL=https://example.com/payment/callback MERCHANT_EMAIL='example@example.com' + +# Optional retry settings +PAYSTACK_RETRY_ATTEMPTS=3 +PAYSTACK_RETRY_DELAY=150 ``` --- From c6ddaf6a05079344fad4cd4ea418da5de9374735 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:21:27 +0100 Subject: [PATCH 22/30] Modify git ignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e329814..e437df5 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,6 @@ tests/Feature/ src/Exceptions/TransactionDashboardException.php src/TransRef.php tests/TestCaseBackup.php +tests/Integration/auth_reference.json + From b0025dfb599ae51c6f71fbc33006e0741830c14b Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:21:55 +0100 Subject: [PATCH 23/30] Modify PR file --- .github/pull_request_template.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3728374..c884069 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ -# ๐Ÿš€ Pull Request +# Pull Request ## Summary @@ -7,14 +7,14 @@ Refactor to enforce Single Responsibility Principle (SRP) in `PaystackClient`. E --- -## ๐Ÿ” Related Issues +## Related Issues Fixes # --- -## โœ… Changes Made +## Changes Made - [x] Extracted HTTP logic into `PaystackClient` - [x] Created `TransactionService`, `CustomerService`, etc. @@ -24,14 +24,14 @@ Fixes # - [x] Configurable retry logic to `PaystackClient` via `retry_attempts` and `retry_delay` in `config/paystack.php`. --- -## ๐Ÿ’ก Motivation +## Motivation Improving maintainability, testability, and adherence to clean architecture (SRP/SOLID principles). --- -## ๐Ÿงช Tests +## Tests - [x] Unit tests for all new service classes @@ -40,7 +40,7 @@ Improving maintainability, testability, and adherence to clean architecture (SRP --- -## ๐Ÿ“‹ Checklist +## Checklist - [x] Code builds without errors - [x] All PHPUnit tests pass @@ -51,13 +51,13 @@ Improving maintainability, testability, and adherence to clean architecture (SRP --- -## ๐Ÿ“ธ Screenshots (UI-related changes only) +## Screenshots (UI-related changes only) --- -## โ— Breaking Changes? +## Breaking Changes? - [ ] Yes - [x] No @@ -66,7 +66,7 @@ Improving maintainability, testability, and adherence to clean architecture (SRP --- -## ๐Ÿ’ฌ Additional Notes +## Additional Notes From 8c2e178c4fdcea145bd536bf8d181d6d88ad4c9a Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:22:21 +0100 Subject: [PATCH 24/30] Modify Changelog file --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce780f4..9fe23dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com) and thi - Implemented `TransRef` helper for generating transaction references (`Paystack::transRef()`). - Wrote unit and integration tests for services using PHPUnit. - Added `setup.sh` for bootstrapping the package setup. -- Added docblocks for all service classes and methods to improve developer experience. +- Added PHPDoc blocks to all service classes and methods to enhance developer experience and IDE support. ### Changed - Refactored core `Paystack.php` class to follow SRP and delegate responsibilities to dedicated service classes. From f0dae1b6a6cb72e45cb07cbd4bd199d5d9eb6678 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:25:30 +0100 Subject: [PATCH 25/30] Improves codes for optimization and documentation --- src/Client/PaystackClient.php | 63 ++++++++---- src/Facades/Paystack.php | 9 +- src/Paystack.php | 2 + src/PaystackServiceProvider.php | 11 +- src/Services/BankService.php | 56 ++++++---- src/Services/CustomerService.php | 147 ++++++++++++++++++++++----- src/Services/PageService.php | 88 ++++++++++++---- src/Services/PlanService.php | 55 ++++++---- src/Services/SubAccountService.php | 63 ++++++++---- src/Services/SubscriptionService.php | 122 ++++++++++++---------- src/Services/TransactionService.php | 117 ++++++++++++++++----- src/Support/TransRef.php | 2 + src/Support/helpers.php | 4 +- 13 files changed, 532 insertions(+), 207 deletions(-) diff --git a/src/Client/PaystackClient.php b/src/Client/PaystackClient.php index 28d38c3..8bbcf14 100644 --- a/src/Client/PaystackClient.php +++ b/src/Client/PaystackClient.php @@ -1,28 +1,37 @@ baseUrl = $baseUrl ?: config('paystack.paymentUrl', 'https://api.paystack.co'); $this->secretKey = $secretKey ?: config('paystack.secretKey'); @@ -33,7 +42,7 @@ public function __construct(?string $secretKey = '', ?string $baseUrl = '') * * @internal * @return \Illuminate\Http\Client\PendingRequest - */ + */ protected function client(): \Illuminate\Http\Client\PendingRequest { return Http::retry( @@ -47,7 +56,7 @@ protected function client(): \Illuminate\Http\Client\PendingRequest ]) ->withOptions([ 'http_errors' => false, - 'version' => 1.1, + 'version' => self::HTTP_VERSION, 'verify' => true, // Set it to false for local debug ]); } @@ -60,12 +69,12 @@ protected function client(): \Illuminate\Http\Client\PendingRequest * @return Response * * @throws PaystackRequestException - */ + */ protected function handleErrors(Response $response): Response { if (! $response->successful()) { $message = $response->json('message') ?? 'Paystack request failed.'; - // \Log::error('Paystack API error', ['response' => $response->body()]); + Log::error('Paystack API error', ['response' => $response->body()]); throw new PaystackRequestException($message, $response); } @@ -78,15 +87,28 @@ protected function handleErrors(Response $response): Response * @internal * @param string $method * @param string $endpoint - * @param array $data + * @param array $data * @return Response * * @throws PaystackRequestException - */ + */ protected function request(string $method, string $endpoint, array $data = []): Response { + $allowedMethods = [self::METHOD_GET, self::METHOD_POST, self::METHOD_PUT, self::METHOD_DELETE]; + $method = strtolower($method); + + if (! in_array($method, $allowedMethods, true)) { + throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"); + } + $url = $this->baseUrl . '/' . ltrim($endpoint, '/'); - $response = $this->client()->{$method}($url, $data); + // $response = $this->client()->{$method}($url, $data); + + $client = $this->client(); + + $response = $method === 'get' + ? $client->get($url, $data) + : $client->{$method}($url, $data); return $this->handleErrors($response); } @@ -95,41 +117,42 @@ protected function request(string $method, string $endpoint, array $data = []): * Send a GET request to a Paystack API endpoint. * * @param string $endpoint + * @param array $queryParams Optional query parameters * @return Response * * @throws PaystackRequestException - */ - public function get(string $endpoint): Response + */ + public function get(string $endpoint, array $queryParams = []): Response { - return $this->request('get', $endpoint); + return $this->request(self::METHOD_GET, $endpoint, $queryParams); } /** * Send a POST request to a Paystack API endpoint. * * @param string $endpoint - * @param array $data + * @param array $data * @return Response * * @throws PaystackRequestException - */ + */ public function post(string $endpoint, array $data): Response { - return $this->request('post', $endpoint, $data); + return $this->request(self::METHOD_POST, $endpoint, $data); } /** * Send a PUT request to a Paystack API endpoint. * * @param string $endpoint - * @param array $data + * @param array $data * @return Response * * @throws PaystackRequestException - */ + */ public function put(string $endpoint, array $data): Response { - return $this->request('put', $endpoint, $data); + return $this->request(self::METHOD_PUT, $endpoint, $data); } /** @@ -142,6 +165,6 @@ public function put(string $endpoint, array $data): Response */ public function delete(string $endpoint): Response { - return $this->request('delete', $endpoint); + return $this->request(self::METHOD_DELETE, $endpoint); } } diff --git a/src/Facades/Paystack.php b/src/Facades/Paystack.php index 95e5074..07bd29c 100644 --- a/src/Facades/Paystack.php +++ b/src/Facades/Paystack.php @@ -7,13 +7,14 @@ /** * @see \Unicodeveloper\Paystack\Paystack * + * @method static \Unicodeveloper\Paystack\Services\BankService bank() * @method static \Unicodeveloper\Paystack\Services\TransactionService transaction() * @method static \Unicodeveloper\Paystack\Services\CustomerService customer() - * @method static \Unicodeveloper\Paystack\Services\SubscriptionService subscription() + * @method static \Unicodeveloper\Paystack\Services\PageService page() * @method static \Unicodeveloper\Paystack\Services\PlanService plan() - * @method static \Unicodeveloper\Paystack\Services\TransferService transfer() - * @method static \Unicodeveloper\Paystack\Services\TransferRecipientService transferRecipient() - * @method static \Unicodeveloper\Paystack\Services\PaymentPageService paymentPage() + * @method static \Unicodeveloper\Paystack\Services\SubAccountService subAccount() + * @method static \Unicodeveloper\Paystack\Services\SubscriptionService subscription() + * @method static \Unicodeveloper\Paystack\Support\TransRef transRef() */ class Paystack extends Facade { diff --git a/src/Paystack.php b/src/Paystack.php index 6cb8c06..9dabef4 100644 --- a/src/Paystack.php +++ b/src/Paystack.php @@ -1,5 +1,7 @@ bootConfig(); // $this->bootViews(); // TODO @@ -41,7 +44,7 @@ public function boot() * * @return void */ - public function register() + public function register(): void { $this->mergeConfigFrom(__DIR__ . '/../config/paystack.php', 'paystack'); @@ -68,9 +71,9 @@ public function register() /** * Get the services provided by the provider - * @return array + * @return array */ - public function provides() + public function provides(): array { return [ PaystackClient::class, diff --git a/src/Services/BankService.php b/src/Services/BankService.php index f92300c..91039a4 100644 --- a/src/Services/BankService.php +++ b/src/Services/BankService.php @@ -1,5 +1,8 @@ client = $client; @@ -37,12 +40,8 @@ public function __construct(PaystackClient $client) * @internal This method is not part of the public API and may change without notice. * * @param callable $callback - * @return array { - * @type bool $status Whether the request was successful. - * @type string $message Message or error information. - * @type mixed $data The response data or null on failure. - * } - */ + * @return array{status: bool, message: string, data: mixed} + */ protected function handle(callable $callback): array { try { @@ -57,19 +56,40 @@ protected function handle(callable $callback): array } /** - * Retrieve a list of available banks from Paystack. + * Get a list of all supported banks and their properties. * - * @return array The response from Paystack API. - */ + * @return array + */ public function list(): array { return $this->handle(fn () => $this->client->get('bank')->json()); } /** - * Resolve a bank account number and bank code to retrieve account details. + * Gets a list of countries that Paystack currently supports. + * + * @return array + */ + public function listCountry(): array + { + return $this->handle(fn () => $this->client->get('country')->json()); + } + + /** + * Get a list of states for a country for address verification. + * + * @param int $countryCode The country code of the states to list. It is gotten after the charge request. + * @return array + */ + public function listState(int $countryCode): array + { + return $this->handle(fn () => $this->client->get("address_verification/states?country={$countryCode}")->json()); + } + + /** + * Confirm an account belongs to the right customer * - * Example payload: + * Example: * ```php * [ * 'account_number' => '1234567890', @@ -77,11 +97,11 @@ public function list(): array * ] * ``` * - * @param array $data An associative array containing `account_number` and `bank_code`. - * @return array The response from Paystack API. + * @param array $params Query parameter containing `account_number` and `bank_code`. + * @return array */ - public function resolveAccount(array $data): array + public function resolveAccount(array $params = []): array { - return $this->handle(fn () => $this->client->get('bank/resolve', $data)->json()); + return $this->handle(fn () => $this->client->get('bank/resolve', $params)->json()); } } diff --git a/src/Services/CustomerService.php b/src/Services/CustomerService.php index 70dfc01..a68b8a6 100644 --- a/src/Services/CustomerService.php +++ b/src/Services/CustomerService.php @@ -1,5 +1,8 @@ client = $client; @@ -37,12 +40,8 @@ public function __construct(PaystackClient $client) * @internal This method is not part of the public API and may change without notice. * * @param callable $callback - * @return array { - * @type bool $status Whether the request was successful. - * @type string $message Message or error information. - * @type mixed $data The response data or null on failure. - * } - */ + * @return array{status: bool, message: string, data: mixed} + */ protected function handle(callable $callback): array { try { @@ -59,7 +58,7 @@ protected function handle(callable $callback): array /** * Create a new customer on Paystack. * - * Example payload: + * Example: * ```php * [ * 'email' => 'customer@example.com', @@ -69,10 +68,10 @@ protected function handle(callable $callback): array * ] * ``` * - * @param array $data Customer data for creation. - * @return array The response from Paystack API. - */ - public function create(array $data): array + * @param array $data Customer data for creation. + * @return array + */ + public function create(array $data = []): array { return $this->handle(fn () => $this->client->post('customer', $data)->json()); } @@ -80,23 +79,121 @@ public function create(array $data): array /** * Fetch a customer by their customer code. * - * @param string $customerCode The unique code identifying the customer. - * @return array The response from Paystack API. - */ - public function fetch(string $customerCode): array + * @param string $email_or_code The unique code identifying the customer. + * @return array + */ + public function fetch(string $email_or_code): array { - return $this->handle(fn () => $this->client->get("customer/{$customerCode}")->json()); + return $this->handle(fn () => $this->client->get("customer/{$email_or_code}")->json()); } /** * Retrieve a paginated list of customers. * - * @param int $perPage Number of customers per page (default: 50). - * @param int $page Page number to retrieve (default: 1). - * @return array The response from Paystack API. - */ - public function list(int $perPage = 50, int $page = 1): array + * @param array $params Optional query parameters. + * @return array + */ + public function list(array $params = []): array + { + return $this->handle(fn () => $this->client->get("customer", $params)->json()); + } + + /** + * Update an existing customer. + * + * Example: + * ```php + * [ + * 'first_name' => 'UpdatedName', + * 'last_name' => 'UpdatedLastName', + * 'phone' => '08076543210' + * ] + * ``` + * + * @param string $customerCode The unique customer code. + * @param array $payload The fields to update. + * @return array + */ + public function update(string $customerCode, array $payload = []): array + { + return $this->handle(fn () => $this->client->put("customer/{$customerCode}", $payload)->json()); + } + + /** + * Validate a customer's identity. + * + * Example: + * ```php + * [ + * 'country' => 'NG', + * 'type' => 'bank_account', + * 'account_number' => '08076543210' + * 'bvn' => '20012345677' + * ] + * ``` + * + * @param string $customerCode The unique customer code. + * @param array $payload The fields to update. + * @return array + */ + public function validateCustomer(string $customerCode, array $payload = []): array { - return $this->handle(fn () => $this->client->get("customer?perPage={$perPage}&page={$page}")->json()); + return $this->handle(fn () => $this->client->post("/customer/{$customerCode}/identification", $payload)->json()); } + + /** + * Whitelist/Blaclist a customer + * + * Example: + * ```php + * [ + * "customer" => "CUS_xr58yrr2ujlft9k", + * "risk_action" => "allow" + * ] + * ``` + * + * @param array $payload The field to update + * @return array + */ + public function setRiskAction(array $payload = []): array + { + return $this->handle(fn () => $this->client->post("/customer/set_risk_action", $payload)->json()); + } + + /** + * Initiate a request to create a reusable authorization code for recurring transactions. + * + * Example: + * ```php + * [ + * 'email' => 'ravi@demo.com', + * 'channel' => 'direct_debit' + * 'callback_url' => 'http://test.url.com' + * ] + * ``` + * + * @param array $payload The fields to update. + * @return array + */ + public function initializeAuthorization(array $payload = []): array + { + return $this->handle(fn () => $this->client->post("/customer/authorization/initialize", $payload)->json()); + } + + /** + * Check the status of an authorization request. + * + * @param string $reference The reference returned in the initialization response + * @return array + */ + public function verifyAuthorization(string $reference): array + { + return $this->handle(fn () => $this->client->get("/authorization/verify/{$reference}")->json()); + } + + // TODO's + // Initialize Direct Debit + // Direct Debit Activation Charge + // Fetch Mandate Authorizations + // Deactivate Authorization } diff --git a/src/Services/PageService.php b/src/Services/PageService.php index d25b3b4..1b3905b 100644 --- a/src/Services/PageService.php +++ b/src/Services/PageService.php @@ -1,5 +1,8 @@ client = $client; @@ -37,12 +40,8 @@ public function __construct(PaystackClient $client) * @internal This method is not part of the public API and may change without notice. * * @param callable $callback - * @return array { - * @type bool $status Whether the request was successful. - * @type string $message Message or error information. - * @type mixed $data The response data or null on failure. - * } - */ + * @return array{status: bool, message: string, data: mixed} + */ protected function handle(callable $callback): array { try { @@ -59,7 +58,7 @@ protected function handle(callable $callback): array /** * Create a new Payment Page on Paystack. * - * Example payload: + * Example: * ```php * [ * 'name' => 'Special Offer', @@ -68,20 +67,20 @@ protected function handle(callable $callback): array * ] * ``` * - * @param array $data Data required to create the page. - * @return array The response from Paystack API. - */ - public function create(array $data): array + * @param array $payload Data required to create the page. + * @return array + */ + public function create(array $payload = []): array { - return $this->handle(fn () => $this->client->post('page', $data)->json()); + return $this->handle(fn () => $this->client->post('page', $payload)->json()); } /** * Fetch a single Payment Page by its ID or slug. * * @param string $pageId The page ID or slug. - * @return array The response from Paystack API. - */ + * @return array + */ public function fetch(string $pageId): array { return $this->handle(fn () => $this->client->get("page/{$pageId}")->json()); @@ -90,10 +89,61 @@ public function fetch(string $pageId): array /** * Retrieve a list of all Payment Pages. * - * @return array The response from Paystack API. - */ + * @return array + */ public function list(): array { return $this->handle(fn () => $this->client->get("page")->json()); } + + /** + * Update a payment page details. + * + * Example: + * ```php + * [ + * "name": "Buttercup Brunch" + * 'amount' => 10000 + * 'description' => 'Gather your friends for the ritual that is brunch' + * ] + * ``` + * + * @param string $id_or_slug The ID/Slug. + * @param array $payload The fields to update. + * @return array + */ + public function update(string $id_or_slug, array $payload = []): array + { + return $this->handle(fn () => $this->client->put("page/{$id_or_slug}", $payload)->json()); + } + + /** + * Check the availability of a slug for a payment page. + * + * @param string $slug URL slug to be confirmed + * @return array + */ + public function checkSlugAvailability(string $slug) + { + return $this->handle(fn () => $this->client->get("page/check_slug_availability/{$slug}")->json()); + } + + /** + * Add products to a payment page + * + * Example: + * ```php + * [ + * "product" => [473, 292] + * ] + * ``` + * + * @param string $id The product ID. + * @param array $payload The fields to update. + * @return array + */ + public function addProducts(string $id, array $payload = []) + { + return $this->handle(fn () => $this->client->post("page/{$id}/product", $payload)->json()); + } } diff --git a/src/Services/PlanService.php b/src/Services/PlanService.php index c35c3f7..bfc145d 100644 --- a/src/Services/PlanService.php +++ b/src/Services/PlanService.php @@ -1,5 +1,7 @@ 'Monthly Gold Plan', @@ -68,20 +66,20 @@ protected function handle(callable $callback): array * ] * ``` * - * @param array $data The data required to create the plan. - * @return array The response from Paystack API. - */ - public function create(array $data): array + * @param array $payload The data required to create the plan. + * @return array + */ + public function create(array $payload = []): array { - return $this->handle(fn () => $this->client->post('plan', $data)->json()); + return $this->handle(fn () => $this->client->post('plan', $payload)->json()); } /** * Fetch a subscription plan by its code. * * @param string $planCode The plan code from Paystack. - * @return array The response from Paystack API. - */ + * @return array + */ public function fetch(string $planCode): array { return $this->handle(fn () => $this->client->get("plan/{$planCode}")->json()); @@ -90,12 +88,31 @@ public function fetch(string $planCode): array /** * List all subscription plans with pagination. * - * @param int $perPage Number of plans per page. - * @param int $page The current page number. - * @return array The response from Paystack API. - */ - public function list(int $perPage = 50, int $page = 1): array + * @param array $params Optional Query parameters. + * @return array + */ + public function list(array $params = []): array + { + return $this->handle(fn () => $this->client->get("plan", $params)->json()); + } + + /** + * Update an existing plan. + * + * Example: + * ```php + * [ + * "name" => "Monthly retainer (renamed)" + * 'amount' => 10000 + * ] + * ``` + * + * @param string $id_or_code The ID/Plan code. + * @param array $payload The fields to update. + * @return array + */ + public function update(string $id_or_code, array $payload = []): array { - return $this->handle(fn () => $this->client->get("plan?perPage={$perPage}&page={$page}")->json()); + return $this->handle(fn () => $this->client->put("plan/{$id_or_code}", $payload)->json()); } } diff --git a/src/Services/SubAccountService.php b/src/Services/SubAccountService.php index 4dc8457..955c816 100644 --- a/src/Services/SubAccountService.php +++ b/src/Services/SubAccountService.php @@ -1,5 +1,7 @@ client = $client; @@ -37,12 +39,8 @@ public function __construct(PaystackClient $client) * @internal This method is not part of the public API and may change without notice. * * @param callable $callback - * @return array { - * @type bool $status Whether the request was successful. - * @type string $message Error or success message. - * @type mixed $data Response data or null if failed. - * } - */ + * @return array{status: bool, message: string, data: mixed} + */ protected function handle(callable $callback): array { try { @@ -59,7 +57,7 @@ protected function handle(callable $callback): array /** * Create a new subaccount on Paystack. * - * Example payload: + * Example: * ```php * [ * 'business_name' => 'My Business', @@ -69,32 +67,53 @@ protected function handle(callable $callback): array * ] * ``` * - * @param array $data Data for creating the subaccount. - * @return array The response from Paystack API. - */ - public function create(array $data): array + * @param array $payload Data for creating the subaccount. + * @return array + */ + public function create(array $payload = []): array + { + return $this->handle(fn () => $this->client->post('subaccount', $payload)->json()); + } + + /** + * List all subaccounts. + * + * @param array $params Optional query parameter + * @return array + */ + public function list(array $params = []): array { - return $this->handle(fn () => $this->client->post('subaccount', $data)->json()); + return $this->handle(fn () => $this->client->get("subaccount", $params)->json()); } /** * Fetch a specific subaccount by its code. * * @param string $subaccountCode The unique code of the subaccount. - * @return array The response from Paystack API. - */ + * @return array + */ public function fetch(string $subaccountCode): array { return $this->handle(fn () => $this->client->get("subaccount/{$subaccountCode}")->json()); } /** - * List all subaccounts. + * Update a subaccount details. + * + * Example: + * ```php + * [ + * "business_name" => "An-Nur Info Tech." + * 'description' => 'Provide IT Service' + * ] + * ``` * - * @return array The response from Paystack API. - */ - public function list(): array + * @param string $id_or_code The ID/Plan code. + * @param array $payload The fields to update. + * @return array + */ + public function update(string $id_or_code, array $payload = []): array { - return $this->handle(fn () => $this->client->get("subaccount")->json()); + return $this->handle(fn () => $this->client->put("plan/{$id_or_code}", $payload)->json()); } } diff --git a/src/Services/SubscriptionService.php b/src/Services/SubscriptionService.php index e24f315..e4aae8a 100644 --- a/src/Services/SubscriptionService.php +++ b/src/Services/SubscriptionService.php @@ -1,5 +1,7 @@ client = $client; @@ -37,12 +39,8 @@ public function __construct(PaystackClient $client) * @internal This method is not part of the public API and may change without notice. * * @param callable $callback - * @return array { - * @type bool $status Whether the request was successful. - * @type string $message Error or success message. - * @type mixed $data Response data or null if failed. - * } - */ + * @return array{status: bool, message: string, data: mixed} + */ protected function handle(callable $callback): array { try { @@ -59,46 +57,58 @@ protected function handle(callable $callback): array /** * Create a subscription between a customer and a plan. * - * Example payload: + * Example: * ```php * [ * 'customer' => 'CUS_xxxxxxx', - * 'plan' => 'PLN_xxxxxxx', - * 'authorization' => 'AUTH_xxxxxxx' // Optional if email token is used + * 'plan' => 'PLN_xxxxxxx' * ] * ``` * - * @param array $payload Subscription creation data. - * @return array The response from Paystack API. - */ - public function create(array $payload): array + * @param array $payload Subscription creation data. + * @return array + */ + public function create(array $payload = []): array { return $this->handle(fn () => $this->client->post('subscription', $payload)->json()); } /** - * Disable a subscription by customer code or email token. + * List all subscriptions or filter them. * - * Example payload: + * Example: * ```php * [ - * 'code' => 'SUB_xxxxxxx', - * 'token' => 'email_token_xxxxxxx' + * 'customer' => 'CUS_xxxxxxx', + * 'plan' => 'PLN_xxxxxxx', + * 'perPage' => 50, + * 'page' => 1 * ] * ``` * - * @param array $payload Disable payload. - * @return array The response from Paystack API. - */ - public function disable(array $payload): array + * @param array $params Optional query parameters. + * @return array + */ + public function list(array $params = []): array { - return $this->handle(fn () => $this->client->post('subscription/disable', $payload)->json()); + return $this->handle(fn () => $this->client->get('subscription', $params)->json()); + } + + /** + * Fetch details of a specific subscription by code. + * + * @param string $subscriptionCode The subscription code. + * @return array + */ + public function fetch(string $subscriptionCode): array + { + return $this->handle(fn () => $this->client->get("subscription/{$subscriptionCode}")->json()); } /** * Enable a subscription using code and token. * - * Example payload: + * Example: * ```php * [ * 'code' => 'SUB_xxxxxxx', @@ -106,43 +116,53 @@ public function disable(array $payload): array * ] * ``` * - * @param array $payload Enable payload. - * @return array The response from Paystack API. - */ - public function enable(array $payload): array + * @param array $payload Enable payload. + * @return array + */ + public function enable(array $payload = []): array { return $this->handle(fn () => $this->client->post('subscription/enable', $payload)->json()); } /** - * Fetch details of a specific subscription by code. + * Disable a subscription by customer code or email token. * - * @param string $subscriptionCode The subscription code. - * @return array The response from Paystack API. - */ - public function fetch(string $subscriptionCode): array + * Example: + * ```php + * [ + * 'code' => 'SUB_xxxxxxx', + * 'token' => 'email_token_xxxxxxx' + * ] + * ``` + * + * @param array $payload Disable payload. + * @return array + */ + public function disable(array $payload = []): array { - return $this->handle(fn () => $this->client->get("subscription/{$subscriptionCode}")->json()); + return $this->handle(fn () => $this->client->post('subscription/disable', $payload)->json()); } /** - * List all subscriptions or filter them. + * Generate a link for updating the card on a subscription. * - * Example filters: - * ```php - * [ - * 'customer' => 'CUS_xxxxxxx', - * 'plan' => 'PLN_xxxxxxx', - * 'perPage' => 50, - * 'page' => 1 - * ] - * ``` + * @param string $subscriptionCode The subscription code. + * @return array + */ + public function generateUpdateSubscriptionLink(string $subscriptionCode): array + { + return $this->handle(fn () => $this->client->get("subscription/{$subscriptionCode}/manage/link")->json()); + } + + /** + * Email a customer a link for updating the card on their subscription * - * @param array $params Optional query parameters. - * @return array The response from Paystack API. - */ - public function list(array $params = []): array + * @param string $subscriptionCode The subscription code. + * @param array $payload Body parameters. + * @return array + */ + public function sendUpdateSubscriptionLink(string $subscriptionCode, array $payload = []): array { - return $this->handle(fn () => $this->client->get('subscription', $params)->json()); + return $this->handle(fn () => $this->client->post("subscription/{$subscriptionCode}/manage/email", $payload)->json()); } } diff --git a/src/Services/TransactionService.php b/src/Services/TransactionService.php index fdd553f..685a55c 100644 --- a/src/Services/TransactionService.php +++ b/src/Services/TransactionService.php @@ -1,5 +1,7 @@ client = $client; @@ -35,17 +37,12 @@ public function __construct(PaystackClient $client) /** * Handle exceptions gracefully and format the result. * - * @internal This method is not part of the public API and may change without notice. * * @internal This method is not part of the public API and may change without notice. * * @param callable $callback - * @return array { - * @type bool $status Indicates success or failure. - * @type string $message Descriptive message or error. - * @type mixed $data The API response data or null on failure. - * } - */ + * @return array{status: bool, message: string, data: mixed} + */ protected function handle(callable $callback): array { try { @@ -75,10 +72,10 @@ protected function handle(callable $callback): array * ] * ``` * - * @param array $payload Transaction initialization data. - * @return array Paystack response including authorization URL. - */ - public function initialize(array $payload): array + * @param array $payload Transaction initialization data. + * @return array + */ + public function initialize(array $payload = []): array { return $this->handle(fn () => $this->client->post('transaction/initialize', $payload)->json()); } @@ -87,8 +84,8 @@ public function initialize(array $payload): array * Verify a transaction by reference code. * * @param string $reference The transaction reference. - * @return array Paystack verification result. - */ + * @return array + */ public function verify(string $reference): array { return $this->handle(fn () => $this->client->get("transaction/verify/{$reference}")->json()); @@ -97,23 +94,95 @@ public function verify(string $reference): array /** * List transactions with pagination support. * - * @param int $perPage Number of results per page. - * @param int $page Current page number. - * @return array Paginated list of transactions. - */ - public function list(int $perPage = 50, int $page = 1): array + * @param array $params Optional query parameter + * @return array + */ + public function list(array $params = []): array { - return $this->handle(fn () => $this->client->get("transaction?perPage={$perPage}&page={$page}")->json()); + return $this->handle(fn () => $this->client->get("transaction", $params)->json()); } /** * Fetch a single transaction by its ID or reference. * * @param int|string $id Transaction ID or reference string. - * @return array Transaction details. - */ + * @return array + */ public function fetch(int|string $id): array { return $this->handle(fn () => $this->client->get("transaction/{$id}")->json()); } + + /** + * All authorizations marked as reusable can be charged with this endpoint whenever you need to receive payments + * + * Example: + * ```php + * [ + * "email" => "customer@email.com", + * "amount" => "20000", + * "authorization_code" => "AUTH_72btv547" + * ] + * ``` + * + * @param array $payload The body params. + * @return array + */ + public function chargeAuthorization(array $payload = []): array + { + return $this->handle(fn () => $this->client->post("transaction/charge_authorization", $payload)->json()); + } + + /** + * View the timeline of a transaction + * + * @param string $id_or_reference + * @return array + */ + public function viewTransactionTimeline(string $id_or_reference): array + { + return $this->handle(fn () => $this->client->get("transaction/timeline/{$id_or_reference}")->json()); + } + + /** + * Total amount received on your account + * + * @param array $params Optional query params. + * @return array + */ + public function transactionTotals(array $params = []): array + { + return $this->handle(fn () => $this->client->get("transaction/totals", $params)->json()); + } + + /** + * Export a list of transactions carried out. + * + * @param array $params Optional query params. + * @return array + */ + public function exportTotal(array $params = []): array + { + return $this->handle(fn () => $this->client->get("transaction/export", $params)->json()); + } + + /** + * Retrieve part of a payment from a customer + * + * Example payload: + * ```php + * [ + * "currency" => "NGN", + * "amount" => "20000", + * "email" => "customer@email.com" + * ] + * ``` + * + * @param array $payload + * @return array + */ + public function partialDebit(array $payload = []): array + { + return $this->handle(fn () => $this->client->post("transaction/partial_debit", $payload)->json()); + } } diff --git a/src/Support/TransRef.php b/src/Support/TransRef.php index 2b8f8fe..960c0b3 100644 --- a/src/Support/TransRef.php +++ b/src/Support/TransRef.php @@ -1,5 +1,7 @@ make('laravel-paystack'); } From 561a68eeba1757e455ffa3707bba4538a45e9a02 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:26:56 +0100 Subject: [PATCH 26/30] Add README doc for Dev --- {doc => docs}/README.md | 53 ++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 11 deletions(-) rename {doc => docs}/README.md (88%) diff --git a/doc/README.md b/docs/README.md similarity index 88% rename from doc/README.md rename to docs/README.md index 4b14b63..39eca19 100644 --- a/doc/README.md +++ b/docs/README.md @@ -1,12 +1,41 @@ # Laravel Paystack SDK -> A Laravel SDK for integrating with the [Paystack API](https://paystack.com/docs/api/), providing a clean and expressive way to interact with transactions, customers, plans, subscriptions, and more. +A simple, expressive Laravel wrapper around the Paystack API. -> ๐Ÿ“ฆ Supports Laravel 9, 10, 11, and 12. Built with PHP 8.2+ features. +--- + +## Setup + +After cloning the repo, run: + +``` +./setup.sh +``` + +# Composer Scripts +To run unit and integration tests (requires PAYSTACK_SECRET_KEY): +``` +composer test +``` + +View unused dependencies: +``` +composer unused +``` + +Automatically fix coding style issues: +``` +composer php-fix +``` + +Run static analysis: +``` +comoposer analyse +``` --- -## ๐Ÿš€ Features +## Features - Simple Laravel-style service and facade structure - Fully modular service classes (e.g., `TransactionService`, `CustomerService`, etc.) @@ -14,12 +43,12 @@ - Strong type declarations and IDE-friendly docblocks - Auto-generated transaction references with `transRef()` - Built-in error handling and retry logic -- PSR-4 compliant and fully testable (unit & integration) +- PSR-4 compliant and fully testable - Built-in retry logic (configurable with PAYSTACK_RETRY_ATTEMPTS and PAYSTACK_RETRY_DELAY) --- -## ๐Ÿ“ฆ Installation +## Installation Install via Composer: @@ -30,7 +59,7 @@ composer require unicodeveloper/laravel-paystack --- -## โš™๏ธ Configuration +## Configuration Publish the config file: ```bash php artisan vendor:publish --provider="Unicodeveloper\Paystack\PaystackServiceProvider" @@ -49,7 +78,7 @@ PAYSTACK_RETRY_DELAY=150 ``` --- -## ๐Ÿงช Usage +## Usage ### Transaction ``` use Paystack; @@ -192,18 +221,20 @@ Route::get('/payment/callback', [App\Http\Controllers\PaymentController::class, | ---------------- | ------------------------------- | | `transaction()` | Handle payment transactions | | `customer()` | Manage customer records | -| `plan()` | Create and manage payment plans | +| `plan()` | Create and manage plans | | `subscription()` | Handle recurring subscriptions | -| `transfer()` | Initiate and manage transfers | | `bank()` | Retrieve bank lists | +| `page()` | Manage payment pages. | +| `subAccount()` | Mangage subaccounts | --- -## โœ… Testing + +## Testing Run tests: ``` composer test ``` -> Integration tests assume PAYSTACK_SECRET_KEY is set in .env.testing. +> Integration require PAYSTACK_SECRET_KEY in .env.testing. --- From b473e2bc45c7f5c2e9faad027798de40e30e164d Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:29:46 +0100 Subject: [PATCH 27/30] Add PHPstan for running static analysis --- composer.json | 4 +++- phpstan.neon | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 7f7bcf6..c825e1f 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "mockery/mockery": "^1.3", "orchestra/testbench": "^8.36", "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^8.4|^9.0|^10.5", "scrutinizer/ocular": "~1.1", "vlucas/phpdotenv": "^5.6" @@ -65,7 +66,8 @@ "scripts": { "test": "vendor/bin/phpunit", "unused": "vendor/bin/composer-unused", - "php-fix": "vendor/bin/php-cs-fixer fix src" + "php-fix": "vendor/bin/php-cs-fixer fix src", + "analyse": "vendor/bin/phpstan analyse -l 6 src tests" }, "extra": { "laravel": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e29f3bb --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + treatPhpDocTypesAsCertain: false + reportUnmatchedIgnoredErrors: false + excludePaths: + - tests/* + level: max + ignoreErrors: + - message: '#Call to an undefined static method Unicodeveloper\\Paystack\\Facades\\Paystack::transRef\(\).#' + \ No newline at end of file From 2b7d75e7c97879fcf2ae3d28ae175a3fe7db7c29 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:31:41 +0100 Subject: [PATCH 28/30] Add Page, Plan, and customer integration tests --- tests/Integration/CustomerServiceTest.php | 187 +++++++++++++++++++ tests/Integration/PageServiceTest.php | 54 ++++++ tests/Integration/PlanServiceTest.php | 70 +++++++ tests/Integration/TransactionServiceTest.php | 36 ++-- 4 files changed, 332 insertions(+), 15 deletions(-) create mode 100644 tests/Integration/CustomerServiceTest.php create mode 100644 tests/Integration/PageServiceTest.php create mode 100644 tests/Integration/PlanServiceTest.php diff --git a/tests/Integration/CustomerServiceTest.php b/tests/Integration/CustomerServiceTest.php new file mode 100644 index 0000000..1c8bbfd --- /dev/null +++ b/tests/Integration/CustomerServiceTest.php @@ -0,0 +1,187 @@ +customer = $this->app->make(CustomerService::class); + } + + public function testCreateCustomerWithRealApi(): void + { + $email = strtolower('test_' . Str::random(5) . '@example.com'); + + $response = $this->customer->create([ + 'email' => $email, + 'first_name' => 'Test', + 'last_name' => 'Customer', + 'phone' => '08011112222' + ]); + + $this->assertTrue($response['status']); + $this->assertEquals($email, $response['data']['email']); + + // Store for other tests + $GLOBALS['__customer_code'] = $response['data']['customer_code']; + } + + public function testFetchCustomerWithRealApi(): void + { + $this->testCreateCustomerWithRealApi(); + $customerCode = $GLOBALS['__customer_code']; + + $response = $this->customer->fetch($customerCode); + + $this->assertTrue($response['status']); + $this->assertEquals($customerCode, $response['data']['customer_code']); + } + + public function testUpdateCustomerWithRealApi(): void + { + $this->testCreateCustomerWithRealApi(); + $customerCode = $GLOBALS['__customer_code']; + + $response = $this->customer->update($customerCode, [ + 'first_name' => 'Updated' + ]); + + $this->assertTrue($response['status']); + $this->assertEquals('Updated', $response['data']['first_name']); + } + + public function testListCustomers(): void + { + $response = $this->customer->list(['perPage' => 10, 'page' => 1]); + + $this->assertTrue($response['status']); + $this->assertArrayHasKey('data', $response); + // $this->assertIsArray($response['data']); + } + + public function testValidateCustomer(): void + { + $this->testCreateCustomerWithRealApi(); + $customerCode = $GLOBALS['__customer_code']; + + // dd($customerCode); + $response = $this->customer->validateCustomer($customerCode, [ + 'email' => 'customer@example.com', + 'first_name' => 'Jane', + 'last_name' => 'Doe', + 'type' => 'bank_account', + 'account_number' => '0123456789', + 'country' => 'NG', + 'bvn' => '12345678901', + 'bank_code' => '058', // GTBank + ]); + + // dd($response); + + $this->assertTrue($response['status']); + $this->assertEquals('Customer Identification in progress', $response['message']); + } + + public function testWhitelistOrBlacklistCustomer(): void + { + $email = 'test_blacklist@example.com'; + + // First, create the customer + $customer = $this->customer->create([ + 'email' => $email, + 'first_name' => 'Block', + 'last_name' => 'List', + ]); + + $code = $customer['data']['customer_code']; + + // Now blacklist + $response = $this->customer->setRiskAction([ + 'customer' => $code, + 'risk_action' => 'deny' // or 'allow' for whitelisting + ]); + + $this->assertTrue($response['status']); + $this->assertEquals('Customer updated', $response['message']); + } + + public function testInitializeAuthorization(): void + { + $response = $this->customer->initializeAuthorization([ + 'email' => 'ravi-' . uniqid() . '@example.com', + 'channel' => 'direct_debit', + 'callback_url' => 'https://an-nur-info-tech.com/payment/callback', // Change this to your callback url + ]); + + // Output for debugging during test + // fwrite(STDERR, print_r($response, true)); + + $this->assertTrue($response['status']); + $this->assertEquals('Authorization initialized', $response['message']); + $this->assertArrayHasKey('redirect_url', $response['data']); + $this->assertArrayHasKey('reference', $response['data']); + + // Uncomment(file_put_contents()) to save reference for verification test (if running in same session) + // file_put_contents(__DIR__ . '/auth_reference.json', json_encode([ + // 'reference' => $response['data']['reference'] + // ])); + } + + // public function testVerifyAuthorization(): void // This method always returned 404 error from the Paystack API + // { + // $path = __DIR__ . '/auth_reference.json'; + // if (!file_exists($path)) { + // $this->markTestSkipped('Authorization reference not found. Run testInitializeAuthorization first.'); + // } + + // $data = json_decode(file_get_contents($path), true); + // $reference = $data['reference']; + + // // Delay to give Paystack time to process the authorization + // sleep(2); + + // $response = $this->customer->verifyAuthorization($reference); + // // dump($response); + + // fwrite(STDERR, print_r($response, true)); + + // // $this->assertIsArray($response); + // $this->assertArrayHasKey('data', $response); + // $this->assertTrue($response['status']); + // $this->assertEquals($reference, $response['data']['authorization_code']); + // } + + // public function testInitializeAndVerifyAuthorization(): void + // { + // $response = $this->customer->initializeAuthorization([ + // 'email' => 'john.doe.' . uniqid() . '@example.com', + // 'amount' => 5000, + // 'channel' => 'direct_debit', + // 'callback_url' => 'https://an-nur-info-tech.com/payment/callback', // Change this to your callback url + // ]); + + // $this->assertTrue($response['status']); + // $this->assertArrayHasKey('reference', $response['data']); + // $reference = $response['data']['reference']; + // // dump('Reference:', $reference); + + // // Give Paystack some time (optional sleep) + // sleep(2); + + // $verifyResponse = $this->customer->verifyAuthorization($reference); + + // $this->assertTrue($verifyResponse['status']); + // $this->assertEquals($reference, $verifyResponse['data']['reference']); + // } +} diff --git a/tests/Integration/PageServiceTest.php b/tests/Integration/PageServiceTest.php new file mode 100644 index 0000000..08b02b4 --- /dev/null +++ b/tests/Integration/PageServiceTest.php @@ -0,0 +1,54 @@ + $title, + 'description' => 'Access premium blog content', + 'amount' => 500000, // NGN 5,000 in kobo + ]; + + $response = Paystack::page()->create($payload); + + $this->assertTrue($response['status']); + $this->assertEquals($title, $response['data']['name']); + } + + public function testFetchPageWithRealApi(): void + { + $title = 'Mini Page ' . uniqid(); + $payload = [ + 'name' => $title, + 'description' => 'One-time offer', + 'amount' => 250000, + ]; + + $create = Paystack::page()->create($payload); + $slug = $create['data']['slug']; + + $fetched = Paystack::page()->fetch($slug); + + $this->assertTrue($fetched['status']); + $this->assertEquals($slug, $fetched['data']['slug']); + } + + public function testListPagesWithRealApi(): void + { + $response = Paystack::page()->list(); + + $this->assertTrue($response['status']); + $this->assertIsArray($response['data']); + } + + // TODO's tests + // public function testCheckSlugAvailability(): void{} + // public function testAddProducts(): void{} +} diff --git a/tests/Integration/PlanServiceTest.php b/tests/Integration/PlanServiceTest.php new file mode 100644 index 0000000..63ae393 --- /dev/null +++ b/tests/Integration/PlanServiceTest.php @@ -0,0 +1,70 @@ + 'Monthly Pro ' . uniqid(), + 'amount' => 1000000, // NGN 10,000 in kobo + 'interval' => 'monthly', + ]; + + $response = Paystack::plan()->create($payload); + + $this->assertTrue($response['status']); + $this->assertEquals($payload['name'], $response['data']['name']); + } + + public function testFetchPlanWithRealApi(): void + { + $payload = [ + 'name' => 'Starter Plan ' . uniqid(), + 'amount' => 300000, + 'interval' => 'weekly', + ]; + + $created = Paystack::plan()->create($payload); + $planCode = $created['data']['plan_code']; + + $fetched = Paystack::plan()->fetch($planCode); + + $this->assertTrue($fetched['status']); + $this->assertEquals($planCode, $fetched['data']['plan_code']); + } + + public function testListPlansWithRealApi(): void + { + $response = Paystack::plan()->list(); + + $this->assertTrue($response['status']); + $this->assertIsArray($response['data']); + } + + public function testUpdatePlanWithRealApi(): void + { + $payload = [ + 'name' => 'Update Plan ' . uniqid(), + 'amount' => 500000, + 'interval' => 'monthly', + ]; + + $created = Paystack::plan()->create($payload); + // dump($created); + // print_r($created); + $planCode = $created['data']['plan_code']; + + $update = Paystack::plan()->update($planCode, [ + 'name' => 'Monthly retainer (renamed)' + ]); + + $this->assertTrue($update['status']); + $this->assertEquals('Plan updated. 0 subscription(s) affected', $update['message']); + } +} diff --git a/tests/Integration/TransactionServiceTest.php b/tests/Integration/TransactionServiceTest.php index d100589..7a8281c 100644 --- a/tests/Integration/TransactionServiceTest.php +++ b/tests/Integration/TransactionServiceTest.php @@ -27,20 +27,19 @@ protected function setUp(): void // dump($this->secretKey); $this->transaction = $this->app->make(TransactionService::class); - - // Example: Log or use secret key from config - } - public function testInitializeTransactionWithRealApi() + public function testInitializeTransactionWithRealApi(): void { if (! str_starts_with($this->baseUrl, 'http')) { throw new \InvalidArgumentException("Invalid Paystack base URL: {$this->baseUrl}"); } - $reference = Str::uuid()->toString(); + // $reference = Str::uuid()->toString(); + $reference = paystack()->transRef(); + // dd($reference); // dd($this->paymentUrl, $this->secretKey, $this->publicKey,); $response = $this->transaction->initialize([ @@ -50,15 +49,15 @@ public function testInitializeTransactionWithRealApi() 'callback_url' => 'https://example.com/callback' ]); - $this->assertIsArray($response); + // $this->assertIsArray($response); $this->assertTrue($response['status']); $this->assertArrayHasKey('authorization_url', $response['data']); $this->assertArrayHasKey('reference', $response['data']); } - public function testVerifyTransactionWithRealApi() + public function testVerifyTransactionWithRealApi(): void { - $reference = Str::uuid()->toString(); + $reference = paystack()->transRef(); $initResponse = $this->transaction->initialize([ 'email' => 'verify@example.com', @@ -72,26 +71,27 @@ public function testVerifyTransactionWithRealApi() // Simulate verifying the same reference $verifyResponse = $this->transaction->verify($reference); - $this->assertIsArray($verifyResponse); + // $this->assertIsArray($verifyResponse); $this->assertTrue($verifyResponse['status']); + $this->assertArrayHasKey('data', $verifyResponse); $this->assertEquals($reference, $verifyResponse['data']['reference']); } - public function testListTransactions() + public function testListTransactions(): void { - $response = $this->transaction->list(perPage: 10, page: 1); + $response = $this->transaction->list(['perPage' => 10, 'page' => 1]); // dd(gettype($response)); - $this->assertIsArray($response); + // $this->assertIsArray($response); $this->assertTrue($response['status']); $this->assertArrayHasKey('data', $response); - $this->assertIsArray($response['data']); + // $this->assertIsArray($response['data']); } - public function testFetchTransactionById() + public function testFetchTransactionById(): void { - $listResponse = $this->transaction->list(perPage: 1); + $listResponse = $this->transaction->list(['perPage' => 1]); $this->assertTrue($listResponse['status']); $transactions = $listResponse['data']; @@ -108,4 +108,10 @@ public function testFetchTransactionById() } } + // TODO's Test + // public function testChargeAuthorization(): void{} + // public function testViewTransactionTimeline(): void{} + // public function testTransactionTotals(): void{} + // public function testExportTotal(): void{} + // public function testPartialDebit(): void{} } From 7186cbe92ee269435028c93d622c465cc2ddd1c8 Mon Sep 17 00:00:00 2001 From: an-nur-info-tech Date: Fri, 8 Aug 2025 11:32:20 +0100 Subject: [PATCH 29/30] Modify some changes --- tests/Unit/CustomerServiceTest.php | 8 ++++---- tests/Unit/PageServiceTest.php | 6 +++--- tests/Unit/PlanServiceTest.php | 8 ++++---- tests/Unit/SubscriptionServiceTest.php | 8 ++++---- tests/Unit/TransactionServiceTest.php | 28 +++++++++----------------- 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/tests/Unit/CustomerServiceTest.php b/tests/Unit/CustomerServiceTest.php index 7736fa5..e5fdaa7 100644 --- a/tests/Unit/CustomerServiceTest.php +++ b/tests/Unit/CustomerServiceTest.php @@ -10,7 +10,7 @@ class CustomerServiceTest extends TestCase { - public function testCreateCustomer() + public function testCreateCustomer(): void { Http::fake([ 'https://api.paystack.co/customer' => Http::response(['status' => true, 'data' => ['email' => 'test@example.com']]) @@ -24,7 +24,7 @@ public function testCreateCustomer() $this->assertEquals('test@example.com', $response['data']['email']); } - public function testListCustomers() + public function testListCustomers(): void { Http::fake([ 'https://api.paystack.co/customer*' => Http::response(['status' => true, 'data' => [['email' => 'test@example.com']]]) @@ -38,9 +38,9 @@ public function testListCustomers() $this->assertIsArray($response['data']); } - public function testFetchCustomer() + public function testFetchCustomer(): void { - $id = 12345; + $id = 'ABC12345'; Http::fake([ "https://api.paystack.co/customer/{$id}" => Http::response(['status' => true, 'data' => ['id' => $id]]) ]); diff --git a/tests/Unit/PageServiceTest.php b/tests/Unit/PageServiceTest.php index 374a118..eb99037 100644 --- a/tests/Unit/PageServiceTest.php +++ b/tests/Unit/PageServiceTest.php @@ -9,7 +9,7 @@ class PageServiceTest extends TestCase { - public function testCreatePaymentPage() + public function testCreatePaymentPage(): void { $payload = [ 'name' => 'Test Page', @@ -40,7 +40,7 @@ public function testCreatePaymentPage() $this->assertEquals('Page created successfully', $response['message']); } - public function testFetchPaymentPage() + public function testFetchPaymentPage(): void { $pageId = 'abc123'; @@ -63,7 +63,7 @@ public function testFetchPaymentPage() $this->assertEquals($pageId, $response['data']['id']); } - public function testListPaymentPages() + public function testListPaymentPages(): void { Http::fake([ 'https://api.paystack.co/page*' => Http::response([ diff --git a/tests/Unit/PlanServiceTest.php b/tests/Unit/PlanServiceTest.php index d945602..2d0151e 100644 --- a/tests/Unit/PlanServiceTest.php +++ b/tests/Unit/PlanServiceTest.php @@ -9,7 +9,7 @@ class PlanServiceTest extends TestCase { - public function testCreatePlan() + public function testCreatePlan(): void { Http::fake([ 'https://api.paystack.co/plan' => Http::response(['status' => true, 'data' => ['name' => 'Basic Plan']]) @@ -23,7 +23,7 @@ public function testCreatePlan() $this->assertEquals('Basic Plan', $response['data']['name']); } - public function testListPlans() + public function testListPlans(): void { Http::fake([ 'https://api.paystack.co/plan*' => Http::response(['status' => true, 'data' => [['name' => 'Basic Plan']]]) @@ -37,9 +37,9 @@ public function testListPlans() $this->assertIsArray($response['data']); } - public function testFetchPlan() + public function testFetchPlan(): void { - $id = 67890; + $id = 'ABC67890'; Http::fake([ "https://api.paystack.co/plan/{$id}" => Http::response(['status' => true, 'data' => ['id' => $id]]) ]); diff --git a/tests/Unit/SubscriptionServiceTest.php b/tests/Unit/SubscriptionServiceTest.php index 7f7b769..7db59e2 100644 --- a/tests/Unit/SubscriptionServiceTest.php +++ b/tests/Unit/SubscriptionServiceTest.php @@ -10,7 +10,7 @@ class SubscriptionServiceTest extends TestCase { - public function testCreateSubscription() + public function testCreateSubscription(): void { Http::fake([ 'https://api.paystack.co/subscription' => Http::response(['status' => true, 'data' => ['email' => 'user@example.com']]) @@ -26,7 +26,7 @@ public function testCreateSubscription() $this->assertEquals('user@example.com', $response['data']['email']); } - public function testDisableSubscription() + public function testDisableSubscription(): void { Http::fake([ 'https://api.paystack.co/subscription/disable' => Http::response(['status' => true]) @@ -37,7 +37,7 @@ public function testDisableSubscription() $this->assertTrue($response['status']); } - public function testEnableSubscription() + public function testEnableSubscription(): void { Http::fake([ 'https://api.paystack.co/subscription/enable' => Http::response(['status' => true]) @@ -50,7 +50,7 @@ public function testEnableSubscription() $this->assertTrue($response['status']); } - public function testFetchSubscription() + public function testFetchSubscription(): void { $code = 'SUB123'; Http::fake([ diff --git a/tests/Unit/TransactionServiceTest.php b/tests/Unit/TransactionServiceTest.php index 7943f82..43d2da7 100644 --- a/tests/Unit/TransactionServiceTest.php +++ b/tests/Unit/TransactionServiceTest.php @@ -11,7 +11,7 @@ class TransactionServiceTest extends TestCase { - public function testInitializeTransactionUsingFacade() + public function testInitializeTransactionUsingFacade(): void { $mockReference = Str::uuid()->toString(); @@ -35,7 +35,7 @@ public function testInitializeTransactionUsingFacade() $this->assertEquals('https://paystack.com/pay/test', $response['data']['authorization_url']); } - public function testVerifyTransaction() + public function testVerifyTransaction(): void { $reference = 'txn_ref_123'; @@ -54,7 +54,7 @@ public function testVerifyTransaction() $this->assertEquals('Verification successful', $response['message']); } - public function testFetchTransaction() + public function testFetchTransaction(): void { $id = 7890; @@ -75,12 +75,8 @@ public function testFetchTransaction() $this->assertEquals($id, $response['data']['id']); } - public function testListPaginatedTransactions() - { - $perPage = 3; - $page = 1; - - + public function testListPaginatedTransactions(): void + { Http::fake([ "https://api.paystack.co/transaction*" => Http::response([ 'status' => true, @@ -94,7 +90,7 @@ public function testListPaginatedTransactions() $client = new PaystackClient(); $service = new TransactionService($client); - $response = $service->list($perPage, $page); + $response = $service->list(['perPage' => 10, 'page' => 1]); $this->assertTrue($response['status']); $this->assertIsArray($response['data']); @@ -103,14 +99,10 @@ public function testListPaginatedTransactions() $this->assertEquals(25000, $response['data'][1]['amount']); } - public function testGenerateTransactionReference() + public function testGenerateTransactionReference(): void { - $ref = Paystack::transRef(); - - // print_r($ref); - $this->assertIsString($ref); - $this->assertStringStartsWith('TXN_', $ref); - $this->assertGreaterThan(10, strlen($ref)); + $this->assertIsString(Paystack::transRef()); + $this->assertStringStartsWith('TXN_', Paystack::transRef()); + $this->assertGreaterThan(10, strlen(Paystack::transRef())); } - } From 579d22e716717dcc7e2e434e59d161ab555f9384 Mon Sep 17 00:00:00 2001 From: Bello-ibrahm Date: Fri, 8 Aug 2025 13:48:16 +0100 Subject: [PATCH 30/30] Modify README.md file --- README.md | 437 +++++++++++++++++++++++++++--------------------------- 1 file changed, 220 insertions(+), 217 deletions(-) diff --git a/README.md b/README.md index f57e23d..8445bb9 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ To get the latest version of Laravel Paystack, simply require it ```bash -composer require unicodeveloper/laravel-paystack +composer require unicodeveloper/laravel-paystack:2.0.0 ``` Or add the following line to the require block of your `composer.json` file. ``` -"unicodeveloper/laravel-paystack": "1.0.*" +"unicodeveloper/laravel-paystack": "2.0.*" ``` You'll then need to run `composer install` or `composer update` to download it and have the autoloader updated. @@ -71,25 +71,45 @@ return [ * Public Key From Paystack Dashboard * */ - 'publicKey' => getenv('PAYSTACK_PUBLIC_KEY'), + 'publicKey' => env('PAYSTACK_PUBLIC_KEY'), /** * Secret Key From Paystack Dashboard * */ - 'secretKey' => getenv('PAYSTACK_SECRET_KEY'), + 'secretKey' => env('PAYSTACK_SECRET_KEY'), /** * Paystack Payment URL * */ - 'paymentUrl' => getenv('PAYSTACK_PAYMENT_URL'), + 'paymentUrl' => env('PAYSTACK_PAYMENT_URL'), /** * Optional email address of the merchant * */ - 'merchantEmail' => getenv('MERCHANT_EMAIL'), + 'merchantEmail' => env('MERCHANT_EMAIL'), + + // Maximum retry attempts for HTTP client (default: 3) + 'retry_attempts' => env('PAYSTACK_RETRY_ATTEMPTS', 3), + + // Delay (ms) between retry attempts (default: 150) + 'retry_delay' => env('PAYSTACK_RETRY_DELAY', 150), + + /* + |-------------------------------------------------------------------------- + | Enable Package Routes - Feature + |-------------------------------------------------------------------------- + | + | This option controls whether the Paystack package should automatically + | load its built-in web routes. You may disable this if you prefer + | to define your own routes or extend the functionality manually. + | + | Default: false + | + */ + 'enable_routes' => false, ]; ``` @@ -134,46 +154,14 @@ Note: Make sure you have `/payment/callback` registered in Paystack Dashboard [h ![payment-callback](https://cloud.githubusercontent.com/assets/2946769/12746754/9bd383fc-c9a0-11e5-94f1-64433fc6a965.png) -```php -// Laravel 5.1.17 and above -Route::post('/pay', 'PaymentController@redirectToGateway')->name('pay'); +## Route Example ``` - -OR - -```php -Route::post('/pay', [ - 'uses' => 'PaymentController@redirectToGateway', - 'as' => 'pay' -]); -``` -OR - -```php -// Laravel 8 & 9 -Route::post('/pay', [App\Http\Controllers\PaymentController::class, 'redirectToGateway'])->name('pay'); +Route::get('/payment', [App\Http\Controllers\PaymentController::class, 'index'])->name('payment.form'); +Route::post('/checkout', [App\Http\Controllers\PaymentController::class, 'redirectToGateway'])->name('checkout.process'); +Route::get('/payment/callback', [App\Http\Controllers\PaymentController::class, 'handleGatewayCallback'])->name('payment.callback'); ``` - -```php -Route::get('/payment/callback', 'PaymentController@handleGatewayCallback'); -``` - -OR - -```php -// Laravel 5.0 -Route::get('payment/callback', [ - 'uses' => 'PaymentController@handleGatewayCallback' -]); -``` - -OR - -```php -// Laravel 8 & 9 -Route::get('/payment/callback', [App\Http\Controllers\PaymentController::class, 'handleGatewayCallback']); -``` +## Controller Example: ```php redirectNow(); - }catch(\Exception $e) { - return Redirect::back()->withMessage(['msg'=>'The paystack token has expired. Please refresh the page and try again.', 'type'=>'error']); - } + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email', + 'amount' => 'required|numeric|min:100', + 'description' => 'nullable|string', + ]); + + $reference = Paystack::transRef(); + + $payload = [ + 'email' => $request->email, + 'amount' => $request->amount * 100, + 'reference' => $reference, + 'callback_url' => route('payment.callback'), + 'metadata' => [ + 'custom_fields' => [ + [ + 'display_name' => 'Name', + 'variable_name' => 'name', + 'value' => $request->name + ], + [ + 'display_name' => 'Description', + 'variable_name' => 'description', + 'value' => $request->description + ] + ] + ] + ]; + + try { + $response = Paystack::transaction()->initialize($payload); + $transAuthURL = $response['data']['authorization_url']; + + return redirect($transAuthURL); + } catch (\Exception $e) { + Log::error('Paystack Error', ['message' => $e->getMessage()]); + return back()->with('error', 'Failed to initiate payment.'); + } } /** - * Obtain Paystack payment information - * @return void + * Handle the callback from Paystack after payment. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse */ - public function handleGatewayCallback() + public function handleGatewayCallback(Request $request) { - $paymentDetails = Paystack::getPaymentData(); - - dd($paymentDetails); - // Now you have the payment details, - // you can store the authorization_code in your db to allow for recurrent subscriptions - // you can then redirect or do whatever you want + $reference = $request->query('reference'); + + try { + $response = Paystack::transaction()->verify($reference); + $data = $response['data']; + + // Here, you could store the payment record, send a receipt email, etc. + return view('payments.success', ['payment' => $data]); + } catch (\Exception $e) { + Log::error('Verification Error', ['message' => $e->getMessage()]); + return redirect('/payment')->with('error', 'Payment verification failed.'); + } } } ``` +## View example `views/payments/payment.blade.php`: + +```php +@extends('layouts.app') + +@section('content') +
+
+
+

Pay with Paystack

+
+ +
+ @if(session('error')) +
+ {{ session('error') }} +
+ @endif + +
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+@endsection +``` ```php /** @@ -237,185 +331,100 @@ $data = array( "orderID" => 23456, ); -return Paystack::getAuthorizationUrl($data)->redirectNow(); +$response = Paystack::transaction()->initialize($data); +return redirect($response['data']['authorization_url']); ``` Let me explain the fluent methods this package provides a bit here. ```php /** - * This fluent method does all the dirty work of sending a POST request with the form data - * to Paystack Api, then it gets the authorization Url and redirects the user to Paystack - * Payment Page. We've abstracted all of it, so you don't have to worry about that. - * Just eat your cookies while coding! + * Initialize a new transaction for a customer. + * + * @param array $data { + * @type string $email Customer's email address (required). + * @type int $amount Amount in kobo (e.g. 5000 = โ‚ฆ50.00) (required). + * @type string $reference Unique transaction reference (optional - auto-generated if omitted). + * @type string $callback_url URL to redirect to after payment (optional). + * @type array $metadata Custom metadata including custom_fields (optional). + * } + * @return array Response from Paystack API. */ -Paystack::getAuthorizationUrl()->redirectNow(); +Paystack::transaction()->initialize(array $data); /** - * Alternatively, use the helper. + * Verify the status of a transaction using its reference. + * + * @param string $ref Unique transaction reference to verify. + * @return array Response from Paystack API containing transaction details. */ -paystack()->getAuthorizationUrl()->redirectNow(); +Paystack::transaction()->verify(string $ref); /** - * This fluent method does all the dirty work of verifying that the just concluded transaction was actually valid, - * It verifies the transaction reference with Paystack Api and then grabs the data returned from Paystack. - * In that data, we have a lot of good stuff, especially the `authorization_code` that you can save in your db - * to allow for easy recurrent subscription. + * Fetch details of a single transaction by its ID or reference. + * + * @param string $id_or_ref Optional transaction ID or reference. + * @return array Response with transaction details. */ -Paystack::getPaymentData(); +Paystack::transaction()->fetch(string $id_or_ref); /** - * Alternatively, use the helper. + * List all transactions for the authenticated Paystack account. + * + * @return array Paginated list of transactions. */ -paystack()->getPaymentData(); +Paystack::transaction()->list(); /** - * This method gets all the customers that have performed transactions on your platform with Paystack - * @returns array + * Charge a customer using a saved authorization code. + * + * @param array $data { + * @type string $authorization_code The saved Paystack authorization code (required). + * @type string $email Customer's email (required). + * @type int $amount Amount in kobo (required). + * @type string $reference Unique reference (optional). + * } + * @return array Response from Paystack API. */ -Paystack::getAllCustomers(); +Paystack::transaction()->chargeAuthorization(array $data); /** - * Alternatively, use the helper. + * Create a new customer on Paystack. + * + * @param array $data { + * @type string $email Customer's email address (required). + * @type string $first_name First name of the customer (optional). + * @type string $last_name Last name of the customer (optional). + * @type string $phone Customer's phone number (optional). + * } + * @return array Response with created customer details. */ -paystack()->getAllCustomers(); - +Paystack::customer()->create(array $data); /** - * This method gets all the plans that you have registered on Paystack - * @returns array + * Fetch a customer's details using email or customer code. + * + * @param string $email_or_code Email address or customer code. + * @return array Customer details from Paystack API. */ -Paystack::getAllPlans(); +Paystack::customer()->fetch(string $email_or_code); /** - * Alternatively, use the helper. + * List all customers on your Paystack account. + * + * @return array Paginated list of customers. */ -paystack()->getAllPlans(); - - -/** - * This method gets all the transactions that have occurred - * @returns array - */ -Paystack::getAllTransactions(); - -/** - * Alternatively, use the helper. - */ -paystack()->getAllTransactions(); +Paystack::customer()->list(); /** - * This method generates a unique super secure cryptographic hash token to use as transaction reference - * @returns string + * Generate a unique transaction reference string. + * + * @return string Unique transaction reference. */ -Paystack::genTranxRef(); - -/** - * Alternatively, use the helper. - */ -paystack()->genTranxRef(); - - -/** -* This method creates a subaccount to be used for split payments -* @return array -*/ -Paystack::createSubAccount(); - -/** - * Alternatively, use the helper. - */ -paystack()->createSubAccount(); - - -/** -* This method fetches the details of a subaccount -* @return array -*/ -Paystack::fetchSubAccount(); - -/** - * Alternatively, use the helper. - */ -paystack()->fetchSubAccount(); - - -/** -* This method lists the subaccounts associated with your paystack account -* @return array -*/ -Paystack::listSubAccounts(); +Paystack::transRef(); -/** - * Alternatively, use the helper. - */ -paystack()->listSubAccounts(); - - -/** -* This method Updates a subaccount to be used for split payments -* @return array -*/ -Paystack::updateSubAccount(); - -/** - * Alternatively, use the helper. - */ -paystack()->updateSubAccount(); -``` - -A sample form will look like so: - -```php - "percentage", - "currency" => "KES", - "subaccounts" => [ - [ "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 10 ], - [ "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 30 ], - ], - "bearer_type" => "all", - "main_account_share" => 70 -]; -?> ``` -```html -
-
-
-

-

- Lagos Eyo Print Tee Shirt - โ‚ฆ 2,950 -
-

- {{-- required --}} - - {{-- required in kobo --}} - - - {{-- For other necessary things you want to add to your payload. it is optional though --}} - {{-- required --}} - - {{-- to support transaction split. more details https://paystack.com/docs/payments/multi-split-payments/#using-transaction-splits-with-payments --}} - {{-- to support dynamic transaction split. More details https://paystack.com/docs/payments/multi-split-payments/#dynamic-splits --}} - {{ csrf_field() }} {{-- works only when using laravel 5.1, 5.2 --}} - - {{-- employ this in place of csrf_field only in laravel 5.0 --}} - -

- -

-
-
-
-``` When clicking the submit button the customer gets redirected to the Paystack site. @@ -423,12 +432,6 @@ So now we've redirected the customer to Paystack. The customer did some actions Paystack will redirect the customer to the url of the route that is specified in the Callback URL of the Web Hooks section on Paystack dashboard. -We must validate if the redirect to our site is a valid request (we don't want imposters to wrongfully place non-paid order). - -In the controller that handles the request coming from the payment provider, we have - -`Paystack::getPaymentData()` - This function calls the verification methods and ensure it is a valid transaction else it throws an exception. - You can test with these details ```bash