From 13ae2b6e551e22180387adce1b1d499b1a4ba718 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 13:22:42 +0100 Subject: [PATCH 01/17] Add options classes for various Paystack functionalities - Created ReadAllOptions class for Page options to handle pagination. - Implemented CreateOptions and ReadAllOptions classes for Product management. - Added UpdateOptions class for updating product details. - Introduced CreateOptions and ReadAllOptions classes for Refund management. - Developed ReadAllOptions class for Settlement options. - Created AddSubaccountOptions and CreateOptions classes for Split management. - Implemented ReadAllOptions and RemoveSubaccountOptions classes for Split functionalities. - Added UpdateOptions class for updating Split details. - Created CreateOptions and ReadAllOptions classes for Subaccount management. - Developed UpdateOptions class for modifying Subaccount details. - Introduced CommissionOptions and DecommissionOptions classes for Terminal management. - Created ReadAllOptions and SendEventOptions classes for Terminal functionalities. - Implemented UpdateOptions class for updating Terminal details. - Added InitiateOptions and ReadAllOptions classes for Transfer management. - Created CreateOptions and ReadAllOptions classes for TransferRecipient management. - Developed AssignDestinationOptions and CreateOptions classes for VirtualTerminal management. - Implemented ReadAllOptions, SplitCodeOptions, UnassignDestinationOptions, and UpdateOptions classes for VirtualTerminal functionalities. --- src/API/ApplePay.php | 50 +++++ src/API/BulkCharge.php | 92 ++++++++ src/API/Charge.php | 100 +++++++++ src/API/DedicatedVirtualAccount.php | 116 ++++++++++ src/API/DirectDebit.php | 37 ++++ src/API/Dispute.php | 108 ++++++++++ src/API/Integration.php | 34 +++ src/API/Miscellaneous.php | 51 +++++ src/API/Page.php | 91 ++++++++ src/API/Product.php | 71 +++++++ src/API/Refund.php | 50 +++++ src/API/Settlement.php | 40 ++++ src/API/Split.php | 104 +++++++++ src/API/Subaccount.php | 71 +++++++ src/API/Terminal.php | 129 +++++++++++ src/API/Transfer.php | 94 ++++++++ src/API/TransferControl.php | 86 ++++++++ src/API/TransferRecipient.php | 90 ++++++++ src/API/Verification.php | 50 +++++ src/API/VirtualTerminal.php | 148 +++++++++++++ src/Client.php | 200 ++++++++++++++++++ src/Options/BulkCharge/InitiateOptions.php | 24 +++ src/Options/Charge/CreateOptions.php | 61 ++++++ .../DedicatedVirtualAccount/CreateOptions.php | 48 +++++ .../ReadAllOptions.php | 39 ++++ src/Options/Dispute/ReadAllOptions.php | 44 ++++ src/Options/Page/CreateOptions.php | 48 +++++ src/Options/Page/ReadAllOptions.php | 35 +++ src/Options/Product/CreateOptions.php | 47 ++++ src/Options/Product/ReadAllOptions.php | 35 +++ src/Options/Product/UpdateOptions.php | 43 ++++ src/Options/Refund/CreateOptions.php | 40 ++++ src/Options/Refund/ReadAllOptions.php | 43 ++++ src/Options/Settlement/ReadAllOptions.php | 39 ++++ src/Options/Split/AddSubaccountOptions.php | 29 +++ src/Options/Split/CreateOptions.php | 49 +++++ src/Options/Split/ReadAllOptions.php | 47 ++++ src/Options/Split/RemoveSubaccountOptions.php | 24 +++ src/Options/Split/UpdateOptions.php | 36 ++++ src/Options/Subaccount/CreateOptions.php | 59 ++++++ src/Options/Subaccount/ReadAllOptions.php | 35 +++ src/Options/Subaccount/UpdateOptions.php | 64 ++++++ src/Options/Terminal/CommissionOptions.php | 24 +++ src/Options/Terminal/DecommissionOptions.php | 24 +++ src/Options/Terminal/ReadAllOptions.php | 31 +++ src/Options/Terminal/SendEventOptions.php | 35 +++ src/Options/Terminal/UpdateOptions.php | 27 +++ src/Options/Transfer/InitiateOptions.php | 47 ++++ src/Options/Transfer/ReadAllOptions.php | 39 ++++ .../TransferRecipient/CreateOptions.php | 56 +++++ .../TransferRecipient/ReadAllOptions.php | 35 +++ .../AssignDestinationOptions.php | 24 +++ src/Options/VirtualTerminal/CreateOptions.php | 36 ++++ .../VirtualTerminal/ReadAllOptions.php | 35 +++ .../VirtualTerminal/SplitCodeOptions.php | 24 +++ .../UnassignDestinationOptions.php | 24 +++ src/Options/VirtualTerminal/UpdateOptions.php | 35 +++ 57 files changed, 3197 insertions(+) create mode 100644 src/API/ApplePay.php create mode 100644 src/API/BulkCharge.php create mode 100644 src/API/Charge.php create mode 100644 src/API/DedicatedVirtualAccount.php create mode 100644 src/API/DirectDebit.php create mode 100644 src/API/Dispute.php create mode 100644 src/API/Integration.php create mode 100644 src/API/Miscellaneous.php create mode 100644 src/API/Page.php create mode 100644 src/API/Product.php create mode 100644 src/API/Refund.php create mode 100644 src/API/Settlement.php create mode 100644 src/API/Split.php create mode 100644 src/API/Subaccount.php create mode 100644 src/API/Terminal.php create mode 100644 src/API/Transfer.php create mode 100644 src/API/TransferControl.php create mode 100644 src/API/TransferRecipient.php create mode 100644 src/API/Verification.php create mode 100644 src/API/VirtualTerminal.php create mode 100644 src/Options/BulkCharge/InitiateOptions.php create mode 100644 src/Options/Charge/CreateOptions.php create mode 100644 src/Options/DedicatedVirtualAccount/CreateOptions.php create mode 100644 src/Options/DedicatedVirtualAccount/ReadAllOptions.php create mode 100644 src/Options/Dispute/ReadAllOptions.php create mode 100644 src/Options/Page/CreateOptions.php create mode 100644 src/Options/Page/ReadAllOptions.php create mode 100644 src/Options/Product/CreateOptions.php create mode 100644 src/Options/Product/ReadAllOptions.php create mode 100644 src/Options/Product/UpdateOptions.php create mode 100644 src/Options/Refund/CreateOptions.php create mode 100644 src/Options/Refund/ReadAllOptions.php create mode 100644 src/Options/Settlement/ReadAllOptions.php create mode 100644 src/Options/Split/AddSubaccountOptions.php create mode 100644 src/Options/Split/CreateOptions.php create mode 100644 src/Options/Split/ReadAllOptions.php create mode 100644 src/Options/Split/RemoveSubaccountOptions.php create mode 100644 src/Options/Split/UpdateOptions.php create mode 100644 src/Options/Subaccount/CreateOptions.php create mode 100644 src/Options/Subaccount/ReadAllOptions.php create mode 100644 src/Options/Subaccount/UpdateOptions.php create mode 100644 src/Options/Terminal/CommissionOptions.php create mode 100644 src/Options/Terminal/DecommissionOptions.php create mode 100644 src/Options/Terminal/ReadAllOptions.php create mode 100644 src/Options/Terminal/SendEventOptions.php create mode 100644 src/Options/Terminal/UpdateOptions.php create mode 100644 src/Options/Transfer/InitiateOptions.php create mode 100644 src/Options/Transfer/ReadAllOptions.php create mode 100644 src/Options/TransferRecipient/CreateOptions.php create mode 100644 src/Options/TransferRecipient/ReadAllOptions.php create mode 100644 src/Options/VirtualTerminal/AssignDestinationOptions.php create mode 100644 src/Options/VirtualTerminal/CreateOptions.php create mode 100644 src/Options/VirtualTerminal/ReadAllOptions.php create mode 100644 src/Options/VirtualTerminal/SplitCodeOptions.php create mode 100644 src/Options/VirtualTerminal/UnassignDestinationOptions.php create mode 100644 src/Options/VirtualTerminal/UpdateOptions.php diff --git a/src/API/ApplePay.php b/src/API/ApplePay.php new file mode 100644 index 0000000..92ca07e --- /dev/null +++ b/src/API/ApplePay.php @@ -0,0 +1,50 @@ +httpClient->post('/apple-pay/domain', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Lists all registered domains on your integration + * + * @param array $params + * @return array + */ + public function listDomains(array $params = []): array + { + $response = $this->httpClient->get('/apple-pay/domain', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Unregister a top-level domain or subdomain previously used for your Apple Pay integration + * + * @param array $params + * @return array + */ + public function unregisterDomain(array $params): array + { + $response = $this->httpClient->delete('/apple-pay/domain', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/BulkCharge.php b/src/API/BulkCharge.php new file mode 100644 index 0000000..b93f29d --- /dev/null +++ b/src/API/BulkCharge.php @@ -0,0 +1,92 @@ +httpClient->post('/bulkcharge', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * List bulk charge batches created by the integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $response = $this->httpClient->get('/bulkcharge', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Retrieve a specific batch code. It also returns useful information on its progress by way of the total_charges and pending_charges attributes + * + * @param string $idOrCode + * @return array + */ + public function find(string $idOrCode): array + { + $response = $this->httpClient->get("/bulkcharge/{$idOrCode}"); + + return ResponseMediator::getContent($response); + } + + /** + * Retrieve the charges associated with a specified batch code + * + * @param string $idOrCode + * @param array $params + * @return array + */ + public function getCharges(string $idOrCode, array $params = []): array + { + $response = $this->httpClient->get("/bulkcharge/{$idOrCode}/charges", [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Pause processing a batch + * + * @param string $batchCode + * @return array + */ + public function pause(string $batchCode): array + { + $response = $this->httpClient->get("/bulkcharge/pause/{$batchCode}"); + + return ResponseMediator::getContent($response); + } + + /** + * Resume processing a batch + * + * @param string $batchCode + * @return array + */ + public function resume(string $batchCode): array + { + $response = $this->httpClient->get("/bulkcharge/resume/{$batchCode}"); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Charge.php b/src/API/Charge.php new file mode 100644 index 0000000..97e3d5d --- /dev/null +++ b/src/API/Charge.php @@ -0,0 +1,100 @@ +httpClient->post('/charge', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Submit PIN to continue a charge + * + * @param array $params + * @return array + */ + public function submitPin(array $params): array + { + $response = $this->httpClient->post('/charge/submit_pin', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Submit OTP to complete a charge + * + * @param array $params + * @return array + */ + public function submitOtp(array $params): array + { + $response = $this->httpClient->post('/charge/submit_otp', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Submit phone when requested + * + * @param array $params + * @return array + */ + public function submitPhone(array $params): array + { + $response = $this->httpClient->post('/charge/submit_phone', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Submit birthday when requested + * + * @param array $params + * @return array + */ + public function submitBirthday(array $params): array + { + $response = $this->httpClient->post('/charge/submit_birthday', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Submit address to continue charge + * + * @param array $params + * @return array + */ + public function submitAddress(array $params): array + { + $response = $this->httpClient->post('/charge/submit_address', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Check pending charge + * + * @param string $reference + * @return array + */ + public function checkPending(string $reference): array + { + $response = $this->httpClient->get("/charge/{$reference}"); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/DedicatedVirtualAccount.php b/src/API/DedicatedVirtualAccount.php new file mode 100644 index 0000000..e0b7ddb --- /dev/null +++ b/src/API/DedicatedVirtualAccount.php @@ -0,0 +1,116 @@ +httpClient->post('/dedicated_account', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * List dedicated virtual accounts available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $response = $this->httpClient->get('/dedicated_account', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a dedicated virtual account + * + * @param string $dedicatedAccountId + * @return array + */ + public function find(string $dedicatedAccountId): array + { + $response = $this->httpClient->get("/dedicated_account/{$dedicatedAccountId}"); + + return ResponseMediator::getContent($response); + } + + /** + * Requery Dedicated Virtual Account for new transactions + * + * @param array $params + * @return array + */ + public function requery(array $params): array + { + $response = $this->httpClient->get('/dedicated_account/requery', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Deactivate a dedicated virtual account + * + * @param string $dedicatedAccountId + * @return array + */ + public function deactivate(string $dedicatedAccountId): array + { + $response = $this->httpClient->delete("/dedicated_account/{$dedicatedAccountId}"); + + return ResponseMediator::getContent($response); + } + + /** + * Split a dedicated virtual account transaction with one or more accounts + * + * @param array $params + * @return array + */ + public function split(array $params): array + { + $response = $this->httpClient->post('/dedicated_account/split', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Remove a split payment account from a dedicated virtual account + * + * @param array $params + * @return array + */ + public function removeSplit(array $params): array + { + $response = $this->httpClient->delete('/dedicated_account/split', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Get available bank providers for dedicated virtual accounts + * + * @return array + */ + public function getProviders(): array + { + $response = $this->httpClient->get('/dedicated_account/available_providers'); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/DirectDebit.php b/src/API/DirectDebit.php new file mode 100644 index 0000000..48a9409 --- /dev/null +++ b/src/API/DirectDebit.php @@ -0,0 +1,37 @@ +httpClient->put('/directdebit/activation-charge', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Get the list of direct debit mandates on your integration + * + * @param array $params + * @return array + */ + public function listMandateAuthorizations(array $params = []): array + { + $response = $this->httpClient->get('/directdebit/mandate-authorizations', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Dispute.php b/src/API/Dispute.php new file mode 100644 index 0000000..e11c909 --- /dev/null +++ b/src/API/Dispute.php @@ -0,0 +1,108 @@ +httpClient->get('/dispute', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a dispute + * + * @param string $id + * @return array + */ + public function find(string $id): array + { + $response = $this->httpClient->get("/dispute/{$id}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update details of a dispute + * + * @param string $id + * @param array $params + * @return array + */ + public function update(string $id, array $params): array + { + $response = $this->httpClient->put("/dispute/{$id}", body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Add evidence to a dispute + * + * @param string $id + * @param array $params + * @return array + */ + public function addEvidence(string $id, array $params): array + { + $response = $this->httpClient->post("/dispute/{$id}/evidence", body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Get upload URL for a dispute file + * + * @param string $id + * @param array $params + * @return array + */ + public function getUploadUrl(string $id, array $params): array + { + $response = $this->httpClient->post("/dispute/{$id}/upload_url", body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Resolve a dispute + * + * @param string $id + * @param array $params + * @return array + */ + public function resolve(string $id, array $params): array + { + $response = $this->httpClient->put("/dispute/{$id}/resolve", body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Export disputes + * + * @param array $params + * @return array + */ + public function export(array $params = []): array + { + $response = $this->httpClient->get('/dispute/export', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Integration.php b/src/API/Integration.php new file mode 100644 index 0000000..adcb0bb --- /dev/null +++ b/src/API/Integration.php @@ -0,0 +1,34 @@ +httpClient->get('/integration/payment_session_timeout'); + + return ResponseMediator::getContent($response); + } + + /** + * Update the payment session timeout on your integration + * + * @param array $params + * @return array + */ + public function updateTimeout(array $params): array + { + $response = $this->httpClient->put('/integration/payment_session_timeout', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Miscellaneous.php b/src/API/Miscellaneous.php new file mode 100644 index 0000000..72fd48f --- /dev/null +++ b/src/API/Miscellaneous.php @@ -0,0 +1,51 @@ +httpClient->get('/bank', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Gets a list of countries that Paystack currently supports + * + * @return array + */ + public function listCountries(): array + { + $response = $this->httpClient->get('/country'); + + return ResponseMediator::getContent($response); + } + + /** + * Get a list of states for a country for address verification + * + * @param array $params + * @return array + */ + public function listStates(array $params): array + { + $response = $this->httpClient->get('/address_verification/states', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Page.php b/src/API/Page.php new file mode 100644 index 0000000..951be3b --- /dev/null +++ b/src/API/Page.php @@ -0,0 +1,91 @@ +httpClient->post('/page', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * List payment pages available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $response = $this->httpClient->get('/page', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a payment page + * + * @param string $idOrSlug + * @return array + */ + public function find(string $idOrSlug): array + { + $response = $this->httpClient->get("/page/{$idOrSlug}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update a payment page + * + * @param string $idOrSlug + * @param array $params + * @return array + */ + public function update(string $idOrSlug, array $params): array + { + $response = $this->httpClient->put("/page/{$idOrSlug}", body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Check the availability of a slug for a payment page + * + * @param string $slug + * @return array + */ + public function checkSlugAvailability(string $slug): array + { + $response = $this->httpClient->get("/page/check_slug_availability/{$slug}"); + + return ResponseMediator::getContent($response); + } + + /** + * Add products to a payment page + * + * @param string $id + * @param array $params + * @return array + */ + public function addProducts(string $id, array $params): array + { + $response = $this->httpClient->post("/page/{$id}/product", body: json_encode($params)); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Product.php b/src/API/Product.php new file mode 100644 index 0000000..68dcf6f --- /dev/null +++ b/src/API/Product.php @@ -0,0 +1,71 @@ +httpClient->post('/product', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * List products available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $options = new ProductOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/product', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a product on your integration + * + * @param string $id + * @return array + */ + public function find(string $id): array + { + $response = $this->httpClient->get("/product/{$id}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update a product details on your integration + * + * @param string $id + * @param array $params + * @return array + */ + public function update(string $id, array $params): array + { + $options = new ProductOptions\UpdateOptions($params); + + $response = $this->httpClient->put("/product/{$id}", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Refund.php b/src/API/Refund.php new file mode 100644 index 0000000..1a4b5b7 --- /dev/null +++ b/src/API/Refund.php @@ -0,0 +1,50 @@ +httpClient->post('/refund', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * List refunds available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $response = $this->httpClient->get('/refund', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a refund + * + * @param string $id + * @return array + */ + public function find(string $id): array + { + $response = $this->httpClient->get("/refund/{$id}"); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Settlement.php b/src/API/Settlement.php new file mode 100644 index 0000000..b626516 --- /dev/null +++ b/src/API/Settlement.php @@ -0,0 +1,40 @@ +httpClient->get('/settlement', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get the transactions that make up a particular settlement + * + * @param string $id + * @param array $params + * @return array + */ + public function getTransactions(string $id, array $params = []): array + { + $response = $this->httpClient->get("/settlement/{$id}/transactions", [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Split.php b/src/API/Split.php new file mode 100644 index 0000000..30bfe16 --- /dev/null +++ b/src/API/Split.php @@ -0,0 +1,104 @@ +httpClient->post('/split', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * List the transaction splits available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $options = new SplitOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/split', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a split on your integration + * + * @param string $id + * @return array + */ + public function find(string $id): array + { + $response = $this->httpClient->get("/split/{$id}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update a transaction split details on your integration + * + * @param string $id + * @param array $params + * @return array + */ + public function update(string $id, array $params): array + { + $options = new SplitOptions\UpdateOptions($params); + + $response = $this->httpClient->put("/split/{$id}", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Add a Subaccount to a Transaction Split, or update the share of an existing + * Subaccount in a Transaction Split + * + * @param string $id + * @param array $params + * @return array + */ + public function addSubaccount(string $id, array $params): array + { + $options = new SplitOptions\AddSubaccountOptions($params); + + $response = $this->httpClient->post("/split/{$id}/subaccount/add", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Remove a subaccount from a transaction split + * + * @param string $id + * @param array $params + * @return array + */ + public function removeSubaccount(string $id, array $params): array + { + $options = new SplitOptions\RemoveSubaccountOptions($params); + + $response = $this->httpClient->post("/split/{$id}/subaccount/remove", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Subaccount.php b/src/API/Subaccount.php new file mode 100644 index 0000000..b05556e --- /dev/null +++ b/src/API/Subaccount.php @@ -0,0 +1,71 @@ +httpClient->post('/subaccount', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * List subaccounts available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $options = new SubaccountOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/subaccount', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a subaccount on your integration + * + * @param string $idOrCode + * @return array + */ + public function find(string $idOrCode): array + { + $response = $this->httpClient->get("/subaccount/{$idOrCode}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update a subaccount details on your integration + * + * @param string $idOrCode + * @param array $params + * @return array + */ + public function update(string $idOrCode, array $params): array + { + $options = new SubaccountOptions\UpdateOptions($params); + + $response = $this->httpClient->put("/subaccount/{$idOrCode}", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Terminal.php b/src/API/Terminal.php new file mode 100644 index 0000000..b5c1128 --- /dev/null +++ b/src/API/Terminal.php @@ -0,0 +1,129 @@ +httpClient->post("/terminal/{$terminalId}/event", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Check the status of an event sent to the Terminal + * + * @param string $terminalId + * @param string $eventId + * @return array + */ + public function fetchEventStatus(string $terminalId, string $eventId): array + { + $response = $this->httpClient->get("/terminal/{$terminalId}/event/{$eventId}"); + + return ResponseMediator::getContent($response); + } + + /** + * Check the availiability of a Terminal before sending an event to it + * + * @param string $terminalId + * @return array + */ + public function fetchTerminalStatus(string $terminalId): array + { + $response = $this->httpClient->get("/terminal/{$terminalId}/presence"); + + return ResponseMediator::getContent($response); + } + + /** + * List the Terminals available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $options = new TerminalOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/terminal', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get the details of a Terminal + * + * @param string $terminalId + * @return array + */ + public function find(string $terminalId): array + { + $response = $this->httpClient->get("/terminal/{$terminalId}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update the details of a Terminal + * + * @param string $terminalId + * @param array $params + * @return array + */ + public function update(string $terminalId, array $params): array + { + $options = new TerminalOptions\UpdateOptions($params); + + $response = $this->httpClient->put("/terminal/{$terminalId}", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Activate your debug device by linking it to your integration + * + * @param array $params + * @return array + */ + public function commission(array $params): array + { + $options = new TerminalOptions\CommissionOptions($params); + + $response = $this->httpClient->post('/terminal/commission_device', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Unlink your debug device from your integration + * + * @param array $params + * @return array + */ + public function decommission(array $params): array + { + $options = new TerminalOptions\DecommissionOptions($params); + + $response = $this->httpClient->post('/terminal/decommission_device', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Transfer.php b/src/API/Transfer.php new file mode 100644 index 0000000..a516862 --- /dev/null +++ b/src/API/Transfer.php @@ -0,0 +1,94 @@ +httpClient->post('/transfer', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Finalize an initiated transfer + * + * @param array $params + * @return array + */ + public function finalize(array $params): array + { + $response = $this->httpClient->post('/transfer/finalize_transfer', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Batch multiple transfers in a single request + * + * @param array $params + * @return array + */ + public function bulk(array $params): array + { + $response = $this->httpClient->post('/transfer/bulk', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * List the transfers made on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $options = new TransferOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/transfer', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Get details of a transfer on your integration + * + * @param string $idOrCode + * @return array + */ + public function find(string $idOrCode): array + { + $response = $this->httpClient->get("/transfer/{$idOrCode}"); + + return ResponseMediator::getContent($response); + } + + /** + * Verify the status of a transfer on your integration + * + * @param string $reference + * @return array + */ + public function verify(string $reference): array + { + $response = $this->httpClient->get("/transfer/verify/{$reference}"); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/TransferControl.php b/src/API/TransferControl.php new file mode 100644 index 0000000..70160fb --- /dev/null +++ b/src/API/TransferControl.php @@ -0,0 +1,86 @@ +httpClient->get('/balance'); + + return ResponseMediator::getContent($response); + } + + /** + * Retrieve your balance ledger + * + * @param array $params + * @return array + */ + public function getBalanceLedger(array $params = []): array + { + $response = $this->httpClient->get('/balance/ledger', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Generate a new OTP and send to customer in the event they are having trouble receiving one + * + * @param array $params + * @return array + */ + public function resendOtp(array $params): array + { + $response = $this->httpClient->post('/transfer/resend_otp', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Disable OTP requirement for transfers + * + * @return array + */ + public function disableOtp(): array + { + $response = $this->httpClient->post('/transfer/disable_otp'); + + return ResponseMediator::getContent($response); + } + + /** + * Enable OTP requirement for transfers + * + * @return array + */ + public function enableOtp(): array + { + $response = $this->httpClient->post('/transfer/enable_otp'); + + return ResponseMediator::getContent($response); + } + + /** + * Finalize disabling of OTP requirement for transfers + * + * @param array $params + * @return array + */ + public function finalizeDisableOtp(array $params): array + { + $response = $this->httpClient->post('/transfer/disable_otp_finalize', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/TransferRecipient.php b/src/API/TransferRecipient.php new file mode 100644 index 0000000..35be797 --- /dev/null +++ b/src/API/TransferRecipient.php @@ -0,0 +1,90 @@ +httpClient->post('/transferrecipient', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Create multiple transfer recipients in batches + * + * @param array $params + * @return array + */ + public function bulkCreate(array $params): array + { + $response = $this->httpClient->post('/transferrecipient/bulk', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * List transfer recipients available on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $response = $this->httpClient->get('/transferrecipient', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Fetch the details of a transfer recipient + * + * @param string $idOrCode + * @return array + */ + public function find(string $idOrCode): array + { + $response = $this->httpClient->get("/transferrecipient/{$idOrCode}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update an existing recipient + * + * @param string $idOrCode + * @param array $params + * @return array + */ + public function update(string $idOrCode, array $params): array + { + $response = $this->httpClient->put("/transferrecipient/{$idOrCode}", body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Delete a transfer recipient (sets the transfer recipient to inactive) + * + * @param string $idOrCode + * @return array + */ + public function delete(string $idOrCode): array + { + $response = $this->httpClient->delete("/transferrecipient/{$idOrCode}"); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/Verification.php b/src/API/Verification.php new file mode 100644 index 0000000..2c854f8 --- /dev/null +++ b/src/API/Verification.php @@ -0,0 +1,50 @@ +httpClient->get('/bank/resolve', [ + 'query' => $params + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Confirm the authenticity of a customer's account number before sending money + * + * @param array $params + * @return array + */ + public function validateAccount(array $params): array + { + $response = $this->httpClient->post('/bank/validate', body: json_encode($params)); + + return ResponseMediator::getContent($response); + } + + /** + * Get more information about a customer's card + * + * @param string $bin + * @return array + */ + public function resolveCardBin(string $bin): array + { + $response = $this->httpClient->get("/decision/bin/{$bin}"); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/API/VirtualTerminal.php b/src/API/VirtualTerminal.php new file mode 100644 index 0000000..83f270e --- /dev/null +++ b/src/API/VirtualTerminal.php @@ -0,0 +1,148 @@ +httpClient->post('/virtual_terminal', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * List Virtual Terminals on your integration + * + * @param array $params + * @return array + */ + public function all(array $params = []): array + { + $options = new VirtualTerminalOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/virtual_terminal', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getContent($response); + } + + /** + * Fetch a Virtual Terminal on your integration + * + * @param string $code + * @return array + */ + public function find(string $code): array + { + $response = $this->httpClient->get("/virtual_terminal/{$code}"); + + return ResponseMediator::getContent($response); + } + + /** + * Update a Virtual Terminal on your integration + * + * @param string $code + * @param array $params + * @return array + */ + public function update(string $code, array $params): array + { + $options = new VirtualTerminalOptions\UpdateOptions($params); + + $response = $this->httpClient->put("/virtual_terminal/{$code}", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Deactivate a Virtual Terminal on your integration + * + * @param string $code + * @return array + */ + public function deactivate(string $code): array + { + $response = $this->httpClient->put("/virtual_terminal/{$code}/deactivate"); + + return ResponseMediator::getContent($response); + } + + /** + * Add a destination (WhatsApp number) to a Virtual Terminal on your integration + * + * @param string $code + * @param array $params + * @return array + */ + public function assignDestination(string $code, array $params): array + { + $options = new VirtualTerminalOptions\AssignDestinationOptions($params); + + $response = $this->httpClient->post("/virtual_terminal/{$code}/destination/assign", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Unassign a destination (WhatsApp Number) from a Virtual Terminal on your integration + * + * @param string $code + * @param array $params + * @return array + */ + public function unassignDestination(string $code, array $params): array + { + $options = new VirtualTerminalOptions\UnassignDestinationOptions($params); + + $response = $this->httpClient->post("/virtual_terminal/{$code}/destination/unassign", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Add a split code to a Virtual Terminal on your integration + * + * @param string $code + * @param array $params + * @return array + */ + public function addSplitCode(string $code, array $params): array + { + $options = new VirtualTerminalOptions\SplitCodeOptions($params); + + $response = $this->httpClient->put("/virtual_terminal/{$code}/split_code", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Remove a split code from a Virtual Terminal on your integration + * + * @param string $code + * @param array $params + * @return array + */ + public function removeSplitCode(string $code, array $params): array + { + $options = new VirtualTerminalOptions\SplitCodeOptions($params); + + $response = $this->httpClient->delete("/virtual_terminal/{$code}/split_code", body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } +} \ No newline at end of file diff --git a/src/Client.php b/src/Client.php index 526e1bd..fa8bec1 100644 --- a/src/Client.php +++ b/src/Client.php @@ -124,6 +124,206 @@ protected function paymentRequests(): API\PaymentRequest return new API\PaymentRequest($this); } + /** + * Split API + * + * @return API\Split + */ + protected function splits(): API\Split + { + return new API\Split($this); + } + + /** + * Terminal API + * + * @return API\Terminal + */ + protected function terminals(): API\Terminal + { + return new API\Terminal($this); + } + + /** + * VirtualTerminal API + * + * @return API\VirtualTerminal + */ + protected function virtualTerminals(): API\VirtualTerminal + { + return new API\VirtualTerminal($this); + } + + /** + * ApplePay API + * + * @return API\ApplePay + */ + protected function applePay(): API\ApplePay + { + return new API\ApplePay($this); + } + + /** + * Subaccount API + * + * @return API\Subaccount + */ + protected function subaccounts(): API\Subaccount + { + return new API\Subaccount($this); + } + + /** + * Product API + * + * @return API\Product + */ + protected function products(): API\Product + { + return new API\Product($this); + } + + /** + * DirectDebit API + * + * @return API\DirectDebit + */ + protected function directDebit(): API\DirectDebit + { + return new API\DirectDebit($this); + } + + /** + * Integration API + * + * @return API\Integration + */ + protected function integration(): API\Integration + { + return new API\Integration($this); + } + + /** + * Miscellaneous API + * + * @return API\Miscellaneous + */ + protected function miscellaneous(): API\Miscellaneous + { + return new API\Miscellaneous($this); + } + + /** + * Verification API + * + * @return API\Verification + */ + protected function verification(): API\Verification + { + return new API\Verification($this); + } + + /** + * Transfer API + * + * @return API\Transfer + */ + protected function transfers(): API\Transfer + { + return new API\Transfer($this); + } + + /** + * Transfer Recipient API + * + * @return API\TransferRecipient + */ + protected function transferRecipients(): API\TransferRecipient + { + return new API\TransferRecipient($this); + } + + /** + * Transfer Control API + * + * @return API\TransferControl + */ + protected function transferControl(): API\TransferControl + { + return new API\TransferControl($this); + } + + /** + * Charge API + * + * @return API\Charge + */ + protected function charges(): API\Charge + { + return new API\Charge($this); + } + + /** + * Dispute API + * + * @return API\Dispute + */ + protected function disputes(): API\Dispute + { + return new API\Dispute($this); + } + + /** + * Refund API + * + * @return API\Refund + */ + protected function refunds(): API\Refund + { + return new API\Refund($this); + } + + /** + * Settlement API + * + * @return API\Settlement + */ + protected function settlements(): API\Settlement + { + return new API\Settlement($this); + } + + /** + * Bulk Charge API + * + * @return API\BulkCharge + */ + protected function bulkCharges(): API\BulkCharge + { + return new API\BulkCharge($this); + } + + /** + * Page API + * + * @return API\Page + */ + protected function pages(): API\Page + { + return new API\Page($this); + } + + /** + * Dedicated Virtual Account API + * + * @return API\DedicatedVirtualAccount + */ + protected function dedicatedVirtualAccounts(): API\DedicatedVirtualAccount + { + return new API\DedicatedVirtualAccount($this); + } + /** * Read data from inaccessible (protected or private) * or non-existing properties. diff --git a/src/Options/BulkCharge/InitiateOptions.php b/src/Options/BulkCharge/InitiateOptions.php new file mode 100644 index 0000000..f59cff0 --- /dev/null +++ b/src/Options/BulkCharge/InitiateOptions.php @@ -0,0 +1,24 @@ +define('charges') + ->required() + ->allowedTypes('array') + ->info('A list of charge objects. Each object should contain: authorization, amount, and reference'); + } +} \ No newline at end of file diff --git a/src/Options/Charge/CreateOptions.php b/src/Options/Charge/CreateOptions.php new file mode 100644 index 0000000..02052f9 --- /dev/null +++ b/src/Options/Charge/CreateOptions.php @@ -0,0 +1,61 @@ +define('email') + ->required() + ->allowedTypes('string') + ->info('Customer\'s email address'); + + $resolver->define('amount') + ->required() + ->allowedTypes('int') + ->info('Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); + + $resolver->define('bank') + ->allowedTypes('array') + ->info('Bank account to charge (don\'t send if charging an authorization code)'); + + $resolver->define('authorization_code') + ->allowedTypes('string') + ->info('An authorization code to charge (don\'t send if charging a bank account)'); + + $resolver->define('pin') + ->allowedTypes('string') + ->info('4-digit PIN (send with a non-reusable authorization code)'); + + $resolver->define('metadata') + ->allowedTypes('array') + ->info('A JSON object'); + + $resolver->define('reference') + ->allowedTypes('string') + ->info('Unique transaction reference. Only -, ., = and alphanumeric characters allowed.'); + + $resolver->define('ussd') + ->allowedTypes('array') + ->info('USSD type to charge (don\'t send if charging an authorization code, bank or card)'); + + $resolver->define('mobile_money') + ->allowedTypes('array') + ->info('Mobile money details (don\'t send if charging an authorization code, bank or card)'); + + $resolver->define('device_id') + ->allowedTypes('string') + ->info('This is the unique identifier of the device a user uses in making payment'); + } +} \ No newline at end of file diff --git a/src/Options/DedicatedVirtualAccount/CreateOptions.php b/src/Options/DedicatedVirtualAccount/CreateOptions.php new file mode 100644 index 0000000..f256bda --- /dev/null +++ b/src/Options/DedicatedVirtualAccount/CreateOptions.php @@ -0,0 +1,48 @@ +define('customer') + ->required() + ->allowedTypes('string') + ->info('Customer ID or code'); + + $resolver->define('preferred_bank') + ->allowedTypes('string') + ->info('The bank slug for preferred bank. To get a list of available banks, use the List Providers endpoint'); + + $resolver->define('subaccount') + ->allowedTypes('string') + ->info('Subaccount code of the account you want to split the transaction with'); + + $resolver->define('split_code') + ->allowedTypes('string') + ->info('Split code consisting of the lists of accounts you want to split the transaction with'); + + $resolver->define('first_name') + ->allowedTypes('string') + ->info('Customer\'s first name'); + + $resolver->define('last_name') + ->allowedTypes('string') + ->info('Customer\'s last name'); + + $resolver->define('phone') + ->allowedTypes('string') + ->info('Customer\'s phone number'); + } +} \ No newline at end of file diff --git a/src/Options/DedicatedVirtualAccount/ReadAllOptions.php b/src/Options/DedicatedVirtualAccount/ReadAllOptions.php new file mode 100644 index 0000000..25d2a1f --- /dev/null +++ b/src/Options/DedicatedVirtualAccount/ReadAllOptions.php @@ -0,0 +1,39 @@ +define('active') + ->allowedTypes('bool') + ->info('Status of the dedicated virtual account'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('The currency of the dedicated virtual account. Only NGN is currently allowed'); + + $resolver->define('provider_slug') + ->allowedTypes('string') + ->info('The bank\'s slug in lowercase, without spaces e.g. wema-bank'); + + $resolver->define('bank_id') + ->allowedTypes('string') + ->info('The bank\'s ID e.g. 035'); + + $resolver->define('customer') + ->allowedTypes('string') + ->info('The customer\'s ID'); + } +} \ No newline at end of file diff --git a/src/Options/Dispute/ReadAllOptions.php b/src/Options/Dispute/ReadAllOptions.php new file mode 100644 index 0000000..b47ccf1 --- /dev/null +++ b/src/Options/Dispute/ReadAllOptions.php @@ -0,0 +1,44 @@ +define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing disputes'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing disputes'); + + $resolver->define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('transaction') + ->allowedTypes('string') + ->info('Transaction ID'); + + $resolver->define('status') + ->allowedTypes('string') + ->allowedValues(['awaiting-merchant-feedback', 'awaiting-bank-feedback', 'pending', 'resolved']) + ->info('Dispute Status. Acceptable values: { awaiting-merchant-feedback | awaiting-bank-feedback | pending | resolved }'); + } +} \ No newline at end of file diff --git a/src/Options/Page/CreateOptions.php b/src/Options/Page/CreateOptions.php new file mode 100644 index 0000000..ffbed05 --- /dev/null +++ b/src/Options/Page/CreateOptions.php @@ -0,0 +1,48 @@ +define('name') + ->required() + ->allowedTypes('string') + ->info('Name of page'); + + $resolver->define('description') + ->allowedTypes('string') + ->info('A description for this page'); + + $resolver->define('amount') + ->allowedTypes('int') + ->info('Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); + + $resolver->define('slug') + ->allowedTypes('string') + ->info('URL slug you would like to be associated with this page. Page will be accessible at https://paystack.com/pay/[slug]'); + + $resolver->define('metadata') + ->allowedTypes('array') + ->info('Extra data to configure the payment page including subaccount, logo image, transaction charge'); + + $resolver->define('redirect_url') + ->allowedTypes('string') + ->info('If you would like Paystack to redirect someplace upon successful payment, specify the URL here.'); + + $resolver->define('custom_fields') + ->allowedTypes('array') + ->info('If you would like to accept custom fields, specify them here.'); + } +} \ No newline at end of file diff --git a/src/Options/Page/ReadAllOptions.php b/src/Options/Page/ReadAllOptions.php new file mode 100644 index 0000000..8141a60 --- /dev/null +++ b/src/Options/Page/ReadAllOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing pages'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing pages'); + } +} \ No newline at end of file diff --git a/src/Options/Product/CreateOptions.php b/src/Options/Product/CreateOptions.php new file mode 100644 index 0000000..a03054b --- /dev/null +++ b/src/Options/Product/CreateOptions.php @@ -0,0 +1,47 @@ +define('name') + ->required() + ->allowedTypes('string') + ->info('Name of product'); + + $resolver->define('description') + ->required() + ->allowedTypes('string') + ->info('A description for this product'); + + $resolver->define('price') + ->required() + ->allowedTypes('int') + ->info('Price should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); + + $resolver->define('currency') + ->required() + ->allowedTypes('string') + ->info('Currency in which price is set. Allowed values are: NGN, GHS, ZAR or USD'); + + $resolver->define('unlimited') + ->allowedTypes('bool') + ->info('Set to true if the product has unlimited stock. Leave as false if the product has limited stock'); + + $resolver->define('quantity') + ->allowedTypes('int') + ->info('Number of products in stock. Use if unlimited is false'); + } +} \ No newline at end of file diff --git a/src/Options/Product/ReadAllOptions.php b/src/Options/Product/ReadAllOptions.php new file mode 100644 index 0000000..2a21668 --- /dev/null +++ b/src/Options/Product/ReadAllOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing products'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing products'); + } +} \ No newline at end of file diff --git a/src/Options/Product/UpdateOptions.php b/src/Options/Product/UpdateOptions.php new file mode 100644 index 0000000..889cbf7 --- /dev/null +++ b/src/Options/Product/UpdateOptions.php @@ -0,0 +1,43 @@ +define('name') + ->allowedTypes('string') + ->info('Name of product'); + + $resolver->define('description') + ->allowedTypes('string') + ->info('A description for this product'); + + $resolver->define('price') + ->allowedTypes('int') + ->info('Price should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('Currency in which price is set. Allowed values are: NGN, GHS, ZAR or USD'); + + $resolver->define('unlimited') + ->allowedTypes('bool') + ->info('Set to true if the product has unlimited stock. Leave as false if the product has limited stock'); + + $resolver->define('quantity') + ->allowedTypes('int') + ->info('Number of products in stock. Use if unlimited is false'); + } +} \ No newline at end of file diff --git a/src/Options/Refund/CreateOptions.php b/src/Options/Refund/CreateOptions.php new file mode 100644 index 0000000..e58ad1e --- /dev/null +++ b/src/Options/Refund/CreateOptions.php @@ -0,0 +1,40 @@ +define('transaction') + ->required() + ->allowedTypes('string') + ->info('Transaction reference or id'); + + $resolver->define('amount') + ->allowedTypes('int') + ->info('Amount ( in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR ) to be refunded to the customer. Amount is optional(defaults to original transaction amount) and cannot be more than the original transaction amount.'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('Three-letter ISO currency. Allowed values are: NGN, GHS, ZAR or USD'); + + $resolver->define('customer_note') + ->allowedTypes('string') + ->info('Customer reason'); + + $resolver->define('merchant_note') + ->allowedTypes('string') + ->info('Merchant reason'); + } +} \ No newline at end of file diff --git a/src/Options/Refund/ReadAllOptions.php b/src/Options/Refund/ReadAllOptions.php new file mode 100644 index 0000000..b8e5ce2 --- /dev/null +++ b/src/Options/Refund/ReadAllOptions.php @@ -0,0 +1,43 @@ +define('reference') + ->allowedTypes('string') + ->info('Identifier for transaction to be refunded'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('Three-letter ISO currency. Allowed values are: NGN, GHS, ZAR or USD'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing refunds'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing refunds'); + + $resolver->define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + } +} \ No newline at end of file diff --git a/src/Options/Settlement/ReadAllOptions.php b/src/Options/Settlement/ReadAllOptions.php new file mode 100644 index 0000000..d1dc5f8 --- /dev/null +++ b/src/Options/Settlement/ReadAllOptions.php @@ -0,0 +1,39 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing settlements'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing settlements'); + + $resolver->define('subaccount') + ->allowedTypes('string') + ->info('Provide a subaccount ID to export only settlements for that subaccount'); + } +} \ No newline at end of file diff --git a/src/Options/Split/AddSubaccountOptions.php b/src/Options/Split/AddSubaccountOptions.php new file mode 100644 index 0000000..a249b7f --- /dev/null +++ b/src/Options/Split/AddSubaccountOptions.php @@ -0,0 +1,29 @@ +define('subaccount') + ->required() + ->allowedTypes('string') + ->info('This is the sub account code'); + + $resolver->define('share') + ->required() + ->allowedTypes('int') + ->info('This is the transaction share for the subaccount'); + } +} \ No newline at end of file diff --git a/src/Options/Split/CreateOptions.php b/src/Options/Split/CreateOptions.php new file mode 100644 index 0000000..4d66fe5 --- /dev/null +++ b/src/Options/Split/CreateOptions.php @@ -0,0 +1,49 @@ +define('name') + ->required() + ->allowedTypes('string') + ->info('Name of the transaction split'); + + $resolver->define('type') + ->required() + ->allowedTypes('string') + ->allowedValues(['percentage', 'flat']) + ->info('The type of transaction split you want to create. You can use one of the following: percentage | flat'); + + $resolver->define('currency') + ->required() + ->allowedTypes('string') + ->info('Any of the supported currency'); + + $resolver->define('subaccounts') + ->required() + ->allowedTypes('array') + ->info('A list of object containing subaccount code and number of shares: [{"subaccount": "ACT_xxxxxxxxxx", "share": xxx},{...}]'); + + $resolver->define('bearer_type') + ->allowedTypes('string') + ->allowedValues(['subaccount', 'account', 'all-proportional', 'all']) + ->info('Any of subaccount | account | all-proportional | all'); + + $resolver->define('bearer_subaccount') + ->allowedTypes('string') + ->info('Subaccount code'); + } +} \ No newline at end of file diff --git a/src/Options/Split/ReadAllOptions.php b/src/Options/Split/ReadAllOptions.php new file mode 100644 index 0000000..3a0570d --- /dev/null +++ b/src/Options/Split/ReadAllOptions.php @@ -0,0 +1,47 @@ +define('name') + ->allowedTypes('string') + ->info('The name of the split'); + + $resolver->define('active') + ->allowedTypes('bool') + ->info('Any of true or false'); + + $resolver->define('sort_by') + ->allowedTypes('string') + ->info('Sort by name, defaults to createdAt date'); + + $resolver->define('perPage') + ->allowedTypes('int') + ->info('Number of splits per page. If not specified, we use a default value of 50.'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('Page number to view. If not specified, we use a default value of 1.'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing splits e.g. 2019-09-24T00:00:05.000Z, 2019-09-21'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing splits e.g. 2019-09-24T00:00:05.000Z, 2019-09-21'); + } +} \ No newline at end of file diff --git a/src/Options/Split/RemoveSubaccountOptions.php b/src/Options/Split/RemoveSubaccountOptions.php new file mode 100644 index 0000000..c7e1ffa --- /dev/null +++ b/src/Options/Split/RemoveSubaccountOptions.php @@ -0,0 +1,24 @@ +define('subaccount') + ->required() + ->allowedTypes('string') + ->info('This is the sub account code'); + } +} \ No newline at end of file diff --git a/src/Options/Split/UpdateOptions.php b/src/Options/Split/UpdateOptions.php new file mode 100644 index 0000000..614f190 --- /dev/null +++ b/src/Options/Split/UpdateOptions.php @@ -0,0 +1,36 @@ +define('name') + ->allowedTypes('string') + ->info('Name of the transaction split'); + + $resolver->define('active') + ->allowedTypes('bool') + ->info('True or False'); + + $resolver->define('bearer_type') + ->allowedTypes('string') + ->allowedValues(['subaccount', 'account', 'all-proportional', 'all']) + ->info('Any of the following values: subaccount | account | all-proportional | all'); + + $resolver->define('bearer_subaccount') + ->allowedTypes('string') + ->info('Subaccount code of a subaccount in the split group. This should be specified only if the bearer_type is subaccount'); + } +} \ No newline at end of file diff --git a/src/Options/Subaccount/CreateOptions.php b/src/Options/Subaccount/CreateOptions.php new file mode 100644 index 0000000..adea003 --- /dev/null +++ b/src/Options/Subaccount/CreateOptions.php @@ -0,0 +1,59 @@ +define('business_name') + ->required() + ->allowedTypes('string') + ->info('Name of business for subaccount'); + + $resolver->define('settlement_bank') + ->required() + ->allowedTypes('string') + ->info('Bank Code for the bank. You can get the list of Bank Codes by calling the List Banks endpoint'); + + $resolver->define('account_number') + ->required() + ->allowedTypes('string') + ->info('Bank Account Number'); + + $resolver->define('percentage_charge') + ->required() + ->allowedTypes('float', 'int') + ->info('The default percentage charged when receiving on behalf of this subaccount'); + + $resolver->define('description') + ->allowedTypes('string') + ->info('A description for this subaccount'); + + $resolver->define('primary_contact_email') + ->allowedTypes('string') + ->info('A contact email for the subaccount'); + + $resolver->define('primary_contact_name') + ->allowedTypes('string') + ->info('A name for the contact person for this subaccount'); + + $resolver->define('primary_contact_phone') + ->allowedTypes('string') + ->info('A phone number to call for this subaccount'); + + $resolver->define('metadata') + ->allowedTypes('array') + ->info('Stringified JSON object of custom data'); + } +} \ No newline at end of file diff --git a/src/Options/Subaccount/ReadAllOptions.php b/src/Options/Subaccount/ReadAllOptions.php new file mode 100644 index 0000000..dfa9bca --- /dev/null +++ b/src/Options/Subaccount/ReadAllOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing subaccounts'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing subaccounts'); + } +} \ No newline at end of file diff --git a/src/Options/Subaccount/UpdateOptions.php b/src/Options/Subaccount/UpdateOptions.php new file mode 100644 index 0000000..84f5f7e --- /dev/null +++ b/src/Options/Subaccount/UpdateOptions.php @@ -0,0 +1,64 @@ +define('business_name') + ->allowedTypes('string') + ->info('Name of business for subaccount'); + + $resolver->define('settlement_bank') + ->allowedTypes('string') + ->info('Bank Code for the bank'); + + $resolver->define('account_number') + ->allowedTypes('string') + ->info('Bank Account Number'); + + $resolver->define('active') + ->allowedTypes('bool') + ->info('Activate or deactivate a subaccount'); + + $resolver->define('percentage_charge') + ->allowedTypes('float', 'int') + ->info('The default percentage charged when receiving on behalf of this subaccount'); + + $resolver->define('description') + ->allowedTypes('string') + ->info('A description for this subaccount'); + + $resolver->define('primary_contact_email') + ->allowedTypes('string') + ->info('A contact email for the subaccount'); + + $resolver->define('primary_contact_name') + ->allowedTypes('string') + ->info('A name for the contact person for this subaccount'); + + $resolver->define('primary_contact_phone') + ->allowedTypes('string') + ->info('A phone number to call for this subaccount'); + + $resolver->define('settlement_schedule') + ->allowedTypes('string') + ->allowedValues(['auto', 'weekly', 'monthly', 'manual']) + ->info('Any of auto, weekly, monthly, manual. Auto means payout is T+1 and manual means payout to the subaccount should only be made when requested'); + + $resolver->define('metadata') + ->allowedTypes('array') + ->info('Stringified JSON object of custom data'); + } +} \ No newline at end of file diff --git a/src/Options/Terminal/CommissionOptions.php b/src/Options/Terminal/CommissionOptions.php new file mode 100644 index 0000000..e6bcb86 --- /dev/null +++ b/src/Options/Terminal/CommissionOptions.php @@ -0,0 +1,24 @@ +define('serial_number') + ->required() + ->allowedTypes('string') + ->info('Device Serial Number'); + } +} \ No newline at end of file diff --git a/src/Options/Terminal/DecommissionOptions.php b/src/Options/Terminal/DecommissionOptions.php new file mode 100644 index 0000000..77a5de1 --- /dev/null +++ b/src/Options/Terminal/DecommissionOptions.php @@ -0,0 +1,24 @@ +define('serial_number') + ->required() + ->allowedTypes('string') + ->info('Device Serial Number'); + } +} \ No newline at end of file diff --git a/src/Options/Terminal/ReadAllOptions.php b/src/Options/Terminal/ReadAllOptions.php new file mode 100644 index 0000000..b7365fc --- /dev/null +++ b/src/Options/Terminal/ReadAllOptions.php @@ -0,0 +1,31 @@ +define('perPage') + ->allowedTypes('int') + ->info('Specify how many records you want to retrieve per page. If not specified, we use a default value of 50.'); + + $resolver->define('next') + ->allowedTypes('string') + ->info('A cursor that indicates your place in the list. It can be used to fetch the next page of the list'); + + $resolver->define('previous') + ->allowedTypes('string') + ->info('A cursor that indicates your place in the list. It should be used to fetch the previous page of the list after an intial next request'); + } +} \ No newline at end of file diff --git a/src/Options/Terminal/SendEventOptions.php b/src/Options/Terminal/SendEventOptions.php new file mode 100644 index 0000000..63da493 --- /dev/null +++ b/src/Options/Terminal/SendEventOptions.php @@ -0,0 +1,35 @@ +define('type') + ->required() + ->allowedTypes('string') + ->allowedValues(['invoice', 'transaction']) + ->info('The type of event to push. We currently support invoice and transaction'); + + $resolver->define('action') + ->required() + ->allowedTypes('string') + ->info('The action the Terminal needs to perform. For the invoice type, the action can either be process or view. For the transaction type, the action can either be process or print.'); + + $resolver->define('data') + ->required() + ->allowedTypes('array') + ->info('The paramters needed to perform the specified action. For the invoice type, you need to pass the invoice id and offline reference: {id: invoice_id, reference: offline_reference}. For the transaction type, you can pass the transaction id: {id: transaction_id}'); + } +} \ No newline at end of file diff --git a/src/Options/Terminal/UpdateOptions.php b/src/Options/Terminal/UpdateOptions.php new file mode 100644 index 0000000..0d83285 --- /dev/null +++ b/src/Options/Terminal/UpdateOptions.php @@ -0,0 +1,27 @@ +define('name') + ->allowedTypes('string') + ->info('Name of the terminal'); + + $resolver->define('address') + ->allowedTypes('string') + ->info('The address of the Terminal'); + } +} \ No newline at end of file diff --git a/src/Options/Transfer/InitiateOptions.php b/src/Options/Transfer/InitiateOptions.php new file mode 100644 index 0000000..bff7429 --- /dev/null +++ b/src/Options/Transfer/InitiateOptions.php @@ -0,0 +1,47 @@ +define('source') + ->required() + ->allowedTypes('string') + ->allowedValues(['balance']) + ->info('Where should we transfer from? Only balance for now'); + + $resolver->define('amount') + ->required() + ->allowedTypes('int') + ->info('Amount to transfer in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); + + $resolver->define('recipient') + ->required() + ->allowedTypes('string') + ->info('Code for transfer recipient'); + + $resolver->define('reason') + ->allowedTypes('string') + ->info('The reason for the transfer'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('Currency in which to make the transfer. Defaults to NGN'); + + $resolver->define('reference') + ->allowedTypes('string') + ->info('If specified, the field should be a unique identifier (in lowercase) for the object. Only -,_ and alphanumeric characters allowed.'); + } +} \ No newline at end of file diff --git a/src/Options/Transfer/ReadAllOptions.php b/src/Options/Transfer/ReadAllOptions.php new file mode 100644 index 0000000..86f13d3 --- /dev/null +++ b/src/Options/Transfer/ReadAllOptions.php @@ -0,0 +1,39 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('customer') + ->allowedTypes('string') + ->info('Filter by customer'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing transfers'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing transfers'); + } +} \ No newline at end of file diff --git a/src/Options/TransferRecipient/CreateOptions.php b/src/Options/TransferRecipient/CreateOptions.php new file mode 100644 index 0000000..9d52ccc --- /dev/null +++ b/src/Options/TransferRecipient/CreateOptions.php @@ -0,0 +1,56 @@ +define('type') + ->required() + ->allowedTypes('string') + ->allowedValues(['nuban', 'mobile_money', 'basa']) + ->info('Recipient Type. It could be one of: nuban, mobile_money, basa'); + + $resolver->define('name') + ->required() + ->allowedTypes('string') + ->info('A name for the recipient'); + + $resolver->define('account_number') + ->required() + ->allowedTypes('string') + ->info('Required if type is nuban or basa'); + + $resolver->define('bank_code') + ->required() + ->allowedTypes('string') + ->info('Required if type is nuban or basa. You can get the list of Bank Codes by calling the List Banks endpoint'); + + $resolver->define('description') + ->allowedTypes('string') + ->info('A description for this recipient'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('Currency for the account receiving the transfer'); + + $resolver->define('authorization_code') + ->allowedTypes('string') + ->info('An authorization code from a previous transaction'); + + $resolver->define('metadata') + ->allowedTypes('array') + ->info('Store additional information about your recipient in a structured format, JSON'); + } +} \ No newline at end of file diff --git a/src/Options/TransferRecipient/ReadAllOptions.php b/src/Options/TransferRecipient/ReadAllOptions.php new file mode 100644 index 0000000..d4b82f7 --- /dev/null +++ b/src/Options/TransferRecipient/ReadAllOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing transfer recipients'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing transfer recipients'); + } +} \ No newline at end of file diff --git a/src/Options/VirtualTerminal/AssignDestinationOptions.php b/src/Options/VirtualTerminal/AssignDestinationOptions.php new file mode 100644 index 0000000..7d664a2 --- /dev/null +++ b/src/Options/VirtualTerminal/AssignDestinationOptions.php @@ -0,0 +1,24 @@ +define('destination') + ->required() + ->allowedTypes('string') + ->info('WhatsApp number to assign as destination'); + } +} \ No newline at end of file diff --git a/src/Options/VirtualTerminal/CreateOptions.php b/src/Options/VirtualTerminal/CreateOptions.php new file mode 100644 index 0000000..d0794ee --- /dev/null +++ b/src/Options/VirtualTerminal/CreateOptions.php @@ -0,0 +1,36 @@ +define('name') + ->required() + ->allowedTypes('string') + ->info('Name of the virtual terminal'); + + $resolver->define('description') + ->allowedTypes('string') + ->info('Description of the virtual terminal'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('Currency for the virtual terminal'); + + $resolver->define('merchant_category_code') + ->allowedTypes('string') + ->info('Merchant category code for the virtual terminal'); + } +} \ No newline at end of file diff --git a/src/Options/VirtualTerminal/ReadAllOptions.php b/src/Options/VirtualTerminal/ReadAllOptions.php new file mode 100644 index 0000000..594670d --- /dev/null +++ b/src/Options/VirtualTerminal/ReadAllOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing virtual terminals'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing virtual terminals'); + } +} \ No newline at end of file diff --git a/src/Options/VirtualTerminal/SplitCodeOptions.php b/src/Options/VirtualTerminal/SplitCodeOptions.php new file mode 100644 index 0000000..0011678 --- /dev/null +++ b/src/Options/VirtualTerminal/SplitCodeOptions.php @@ -0,0 +1,24 @@ +define('split_code') + ->required() + ->allowedTypes('string') + ->info('Split code to add or remove from the virtual terminal'); + } +} \ No newline at end of file diff --git a/src/Options/VirtualTerminal/UnassignDestinationOptions.php b/src/Options/VirtualTerminal/UnassignDestinationOptions.php new file mode 100644 index 0000000..763559f --- /dev/null +++ b/src/Options/VirtualTerminal/UnassignDestinationOptions.php @@ -0,0 +1,24 @@ +define('destination') + ->required() + ->allowedTypes('string') + ->info('WhatsApp number to unassign from destination'); + } +} \ No newline at end of file diff --git a/src/Options/VirtualTerminal/UpdateOptions.php b/src/Options/VirtualTerminal/UpdateOptions.php new file mode 100644 index 0000000..de40326 --- /dev/null +++ b/src/Options/VirtualTerminal/UpdateOptions.php @@ -0,0 +1,35 @@ +define('name') + ->allowedTypes('string') + ->info('Name of the virtual terminal'); + + $resolver->define('description') + ->allowedTypes('string') + ->info('Description of the virtual terminal'); + + $resolver->define('currency') + ->allowedTypes('string') + ->info('Currency for the virtual terminal'); + + $resolver->define('merchant_category_code') + ->allowedTypes('string') + ->info('Merchant category code for the virtual terminal'); + } +} \ No newline at end of file From b1aa7cfefdb4bac8cc2da30dcb98ac91e5b8afee Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 13:23:37 +0100 Subject: [PATCH 02/17] Refactor code structure for improved readability and maintainability --- .gitignore | 1 - composer.json | 6 +- composer.lock | 3425 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 3430 insertions(+), 2 deletions(-) create mode 100644 composer.lock diff --git a/.gitignore b/.gitignore index 536b1d9..5a25a5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /vendor /build .phpunit.result.cache -composer.lock diff --git a/composer.json b/composer.json index 627a63b..61b8fc5 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,10 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": true + } }, "require": { "php": "^8.0", @@ -30,6 +33,7 @@ "psr/http-client": "^1.0.1", "psr/http-client-implementation": "^1.0", "psr/http-factory": "^1.0.1", + "symfony/http-client": "^7.3", "symfony/options-resolver": "^6.2" }, "require-dev": { diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..fc8bc43 --- /dev/null +++ b/composer.lock @@ -0,0 +1,3425 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0b48264d2f88993f742ddcdabfd89b8e", + "packages": [ + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "laminas/laminas-diactoros", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "60c182916b2749480895601649563970f3f12ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.1.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-12T15:31:36+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.2" + }, + "time": "2024-09-24T06:21:48+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "time": "2024-09-23T11:39:58+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.2" + }, + "time": "2024-10-02T11:34:13+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-29T11:18:49+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.4.25", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d28e7e2db8a73e9511df892d36445f61314bbebe", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.4.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-04T17:06:28+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + }, + "time": "2025-10-21T19:32:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-http/curl-client", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/php-http/curl-client.git", + "reference": "f3eb48d266341afec0229a7a37a03521d3646b81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/curl-client/zipball/f3eb48d266341afec0229a7a37a03521d3646b81", + "reference": "f3eb48d266341afec0229a7a37a03521d3646b81", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": "^7.4 || ^8.0", + "php-http/discovery": "^1.6", + "php-http/httplug": "^2.0", + "php-http/message": "^1.2", + "psr/http-client": "^1.0", + "psr/http-factory-implementation": "^1.0", + "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/client-integration-tests": "^3.0", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^7.5 || ^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Curl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Михаил Красильников", + "email": "m.krasilnikov@yandex.ru" + } + ], + "description": "PSR-18 and HTTPlug Async client with cURL", + "homepage": "http://php-http.org", + "keywords": [ + "curl", + "http", + "psr-18" + ], + "support": { + "issues": "https://github.com/php-http/curl-client/issues", + "source": "https://github.com/php-http/curl-client/tree/2.3.3" + }, + "time": "2024-10-31T07:36:58+00:00" + }, + { + "name": "php-http/mock-client", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/mock-client.git", + "reference": "81f558234421f7da58ed015604a03808996017d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/mock-client/zipball/81f558234421f7da58ed015604a03808996017d0", + "reference": "81f558234421f7da58ed015604a03808996017d0", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/client-common": "^2.0", + "php-http/discovery": "^1.16", + "php-http/httplug": "^2.0", + "psr/http-client": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/polyfill-php80": "^1.17" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Mock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David de Boer", + "email": "david@ddeboer.nl" + } + ], + "description": "Mock HTTP client", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http", + "mock", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/mock-client/issues", + "source": "https://github.com/php-http/mock-client/tree/1.6.1" + }, + "time": "2024-10-31T10:30:18+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.29", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.9", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:29:11+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:51:50+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfae1497a2f1eaad78dbc0590311c599c7178d4a", + "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-25T15:37:27+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.0" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} From 8f6a5d54b9f71bda309c5d0271e8d11440d939d1 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 13:56:28 +0100 Subject: [PATCH 03/17] Refactor tests for PaymentRequest, Plan, Subaccount, Transaction, and Transfer - Updated PaymentRequestTest to use a mock client and improved test methods. - Added comprehensive tests for Plan including create, list, fetch, and update functionalities. - Created SubaccountTest with tests for creating, listing, fetching, and updating subaccounts. - Implemented TransactionTest covering initialization, verification, listing, fetching, charging authorization, checking authorization, and retrieving transaction timeline and totals. - Introduced TransferTest with tests for initiating, finalizing, bulk transferring, listing, fetching, and verifying transfers. --- src/ClientBuilder.php | 6 +- src/Options/Transfer/InitiateOptions.php | 2 +- tests/CustomerTest.php | 337 ++++++++++++++++++++++ tests/InvoiceTest.php | 304 ++++++++++++++++++++ tests/PaymentRequestTest.php | 36 +-- tests/PlanTest.php | 184 +++++++++++- tests/SubaccountTest.php | 199 +++++++++++++ tests/TransactionTest.php | 343 +++++++++++++++++++++++ tests/TransferTest.php | 296 +++++++++++++++++++ 9 files changed, 1673 insertions(+), 34 deletions(-) create mode 100644 tests/CustomerTest.php create mode 100644 tests/InvoiceTest.php create mode 100644 tests/SubaccountTest.php create mode 100644 tests/TransactionTest.php create mode 100644 tests/TransferTest.php diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index 0ecf927..d2dd3e5 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -23,9 +23,9 @@ class ClientBuilder private array $plugins = []; public function __construct( - ClientInterface $httpClient = null, - RequestFactoryInterface $requestFactoryInterface = null, - StreamFactoryInterface $streamFactoryInterface = null + ?ClientInterface $httpClient = null, + ?RequestFactoryInterface $requestFactoryInterface = null, + ?StreamFactoryInterface $streamFactoryInterface = null ) { $this->httpClient = $httpClient ?: HttpClientDiscovery::find(); $this->requestFactoryInterface = $requestFactoryInterface ?: Psr17FactoryDiscovery::findRequestFactory(); diff --git a/src/Options/Transfer/InitiateOptions.php b/src/Options/Transfer/InitiateOptions.php index bff7429..6e45f4f 100644 --- a/src/Options/Transfer/InitiateOptions.php +++ b/src/Options/Transfer/InitiateOptions.php @@ -19,7 +19,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->define('source') ->required() ->allowedTypes('string') - ->allowedValues(['balance']) + ->allowedValues('balance') ->info('Where should we transfer from? Only balance for now'); $resolver->define('amount') diff --git a/tests/CustomerTest.php b/tests/CustomerTest.php new file mode 100644 index 0000000..917a4e1 --- /dev/null +++ b/tests/CustomerTest.php @@ -0,0 +1,337 @@ + true, + 'message' => 'Customer created', + 'data' => [ + 'email' => 'customer@email.com', + 'integration' => 463433, + 'domain' => 'test', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx', + 'id' => 1173, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '08012345678' + ] + ]; + + $expectedBody = json_encode([ + 'email' => 'customer@email.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '08012345678' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->create([ + 'email' => 'customer@email.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '08012345678' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/customer', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListCustomers(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Customers retrieved', + 'data' => [ + [ + 'id' => 1173, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'customer@email.com', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx', + 'phone' => '08012345678', + 'metadata' => null, + 'risk_action' => 'default' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/customer', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchCustomer(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Customer retrieved', + 'data' => [ + 'id' => 1173, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'customer@email.com', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx', + 'phone' => '08012345678', + 'metadata' => null, + 'risk_action' => 'default', + 'authorizations' => [] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->find('CUS_xnxdt6s1zg5f4tx'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/customer/CUS_xnxdt6s1zg5f4tx', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchCustomerByEmail(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Customer retrieved', + 'data' => [ + 'id' => 1173, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'customer@email.com', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx', + 'phone' => '08012345678', + 'metadata' => null, + 'risk_action' => 'default', + 'authorizations' => [] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->find('customer@email.com'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/customer/customer@email.com', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateCustomer(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Customer updated', + 'data' => [ + 'id' => 1173, + 'first_name' => 'Jane', + 'last_name' => 'Doe', + 'email' => 'customer@email.com', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx', + 'phone' => '08087654321', + 'metadata' => null, + 'risk_action' => 'default' + ] + ]; + + $expectedBody = json_encode([ + 'first_name' => 'Jane', + 'last_name' => 'Doe', + 'phone' => '08087654321' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->update('CUS_xnxdt6s1zg5f4tx', [ + 'first_name' => 'Jane', + 'last_name' => 'Doe', + 'phone' => '08087654321' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/customer/CUS_xnxdt6s1zg5f4tx', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testValidateCustomer(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Customer Identification in progress', + 'data' => [ + 'country' => 'NG', + 'type' => 'bvn', + 'value' => '***********', + 'verified' => false + ] + ]; + + $expectedBody = json_encode([ + 'country' => 'NG', + 'type' => 'bank_account', + 'value' => '12345678901', + 'account_number' => '1234567890', + 'bvn' => '12345678901', + 'bank_code' => '058', + 'first_name' => 'John', + 'last_name' => 'Doe' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->validate('CUS_xnxdt6s1zg5f4tx', [ + 'country' => 'NG', + 'type' => 'bank_account', + 'value' => '12345678901', + 'account_number' => '1234567890', + 'bvn' => '12345678901', + 'bank_code' => '058', + 'first_name' => 'John', + 'last_name' => 'Doe' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/customer/CUS_xnxdt6s1zg5f4tx/identification', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testSetRiskAction(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Customer updated', + 'data' => [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'customer@email.com', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx', + 'phone' => '08012345678', + 'risk_action' => 'allow' + ] + ]; + + $expectedBody = json_encode([ + 'code' => 'CUS_xnxdt6s1zg5f4tx', + 'risk_action' => 'allow' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->setRiskAction('CUS_xnxdt6s1zg5f4tx', 'allow'); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/customer/set_risk_action', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testDeactivateAuthorization(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Authorization has been deactivated' + ]; + + $expectedBody = json_encode([ + 'authorization_code' => 'AUTH_6tmt288t0o' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->customers->deactivateAuthorization('AUTH_6tmt288t0o'); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/customer/deactivate_authorization', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/InvoiceTest.php b/tests/InvoiceTest.php new file mode 100644 index 0000000..bd6a039 --- /dev/null +++ b/tests/InvoiceTest.php @@ -0,0 +1,304 @@ + true, + 'message' => 'Payment request created', + 'data' => [ + 'id' => 3136406, + 'domain' => 'test', + 'amount' => 42000, + 'currency' => 'NGN', + 'due_date' => '2020-07-08T00:00:00.000Z', + 'has_invoice' => true, + 'invoice_number' => 1, + 'description' => 'a test invoice', + 'request_code' => 'PRQ_1weqqsn2wwzgft8', + 'status' => 'pending', + 'paid' => false, + ] + ]; + + $dateTime = new \DateTime('2020-07-08'); + $expectedBody = json_encode([ + 'description' => 'a test invoice', + 'amount' => 42000, + 'line_items' => [ + ['name' => 'item 1', 'amount' => 20000], + ['name' => 'item 2', 'amount' => 20000] + ], + 'tax' => [ + ['name' => 'VAT', 'amount' => 2000] + ], + 'customer' => 'CUS_xwaj0txjryg393b', + 'due_date' => $dateTime + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->create([ + 'description' => 'a test invoice', + 'amount' => 42000, + 'line_items' => [ + ['name' => 'item 1', 'amount' => 20000], + ['name' => 'item 2', 'amount' => 20000] + ], + 'tax' => [ + ['name' => 'VAT', 'amount' => 2000] + ], + 'customer' => 'CUS_xwaj0txjryg393b', + 'due_date' => $dateTime + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListInvoices(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Payment requests retrieved', + 'data' => [ + [ + 'id' => 3136406, + 'domain' => 'test', + 'amount' => 42000, + 'currency' => 'NGN', + 'request_code' => 'PRQ_1weqqsn2wwzgft8', + 'status' => 'pending', + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchInvoice(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Payment request retrieved', + 'data' => [ + 'id' => 3136406, + 'domain' => 'test', + 'amount' => 42000, + 'currency' => 'NGN', + 'request_code' => 'PRQ_1weqqsn2wwzgft8', + 'status' => 'pending', + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->find('PRQ_1weqqsn2wwzgft8'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest/PRQ_1weqqsn2wwzgft8', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testVerifyInvoice(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Payment request retrieved', + 'data' => [ + 'id' => 3136406, + 'domain' => 'test', + 'amount' => 42000, + 'currency' => 'NGN', + 'request_code' => 'PRQ_1weqqsn2wwzgft8', + 'status' => 'success', + 'paid' => true, + 'transactions' => [ + [ + 'id' => 2009945086, + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 42000 + ] + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->verify('PRQ_1weqqsn2wwzgft8'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest/verify/PRQ_1weqqsn2wwzgft8', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testNotifyInvoice(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Notification sent' + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->notify('PRQ_1weqqsn2wwzgft8'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest/notify/PRQ_1weqqsn2wwzgft8', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFinalizeInvoice(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Payment request finalized', + 'data' => [ + 'id' => 3136406, + 'request_code' => 'PRQ_1weqqsn2wwzgft8', + 'status' => 'pending' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->finalize('PRQ_1weqqsn2wwzgft8'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest/finalize/PRQ_1weqqsn2wwzgft8', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testArchiveInvoice(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Payment request archived' + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->archive('PRQ_1weqqsn2wwzgft8'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest/archive/PRQ_1weqqsn2wwzgft8', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testInvoiceStats(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Payment request totals', + 'data' => [ + 'pending' => [ + 'count' => 5, + 'amount' => 500000 + ], + 'successful' => [ + 'count' => 3, + 'amount' => 300000 + ], + 'total' => [ + 'count' => 8, + 'amount' => 800000 + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->invoices->stats(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/paymentrequest/totals', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/PaymentRequestTest.php b/tests/PaymentRequestTest.php index da5dd64..552fb6a 100644 --- a/tests/PaymentRequestTest.php +++ b/tests/PaymentRequestTest.php @@ -2,27 +2,11 @@ namespace StarfolkSoftware\Paystack\Tests; -use PHPUnit\Framework\TestCase as BaseTestCase; -use StarfolkSoftware\Paystack\Client; -use Http\Mock\Client as MockClient; use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; -class PaymentRequestTest extends BaseTestCase +class PaymentRequestTest extends TestCase { - private Client $client; - private MockClient $httpClient; - - protected function setUp(): void - { - $this->httpClient = new MockClient(); - - $this->client = new Client([ - 'secretKey' => 'secret', - 'clientBuilder' => new \StarfolkSoftware\Paystack\ClientBuilder($this->httpClient), - ]); - } - public function testCreatePaymentRequest(): void { $responseData = [ @@ -62,9 +46,9 @@ public function testCreatePaymentRequest(): void $response = new Response($stream, 200, ['Content-Type' => 'application/json']); - $this->httpClient->addResponse($response); + $this->mockClient->addResponse($response); - $data = $this->client->paymentRequests->create([ + $data = $this->client()->paymentRequests->create([ 'description' => 'a test invoice', 'line_items' => [ ['name' => 'item 1', 'amount' => 20000], @@ -77,7 +61,7 @@ public function testCreatePaymentRequest(): void 'due_date' => '2020-07-08' ]); - $sentRequest = $this->httpClient->getLastRequest(); + $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -117,11 +101,11 @@ public function testListPaymentRequests(): void $response = new Response($stream, 200, ['Content-Type' => 'application/json']); - $this->httpClient->addResponse($response); + $this->mockClient->addResponse($response); - $data = $this->client->paymentRequests->all(['page' => 1, 'perPage' => 50]); + $data = $this->client()->paymentRequests->all(['page' => 1, 'perPage' => 50]); - $sentRequest = $this->httpClient->getLastRequest(); + $sentRequest = $this->mockClient->getLastRequest(); $this->assertEquals('GET', $sentRequest->getMethod()); $this->assertEquals('/paymentrequest', $sentRequest->getUri()->getPath()); @@ -149,11 +133,11 @@ public function testFetchPaymentRequest(): void $response = new Response($stream, 200, ['Content-Type' => 'application/json']); - $this->httpClient->addResponse($response); + $this->mockClient->addResponse($response); - $data = $this->client->paymentRequests->fetch('PRQ_1weqqsn2wwzgft8'); + $data = $this->client()->paymentRequests->fetch('PRQ_1weqqsn2wwzgft8'); - $sentRequest = $this->httpClient->getLastRequest(); + $sentRequest = $this->mockClient->getLastRequest(); $this->assertEquals('GET', $sentRequest->getMethod()); $this->assertEquals('/paymentrequest/PRQ_1weqqsn2wwzgft8', $sentRequest->getUri()->getPath()); diff --git a/tests/PlanTest.php b/tests/PlanTest.php index 6c063b7..d55d8ec 100644 --- a/tests/PlanTest.php +++ b/tests/PlanTest.php @@ -1,13 +1,189 @@ -assertEquals('ok', 'ok'); + $responseData = [ + 'status' => true, + 'message' => 'Plan created', + 'data' => [ + 'name' => 'Monthly retainer', + 'description' => 'Monthly retainer subscription', + 'amount' => 500000, + 'interval' => 'monthly', + 'integration' => 463433, + 'domain' => 'test', + 'plan_code' => 'PLN_gx2wn530m0i3w3m', + 'send_invoices' => true, + 'send_sms' => true, + 'currency' => 'NGN', + 'id' => 28, + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Monthly retainer', + 'amount' => 500000, + 'interval' => 'monthly', + 'description' => 'Monthly retainer subscription', + 'send_invoices' => true, + 'send_sms' => true, + 'currency' => 'NGN' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->plans->create([ + 'name' => 'Monthly retainer', + 'amount' => 500000, + 'interval' => 'monthly', + 'description' => 'Monthly retainer subscription', + 'send_invoices' => true, + 'send_sms' => true, + 'currency' => 'NGN' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/plan', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListPlans(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Plans retrieved', + 'data' => [ + [ + 'id' => 28, + 'name' => 'Monthly retainer', + 'description' => 'Monthly retainer subscription', + 'amount' => 500000, + 'interval' => 'monthly', + 'plan_code' => 'PLN_gx2wn530m0i3w3m', + 'currency' => 'NGN', + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->plans->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/plan', $sentRequest->getUri()->getPath()); + // Query parameters might be empty if not properly handled by mock client + $this->assertEquals($responseData, $data); + } + + public function testFetchPlan(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Plan retrieved', + 'data' => [ + 'id' => 28, + 'name' => 'Monthly retainer', + 'description' => 'Monthly retainer subscription', + 'amount' => 500000, + 'interval' => 'monthly', + 'plan_code' => 'PLN_gx2wn530m0i3w3m', + 'currency' => 'NGN', + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->plans->find('PLN_gx2wn530m0i3w3m'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/plan/PLN_gx2wn530m0i3w3m', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdatePlan(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Plan updated', + 'data' => [ + 'id' => 28, + 'name' => 'Updated Monthly retainer', + 'description' => 'Updated Monthly retainer subscription', + 'amount' => 500000, + 'interval' => 'monthly', + 'plan_code' => 'PLN_gx2wn530m0i3w3m', + 'currency' => 'NGN', + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Updated Monthly retainer', + 'amount' => 500000, + 'interval' => 'monthly', + 'description' => 'Updated Monthly retainer subscription' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->plans->update('PLN_gx2wn530m0i3w3m', [ + 'name' => 'Updated Monthly retainer', + 'amount' => 500000, + 'interval' => 'monthly', + 'description' => 'Updated Monthly retainer subscription' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/plan/PLN_gx2wn530m0i3w3m', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); } } \ No newline at end of file diff --git a/tests/SubaccountTest.php b/tests/SubaccountTest.php new file mode 100644 index 0000000..52fe7b4 --- /dev/null +++ b/tests/SubaccountTest.php @@ -0,0 +1,199 @@ + true, + 'message' => 'Subaccount created', + 'data' => [ + 'business_name' => 'Sunshine Stores', + 'account_number' => '0123456789', + 'percentage_charge' => 18.2, + 'description' => 'A store for sunshine', + 'primary_contact_email' => 'store@sunnystores.com', + 'primary_contact_name' => 'Store Owner', + 'primary_contact_phone' => '+234234234234', + 'metadata' => ['ref' => 'ref'], + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj', + 'is_verified' => false, + 'bank' => [ + 'id' => 9, + 'name' => 'First City Monument Bank', + 'slug' => 'first-city-monument-bank' + ] + ] + ]; + + $expectedBody = json_encode([ + 'business_name' => 'Sunshine Stores', + 'settlement_bank' => '044', + 'account_number' => '0123456789', + 'percentage_charge' => 18.2, + 'description' => 'A store for sunshine' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subaccounts->create([ + 'business_name' => 'Sunshine Stores', + 'settlement_bank' => '044', + 'account_number' => '0123456789', + 'percentage_charge' => 18.2, + 'description' => 'A store for sunshine' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/subaccount', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListSubaccounts(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subaccounts retrieved', + 'data' => [ + [ + 'id' => 37, + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj', + 'business_name' => 'Sunshine Stores', + 'description' => 'A store for sunshine', + 'primary_contact_name' => 'Store Owner', + 'primary_contact_email' => 'store@sunnystores.com', + 'primary_contact_phone' => '+234234234234', + 'percentage_charge' => 18.2, + 'is_verified' => false, + 'settlement_bank' => 'First City Monument Bank', + 'account_number' => '0123456789' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subaccounts->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/subaccount', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchSubaccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subaccount retrieved', + 'data' => [ + 'id' => 37, + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj', + 'business_name' => 'Sunshine Stores', + 'description' => 'A store for sunshine', + 'primary_contact_name' => 'Store Owner', + 'primary_contact_email' => 'store@sunnystores.com', + 'primary_contact_phone' => '+234234234234', + 'percentage_charge' => 18.2, + 'is_verified' => false, + 'settlement_bank' => 'First City Monument Bank', + 'account_number' => '0123456789', + 'metadata' => ['ref' => 'ref'] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subaccounts->find('ACCT_8f4s1eq7ml6rlzj'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/subaccount/ACCT_8f4s1eq7ml6rlzj', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateSubaccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subaccount updated', + 'data' => [ + 'id' => 37, + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj', + 'business_name' => 'Updated Sunshine Stores', + 'description' => 'An updated store for sunshine', + 'primary_contact_name' => 'Store Owner', + 'primary_contact_email' => 'store@sunnystores.com', + 'primary_contact_phone' => '+234234234234', + 'percentage_charge' => 20.0, + 'is_verified' => false, + 'settlement_bank' => 'First City Monument Bank', + 'account_number' => '0123456789' + ] + ]; + + $expectedBody = json_encode([ + 'business_name' => 'Updated Sunshine Stores', + 'description' => 'An updated store for sunshine', + 'percentage_charge' => 20.0 + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subaccounts->update('ACCT_8f4s1eq7ml6rlzj', [ + 'business_name' => 'Updated Sunshine Stores', + 'description' => 'An updated store for sunshine', + 'percentage_charge' => 20.0 + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/subaccount/ACCT_8f4s1eq7ml6rlzj', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php new file mode 100644 index 0000000..a74bbb4 --- /dev/null +++ b/tests/TransactionTest.php @@ -0,0 +1,343 @@ + true, + 'message' => 'Authorization URL created', + 'data' => [ + 'authorization_url' => 'https://checkout.paystack.com/0peioxfhpn', + 'access_code' => '0peioxfhpn', + 'reference' => 'T563902343_1628168464' + ] + ]; + + $expectedBody = json_encode([ + 'email' => 'customer@email.com', + 'amount' => '10000', + 'reference' => 'T563902343_1628168464' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->initialize([ + 'email' => 'customer@email.com', + 'amount' => '10000', + 'reference' => 'T563902343_1628168464' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transaction/initialize', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testVerifyTransaction(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Verification successful', + 'data' => [ + 'id' => 2009945086, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card', + 'fees' => 150, + 'authorization' => [ + 'authorization_code' => 'AUTH_6tmt288t0o', + 'bin' => '408408', + 'last4' => '4081', + 'exp_month' => '12', + 'exp_year' => '2030', + 'channel' => 'card', + 'card_type' => 'visa', + 'bank' => 'TEST BANK', + 'country_code' => 'NG', + 'brand' => 'visa', + 'reusable' => true, + 'signature' => 'SIG_MRn7q9jJeqEWV3fD25vD' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->verify('T563902343_1628168464'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transaction/verify/T563902343_1628168464', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListTransactions(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Transactions retrieved', + 'data' => [ + [ + 'id' => 2009945086, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transaction', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchTransaction(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Transaction retrieved', + 'data' => [ + 'id' => 2009945086, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->find('2009945086'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transaction/2009945086', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testChargeAuthorization(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Charge attempted', + 'data' => [ + 'id' => 2009945086, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card', + 'authorization' => [ + 'authorization_code' => 'AUTH_6tmt288t0o' + ] + ] + ]; + + $expectedBody = json_encode([ + 'authorization_code' => 'AUTH_6tmt288t0o', + 'email' => 'customer@email.com', + 'amount' => '10000' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->charge([ + 'authorization_code' => 'AUTH_6tmt288t0o', + 'email' => 'customer@email.com', + 'amount' => '10000' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transaction/charge_authorization', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testCheckAuthorization(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Authorization details retrieved', + 'data' => [ + 'amount' => 10000, + 'currency' => 'NGN', + 'transaction_date' => '2024-10-30T14:55:19.000Z', + 'status' => 'success' + ] + ]; + + $expectedBody = json_encode([ + 'amount' => '10000', + 'email' => 'customer@email.com', + 'authorization_code' => 'AUTH_6tmt288t0o', + 'currency' => 'NGN' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->checkAuthorization( + '10000', + 'customer@email.com', + 'AUTH_6tmt288t0o', + 'NGN' + ); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transaction/check_authorization', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testGetTransactionTimeline(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Timeline retrieved', + 'data' => [ + 'time_spent' => 5, + 'attempts' => 1, + 'authentication' => 'pin', + 'errors' => 0, + 'success' => true, + 'mobile' => false, + 'input' => [], + 'channel' => 'card', + 'history' => [ + [ + 'type' => 'action', + 'message' => 'Attempted to pay', + 'time' => 7 + ] + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->timeline('2009945086'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transaction/timeline/2009945086', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testGetTransactionTotals(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Transaction totals', + 'data' => [ + 'total_transactions' => 1, + 'unique_customers' => 1, + 'total_volume' => 10000, + 'total_volume_by_currency' => [ + [ + 'currency' => 'NGN', + 'amount' => 10000 + ] + ], + 'pending_transfers' => 0, + 'pending_transfers_by_currency' => [] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transactions->stats(['page' => 1]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transaction/totals', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/TransferTest.php b/tests/TransferTest.php new file mode 100644 index 0000000..2efe199 --- /dev/null +++ b/tests/TransferTest.php @@ -0,0 +1,296 @@ + true, + 'message' => 'Transfer requires OTP to continue', + 'data' => [ + 'integration' => 463433, + 'domain' => 'test', + 'amount' => 3794800, + 'currency' => 'NGN', + 'source' => 'balance', + 'reason' => 'Calm down', + 'recipient' => 1943003, + 'status' => 'otp', + 'transfer_code' => 'TRF_vsyqdmlzble3uii', + 'id' => 14956454 + ] + ]; + + $expectedBody = json_encode([ + 'source' => 'balance', + 'amount' => 3794800, + 'recipient' => 'RCP_gx2wn530m0i3w3m', + 'reason' => 'Calm down' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transfers->initiate([ + 'source' => 'balance', + 'amount' => 3794800, + 'recipient' => 'RCP_gx2wn530m0i3w3m', + 'reason' => 'Calm down' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transfer', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testFinalizeTransfer(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Transfer has been queued', + 'data' => [ + 'domain' => 'test', + 'amount' => 3794800, + 'currency' => 'NGN', + 'reference' => '1jhbs3ozmen0k7y5q2', + 'source' => 'balance', + 'reason' => 'Calm down', + 'status' => 'success', + 'transfer_code' => 'TRF_vsyqdmlzble3uii', + 'id' => 14956454 + ] + ]; + + $expectedBody = json_encode([ + 'transfer_code' => 'TRF_vsyqdmlzble3uii', + 'otp' => '928783' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transfers->finalize([ + 'transfer_code' => 'TRF_vsyqdmlzble3uii', + 'otp' => '928783' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transfer/finalize_transfer', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testBulkTransfer(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Bulk transfer initiated', + 'data' => [ + [ + 'amount' => 100000, + 'reference' => 'ref_001', + 'status' => 'success', + 'transfer_code' => 'TRF_1', + 'recipient' => 1943003 + ], + [ + 'amount' => 200000, + 'reference' => 'ref_002', + 'status' => 'success', + 'transfer_code' => 'TRF_2', + 'recipient' => 1943004 + ] + ] + ]; + + $expectedBody = json_encode([ + 'source' => 'balance', + 'transfers' => [ + [ + 'amount' => 100000, + 'reference' => 'ref_001', + 'reason' => 'Payment 1', + 'recipient' => 'RCP_gx2wn530m0i3w3m' + ], + [ + 'amount' => 200000, + 'reference' => 'ref_002', + 'reason' => 'Payment 2', + 'recipient' => 'RCP_gx2wn530m0i3w3n' + ] + ] + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transfers->bulk([ + 'source' => 'balance', + 'transfers' => [ + [ + 'amount' => 100000, + 'reference' => 'ref_001', + 'reason' => 'Payment 1', + 'recipient' => 'RCP_gx2wn530m0i3w3m' + ], + [ + 'amount' => 200000, + 'reference' => 'ref_002', + 'reason' => 'Payment 2', + 'recipient' => 'RCP_gx2wn530m0i3w3n' + ] + ] + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transfer/bulk', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListTransfers(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Transfers retrieved', + 'data' => [ + [ + 'id' => 14956454, + 'amount' => 3794800, + 'currency' => 'NGN', + 'reference' => '1jhbs3ozmen0k7y5q2', + 'source' => 'balance', + 'reason' => 'Calm down', + 'status' => 'success', + 'transfer_code' => 'TRF_vsyqdmlzble3uii' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transfers->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transfer', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchTransfer(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Transfer retrieved', + 'data' => [ + 'id' => 14956454, + 'amount' => 3794800, + 'currency' => 'NGN', + 'reference' => '1jhbs3ozmen0k7y5q2', + 'source' => 'balance', + 'reason' => 'Calm down', + 'status' => 'success', + 'transfer_code' => 'TRF_vsyqdmlzble3uii', + 'recipient' => [ + 'id' => 1943003, + 'name' => 'ABDUL-HALEEM ISHAQ', + 'email' => 'ish@gmail.com' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transfers->find('TRF_vsyqdmlzble3uii'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transfer/TRF_vsyqdmlzble3uii', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testVerifyTransfer(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Transfer retrieved', + 'data' => [ + 'id' => 14956454, + 'amount' => 3794800, + 'currency' => 'NGN', + 'reference' => '1jhbs3ozmen0k7y5q2', + 'source' => 'balance', + 'reason' => 'Calm down', + 'status' => 'success', + 'transfer_code' => 'TRF_vsyqdmlzble3uii' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transfers->verify('1jhbs3ozmen0k7y5q2'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transfer/verify/1jhbs3ozmen0k7y5q2', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file From 9a96ddb9d37e442f67281932d1030b9227dbae5d Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 14:18:36 +0100 Subject: [PATCH 04/17] Add tests for Settlement, Split, Subscription, and Transfer Recipient functionalities - Implement SettlementTest with tests for listing settlements and retrieving settlement transactions. - Implement SplitTest with tests for creating, listing, fetching, updating splits, and managing subaccounts. - Implement SubscriptionTest with tests for creating, listing, fetching, enabling, disabling subscriptions, and generating update links. - Implement TransferRecipientTest with tests for creating, bulk creating, listing, fetching, updating, and deleting transfer recipients. --- src/API/Settlement.php | 25 +- .../Settlement/TransactionsOptions.php | 35 ++ src/Options/Split/CreateOptions.php | 4 +- tests/BulkChargeTest.php | 281 +++++++++++++++ tests/ChargeTest.php | 303 +++++++++++++++++ tests/DisputeTest.php | 319 ++++++++++++++++++ tests/PageTest.php | 262 ++++++++++++++ tests/ProductTest.php | 208 ++++++++++++ tests/RefundTest.php | 171 ++++++++++ tests/SettlementTest.php | 195 +++++++++++ tests/SplitTest.php | 315 +++++++++++++++++ tests/SubscriptionTest.php | 279 +++++++++++++++ tests/TransferRecipientTest.php | 300 ++++++++++++++++ 13 files changed, 2689 insertions(+), 8 deletions(-) create mode 100644 src/Options/Settlement/TransactionsOptions.php create mode 100644 tests/BulkChargeTest.php create mode 100644 tests/ChargeTest.php create mode 100644 tests/DisputeTest.php create mode 100644 tests/PageTest.php create mode 100644 tests/ProductTest.php create mode 100644 tests/RefundTest.php create mode 100644 tests/SettlementTest.php create mode 100644 tests/SplitTest.php create mode 100644 tests/SubscriptionTest.php create mode 100644 tests/TransferRecipientTest.php diff --git a/src/API/Settlement.php b/src/API/Settlement.php index b626516..ced0f5d 100644 --- a/src/API/Settlement.php +++ b/src/API/Settlement.php @@ -4,6 +4,7 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\Settlement as SettlementOptions; class Settlement extends ApiAbstract { @@ -15,9 +16,15 @@ class Settlement extends ApiAbstract */ public function all(array $params = []): array { - $response = $this->httpClient->get('/settlement', [ - 'query' => $params - ]); + $options = new SettlementOptions\ReadAllOptions($params); + $query = $options->all(); + + $requestOptions = []; + if (!empty($query)) { + $requestOptions['query'] = $query; + } + + $response = $this->httpClient->get('/settlement', $requestOptions); return ResponseMediator::getContent($response); } @@ -31,9 +38,15 @@ public function all(array $params = []): array */ public function getTransactions(string $id, array $params = []): array { - $response = $this->httpClient->get("/settlement/{$id}/transactions", [ - 'query' => $params - ]); + $options = new SettlementOptions\TransactionsOptions($params); + $query = $options->all(); + + $requestOptions = []; + if (!empty($query)) { + $requestOptions['query'] = $query; + } + + $response = $this->httpClient->get("/settlement/{$id}/transactions", $requestOptions); return ResponseMediator::getContent($response); } diff --git a/src/Options/Settlement/TransactionsOptions.php b/src/Options/Settlement/TransactionsOptions.php new file mode 100644 index 0000000..f46960f --- /dev/null +++ b/src/Options/Settlement/TransactionsOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info('Number of records to fetch per page'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('The section to retrieve'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing transactions'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing transactions'); + } +} \ No newline at end of file diff --git a/src/Options/Split/CreateOptions.php b/src/Options/Split/CreateOptions.php index 4d66fe5..a1b1780 100644 --- a/src/Options/Split/CreateOptions.php +++ b/src/Options/Split/CreateOptions.php @@ -24,7 +24,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->define('type') ->required() ->allowedTypes('string') - ->allowedValues(['percentage', 'flat']) + ->allowedValues('percentage', 'flat') ->info('The type of transaction split you want to create. You can use one of the following: percentage | flat'); $resolver->define('currency') @@ -39,7 +39,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->define('bearer_type') ->allowedTypes('string') - ->allowedValues(['subaccount', 'account', 'all-proportional', 'all']) + ->allowedValues('subaccount', 'account', 'all-proportional', 'all') ->info('Any of subaccount | account | all-proportional | all'); $resolver->define('bearer_subaccount') diff --git a/tests/BulkChargeTest.php b/tests/BulkChargeTest.php new file mode 100644 index 0000000..d488c5d --- /dev/null +++ b/tests/BulkChargeTest.php @@ -0,0 +1,281 @@ + true, + 'message' => 'Bulk charges initiated', + 'data' => [ + 'batch_code' => 'BCH_abc123def456', + 'reference' => 'bulkcharge_abc123', + 'total_charges' => 2, + 'pending_charges' => 2, + 'status' => 'pending', + 'id' => 789, + 'integration' => 463433, + 'domain' => 'test', + 'batch_limit' => 200, + 'current_batch' => 1, + 'created_at' => '2023-11-16T11:00:00.000Z', + 'updated_at' => '2023-11-16T11:00:00.000Z' + ] + ]; + + $expectedBody = json_encode([ + [ + 'authorization' => 'AUTH_6tmt288t0o', + 'amount' => 50000, + 'reference' => 'bulk_ref_001' + ], + [ + 'authorization' => 'AUTH_abc123def456', + 'amount' => 75000, + 'reference' => 'bulk_ref_002' + ] + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->bulkCharges->initiate([ + [ + 'authorization' => 'AUTH_6tmt288t0o', + 'amount' => 50000, + 'reference' => 'bulk_ref_001' + ], + [ + 'authorization' => 'AUTH_abc123def456', + 'amount' => 75000, + 'reference' => 'bulk_ref_002' + ] + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/bulkcharge', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListBulkCharges(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Bulk charges retrieved', + 'data' => [ + [ + 'batch_code' => 'BCH_abc123def456', + 'reference' => 'bulkcharge_abc123', + 'total_charges' => 2, + 'pending_charges' => 0, + 'status' => 'complete', + 'id' => 789, + 'created_at' => '2023-11-16T11:00:00.000Z', + 'updated_at' => '2023-11-16T11:05:00.000Z' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->bulkCharges->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bulkcharge', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchBulkCharge(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Bulk charge retrieved', + 'data' => [ + 'batch_code' => 'BCH_abc123def456', + 'reference' => 'bulkcharge_abc123', + 'total_charges' => 2, + 'pending_charges' => 0, + 'status' => 'complete', + 'id' => 789, + 'integration' => 463433, + 'domain' => 'test', + 'batch_limit' => 200, + 'current_batch' => 1, + 'created_at' => '2023-11-16T11:00:00.000Z', + 'updated_at' => '2023-11-16T11:05:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->bulkCharges->find('BCH_abc123def456'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bulkcharge/BCH_abc123def456', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testGetBulkChargeCharges(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Bulk charge charges retrieved', + 'data' => [ + [ + 'id' => 54321, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'bulk_ref_001', + 'amount' => 50000, + 'currency' => 'NGN', + 'bulk_charge' => 'BCH_abc123def456', + 'customer' => [ + 'id' => 98765, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com' + ], + 'created_at' => '2023-11-16T11:01:00.000Z' + ], + [ + 'id' => 54322, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'bulk_ref_002', + 'amount' => 75000, + 'currency' => 'NGN', + 'bulk_charge' => 'BCH_abc123def456', + 'customer' => [ + 'id' => 98766, + 'first_name' => 'Jane', + 'last_name' => 'Smith', + 'email' => 'jane.smith@example.com' + ], + 'created_at' => '2023-11-16T11:01:30.000Z' + ] + ], + 'meta' => [ + 'total' => 2, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->bulkCharges->getCharges('BCH_abc123def456', ['page' => 1]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bulkcharge/BCH_abc123def456/charges', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testPauseBulkCharge(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Bulk charge batch has been paused', + 'data' => [ + 'batch_code' => 'BCH_abc123def456', + 'status' => 'paused', + 'total_charges' => 100, + 'pending_charges' => 50, + 'processed_charges' => 50 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->bulkCharges->pause('BCH_abc123def456'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bulkcharge/pause/BCH_abc123def456', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testResumeBulkCharge(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Bulk charge batch has been resumed', + 'data' => [ + 'batch_code' => 'BCH_abc123def456', + 'status' => 'active', + 'total_charges' => 100, + 'pending_charges' => 50, + 'processed_charges' => 50 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->bulkCharges->resume('BCH_abc123def456'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bulkcharge/resume/BCH_abc123def456', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/ChargeTest.php b/tests/ChargeTest.php new file mode 100644 index 0000000..b63c7bf --- /dev/null +++ b/tests/ChargeTest.php @@ -0,0 +1,303 @@ + true, + 'message' => 'Charge attempted', + 'data' => [ + 'id' => 2009945086, + 'domain' => 'test', + 'status' => 'send_pin', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card', + 'metadata' => [], + 'display_text' => 'Please enter your 4-digit PIN to continue' + ] + ]; + + $expectedBody = json_encode([ + 'email' => 'customer@email.com', + 'amount' => '10000', + 'card' => [ + 'number' => '4084084084084081', + 'cvv' => '408', + 'expiry_month' => '12', + 'expiry_year' => '2030' + ] + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->charges->create([ + 'email' => 'customer@email.com', + 'amount' => '10000', + 'card' => [ + 'number' => '4084084084084081', + 'cvv' => '408', + 'expiry_month' => '12', + 'expiry_year' => '2030' + ] + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/charge', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testSubmitPin(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Charge attempted', + 'data' => [ + 'id' => 2009945086, + 'status' => 'send_otp', + 'reference' => 'T563902343_1628168464', + 'display_text' => 'Please enter the OTP sent to your mobile number ***-***-4321' + ] + ]; + + $expectedBody = json_encode([ + 'pin' => '1234', + 'reference' => 'T563902343_1628168464' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->charges->submitPin([ + 'pin' => '1234', + 'reference' => 'T563902343_1628168464' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/charge/submit_pin', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testSubmitOtp(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Charge completed', + 'data' => [ + 'id' => 2009945086, + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card' + ] + ]; + + $expectedBody = json_encode([ + 'otp' => '123456', + 'reference' => 'T563902343_1628168464' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->charges->submitOtp([ + 'otp' => '123456', + 'reference' => 'T563902343_1628168464' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/charge/submit_otp', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testSubmitPhone(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Charge attempted', + 'data' => [ + 'id' => 2009945086, + 'status' => 'send_otp', + 'reference' => 'T563902343_1628168464' + ] + ]; + + $expectedBody = json_encode([ + 'phone' => '+2348012345678', + 'reference' => 'T563902343_1628168464' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->charges->submitPhone([ + 'phone' => '+2348012345678', + 'reference' => 'T563902343_1628168464' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/charge/submit_phone', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testSubmitBirthday(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Charge attempted', + 'data' => [ + 'id' => 2009945086, + 'status' => 'success', + 'reference' => 'T563902343_1628168464' + ] + ]; + + $expectedBody = json_encode([ + 'birthday' => '1990-01-01', + 'reference' => 'T563902343_1628168464' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->charges->submitBirthday([ + 'birthday' => '1990-01-01', + 'reference' => 'T563902343_1628168464' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/charge/submit_birthday', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testSubmitAddress(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Charge attempted', + 'data' => [ + 'id' => 2009945086, + 'status' => 'success', + 'reference' => 'T563902343_1628168464' + ] + ]; + + $expectedBody = json_encode([ + 'address' => '123 Main Street', + 'city' => 'Lagos', + 'state' => 'Lagos', + 'zipcode' => '100001', + 'reference' => 'T563902343_1628168464' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->charges->submitAddress([ + 'address' => '123 Main Street', + 'city' => 'Lagos', + 'state' => 'Lagos', + 'zipcode' => '100001', + 'reference' => 'T563902343_1628168464' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/charge/submit_address', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testCheckPendingCharge(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Charge retrieved', + 'data' => [ + 'id' => 2009945086, + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->charges->checkPending('T563902343_1628168464'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/charge/T563902343_1628168464', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/DisputeTest.php b/tests/DisputeTest.php new file mode 100644 index 0000000..526c06f --- /dev/null +++ b/tests/DisputeTest.php @@ -0,0 +1,319 @@ + true, + 'message' => 'Disputes retrieved successfully', + 'data' => [ + [ + 'id' => 827179, + 'refund_amount' => 0, + 'currency' => 'NGN', + 'status' => 'pending', + 'resolution' => null, + 'domain' => 'test', + 'transaction' => [ + 'id' => 54321, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'txn_12345abcde', + 'amount' => 50000, + 'currency' => 'NGN' + ], + 'transaction_reference' => 'txn_12345abcde', + 'category' => 'chargeback', + 'customer' => [ + 'id' => 98765, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com' + ], + 'bin' => '408408', + 'last4' => '4081', + 'dcc' => false, + 'created_at' => '2023-11-15T10:30:00.000Z', + 'updated_at' => '2023-11-15T10:30:00.000Z' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->disputes->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/dispute', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchDispute(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dispute retrieved successfully', + 'data' => [ + 'id' => 827179, + 'refund_amount' => 0, + 'currency' => 'NGN', + 'status' => 'pending', + 'resolution' => null, + 'domain' => 'test', + 'transaction' => [ + 'id' => 54321, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'txn_12345abcde', + 'amount' => 50000, + 'currency' => 'NGN' + ], + 'category' => 'chargeback', + 'customer' => [ + 'id' => 98765, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com' + ], + 'evidence' => [], + 'created_at' => '2023-11-15T10:30:00.000Z', + 'updated_at' => '2023-11-15T10:30:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->disputes->find('827179'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/dispute/827179', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateDispute(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dispute updated successfully', + 'data' => [ + 'id' => 827179, + 'refund_amount' => 25000, + 'currency' => 'NGN', + 'status' => 'pending', + 'resolution' => null, + 'domain' => 'test' + ] + ]; + + $expectedBody = json_encode([ + 'refund_amount' => 25000 + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->disputes->update('827179', [ + 'refund_amount' => 25000 + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/dispute/827179', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testAddEvidence(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Evidence added successfully', + 'data' => [ + 'id' => 827179, + 'status' => 'pending', + 'evidence' => [ + [ + 'delivery_address' => '123 Main Street, Lagos, Nigeria', + 'delivery_date' => '2023-11-10' + ] + ] + ] + ]; + + $expectedBody = json_encode([ + 'customer_email' => 'john.doe@example.com', + 'customer_name' => 'John Doe', + 'customer_phone' => '+2348012345678', + 'delivery_address' => '123 Main Street, Lagos, Nigeria', + 'delivery_date' => '2023-11-10' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->disputes->addEvidence('827179', [ + 'customer_email' => 'john.doe@example.com', + 'customer_name' => 'John Doe', + 'customer_phone' => '+2348012345678', + 'delivery_address' => '123 Main Street, Lagos, Nigeria', + 'delivery_date' => '2023-11-10' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/dispute/827179/evidence', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testGetUploadUrl(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Upload URL generated successfully', + 'data' => [ + 'upload_url' => 'https://files.paystack.co/dispute/827179/upload?signature=abc123def456', + 'upload_filename' => 'evidence_receipt.pdf' + ] + ]; + + $expectedBody = json_encode([ + 'upload_filename' => 'evidence_receipt.pdf' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->disputes->getUploadUrl('827179', [ + 'upload_filename' => 'evidence_receipt.pdf' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/dispute/827179/upload_url', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testResolveDispute(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dispute resolved successfully', + 'data' => [ + 'id' => 827179, + 'status' => 'resolved', + 'resolution' => 'merchant_accepted', + 'resolved_at' => '2023-11-16T14:30:00.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'resolution' => 'merchant_accepted', + 'message' => 'Customer contacted and resolved amicably', + 'refund_amount' => 0, + 'uploaded_filename' => 'evidence_receipt.pdf' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->disputes->resolve('827179', [ + 'resolution' => 'merchant_accepted', + 'message' => 'Customer contacted and resolved amicably', + 'refund_amount' => 0, + 'uploaded_filename' => 'evidence_receipt.pdf' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/dispute/827179/resolve', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testExportDisputes(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Export successful', + 'data' => [ + 'export_id' => 'exp_abc123def456', + 'path' => '/exports/disputes/2023/11/disputes_export_20231116.csv' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->disputes->export(['from' => '2023-11-01', 'to' => '2023-11-16']); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/dispute/export', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/PageTest.php b/tests/PageTest.php new file mode 100644 index 0000000..20eb7fe --- /dev/null +++ b/tests/PageTest.php @@ -0,0 +1,262 @@ + true, + 'message' => 'Page created', + 'data' => [ + 'name' => 'Test Payment Page', + 'description' => 'A test payment page for demonstration', + 'integration' => 463433, + 'domain' => 'test', + 'slug' => 'test-payment-page', + 'currency' => 'NGN', + 'type' => 'donation', + 'collect_phone' => false, + 'active' => true, + 'published' => true, + 'migrate' => false, + 'id' => 12345, + 'created_at' => '2023-11-16T10:30:00.000Z', + 'updated_at' => '2023-11-16T10:30:00.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Test Payment Page', + 'description' => 'A test payment page for demonstration', + 'amount' => 50000, + 'slug' => 'test-payment-page', + 'redirect_url' => 'https://example.com/success' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->pages->create([ + 'name' => 'Test Payment Page', + 'description' => 'A test payment page for demonstration', + 'amount' => 50000, + 'slug' => 'test-payment-page', + 'redirect_url' => 'https://example.com/success' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/page', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListPages(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Pages retrieved', + 'data' => [ + [ + 'id' => 12345, + 'name' => 'Test Payment Page', + 'description' => 'A test payment page for demonstration', + 'slug' => 'test-payment-page', + 'currency' => 'NGN', + 'type' => 'donation', + 'active' => true, + 'published' => true + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->pages->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/page', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchPage(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Page retrieved', + 'data' => [ + 'id' => 12345, + 'name' => 'Test Payment Page', + 'description' => 'A test payment page for demonstration', + 'slug' => 'test-payment-page', + 'currency' => 'NGN', + 'type' => 'donation', + 'collect_phone' => false, + 'active' => true, + 'published' => true, + 'amount' => 50000, + 'redirect_url' => 'https://example.com/success', + 'products' => [], + 'created_at' => '2023-11-16T10:30:00.000Z', + 'updated_at' => '2023-11-16T10:30:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->pages->find('test-payment-page'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/page/test-payment-page', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdatePage(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Page updated', + 'data' => [ + 'id' => 12345, + 'name' => 'Updated Payment Page', + 'description' => 'An updated test payment page', + 'slug' => 'test-payment-page', + 'active' => false + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Updated Payment Page', + 'description' => 'An updated test payment page', + 'active' => false + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->pages->update('12345', [ + 'name' => 'Updated Payment Page', + 'description' => 'An updated test payment page', + 'active' => false + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/page/12345', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testCheckSlugAvailability(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Slug is available', + 'data' => [ + 'available' => true + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->pages->checkSlugAvailability('my-new-page'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/page/check_slug_availability/my-new-page', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testAddProducts(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Products added to page', + 'data' => [ + 'id' => 12345, + 'products' => [ + [ + 'product_id' => 67890, + 'name' => 'Test Product', + 'description' => 'A test product', + 'price' => 25000, + 'currency' => 'NGN' + ] + ] + ] + ]; + + $expectedBody = json_encode([ + 'product' => [67890, 54321] + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->pages->addProducts('12345', [ + 'product' => [67890, 54321] + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/page/12345/product', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/ProductTest.php b/tests/ProductTest.php new file mode 100644 index 0000000..4a26b4e --- /dev/null +++ b/tests/ProductTest.php @@ -0,0 +1,208 @@ + true, + 'message' => 'Product successfully created', + 'data' => [ + 'name' => 'Puff Puff', + 'description' => 'Flour-based ball that is fried', + 'product_code' => 'PROD_wbkemh', + 'price' => 2000, + 'currency' => 'NGN', + 'quantity' => 7, + 'quantity_sold' => null, + 'type' => 'good', + 'image_path' => null, + 'file_path' => null, + 'is_shippable' => false, + 'unlimited' => true, + 'integration' => 463433, + 'domain' => 'test', + 'active' => true, + 'in_stock' => true, + 'id' => 72, + 'created_at' => '2020-06-29T16:06:05.000Z', + 'updated_at' => '2020-06-29T16:06:05.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Puff Puff', + 'description' => 'Flour-based ball that is fried', + 'price' => 2000, + 'currency' => 'NGN', + 'unlimited' => true, + 'quantity' => 7 + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->products->create([ + 'name' => 'Puff Puff', + 'description' => 'Flour-based ball that is fried', + 'price' => 2000, + 'currency' => 'NGN', + 'unlimited' => true, + 'quantity' => 7 + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/product', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListProducts(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Products retrieved', + 'data' => [ + [ + 'id' => 72, + 'name' => 'Puff Puff', + 'description' => 'Flour-based ball that is fried', + 'product_code' => 'PROD_wbkemh', + 'price' => 2000, + 'currency' => 'NGN', + 'quantity' => 7, + 'type' => 'good', + 'active' => true, + 'in_stock' => true + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->products->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/product', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchProduct(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Product retrieved', + 'data' => [ + 'id' => 72, + 'name' => 'Puff Puff', + 'description' => 'Flour-based ball that is fried', + 'product_code' => 'PROD_wbkemh', + 'price' => 2000, + 'currency' => 'NGN', + 'quantity' => 7, + 'quantity_sold' => null, + 'type' => 'good', + 'image_path' => null, + 'file_path' => null, + 'is_shippable' => false, + 'unlimited' => true, + 'active' => true, + 'in_stock' => true + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->products->find('72'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/product/72', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateProduct(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Product successfully updated', + 'data' => [ + 'id' => 72, + 'name' => 'Updated Puff Puff', + 'description' => 'Updated Flour-based ball that is fried', + 'product_code' => 'PROD_wbkemh', + 'price' => 2500, + 'currency' => 'NGN', + 'quantity' => 10, + 'type' => 'good', + 'active' => true, + 'in_stock' => true + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Updated Puff Puff', + 'description' => 'Updated Flour-based ball that is fried', + 'price' => 2500, + 'quantity' => 10 + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->products->update('72', [ + 'name' => 'Updated Puff Puff', + 'description' => 'Updated Flour-based ball that is fried', + 'price' => 2500, + 'quantity' => 10 + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/product/72', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/RefundTest.php b/tests/RefundTest.php new file mode 100644 index 0000000..744e984 --- /dev/null +++ b/tests/RefundTest.php @@ -0,0 +1,171 @@ + true, + 'message' => 'Refund has been queued for processing', + 'data' => [ + 'transaction' => [ + 'id' => 2009945086, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'channel' => 'card' + ], + 'integration' => 463433, + 'deducted_amount' => 0, + 'channel' => null, + 'merchant_note' => 'Defective product', + 'customer_note' => 'Product is defective', + 'status' => 'pending', + 'refunded_by' => 'hello@example.com', + 'expected_at' => '2025-10-30T14:55:19.000Z', + 'currency' => 'NGN', + 'domain' => 'test', + 'amount' => 5000, + 'fully_deducted' => false, + 'id' => 1, + 'created_at' => '2025-10-30T14:55:19.000Z', + 'updated_at' => '2025-10-30T14:55:19.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'transaction' => 'T563902343_1628168464', + 'amount' => 5000, + 'merchant_note' => 'Defective product', + 'customer_note' => 'Product is defective' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->refunds->create([ + 'transaction' => 'T563902343_1628168464', + 'amount' => 5000, + 'merchant_note' => 'Defective product', + 'customer_note' => 'Product is defective' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/refund', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListRefunds(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Refunds retrieved', + 'data' => [ + [ + 'id' => 1, + 'amount' => 5000, + 'currency' => 'NGN', + 'status' => 'processed', + 'merchant_note' => 'Defective product', + 'customer_note' => 'Product is defective', + 'transaction' => [ + 'id' => 2009945086, + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'status' => 'success' + ], + 'created_at' => '2025-10-30T14:55:19.000Z' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->refunds->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/refund', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchRefund(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Refund retrieved', + 'data' => [ + 'id' => 1, + 'amount' => 5000, + 'currency' => 'NGN', + 'status' => 'processed', + 'merchant_note' => 'Defective product', + 'customer_note' => 'Product is defective', + 'transaction' => [ + 'id' => 2009945086, + 'reference' => 'T563902343_1628168464', + 'amount' => 10000, + 'currency' => 'NGN', + 'status' => 'success', + 'channel' => 'card' + ], + 'integration' => 463433, + 'deducted_amount' => 0, + 'channel' => null, + 'refunded_by' => 'hello@example.com', + 'expected_at' => '2025-10-30T14:55:19.000Z', + 'domain' => 'test', + 'fully_deducted' => false, + 'created_at' => '2025-10-30T14:55:19.000Z', + 'updated_at' => '2025-10-30T14:55:19.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->refunds->find('1'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/refund/1', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/SettlementTest.php b/tests/SettlementTest.php new file mode 100644 index 0000000..908f8ea --- /dev/null +++ b/tests/SettlementTest.php @@ -0,0 +1,195 @@ + true, + 'message' => 'Settlements retrieved', + 'data' => [ + [ + 'id' => 123456, + 'domain' => 'test', + 'status' => 'success', + 'currency' => 'NGN', + 'integration' => 463433, + 'total_amount' => 500000, + 'effective_amount' => 485000, + 'total_fees' => 15000, + 'total_processed' => 500000, + 'deductions' => 0, + 'settlement_date' => '2023-11-15T00:00:00.000Z', + 'settled_by' => 'Auto Settlement' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->settlements->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/settlement', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListSettlementsWithNoParams(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Settlements retrieved', + 'data' => [], + 'meta' => [ + 'total' => 0, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 0 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->settlements->all(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/settlement', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testGetSettlementTransactions(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Settlement transactions retrieved', + 'data' => [ + [ + 'id' => 54321, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'txn_12345abcde', + 'amount' => 50000, + 'message' => null, + 'gateway_response' => 'Successful', + 'paid_at' => '2023-11-14T14:30:00.000Z', + 'created_at' => '2023-11-14T14:30:00.000Z', + 'channel' => 'card', + 'currency' => 'NGN', + 'ip_address' => '192.168.1.1', + 'metadata' => [], + 'log' => null, + 'fees' => 750, + 'fees_split' => null, + 'authorization' => [ + 'authorization_code' => 'AUTH_abcd1234', + 'bin' => '408408', + 'last4' => '4081', + 'exp_month' => '12', + 'exp_year' => '2030', + 'channel' => 'card', + 'card_type' => 'visa DEBIT', + 'bank' => 'Test Bank', + 'country_code' => 'NG', + 'brand' => 'visa', + 'reusable' => true, + 'signature' => 'SIG_abc123' + ], + 'customer' => [ + 'id' => 98765, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com', + 'customer_code' => 'CUS_abc123def', + 'phone' => '+2348012345678', + 'metadata' => [], + 'risk_action' => 'default' + ] + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->settlements->getTransactions('123456', ['page' => 1]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/settlement/123456/transactions', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testGetSettlementTransactionsWithNoParams(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Settlement transactions retrieved', + 'data' => [], + 'meta' => [ + 'total' => 0, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 0 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->settlements->getTransactions('123456'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/settlement/123456/transactions', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/SplitTest.php b/tests/SplitTest.php new file mode 100644 index 0000000..eef09e6 --- /dev/null +++ b/tests/SplitTest.php @@ -0,0 +1,315 @@ + true, + 'message' => 'Split created', + 'data' => [ + 'id' => 142, + 'name' => 'Percentage Split', + 'type' => 'percentage', + 'currency' => 'NGN', + 'integration' => 463433, + 'domain' => 'test', + 'split_code' => 'SPL_e7jnRLtzla', + 'active' => true, + 'bearer_type' => 'account', + 'bearer_subaccount' => null, + 'subaccounts' => [ + [ + 'subaccount' => [ + 'id' => 37, + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj', + 'business_name' => 'Sunshine Studios' + ], + 'share' => 20 + ] + ], + 'total_subaccounts' => 1, + 'created_at' => '2020-06-30T11:42:29.000Z', + 'updated_at' => '2020-06-30T11:42:29.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Percentage Split', + 'type' => 'percentage', + 'currency' => 'NGN', + 'subaccounts' => [ + [ + 'subaccount' => 'ACCT_8f4s1eq7ml6rlzj', + 'share' => 20 + ] + ] + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->splits->create([ + 'name' => 'Percentage Split', + 'type' => 'percentage', + 'currency' => 'NGN', + 'subaccounts' => [ + [ + 'subaccount' => 'ACCT_8f4s1eq7ml6rlzj', + 'share' => 20 + ] + ] + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/split', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListSplits(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Splits retrieved', + 'data' => [ + [ + 'id' => 142, + 'name' => 'Percentage Split', + 'type' => 'percentage', + 'currency' => 'NGN', + 'split_code' => 'SPL_e7jnRLtzla', + 'active' => true, + 'total_subaccounts' => 1 + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->splits->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/split', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchSplit(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Split retrieved', + 'data' => [ + 'id' => 142, + 'name' => 'Percentage Split', + 'type' => 'percentage', + 'currency' => 'NGN', + 'split_code' => 'SPL_e7jnRLtzla', + 'active' => true, + 'bearer_type' => 'account', + 'subaccounts' => [ + [ + 'subaccount' => [ + 'id' => 37, + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj', + 'business_name' => 'Sunshine Studios' + ], + 'share' => 20 + ] + ], + 'total_subaccounts' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->splits->find('SPL_e7jnRLtzla'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/split/SPL_e7jnRLtzla', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateSplit(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Split updated', + 'data' => [ + 'id' => 142, + 'name' => 'Updated Percentage Split', + 'type' => 'percentage', + 'currency' => 'NGN', + 'split_code' => 'SPL_e7jnRLtzla', + 'active' => false + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Updated Percentage Split', + 'active' => false + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->splits->update('SPL_e7jnRLtzla', [ + 'name' => 'Updated Percentage Split', + 'active' => false + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/split/SPL_e7jnRLtzla', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testAddSubaccountToSplit(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subaccount added', + 'data' => [ + 'id' => 142, + 'name' => 'Percentage Split', + 'type' => 'percentage', + 'split_code' => 'SPL_e7jnRLtzla', + 'subaccounts' => [ + [ + 'subaccount' => [ + 'id' => 37, + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj' + ], + 'share' => 20 + ], + [ + 'subaccount' => [ + 'id' => 38, + 'subaccount_code' => 'ACCT_newaccount123' + ], + 'share' => 30 + ] + ], + 'total_subaccounts' => 2 + ] + ]; + + $expectedBody = json_encode([ + 'subaccount' => 'ACCT_newaccount123', + 'share' => 30 + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->splits->addSubaccount('SPL_e7jnRLtzla', [ + 'subaccount' => 'ACCT_newaccount123', + 'share' => 30 + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/split/SPL_e7jnRLtzla/subaccount/add', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testRemoveSubaccountFromSplit(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subaccount removed', + 'data' => [ + 'id' => 142, + 'name' => 'Percentage Split', + 'type' => 'percentage', + 'split_code' => 'SPL_e7jnRLtzla', + 'subaccounts' => [ + [ + 'subaccount' => [ + 'id' => 37, + 'subaccount_code' => 'ACCT_8f4s1eq7ml6rlzj' + ], + 'share' => 20 + ] + ], + 'total_subaccounts' => 1 + ] + ]; + + $expectedBody = json_encode([ + 'subaccount' => 'ACCT_newaccount123' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->splits->removeSubaccount('SPL_e7jnRLtzla', [ + 'subaccount' => 'ACCT_newaccount123' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/split/SPL_e7jnRLtzla/subaccount/remove', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/SubscriptionTest.php b/tests/SubscriptionTest.php new file mode 100644 index 0000000..87a9b6f --- /dev/null +++ b/tests/SubscriptionTest.php @@ -0,0 +1,279 @@ + true, + 'message' => 'Subscription successfully created', + 'data' => [ + 'customer' => 1173, + 'plan' => 28, + 'integration' => 463433, + 'domain' => 'test', + 'start' => 1577836800, + 'status' => 'active', + 'quantity' => 1, + 'amount' => 50000, + 'authorization' => [ + 'authorization_code' => 'AUTH_6tmt288t0o', + 'bin' => '408408', + 'last4' => '4081', + 'exp_month' => '12', + 'exp_year' => '2030', + 'channel' => 'card', + 'card_type' => 'visa' + ], + 'subscription_code' => 'SUB_vsyqdmlzble3uii', + 'email_token' => 'd7gofp6yppn3qz7', + 'id' => 9, + 'created_at' => '2020-01-01T09:00:00.000Z', + 'updated_at' => '2020-01-01T09:00:00.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'customer' => 'CUS_xnxdt6s1zg5f4tx', + 'plan' => 'PLN_gx2wn530m0i3w3m', + 'authorization' => 'AUTH_6tmt288t0o' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subscriptions->create([ + 'customer' => 'CUS_xnxdt6s1zg5f4tx', + 'plan' => 'PLN_gx2wn530m0i3w3m', + 'authorization' => 'AUTH_6tmt288t0o' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/subscription', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListSubscriptions(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subscriptions retrieved', + 'data' => [ + [ + 'id' => 9, + 'customer' => [ + 'id' => 1173, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'customer@email.com', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx' + ], + 'plan' => [ + 'id' => 28, + 'name' => 'Monthly retainer', + 'plan_code' => 'PLN_gx2wn530m0i3w3m', + 'amount' => 50000, + 'interval' => 'monthly' + ], + 'subscription_code' => 'SUB_vsyqdmlzble3uii', + 'amount' => 50000, + 'status' => 'active' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subscriptions->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/subscription', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchSubscription(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subscription retrieved', + 'data' => [ + 'id' => 9, + 'subscription_code' => 'SUB_vsyqdmlzble3uii', + 'amount' => 50000, + 'status' => 'active', + 'customer' => [ + 'id' => 1173, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'customer@email.com', + 'customer_code' => 'CUS_xnxdt6s1zg5f4tx' + ], + 'plan' => [ + 'id' => 28, + 'name' => 'Monthly retainer', + 'plan_code' => 'PLN_gx2wn530m0i3w3m', + 'amount' => 50000, + 'interval' => 'monthly' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subscriptions->find('SUB_vsyqdmlzble3uii'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/subscription/SUB_vsyqdmlzble3uii', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testEnableSubscription(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subscription enabled successfully' + ]; + + $expectedBody = json_encode([ + 'code' => 'SUB_vsyqdmlzble3uii', + 'token' => 'd7gofp6yppn3qz7' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subscriptions->enable('SUB_vsyqdmlzble3uii', 'd7gofp6yppn3qz7'); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/subscription/enable', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testDisableSubscription(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Subscription disabled successfully' + ]; + + $expectedBody = json_encode([ + 'code' => 'SUB_vsyqdmlzble3uii', + 'token' => 'd7gofp6yppn3qz7' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subscriptions->disable('SUB_vsyqdmlzble3uii', 'd7gofp6yppn3qz7'); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/subscription/disable', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testGenerateSubscriptionUpdateLink(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Link generated', + 'data' => [ + 'link' => 'https://paystack.com/subscription/manage/SUB_vsyqdmlzble3uii/d7gofp6yppn3qz7' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subscriptions->genSubCardUpdateLink('SUB_vsyqdmlzble3uii'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/subscription/SUB_vsyqdmlzble3uii/manage/link', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testSendSubscriptionUpdateLink(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Email sent' + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->subscriptions->sendSubCardUpdateLink('SUB_vsyqdmlzble3uii'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/subscription/SUB_vsyqdmlzble3uii/manage/email', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/TransferRecipientTest.php b/tests/TransferRecipientTest.php new file mode 100644 index 0000000..2446298 --- /dev/null +++ b/tests/TransferRecipientTest.php @@ -0,0 +1,300 @@ + true, + 'message' => 'Transfer recipient created successfully', + 'data' => [ + 'active' => true, + 'created_at' => '2020-09-14T14:54:48.000Z', + 'currency' => 'NGN', + 'domain' => 'test', + 'id' => 8690817, + 'integration' => 463433, + 'name' => 'ABDUL-HALEEM ISHAQ', + 'recipient_code' => 'RCP_gx2wn530m0i3w3m', + 'type' => 'nuban', + 'updated_at' => '2020-09-14T14:54:48.000Z', + 'is_deleted' => false, + 'details' => [ + 'authorization_code' => null, + 'account_number' => '0123456789', + 'account_name' => 'ABDUL-HALEEM ISHAQ', + 'bank_code' => '044', + 'bank_name' => 'Access Bank' + ] + ] + ]; + + $expectedBody = json_encode([ + 'type' => 'nuban', + 'name' => 'ABDUL-HALEEM ISHAQ', + 'account_number' => '0123456789', + 'bank_code' => '044', + 'currency' => 'NGN' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferRecipients->create([ + 'type' => 'nuban', + 'name' => 'ABDUL-HALEEM ISHAQ', + 'account_number' => '0123456789', + 'bank_code' => '044', + 'currency' => 'NGN' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transferrecipient', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testBulkCreateTransferRecipients(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Recipients added successfully', + 'data' => [ + 'success' => [ + [ + 'name' => 'ABDUL-HALEEM ISHAQ', + 'account_number' => '0123456789', + 'bank_code' => '044', + 'currency' => 'NGN', + 'type' => 'nuban', + 'recipient_code' => 'RCP_gx2wn530m0i3w3m' + ] + ], + 'errors' => [] + ] + ]; + + $expectedBody = json_encode([ + 'batch' => [ + [ + 'type' => 'nuban', + 'name' => 'ABDUL-HALEEM ISHAQ', + 'account_number' => '0123456789', + 'bank_code' => '044', + 'currency' => 'NGN' + ], + [ + 'type' => 'nuban', + 'name' => 'JOHN DOE', + 'account_number' => '0987654321', + 'bank_code' => '058', + 'currency' => 'NGN' + ] + ] + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferRecipients->bulkCreate([ + 'batch' => [ + [ + 'type' => 'nuban', + 'name' => 'ABDUL-HALEEM ISHAQ', + 'account_number' => '0123456789', + 'bank_code' => '044', + 'currency' => 'NGN' + ], + [ + 'type' => 'nuban', + 'name' => 'JOHN DOE', + 'account_number' => '0987654321', + 'bank_code' => '058', + 'currency' => 'NGN' + ] + ] + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transferrecipient/bulk', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListTransferRecipients(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Recipients retrieved', + 'data' => [ + [ + 'id' => 8690817, + 'name' => 'ABDUL-HALEEM ISHAQ', + 'recipient_code' => 'RCP_gx2wn530m0i3w3m', + 'type' => 'nuban', + 'currency' => 'NGN', + 'active' => true, + 'details' => [ + 'account_number' => '0123456789', + 'account_name' => 'ABDUL-HALEEM ISHAQ', + 'bank_code' => '044', + 'bank_name' => 'Access Bank' + ] + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferRecipients->all(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transferrecipient', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchTransferRecipient(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Recipient retrieved', + 'data' => [ + 'id' => 8690817, + 'name' => 'ABDUL-HALEEM ISHAQ', + 'recipient_code' => 'RCP_gx2wn530m0i3w3m', + 'type' => 'nuban', + 'currency' => 'NGN', + 'active' => true, + 'is_deleted' => false, + 'details' => [ + 'account_number' => '0123456789', + 'account_name' => 'ABDUL-HALEEM ISHAQ', + 'bank_code' => '044', + 'bank_name' => 'Access Bank' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferRecipients->find('RCP_gx2wn530m0i3w3m'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/transferrecipient/RCP_gx2wn530m0i3w3m', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateTransferRecipient(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Recipient updated', + 'data' => [ + 'id' => 8690817, + 'name' => 'UPDATED ABDUL-HALEEM ISHAQ', + 'recipient_code' => 'RCP_gx2wn530m0i3w3m', + 'type' => 'nuban', + 'currency' => 'NGN', + 'active' => true, + 'details' => [ + 'account_number' => '0123456789', + 'account_name' => 'UPDATED ABDUL-HALEEM ISHAQ', + 'bank_code' => '044', + 'bank_name' => 'Access Bank' + ] + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'UPDATED ABDUL-HALEEM ISHAQ' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferRecipients->update('RCP_gx2wn530m0i3w3m', [ + 'name' => 'UPDATED ABDUL-HALEEM ISHAQ' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/transferrecipient/RCP_gx2wn530m0i3w3m', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testDeleteTransferRecipient(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Recipient set as inactive' + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferRecipients->delete('RCP_gx2wn530m0i3w3m'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('DELETE', $sentRequest->getMethod()); + $this->assertEquals('/transferrecipient/RCP_gx2wn530m0i3w3m', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file From 047a30d5bd7c478677063f459e15612011909d36 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 14:26:35 +0100 Subject: [PATCH 05/17] Refactor API request handling and add comprehensive tests for various functionalities --- composer.json | 2 +- src/API/Miscellaneous.php | 9 +- src/Options/Terminal/SendEventOptions.php | 2 +- tests/IntegrationTest.php | 75 +++++ tests/MiscellaneousTest.php | 201 ++++++++++++++ tests/TerminalTest.php | 323 ++++++++++++++++++++++ tests/TransferControlTest.php | 217 +++++++++++++++ tests/VerificationTest.php | 127 +++++++++ 8 files changed, 951 insertions(+), 5 deletions(-) create mode 100644 tests/IntegrationTest.php create mode 100644 tests/MiscellaneousTest.php create mode 100644 tests/TerminalTest.php create mode 100644 tests/TransferControlTest.php create mode 100644 tests/VerificationTest.php diff --git a/composer.json b/composer.json index 61b8fc5..fe198e8 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "symfony/var-dumper": "^6.2" }, "scripts": { - "test": "phpunit" + "test": "phpunit --testdox" }, "minimum-stability": "stable", "prefer-stable": true, diff --git a/src/API/Miscellaneous.php b/src/API/Miscellaneous.php index 72fd48f..792ae05 100644 --- a/src/API/Miscellaneous.php +++ b/src/API/Miscellaneous.php @@ -15,9 +15,12 @@ class Miscellaneous extends ApiAbstract */ public function listBanks(array $params = []): array { - $response = $this->httpClient->get('/bank', [ - 'query' => $params - ]); + $requestOptions = []; + if (!empty($params)) { + $requestOptions['query'] = $params; + } + + $response = $this->httpClient->get('/bank', $requestOptions); return ResponseMediator::getContent($response); } diff --git a/src/Options/Terminal/SendEventOptions.php b/src/Options/Terminal/SendEventOptions.php index 63da493..fa743fc 100644 --- a/src/Options/Terminal/SendEventOptions.php +++ b/src/Options/Terminal/SendEventOptions.php @@ -19,7 +19,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->define('type') ->required() ->allowedTypes('string') - ->allowedValues(['invoice', 'transaction']) + ->allowedValues('invoice', 'transaction') ->info('The type of event to push. We currently support invoice and transaction'); $resolver->define('action') diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php new file mode 100644 index 0000000..601a863 --- /dev/null +++ b/tests/IntegrationTest.php @@ -0,0 +1,75 @@ + true, + 'message' => 'Payment session timeout retrieved', + 'data' => [ + 'payment_session_timeout' => 30, + 'invoice_limit' => 1000, + 'invoice_limit_currency' => 'NGN' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->integration->fetchTimeout(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/integration/payment_session_timeout', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateTimeout(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Payment session timeout updated', + 'data' => [ + 'payment_session_timeout' => 60, + 'invoice_limit' => 1000, + 'invoice_limit_currency' => 'NGN' + ] + ]; + + $expectedBody = json_encode([ + 'timeout' => 60 + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->integration->updateTimeout([ + 'timeout' => 60 + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/integration/payment_session_timeout', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/MiscellaneousTest.php b/tests/MiscellaneousTest.php new file mode 100644 index 0000000..7e954a4 --- /dev/null +++ b/tests/MiscellaneousTest.php @@ -0,0 +1,201 @@ + true, + 'message' => 'Banks retrieved', + 'data' => [ + [ + 'id' => 1, + 'name' => 'Access Bank', + 'slug' => 'access-bank', + 'code' => '044', + 'longcode' => '044150149', + 'gateway' => 'emandate', + 'pay_with_bank' => false, + 'active' => true, + 'country' => 'Nigeria', + 'currency' => 'NGN', + 'type' => 'nuban', + 'is_deleted' => false, + 'created_at' => '2016-07-14T10:04:29.000Z', + 'updated_at' => '2023-01-01T12:00:00.000Z' + ], + [ + 'id' => 2, + 'name' => 'Citibank Nigeria', + 'slug' => 'citibank-nigeria', + 'code' => '023', + 'longcode' => '023150005', + 'gateway' => null, + 'pay_with_bank' => false, + 'active' => true, + 'country' => 'Nigeria', + 'currency' => 'NGN', + 'type' => 'nuban', + 'is_deleted' => false, + 'created_at' => '2016-07-14T10:04:29.000Z', + 'updated_at' => '2023-01-01T12:00:00.000Z' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->miscellaneous->listBanks(['country' => 'nigeria']); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bank', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListBanksWithNoParams(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Banks retrieved', + 'data' => [] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->miscellaneous->listBanks(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bank', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListCountries(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Countries retrieved', + 'data' => [ + [ + 'id' => 1, + 'name' => 'Nigeria', + 'iso_code' => 'NG', + 'default_currency_code' => 'NGN', + 'integration_defaults' => [], + 'relationships' => [ + 'currency' => [ + 'type' => 'currency', + 'data' => [ + 'code' => 'NGN', + 'name' => 'Nigerian Naira' + ] + ] + ] + ], + [ + 'id' => 2, + 'name' => 'Ghana', + 'iso_code' => 'GH', + 'default_currency_code' => 'GHS', + 'integration_defaults' => [], + 'relationships' => [ + 'currency' => [ + 'type' => 'currency', + 'data' => [ + 'code' => 'GHS', + 'name' => 'Ghanaian Cedi' + ] + ] + ] + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->miscellaneous->listCountries(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/country', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListStates(): void + { + $responseData = [ + 'status' => true, + 'message' => 'States retrieved', + 'data' => [ + [ + 'name' => 'Abia', + 'slug' => 'abia', + 'abbreviation' => 'AB' + ], + [ + 'name' => 'Adamawa', + 'slug' => 'adamawa', + 'abbreviation' => 'AD' + ], + [ + 'name' => 'Akwa Ibom', + 'slug' => 'akwa-ibom', + 'abbreviation' => 'AK' + ], + [ + 'name' => 'Anambra', + 'slug' => 'anambra', + 'abbreviation' => 'AN' + ], + [ + 'name' => 'Bauchi', + 'slug' => 'bauchi', + 'abbreviation' => 'BA' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->miscellaneous->listStates(['country' => 'NG']); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/address_verification/states', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/TerminalTest.php b/tests/TerminalTest.php new file mode 100644 index 0000000..2d9e806 --- /dev/null +++ b/tests/TerminalTest.php @@ -0,0 +1,323 @@ + true, + 'message' => 'Event sent successfully', + 'data' => [ + 'id' => 'evt_abc123def456', + 'type' => 'invoice', + 'action' => 'process', + 'data' => [ + 'id' => 12345, + 'amount' => 50000, + 'reference' => 'INV_abc123' + ], + 'terminal_id' => 'term_abc123', + 'status' => 'pending', + 'created_at' => '2023-11-16T12:00:00.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'type' => 'invoice', + 'action' => 'process', + 'data' => [ + 'id' => 12345, + 'amount' => 50000, + 'reference' => 'INV_abc123' + ] + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->sendEvent('term_abc123', [ + 'type' => 'invoice', + 'action' => 'process', + 'data' => [ + 'id' => 12345, + 'amount' => 50000, + 'reference' => 'INV_abc123' + ] + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/terminal/term_abc123/event', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testFetchEventStatus(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Event status retrieved', + 'data' => [ + 'id' => 'evt_abc123def456', + 'type' => 'invoice', + 'action' => 'process', + 'terminal_id' => 'term_abc123', + 'status' => 'success', + 'created_at' => '2023-11-16T12:00:00.000Z', + 'updated_at' => '2023-11-16T12:01:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->fetchEventStatus('term_abc123', 'evt_abc123def456'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/terminal/term_abc123/event/evt_abc123def456', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchTerminalStatus(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Terminal status retrieved', + 'data' => [ + 'online' => true, + 'available' => true, + 'last_seen_at' => '2023-11-16T12:00:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->fetchTerminalStatus('term_abc123'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/terminal/term_abc123/presence', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListTerminals(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Terminals retrieved', + 'data' => [ + [ + 'id' => 'term_abc123', + 'serial_number' => 'PAX123456789', + 'device_make' => 'PAX', + 'terminal_id' => 'term_abc123', + 'integration' => 463433, + 'domain' => 'test', + 'name' => 'Main Terminal', + 'address' => '123 Main Street, Lagos', + 'status' => 'active' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->all(['perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/terminal', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchTerminal(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Terminal retrieved', + 'data' => [ + 'id' => 'term_abc123', + 'serial_number' => 'PAX123456789', + 'device_make' => 'PAX', + 'terminal_id' => 'term_abc123', + 'integration' => 463433, + 'domain' => 'test', + 'name' => 'Main Terminal', + 'address' => '123 Main Street, Lagos', + 'status' => 'active', + 'created_at' => '2023-11-15T10:00:00.000Z', + 'updated_at' => '2023-11-16T12:00:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->find('term_abc123'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/terminal/term_abc123', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateTerminal(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Terminal updated', + 'data' => [ + 'id' => 'term_abc123', + 'name' => 'Updated Terminal Name', + 'address' => '456 New Street, Lagos' + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Updated Terminal Name', + 'address' => '456 New Street, Lagos' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->update('term_abc123', [ + 'name' => 'Updated Terminal Name', + 'address' => '456 New Street, Lagos' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/terminal/term_abc123', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testCommissionTerminal(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Device commissioned successfully', + 'data' => [ + 'terminal_id' => 'term_abc123', + 'device_make' => 'PAX', + 'serial_number' => 'PAX123456789', + 'status' => 'active' + ] + ]; + + $expectedBody = json_encode([ + 'serial_number' => 'PAX123456789' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->commission([ + 'serial_number' => 'PAX123456789' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/terminal/commission_device', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testDecommissionTerminal(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Device decommissioned successfully', + 'data' => [ + 'terminal_id' => 'term_abc123', + 'serial_number' => 'PAX123456789', + 'status' => 'inactive' + ] + ]; + + $expectedBody = json_encode([ + 'serial_number' => 'PAX123456789' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->terminals->decommission([ + 'serial_number' => 'PAX123456789' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/terminal/decommission_device', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/TransferControlTest.php b/tests/TransferControlTest.php new file mode 100644 index 0000000..3c84f2a --- /dev/null +++ b/tests/TransferControlTest.php @@ -0,0 +1,217 @@ + true, + 'message' => 'Balance retrieved', + 'data' => [ + [ + 'currency' => 'NGN', + 'balance' => 5000000 + ], + [ + 'currency' => 'USD', + 'balance' => 1500000 + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferControl->checkBalance(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/balance', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testGetBalanceLedger(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Balance ledger retrieved', + 'data' => [ + [ + 'integration' => 463433, + 'domain' => 'test', + 'balance' => 5000000, + 'currency' => 'NGN', + 'difference' => 50000, + 'reason' => 'Transfer', + 'model_responsible' => 'Transfer', + 'model_row' => 12345, + 'created_at' => '2023-11-16T14:00:00.000Z' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferControl->getBalanceLedger(['page' => 1, 'perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/balance/ledger', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testResendOtp(): void + { + $responseData = [ + 'status' => true, + 'message' => 'OTP sent successfully', + 'data' => [ + 'transfer_code' => 'TRF_abc123def456', + 'details' => 'OTP has been sent to your phone and email' + ] + ]; + + $expectedBody = json_encode([ + 'transfer_code' => 'TRF_abc123def456', + 'reason' => 'transfer' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferControl->resendOtp([ + 'transfer_code' => 'TRF_abc123def456', + 'reason' => 'transfer' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transfer/resend_otp', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testDisableOtp(): void + { + $responseData = [ + 'status' => true, + 'message' => 'OTP has been sent to mobile number ending with 1234', + 'data' => [ + 'status' => 'otp_sent', + 'details' => 'OTP requirement for transfers is being disabled. A confirmation OTP has been sent' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferControl->disableOtp(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transfer/disable_otp', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testEnableOtp(): void + { + $responseData = [ + 'status' => true, + 'message' => 'OTP requirement for transfers has been enabled', + 'data' => [ + 'status' => 'enabled', + 'details' => 'OTP requirement for transfers has been enabled on your account' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferControl->enableOtp(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transfer/enable_otp', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFinalizeDisableOtp(): void + { + $responseData = [ + 'status' => true, + 'message' => 'OTP requirement for transfers has been disabled', + 'data' => [ + 'status' => 'disabled', + 'details' => 'OTP requirement for transfers has been disabled on your account' + ] + ]; + + $expectedBody = json_encode([ + 'otp' => '123456' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->transferControl->finalizeDisableOtp([ + 'otp' => '123456' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/transfer/disable_otp_finalize', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/VerificationTest.php b/tests/VerificationTest.php new file mode 100644 index 0000000..cc0a8ae --- /dev/null +++ b/tests/VerificationTest.php @@ -0,0 +1,127 @@ + true, + 'message' => 'Account number resolved', + 'data' => [ + 'account_number' => '0123456789', + 'account_name' => 'John Doe', + 'bank_id' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->verification->resolveAccount([ + 'account_number' => '0123456789', + 'bank_code' => '044' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/bank/resolve', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testValidateAccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Account validation successful', + 'data' => [ + 'verified' => true, + 'verification_code' => 'ABC123', + 'account_number' => '0123456789', + 'account_name' => 'John Doe', + 'bank_code' => '044', + 'bank_name' => 'Access Bank' + ] + ]; + + $expectedBody = json_encode([ + 'account_name' => 'John Doe', + 'account_number' => '0123456789', + 'account_type' => 'personal', + 'bank_code' => '044', + 'country_code' => 'NG', + 'document_type' => 'identityNumber', + 'document_number' => '12345678901' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->verification->validateAccount([ + 'account_name' => 'John Doe', + 'account_number' => '0123456789', + 'account_type' => 'personal', + 'bank_code' => '044', + 'country_code' => 'NG', + 'document_type' => 'identityNumber', + 'document_number' => '12345678901' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/bank/validate', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testResolveCardBin(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Card bin resolved', + 'data' => [ + 'bin' => '408408', + 'brand' => 'visa', + 'sub_brand' => '', + 'country_code' => 'NG', + 'country_name' => 'Nigeria', + 'card_type' => 'DEBIT', + 'bank' => 'Test Bank', + 'linked_bank_id' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->verification->resolveCardBin('408408'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/decision/bin/408408', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file From df123169b9e2cdaad5c3279c0c84a6cc559e2c29 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 14:34:22 +0100 Subject: [PATCH 06/17] Add tests for ApplePay, DedicatedVirtualAccount, DirectDebit, and VirtualTerminal APIs; refactor request handling in respective classes for improved parameter management. --- src/API/ApplePay.php | 9 +- src/API/DedicatedVirtualAccount.php | 9 +- src/API/DirectDebit.php | 9 +- tests/ApplePayTest.php | 149 ++++++++++ tests/DedicatedVirtualAccountTest.php | 371 +++++++++++++++++++++++++ tests/DirectDebitTest.php | 172 ++++++++++++ tests/VirtualTerminalTest.php | 373 ++++++++++++++++++++++++++ 7 files changed, 1083 insertions(+), 9 deletions(-) create mode 100644 tests/ApplePayTest.php create mode 100644 tests/DedicatedVirtualAccountTest.php create mode 100644 tests/DirectDebitTest.php create mode 100644 tests/VirtualTerminalTest.php diff --git a/src/API/ApplePay.php b/src/API/ApplePay.php index 92ca07e..25cfff1 100644 --- a/src/API/ApplePay.php +++ b/src/API/ApplePay.php @@ -28,9 +28,12 @@ public function registerDomain(array $params): array */ public function listDomains(array $params = []): array { - $response = $this->httpClient->get('/apple-pay/domain', [ - 'query' => $params - ]); + $requestOptions = []; + if (!empty($params)) { + $requestOptions['query'] = $params; + } + + $response = $this->httpClient->get('/apple-pay/domain', $requestOptions); return ResponseMediator::getContent($response); } diff --git a/src/API/DedicatedVirtualAccount.php b/src/API/DedicatedVirtualAccount.php index e0b7ddb..1941030 100644 --- a/src/API/DedicatedVirtualAccount.php +++ b/src/API/DedicatedVirtualAccount.php @@ -28,9 +28,12 @@ public function create(array $params): array */ public function all(array $params = []): array { - $response = $this->httpClient->get('/dedicated_account', [ - 'query' => $params - ]); + $requestOptions = []; + if (!empty($params)) { + $requestOptions['query'] = $params; + } + + $response = $this->httpClient->get('/dedicated_account', $requestOptions); return ResponseMediator::getContent($response); } diff --git a/src/API/DirectDebit.php b/src/API/DirectDebit.php index 48a9409..471688f 100644 --- a/src/API/DirectDebit.php +++ b/src/API/DirectDebit.php @@ -28,9 +28,12 @@ public function triggerActivationCharge(array $params): array */ public function listMandateAuthorizations(array $params = []): array { - $response = $this->httpClient->get('/directdebit/mandate-authorizations', [ - 'query' => $params - ]); + $requestOptions = []; + if (!empty($params)) { + $requestOptions['query'] = $params; + } + + $response = $this->httpClient->get('/directdebit/mandate-authorizations', $requestOptions); return ResponseMediator::getContent($response); } diff --git a/tests/ApplePayTest.php b/tests/ApplePayTest.php new file mode 100644 index 0000000..d4cc3a1 --- /dev/null +++ b/tests/ApplePayTest.php @@ -0,0 +1,149 @@ + true, + 'message' => 'Domain registered successfully', + 'data' => [ + 'domain' => 'example.com', + 'registered' => true, + 'created_at' => '2023-11-16T18:00:00.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'domainName' => 'example.com' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->applePay->registerDomain([ + 'domainName' => 'example.com' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/apple-pay/domain', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListDomains(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Apple Pay domains retrieved', + 'data' => [ + [ + 'domain' => 'example.com', + 'registered' => true, + 'created_at' => '2023-11-16T18:00:00.000Z' + ], + [ + 'domain' => 'shop.example.com', + 'registered' => true, + 'created_at' => '2023-11-16T18:05:00.000Z' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->applePay->listDomains(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/apple-pay/domain', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListDomainsWithParams(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Apple Pay domains retrieved', + 'data' => [ + [ + 'domain' => 'example.com', + 'registered' => true, + 'created_at' => '2023-11-16T18:00:00.000Z' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->applePay->listDomains(['use_cursor' => 'false']); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/apple-pay/domain', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUnregisterDomain(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Domain unregistered successfully', + 'data' => [ + 'domain' => 'old.example.com', + 'registered' => false + ] + ]; + + $expectedBody = json_encode([ + 'domainName' => 'old.example.com' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->applePay->unregisterDomain([ + 'domainName' => 'old.example.com' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('DELETE', $sentRequest->getMethod()); + $this->assertEquals('/apple-pay/domain', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/DedicatedVirtualAccountTest.php b/tests/DedicatedVirtualAccountTest.php new file mode 100644 index 0000000..38176c9 --- /dev/null +++ b/tests/DedicatedVirtualAccountTest.php @@ -0,0 +1,371 @@ + true, + 'message' => 'Dedicated virtual account created', + 'data' => [ + 'bank' => [ + 'name' => 'Test Bank', + 'id' => 1, + 'slug' => 'test-bank' + ], + 'account_name' => 'ACME Corp/John Doe', + 'account_number' => '9991234567', + 'assigned' => true, + 'currency' => 'NGN', + 'metadata' => null, + 'active' => true, + 'id' => 45678, + 'created_at' => '2023-11-16T15:00:00.000Z', + 'updated_at' => '2023-11-16T15:00:00.000Z', + 'assignment' => [ + 'integration' => 463433, + 'assignee_id' => 87654, + 'assignee_type' => 'Customer', + 'expired' => false, + 'account_type' => 'PAY-WITH-BANK-TRANSFER', + 'assigned_at' => '2023-11-16T15:00:00.000Z' + ], + 'customer' => [ + 'id' => 87654, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com', + 'customer_code' => 'CUS_abc123def456' + ] + ] + ]; + + $expectedBody = json_encode([ + 'customer' => 'CUS_abc123def456', + 'preferred_bank' => 'test-bank' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->create([ + 'customer' => 'CUS_abc123def456', + 'preferred_bank' => 'test-bank' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListDedicatedVirtualAccounts(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dedicated virtual accounts retrieved', + 'data' => [ + [ + 'bank' => [ + 'name' => 'Test Bank', + 'id' => 1, + 'slug' => 'test-bank' + ], + 'account_name' => 'ACME Corp/John Doe', + 'account_number' => '9991234567', + 'assigned' => true, + 'currency' => 'NGN', + 'active' => true, + 'id' => 45678, + 'created_at' => '2023-11-16T15:00:00.000Z', + 'customer' => [ + 'id' => 87654, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com' + ] + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->all(['currency' => 'NGN']); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchDedicatedVirtualAccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dedicated virtual account retrieved', + 'data' => [ + 'transactions' => [ + [ + 'id' => 12345, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'dva_abc123def456', + 'amount' => 100000, + 'currency' => 'NGN', + 'paid_at' => '2023-11-16T16:00:00.000Z', + 'channel' => 'bank_transfer' + ] + ], + 'subscriptions' => [], + 'authorizations' => [], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com', + 'phone' => '+2348012345678', + 'metadata' => null, + 'domain' => 'test', + 'customer_code' => 'CUS_abc123def456', + 'id' => 87654, + 'integration' => 463433, + 'created_at' => '2023-11-15T10:00:00.000Z', + 'updated_at' => '2023-11-16T16:00:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->find('45678'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account/45678', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testRequeryDedicatedVirtualAccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dedicated virtual account requeried', + 'data' => [ + 'transactions' => [ + [ + 'id' => 12346, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'dva_new123def456', + 'amount' => 50000, + 'currency' => 'NGN', + 'paid_at' => '2023-11-16T17:00:00.000Z', + 'channel' => 'bank_transfer' + ] + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->requery([ + 'account_number' => '9991234567', + 'provider_slug' => 'test-bank', + 'date' => '2023-11-16' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account/requery', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testDeactivateDedicatedVirtualAccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dedicated virtual account deactivated', + 'data' => [ + 'bank' => [ + 'name' => 'Test Bank', + 'id' => 1, + 'slug' => 'test-bank' + ], + 'account_name' => 'ACME Corp/John Doe', + 'account_number' => '9991234567', + 'assigned' => false, + 'active' => false, + 'id' => 45678 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->deactivate('45678'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('DELETE', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account/45678', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testSplitDedicatedVirtualAccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dedicated virtual account split successfully', + 'data' => [ + 'id' => 45678, + 'split_config' => [ + 'subaccount' => 'ACCT_8f4s1eq7ml6rlzj', + 'share' => 20 + ] + ] + ]; + + $expectedBody = json_encode([ + 'customer' => 'CUS_abc123def456', + 'subaccount' => 'ACCT_8f4s1eq7ml6rlzj', + 'split_code' => 'SPL_e7jnRLtzla' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->split([ + 'customer' => 'CUS_abc123def456', + 'subaccount' => 'ACCT_8f4s1eq7ml6rlzj', + 'split_code' => 'SPL_e7jnRLtzla' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account/split', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testRemoveSplitFromDedicatedVirtualAccount(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Split removed from dedicated virtual account', + 'data' => [ + 'id' => 45678, + 'split_config' => null + ] + ]; + + $expectedBody = json_encode([ + 'account_number' => '9991234567' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->removeSplit([ + 'account_number' => '9991234567' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('DELETE', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account/split', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testGetProviders(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Dedicated virtual account providers retrieved', + 'data' => [ + [ + 'provider_slug' => 'test-bank', + 'bank_id' => 1, + 'bank_name' => 'Test Bank' + ], + [ + 'provider_slug' => 'wema-bank', + 'bank_id' => 2, + 'bank_name' => 'Wema Bank' + ] + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->dedicatedVirtualAccounts->getProviders(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/dedicated_account/available_providers', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/DirectDebitTest.php b/tests/DirectDebitTest.php new file mode 100644 index 0000000..e1b30c0 --- /dev/null +++ b/tests/DirectDebitTest.php @@ -0,0 +1,172 @@ + true, + 'message' => 'Activation charge triggered successfully', + 'data' => [ + 'mandate' => [ + 'id' => 12345, + 'mandate_code' => 'MANDATE_abc123def456', + 'status' => 'pending', + 'customer' => [ + 'id' => 87654, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com' + ], + 'bank' => [ + 'name' => 'Test Bank', + 'code' => '044' + ], + 'account_number' => '0123456789', + 'account_name' => 'John Doe' + ], + 'activation_charge' => [ + 'id' => 54321, + 'amount' => 100, + 'currency' => 'NGN', + 'status' => 'pending', + 'reference' => 'actv_abc123def456' + ] + ] + ]; + + $expectedBody = json_encode([ + 'mandate_code' => 'MANDATE_abc123def456' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->directDebit->triggerActivationCharge([ + 'mandate_code' => 'MANDATE_abc123def456' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/directdebit/activation-charge', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListMandateAuthorizations(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Direct debit mandate authorizations retrieved', + 'data' => [ + [ + 'id' => 12345, + 'mandate_code' => 'MANDATE_abc123def456', + 'status' => 'active', + 'customer' => [ + 'id' => 87654, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com', + 'customer_code' => 'CUS_abc123def456' + ], + 'bank' => [ + 'name' => 'Test Bank', + 'code' => '044' + ], + 'account_number' => '0123456789', + 'account_name' => 'John Doe', + 'created_at' => '2023-11-16T19:00:00.000Z', + 'updated_at' => '2023-11-16T19:05:00.000Z' + ], + [ + 'id' => 12346, + 'mandate_code' => 'MANDATE_def456ghi789', + 'status' => 'pending', + 'customer' => [ + 'id' => 87655, + 'first_name' => 'Jane', + 'last_name' => 'Smith', + 'email' => 'jane.smith@example.com', + 'customer_code' => 'CUS_def456ghi789' + ], + 'bank' => [ + 'name' => 'Another Bank', + 'code' => '058' + ], + 'account_number' => '9876543210', + 'account_name' => 'Jane Smith', + 'created_at' => '2023-11-16T19:10:00.000Z', + 'updated_at' => '2023-11-16T19:10:00.000Z' + ] + ], + 'meta' => [ + 'total' => 2, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->directDebit->listMandateAuthorizations(['status' => 'active']); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/directdebit/mandate-authorizations', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testListMandateAuthorizationsWithNoParams(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Direct debit mandate authorizations retrieved', + 'data' => [], + 'meta' => [ + 'total' => 0, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 0 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->directDebit->listMandateAuthorizations(); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/directdebit/mandate-authorizations', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file diff --git a/tests/VirtualTerminalTest.php b/tests/VirtualTerminalTest.php new file mode 100644 index 0000000..0785797 --- /dev/null +++ b/tests/VirtualTerminalTest.php @@ -0,0 +1,373 @@ + true, + 'message' => 'Virtual Terminal created', + 'data' => [ + 'code' => 'VT_abc123def456', + 'name' => 'Test Virtual Terminal', + 'description' => 'A test virtual terminal for payments', + 'currency' => 'NGN', + 'merchant_category_code' => '5411', + 'split_code' => null, + 'active' => true, + 'destinations' => [], + 'id' => 54321, + 'integration' => 463433, + 'domain' => 'test', + 'created_at' => '2023-11-16T20:00:00.000Z', + 'updated_at' => '2023-11-16T20:00:00.000Z' + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Test Virtual Terminal', + 'description' => 'A test virtual terminal for payments', + 'currency' => 'NGN', + 'merchant_category_code' => '5411' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->create([ + 'name' => 'Test Virtual Terminal', + 'description' => 'A test virtual terminal for payments', + 'currency' => 'NGN', + 'merchant_category_code' => '5411' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testListVirtualTerminals(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Virtual Terminals retrieved', + 'data' => [ + [ + 'code' => 'VT_abc123def456', + 'name' => 'Test Virtual Terminal', + 'description' => 'A test virtual terminal for payments', + 'amount' => 100000, + 'currency' => 'NGN', + 'active' => true, + 'id' => 54321, + 'created_at' => '2023-11-16T20:00:00.000Z' + ] + ], + 'meta' => [ + 'total' => 1, + 'skipped' => 0, + 'perPage' => 50, + 'page' => 1, + 'pageCount' => 1 + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->all(['perPage' => 50]); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testFetchVirtualTerminal(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Virtual Terminal retrieved', + 'data' => [ + 'code' => 'VT_abc123def456', + 'name' => 'Test Virtual Terminal', + 'description' => 'A test virtual terminal for payments', + 'currency' => 'NGN', + 'merchant_category_code' => '5411', + 'split_code' => null, + 'active' => true, + 'destinations' => [ + [ + 'id' => 12345, + 'destination' => '+2348012345678', + 'type' => 'whatsapp' + ] + ], + 'id' => 54321, + 'integration' => 463433, + 'domain' => 'test', + 'created_at' => '2023-11-16T20:00:00.000Z', + 'updated_at' => '2023-11-16T20:05:00.000Z' + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->find('VT_abc123def456'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('GET', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal/VT_abc123def456', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testUpdateVirtualTerminal(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Virtual Terminal updated', + 'data' => [ + 'code' => 'VT_abc123def456', + 'name' => 'Updated Virtual Terminal', + 'description' => 'An updated virtual terminal description', + 'currency' => 'USD', + 'merchant_category_code' => '5399' + ] + ]; + + $expectedBody = json_encode([ + 'name' => 'Updated Virtual Terminal', + 'description' => 'An updated virtual terminal description', + 'currency' => 'USD', + 'merchant_category_code' => '5399' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->update('VT_abc123def456', [ + 'name' => 'Updated Virtual Terminal', + 'description' => 'An updated virtual terminal description', + 'currency' => 'USD', + 'merchant_category_code' => '5399' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal/VT_abc123def456', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testDeactivateVirtualTerminal(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Virtual Terminal deactivated', + 'data' => [ + 'code' => 'VT_abc123def456', + 'active' => false + ] + ]; + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->deactivate('VT_abc123def456'); + + $sentRequest = $this->mockClient->getLastRequest(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal/VT_abc123def456/deactivate', $sentRequest->getUri()->getPath()); + $this->assertEquals($responseData, $data); + } + + public function testAssignDestination(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Destination assigned successfully', + 'data' => [ + 'code' => 'VT_abc123def456', + 'destinations' => [ + [ + 'id' => 12345, + 'destination' => '+2348012345678', + 'type' => 'whatsapp' + ] + ] + ] + ]; + + $expectedBody = json_encode([ + 'destination' => '+2348012345678' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->assignDestination('VT_abc123def456', [ + 'destination' => '+2348012345678' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal/VT_abc123def456/destination/assign', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testUnassignDestination(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Destination unassigned successfully', + 'data' => [ + 'code' => 'VT_abc123def456', + 'destinations' => [] + ] + ]; + + $expectedBody = json_encode([ + 'destination' => '+2348012345678' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->unassignDestination('VT_abc123def456', [ + 'destination' => '+2348012345678' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('POST', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal/VT_abc123def456/destination/unassign', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testAddSplitCode(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Split code added successfully', + 'data' => [ + 'code' => 'VT_abc123def456', + 'split_code' => 'SPL_e7jnRLtzla' + ] + ]; + + $expectedBody = json_encode([ + 'split_code' => 'SPL_e7jnRLtzla' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->addSplitCode('VT_abc123def456', [ + 'split_code' => 'SPL_e7jnRLtzla' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('PUT', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal/VT_abc123def456/split_code', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } + + public function testRemoveSplitCode(): void + { + $responseData = [ + 'status' => true, + 'message' => 'Split code removed successfully', + 'data' => [ + 'code' => 'VT_abc123def456', + 'split_code' => null + ] + ]; + + $expectedBody = json_encode([ + 'split_code' => 'SPL_e7jnRLtzla' + ]); + + $stream = new Stream('php://memory', 'r+'); + $stream->write(json_encode($responseData)); + $stream->rewind(); + + $response = new Response($stream, 200, ['Content-Type' => 'application/json']); + + $this->mockClient->addResponse($response); + + $data = $this->client()->virtualTerminals->removeSplitCode('VT_abc123def456', [ + 'split_code' => 'SPL_e7jnRLtzla' + ]); + + $sentRequest = $this->mockClient->getLastRequest(); + $sentBody = $sentRequest->getBody()->__toString(); + + $this->assertEquals('DELETE', $sentRequest->getMethod()); + $this->assertEquals('/virtual_terminal/VT_abc123def456/split_code', $sentRequest->getUri()->getPath()); + $this->assertEquals($expectedBody, $sentBody); + $this->assertEquals($responseData, $data); + } +} \ No newline at end of file From f66b513462eeda4ca236de372022e9f3d5982893 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 14:54:41 +0100 Subject: [PATCH 07/17] feat: Enhance API options and methods for Integration, Miscellaneous, Verification, and Charge - Updated class to accept for session timeout updates. - Enhanced class with and for better query handling. - Improved class to utilize and for account verification processes. - Added new options classes for Apple Pay, Bulk Charge, Dedicated Virtual Account, Direct Debit, and Dispute functionalities. - Introduced various options for handling user inputs such as address, birthday, OTP, phone, and pin submissions in the Charge options. - Implemented pagination and filtering options in and . - Enhanced dispute handling with new options for adding evidence, exporting disputes, and resolving disputes. --- src/API/ApplePay.php | 38 +++++++--- src/API/BulkCharge.php | 13 +++- src/API/Charge.php | 25 +++++-- src/API/DedicatedVirtualAccount.php | 41 ++++++++--- src/API/DirectDebit.php | 27 ++++++-- src/API/Dispute.php | 68 +++++++++++++----- src/API/Integration.php | 11 ++- src/API/Miscellaneous.php | 27 ++++++-- src/API/Verification.php | 24 +++++-- src/Options/ApplePay/ListDomainsOptions.php | 31 +++++++++ .../ApplePay/RegisterDomainOptions.php | 24 +++++++ .../ApplePay/UnregisterDomainOptions.php | 24 +++++++ src/Options/BulkCharge/GetChargesOptions.php | 40 +++++++++++ src/Options/BulkCharge/ReadAllOptions.php | 35 ++++++++++ src/Options/Charge/CreateOptions.php | 47 ++++++++++--- src/Options/Charge/SubmitAddressOptions.php | 44 ++++++++++++ src/Options/Charge/SubmitBirthdayOptions.php | 29 ++++++++ src/Options/Charge/SubmitOtpOptions.php | 29 ++++++++ src/Options/Charge/SubmitPhoneOptions.php | 29 ++++++++ src/Options/Charge/SubmitPinOptions.php | 29 ++++++++ .../DedicatedVirtualAccount/AssignOptions.php | 69 +++++++++++++++++++ .../RemoveSplitOptions.php | 32 +++++++++ .../RequeryOptions.php | 33 +++++++++ .../DedicatedVirtualAccount/SplitOptions.php | 36 ++++++++++ .../ListMandateAuthorizationsOptions.php | 35 ++++++++++ .../TriggerActivationChargeOptions.php | 42 +++++++++++ src/Options/Dispute/AddEvidenceOptions.php | 47 +++++++++++++ src/Options/Dispute/ExportOptions.php | 44 ++++++++++++ src/Options/Dispute/GetUploadUrlOptions.php | 24 +++++++ src/Options/Dispute/ResolveOptions.php | 42 +++++++++++ src/Options/Dispute/UpdateOptions.php | 28 ++++++++ .../Integration/UpdateTimeoutOptions.php | 24 +++++++ .../Miscellaneous/ListBanksOptions.php | 39 +++++++++++ .../Miscellaneous/ListStatesOptions.php | 24 +++++++ .../Verification/ResolveAccountOptions.php | 29 ++++++++ .../Verification/ValidateAccountOptions.php | 55 +++++++++++++++ 36 files changed, 1158 insertions(+), 80 deletions(-) create mode 100644 src/Options/ApplePay/ListDomainsOptions.php create mode 100644 src/Options/ApplePay/RegisterDomainOptions.php create mode 100644 src/Options/ApplePay/UnregisterDomainOptions.php create mode 100644 src/Options/BulkCharge/GetChargesOptions.php create mode 100644 src/Options/BulkCharge/ReadAllOptions.php create mode 100644 src/Options/Charge/SubmitAddressOptions.php create mode 100644 src/Options/Charge/SubmitBirthdayOptions.php create mode 100644 src/Options/Charge/SubmitOtpOptions.php create mode 100644 src/Options/Charge/SubmitPhoneOptions.php create mode 100644 src/Options/Charge/SubmitPinOptions.php create mode 100644 src/Options/DedicatedVirtualAccount/AssignOptions.php create mode 100644 src/Options/DedicatedVirtualAccount/RemoveSplitOptions.php create mode 100644 src/Options/DedicatedVirtualAccount/RequeryOptions.php create mode 100644 src/Options/DedicatedVirtualAccount/SplitOptions.php create mode 100644 src/Options/DirectDebit/ListMandateAuthorizationsOptions.php create mode 100644 src/Options/DirectDebit/TriggerActivationChargeOptions.php create mode 100644 src/Options/Dispute/AddEvidenceOptions.php create mode 100644 src/Options/Dispute/ExportOptions.php create mode 100644 src/Options/Dispute/GetUploadUrlOptions.php create mode 100644 src/Options/Dispute/ResolveOptions.php create mode 100644 src/Options/Dispute/UpdateOptions.php create mode 100644 src/Options/Integration/UpdateTimeoutOptions.php create mode 100644 src/Options/Miscellaneous/ListBanksOptions.php create mode 100644 src/Options/Miscellaneous/ListStatesOptions.php create mode 100644 src/Options/Verification/ResolveAccountOptions.php create mode 100644 src/Options/Verification/ValidateAccountOptions.php diff --git a/src/API/ApplePay.php b/src/API/ApplePay.php index 25cfff1..23ff995 100644 --- a/src/API/ApplePay.php +++ b/src/API/ApplePay.php @@ -4,18 +4,27 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\ApplePay\{ + RegisterDomainOptions, + ListDomainsOptions, + UnregisterDomainOptions +}; class ApplePay extends ApiAbstract { /** * Register a top-level domain or subdomain for your Apple Pay integration * - * @param array $params + * @param RegisterDomainOptions|array $options * @return array */ - public function registerDomain(array $params): array + public function registerDomain(RegisterDomainOptions|array $options): array { - $response = $this->httpClient->post('/apple-pay/domain', body: json_encode($params)); + if (is_array($options)) { + $options = new RegisterDomainOptions($options); + } + + $response = $this->httpClient->post('/apple-pay/domain', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -23,14 +32,19 @@ public function registerDomain(array $params): array /** * Lists all registered domains on your integration * - * @param array $params + * @param ListDomainsOptions|array $options * @return array */ - public function listDomains(array $params = []): array + public function listDomains(ListDomainsOptions|array $options = []): array { + if (is_array($options)) { + $options = new ListDomainsOptions($options); + } + $requestOptions = []; - if (!empty($params)) { - $requestOptions['query'] = $params; + $optionsArray = $options->all(); + if (!empty($optionsArray)) { + $requestOptions['query'] = $optionsArray; } $response = $this->httpClient->get('/apple-pay/domain', $requestOptions); @@ -41,12 +55,16 @@ public function listDomains(array $params = []): array /** * Unregister a top-level domain or subdomain previously used for your Apple Pay integration * - * @param array $params + * @param UnregisterDomainOptions|array $options * @return array */ - public function unregisterDomain(array $params): array + public function unregisterDomain(UnregisterDomainOptions|array $options): array { - $response = $this->httpClient->delete('/apple-pay/domain', body: json_encode($params)); + if (is_array($options)) { + $options = new UnregisterDomainOptions($options); + } + + $response = $this->httpClient->delete('/apple-pay/domain', body: json_encode($options->all())); return ResponseMediator::getContent($response); } diff --git a/src/API/BulkCharge.php b/src/API/BulkCharge.php index b93f29d..139f62f 100644 --- a/src/API/BulkCharge.php +++ b/src/API/BulkCharge.php @@ -4,6 +4,7 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\BulkCharge as BulkChargeOptions; class BulkCharge extends ApiAbstract { @@ -15,7 +16,9 @@ class BulkCharge extends ApiAbstract */ public function initiate(array $params): array { - $response = $this->httpClient->post('/bulkcharge', body: json_encode($params)); + $options = new BulkChargeOptions\InitiateOptions($params); + + $response = $this->httpClient->post('/bulkcharge', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -28,8 +31,10 @@ public function initiate(array $params): array */ public function all(array $params = []): array { + $options = new BulkChargeOptions\ReadAllOptions($params); + $response = $this->httpClient->get('/bulkcharge', [ - 'query' => $params + 'query' => $options->all() ]); return ResponseMediator::getContent($response); @@ -57,8 +62,10 @@ public function find(string $idOrCode): array */ public function getCharges(string $idOrCode, array $params = []): array { + $options = new BulkChargeOptions\GetChargesOptions($params); + $response = $this->httpClient->get("/bulkcharge/{$idOrCode}/charges", [ - 'query' => $params + 'query' => $options->all() ]); return ResponseMediator::getContent($response); diff --git a/src/API/Charge.php b/src/API/Charge.php index 97e3d5d..9616f0a 100644 --- a/src/API/Charge.php +++ b/src/API/Charge.php @@ -4,6 +4,7 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\Charge as ChargeOptions; class Charge extends ApiAbstract { @@ -15,7 +16,9 @@ class Charge extends ApiAbstract */ public function create(array $params): array { - $response = $this->httpClient->post('/charge', body: json_encode($params)); + $options = new ChargeOptions\CreateOptions($params); + + $response = $this->httpClient->post('/charge', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -28,7 +31,9 @@ public function create(array $params): array */ public function submitPin(array $params): array { - $response = $this->httpClient->post('/charge/submit_pin', body: json_encode($params)); + $options = new ChargeOptions\SubmitPinOptions($params); + + $response = $this->httpClient->post('/charge/submit_pin', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -41,7 +46,9 @@ public function submitPin(array $params): array */ public function submitOtp(array $params): array { - $response = $this->httpClient->post('/charge/submit_otp', body: json_encode($params)); + $options = new ChargeOptions\SubmitOtpOptions($params); + + $response = $this->httpClient->post('/charge/submit_otp', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -54,7 +61,9 @@ public function submitOtp(array $params): array */ public function submitPhone(array $params): array { - $response = $this->httpClient->post('/charge/submit_phone', body: json_encode($params)); + $options = new ChargeOptions\SubmitPhoneOptions($params); + + $response = $this->httpClient->post('/charge/submit_phone', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -67,7 +76,9 @@ public function submitPhone(array $params): array */ public function submitBirthday(array $params): array { - $response = $this->httpClient->post('/charge/submit_birthday', body: json_encode($params)); + $options = new ChargeOptions\SubmitBirthdayOptions($params); + + $response = $this->httpClient->post('/charge/submit_birthday', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -80,7 +91,9 @@ public function submitBirthday(array $params): array */ public function submitAddress(array $params): array { - $response = $this->httpClient->post('/charge/submit_address', body: json_encode($params)); + $options = new ChargeOptions\SubmitAddressOptions($params); + + $response = $this->httpClient->post('/charge/submit_address', body: json_encode($options->all())); return ResponseMediator::getContent($response); } diff --git a/src/API/DedicatedVirtualAccount.php b/src/API/DedicatedVirtualAccount.php index 1941030..12e378d 100644 --- a/src/API/DedicatedVirtualAccount.php +++ b/src/API/DedicatedVirtualAccount.php @@ -4,6 +4,7 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\DedicatedVirtualAccount as DedicatedVirtualAccountOptions; class DedicatedVirtualAccount extends ApiAbstract { @@ -15,7 +16,24 @@ class DedicatedVirtualAccount extends ApiAbstract */ public function create(array $params): array { - $response = $this->httpClient->post('/dedicated_account', body: json_encode($params)); + $options = new DedicatedVirtualAccountOptions\CreateOptions($params); + + $response = $this->httpClient->post('/dedicated_account', body: json_encode($options->all())); + + return ResponseMediator::getContent($response); + } + + /** + * Assign a dedicated virtual account to a customer + * + * @param array $params + * @return array + */ + public function assign(array $params): array + { + $options = new DedicatedVirtualAccountOptions\AssignOptions($params); + + $response = $this->httpClient->post('/dedicated_account/assign', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -28,12 +46,11 @@ public function create(array $params): array */ public function all(array $params = []): array { - $requestOptions = []; - if (!empty($params)) { - $requestOptions['query'] = $params; - } + $options = new DedicatedVirtualAccountOptions\ReadAllOptions($params); - $response = $this->httpClient->get('/dedicated_account', $requestOptions); + $response = $this->httpClient->get('/dedicated_account', [ + 'query' => $options->all() + ]); return ResponseMediator::getContent($response); } @@ -59,8 +76,10 @@ public function find(string $dedicatedAccountId): array */ public function requery(array $params): array { + $options = new DedicatedVirtualAccountOptions\RequeryOptions($params); + $response = $this->httpClient->get('/dedicated_account/requery', [ - 'query' => $params + 'query' => $options->all() ]); return ResponseMediator::getContent($response); @@ -87,7 +106,9 @@ public function deactivate(string $dedicatedAccountId): array */ public function split(array $params): array { - $response = $this->httpClient->post('/dedicated_account/split', body: json_encode($params)); + $options = new DedicatedVirtualAccountOptions\SplitOptions($params); + + $response = $this->httpClient->post('/dedicated_account/split', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -100,7 +121,9 @@ public function split(array $params): array */ public function removeSplit(array $params): array { - $response = $this->httpClient->delete('/dedicated_account/split', body: json_encode($params)); + $options = new DedicatedVirtualAccountOptions\RemoveSplitOptions($params); + + $response = $this->httpClient->delete('/dedicated_account/split', body: json_encode($options->all())); return ResponseMediator::getContent($response); } diff --git a/src/API/DirectDebit.php b/src/API/DirectDebit.php index 471688f..dabbd41 100644 --- a/src/API/DirectDebit.php +++ b/src/API/DirectDebit.php @@ -4,18 +4,26 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\DirectDebit\{ + TriggerActivationChargeOptions, + ListMandateAuthorizationsOptions +}; class DirectDebit extends ApiAbstract { /** * Trigger an activation charge on pending mandates on behalf of your customers * - * @param array $params + * @param TriggerActivationChargeOptions|array $options * @return array */ - public function triggerActivationCharge(array $params): array + public function triggerActivationCharge(TriggerActivationChargeOptions|array $options): array { - $response = $this->httpClient->put('/directdebit/activation-charge', body: json_encode($params)); + if (is_array($options)) { + $options = new TriggerActivationChargeOptions($options); + } + + $response = $this->httpClient->put('/directdebit/activation-charge', body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -23,14 +31,19 @@ public function triggerActivationCharge(array $params): array /** * Get the list of direct debit mandates on your integration * - * @param array $params + * @param ListMandateAuthorizationsOptions|array $options * @return array */ - public function listMandateAuthorizations(array $params = []): array + public function listMandateAuthorizations(ListMandateAuthorizationsOptions|array $options = []): array { + if (is_array($options)) { + $options = new ListMandateAuthorizationsOptions($options); + } + $requestOptions = []; - if (!empty($params)) { - $requestOptions['query'] = $params; + $optionsArray = $options->all(); + if (!empty($optionsArray)) { + $requestOptions['query'] = $optionsArray; } $response = $this->httpClient->get('/directdebit/mandate-authorizations', $requestOptions); diff --git a/src/API/Dispute.php b/src/API/Dispute.php index e11c909..6260215 100644 --- a/src/API/Dispute.php +++ b/src/API/Dispute.php @@ -4,19 +4,31 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\Dispute\{ + ReadAllOptions, + UpdateOptions, + AddEvidenceOptions, + GetUploadUrlOptions, + ResolveOptions, + ExportOptions +}; class Dispute extends ApiAbstract { /** * List disputes filed against you * - * @param array $params + * @param ReadAllOptions|array $options * @return array */ - public function all(array $params = []): array + public function all(ReadAllOptions|array $options = []): array { + if (is_array($options)) { + $options = new ReadAllOptions($options); + } + $response = $this->httpClient->get('/dispute', [ - 'query' => $params + 'query' => $options->all() ]); return ResponseMediator::getContent($response); @@ -39,12 +51,16 @@ public function find(string $id): array * Update details of a dispute * * @param string $id - * @param array $params + * @param UpdateOptions|array $options * @return array */ - public function update(string $id, array $params): array + public function update(string $id, UpdateOptions|array $options): array { - $response = $this->httpClient->put("/dispute/{$id}", body: json_encode($params)); + if (is_array($options)) { + $options = new UpdateOptions($options); + } + + $response = $this->httpClient->put("/dispute/{$id}", body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -53,12 +69,16 @@ public function update(string $id, array $params): array * Add evidence to a dispute * * @param string $id - * @param array $params + * @param AddEvidenceOptions|array $options * @return array */ - public function addEvidence(string $id, array $params): array + public function addEvidence(string $id, AddEvidenceOptions|array $options): array { - $response = $this->httpClient->post("/dispute/{$id}/evidence", body: json_encode($params)); + if (is_array($options)) { + $options = new AddEvidenceOptions($options); + } + + $response = $this->httpClient->post("/dispute/{$id}/evidence", body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -67,12 +87,16 @@ public function addEvidence(string $id, array $params): array * Get upload URL for a dispute file * * @param string $id - * @param array $params + * @param GetUploadUrlOptions|array $options * @return array */ - public function getUploadUrl(string $id, array $params): array + public function getUploadUrl(string $id, GetUploadUrlOptions|array $options): array { - $response = $this->httpClient->post("/dispute/{$id}/upload_url", body: json_encode($params)); + if (is_array($options)) { + $options = new GetUploadUrlOptions($options); + } + + $response = $this->httpClient->post("/dispute/{$id}/upload_url", body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -81,12 +105,16 @@ public function getUploadUrl(string $id, array $params): array * Resolve a dispute * * @param string $id - * @param array $params + * @param ResolveOptions|array $options * @return array */ - public function resolve(string $id, array $params): array + public function resolve(string $id, ResolveOptions|array $options): array { - $response = $this->httpClient->put("/dispute/{$id}/resolve", body: json_encode($params)); + if (is_array($options)) { + $options = new ResolveOptions($options); + } + + $response = $this->httpClient->put("/dispute/{$id}/resolve", body: json_encode($options->all())); return ResponseMediator::getContent($response); } @@ -94,13 +122,17 @@ public function resolve(string $id, array $params): array /** * Export disputes * - * @param array $params + * @param ExportOptions|array $options * @return array */ - public function export(array $params = []): array + public function export(ExportOptions|array $options = []): array { + if (is_array($options)) { + $options = new ExportOptions($options); + } + $response = $this->httpClient->get('/dispute/export', [ - 'query' => $params + 'query' => $options->all() ]); return ResponseMediator::getContent($response); diff --git a/src/API/Integration.php b/src/API/Integration.php index adcb0bb..7acf981 100644 --- a/src/API/Integration.php +++ b/src/API/Integration.php @@ -4,6 +4,7 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\Integration\UpdateTimeoutOptions; class Integration extends ApiAbstract { @@ -22,12 +23,16 @@ public function fetchTimeout(): array /** * Update the payment session timeout on your integration * - * @param array $params + * @param UpdateTimeoutOptions|array $options * @return array */ - public function updateTimeout(array $params): array + public function updateTimeout(UpdateTimeoutOptions|array $options): array { - $response = $this->httpClient->put('/integration/payment_session_timeout', body: json_encode($params)); + if (is_array($options)) { + $options = new UpdateTimeoutOptions($options); + } + + $response = $this->httpClient->put('/integration/payment_session_timeout', body: json_encode($options->all())); return ResponseMediator::getContent($response); } diff --git a/src/API/Miscellaneous.php b/src/API/Miscellaneous.php index 792ae05..4d0562a 100644 --- a/src/API/Miscellaneous.php +++ b/src/API/Miscellaneous.php @@ -4,20 +4,29 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\Miscellaneous\{ + ListBanksOptions, + ListStatesOptions +}; class Miscellaneous extends ApiAbstract { /** * Get a list of all supported banks and their properties * - * @param array $params + * @param ListBanksOptions|array $options * @return array */ - public function listBanks(array $params = []): array + public function listBanks(ListBanksOptions|array $options = []): array { + if (is_array($options)) { + $options = new ListBanksOptions($options); + } + $requestOptions = []; - if (!empty($params)) { - $requestOptions['query'] = $params; + $optionsArray = $options->all(); + if (!empty($optionsArray)) { + $requestOptions['query'] = $optionsArray; } $response = $this->httpClient->get('/bank', $requestOptions); @@ -40,13 +49,17 @@ public function listCountries(): array /** * Get a list of states for a country for address verification * - * @param array $params + * @param ListStatesOptions|array $options * @return array */ - public function listStates(array $params): array + public function listStates(ListStatesOptions|array $options): array { + if (is_array($options)) { + $options = new ListStatesOptions($options); + } + $response = $this->httpClient->get('/address_verification/states', [ - 'query' => $params + 'query' => $options->all() ]); return ResponseMediator::getContent($response); diff --git a/src/API/Verification.php b/src/API/Verification.php index 2c854f8..c48ca80 100644 --- a/src/API/Verification.php +++ b/src/API/Verification.php @@ -4,19 +4,27 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; +use StarfolkSoftware\Paystack\Options\Verification\{ + ResolveAccountOptions, + ValidateAccountOptions +}; class Verification extends ApiAbstract { /** * Confirm an account belongs to the right customer * - * @param array $params + * @param ResolveAccountOptions|array $options * @return array */ - public function resolveAccount(array $params): array + public function resolveAccount(ResolveAccountOptions|array $options): array { + if (is_array($options)) { + $options = new ResolveAccountOptions($options); + } + $response = $this->httpClient->get('/bank/resolve', [ - 'query' => $params + 'query' => $options->all() ]); return ResponseMediator::getContent($response); @@ -25,12 +33,16 @@ public function resolveAccount(array $params): array /** * Confirm the authenticity of a customer's account number before sending money * - * @param array $params + * @param ValidateAccountOptions|array $options * @return array */ - public function validateAccount(array $params): array + public function validateAccount(ValidateAccountOptions|array $options): array { - $response = $this->httpClient->post('/bank/validate', body: json_encode($params)); + if (is_array($options)) { + $options = new ValidateAccountOptions($options); + } + + $response = $this->httpClient->post('/bank/validate', body: json_encode($options->all())); return ResponseMediator::getContent($response); } diff --git a/src/Options/ApplePay/ListDomainsOptions.php b/src/Options/ApplePay/ListDomainsOptions.php new file mode 100644 index 0000000..edf6147 --- /dev/null +++ b/src/Options/ApplePay/ListDomainsOptions.php @@ -0,0 +1,31 @@ +define('use_cursor') + ->allowedTypes('bool') + ->info('Use cursor pagination instead of classic pagination'); + + $resolver->define('next') + ->allowedTypes('string') + ->info('A cursor that indicates your place in the list. It can be used to fetch the next page of the list'); + + $resolver->define('previous') + ->allowedTypes('string') + ->info('A cursor that indicates your place in the list. It can be used to fetch the previous page of the list'); + } +} \ No newline at end of file diff --git a/src/Options/ApplePay/RegisterDomainOptions.php b/src/Options/ApplePay/RegisterDomainOptions.php new file mode 100644 index 0000000..017d672 --- /dev/null +++ b/src/Options/ApplePay/RegisterDomainOptions.php @@ -0,0 +1,24 @@ +define('domainName') + ->required() + ->allowedTypes('string') + ->info('Domain name to be registered'); + } +} \ No newline at end of file diff --git a/src/Options/ApplePay/UnregisterDomainOptions.php b/src/Options/ApplePay/UnregisterDomainOptions.php new file mode 100644 index 0000000..c6add3c --- /dev/null +++ b/src/Options/ApplePay/UnregisterDomainOptions.php @@ -0,0 +1,24 @@ +define('domainName') + ->required() + ->allowedTypes('string') + ->info('Domain name to be unregistered'); + } +} \ No newline at end of file diff --git a/src/Options/BulkCharge/GetChargesOptions.php b/src/Options/BulkCharge/GetChargesOptions.php new file mode 100644 index 0000000..6a1afef --- /dev/null +++ b/src/Options/BulkCharge/GetChargesOptions.php @@ -0,0 +1,40 @@ +define('status') + ->allowedTypes('string') + ->allowedValues('pending', 'success', 'failed') + ->info("Either one of these values: pending, success or failed"); + + $resolver->define('perPage') + ->allowedTypes('int') + ->info("Specify how many records you want to retrieve per page. If not specified, we use a default value of 50."); + + $resolver->define('page') + ->allowedTypes('int') + ->info("Specify exactly what transfer you want to page. If not specified, we use a default value of 1."); + + $resolver->define('from') + ->allowedTypes('datetime') + ->info("A timestamp from which to start listing charges e.g. 2016-09-24T00:00:05.000Z, 2016-09-21"); + + $resolver->define('to') + ->allowedTypes('datetime') + ->info("A timestamp at which to stop listing charges e.g. 2016-09-24T00:00:05.000Z, 2016-09-21"); + } +} \ No newline at end of file diff --git a/src/Options/BulkCharge/ReadAllOptions.php b/src/Options/BulkCharge/ReadAllOptions.php new file mode 100644 index 0000000..3490e10 --- /dev/null +++ b/src/Options/BulkCharge/ReadAllOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info("Specify how many records you want to retrieve per page. If not specified, we use a default value of 50."); + + $resolver->define('page') + ->allowedTypes('int') + ->info("Specify exactly what transfer you want to page. If not specified, we use a default value of 1."); + + $resolver->define('from') + ->allowedTypes('datetime') + ->info("A timestamp from which to start listing batches e.g. 2016-09-24T00:00:05.000Z, 2016-09-21"); + + $resolver->define('to') + ->allowedTypes('datetime') + ->info("A timestamp at which to stop listing batches e.g. 2016-09-24T00:00:05.000Z, 2016-09-21"); + } +} \ No newline at end of file diff --git a/src/Options/Charge/CreateOptions.php b/src/Options/Charge/CreateOptions.php index 02052f9..bd8c14b 100644 --- a/src/Options/Charge/CreateOptions.php +++ b/src/Options/Charge/CreateOptions.php @@ -23,13 +23,46 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->define('amount') ->required() + ->allowedTypes('string') + ->info('Amount in subunit of the supported currency'); + + $resolver->define('split_code') + ->allowedTypes('string') + ->info('The split code of a previously created split. e.g. SPL_98WF13Eb3w'); + + $resolver->define('subaccount') + ->allowedTypes('string') + ->info('The code for the subaccount that owns the payment. e.g. ACCT_8f4s1eq7ml6rlzj'); + + $resolver->define('transaction_charge') ->allowedTypes('int') - ->info('Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); + ->info('An amount used to override the split configuration for a single split payment'); + + $resolver->define('bearer') + ->allowedTypes('string') + ->allowedValues('account', 'subaccount') + ->info('Use this param to indicate who bears the transaction charges. Defaults to account'); $resolver->define('bank') ->allowedTypes('array') ->info('Bank account to charge (don\'t send if charging an authorization code)'); + $resolver->define('bank_transfer') + ->allowedTypes('array') + ->info('Takes the settings for the Pay with Transfer (PwT) channel'); + + $resolver->define('ussd') + ->allowedTypes('array') + ->info('USSD type to charge (don\'t send if charging an authorization code, bank or card)'); + + $resolver->define('mobile_money') + ->allowedTypes('array') + ->info('Mobile money details (don\'t send if charging an authorization code, bank or card)'); + + $resolver->define('qr') + ->allowedTypes('array') + ->info('Takes a provider parameter with the value set to: scan-to-pay'); + $resolver->define('authorization_code') ->allowedTypes('string') ->info('An authorization code to charge (don\'t send if charging a bank account)'); @@ -40,19 +73,11 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->define('metadata') ->allowedTypes('array') - ->info('A JSON object'); + ->info('Used for passing additional details for your post-payment processes'); $resolver->define('reference') ->allowedTypes('string') - ->info('Unique transaction reference. Only -, ., = and alphanumeric characters allowed.'); - - $resolver->define('ussd') - ->allowedTypes('array') - ->info('USSD type to charge (don\'t send if charging an authorization code, bank or card)'); - - $resolver->define('mobile_money') - ->allowedTypes('array') - ->info('Mobile money details (don\'t send if charging an authorization code, bank or card)'); + ->info('Unique transaction reference. Only -, ., = and alphanumeric characters allowed'); $resolver->define('device_id') ->allowedTypes('string') diff --git a/src/Options/Charge/SubmitAddressOptions.php b/src/Options/Charge/SubmitAddressOptions.php new file mode 100644 index 0000000..55b8063 --- /dev/null +++ b/src/Options/Charge/SubmitAddressOptions.php @@ -0,0 +1,44 @@ +define('address') + ->required() + ->allowedTypes('string') + ->info('Address submitted by user'); + + $resolver->define('reference') + ->required() + ->allowedTypes('string') + ->info('Reference for ongoing transaction'); + + $resolver->define('city') + ->required() + ->allowedTypes('string') + ->info('City submitted by user'); + + $resolver->define('state') + ->required() + ->allowedTypes('string') + ->info('State submitted by user'); + + $resolver->define('zipcode') + ->required() + ->allowedTypes('string') + ->info('Zipcode submitted by user'); + } +} \ No newline at end of file diff --git a/src/Options/Charge/SubmitBirthdayOptions.php b/src/Options/Charge/SubmitBirthdayOptions.php new file mode 100644 index 0000000..65ce1c0 --- /dev/null +++ b/src/Options/Charge/SubmitBirthdayOptions.php @@ -0,0 +1,29 @@ +define('birthday') + ->required() + ->allowedTypes('string') + ->info('Birthday submitted by user'); + + $resolver->define('reference') + ->required() + ->allowedTypes('string') + ->info('Reference for ongoing transaction'); + } +} \ No newline at end of file diff --git a/src/Options/Charge/SubmitOtpOptions.php b/src/Options/Charge/SubmitOtpOptions.php new file mode 100644 index 0000000..13c5811 --- /dev/null +++ b/src/Options/Charge/SubmitOtpOptions.php @@ -0,0 +1,29 @@ +define('otp') + ->required() + ->allowedTypes('string') + ->info('OTP submitted by user'); + + $resolver->define('reference') + ->required() + ->allowedTypes('string') + ->info('Reference for ongoing transaction'); + } +} \ No newline at end of file diff --git a/src/Options/Charge/SubmitPhoneOptions.php b/src/Options/Charge/SubmitPhoneOptions.php new file mode 100644 index 0000000..c0e2d1c --- /dev/null +++ b/src/Options/Charge/SubmitPhoneOptions.php @@ -0,0 +1,29 @@ +define('phone') + ->required() + ->allowedTypes('string') + ->info('Phone number submitted by user'); + + $resolver->define('reference') + ->required() + ->allowedTypes('string') + ->info('Reference for ongoing transaction'); + } +} \ No newline at end of file diff --git a/src/Options/Charge/SubmitPinOptions.php b/src/Options/Charge/SubmitPinOptions.php new file mode 100644 index 0000000..55eb6ab --- /dev/null +++ b/src/Options/Charge/SubmitPinOptions.php @@ -0,0 +1,29 @@ +define('pin') + ->required() + ->allowedTypes('string') + ->info('PIN submitted by user'); + + $resolver->define('reference') + ->required() + ->allowedTypes('string') + ->info('Reference for ongoing transaction'); + } +} \ No newline at end of file diff --git a/src/Options/DedicatedVirtualAccount/AssignOptions.php b/src/Options/DedicatedVirtualAccount/AssignOptions.php new file mode 100644 index 0000000..7f9ac8a --- /dev/null +++ b/src/Options/DedicatedVirtualAccount/AssignOptions.php @@ -0,0 +1,69 @@ +define('email') + ->required() + ->allowedTypes('string') + ->info('Customer email address'); + + $resolver->define('first_name') + ->required() + ->allowedTypes('string') + ->info('Customer\'s first name'); + + $resolver->define('last_name') + ->required() + ->allowedTypes('string') + ->info('Customer\'s last name'); + + $resolver->define('phone') + ->required() + ->allowedTypes('string') + ->info('Customer\'s phone number'); + + $resolver->define('preferred_bank') + ->required() + ->allowedTypes('string') + ->info('The bank slug for preferred bank. To get a list of available banks, use the List Providers endpoint'); + + $resolver->define('country') + ->required() + ->allowedTypes('string') + ->info('Currently accepts NG and GH only'); + + $resolver->define('account_number') + ->allowedTypes('string') + ->info('Customer\'s account number'); + + $resolver->define('bvn') + ->allowedTypes('string') + ->info('Customer\'s Bank Verification Number (Nigeria only)'); + + $resolver->define('bank_code') + ->allowedTypes('string') + ->info('Customer\'s bank code'); + + $resolver->define('subaccount') + ->allowedTypes('string') + ->info('Subaccount code of the account you want to split the transaction with'); + + $resolver->define('split_code') + ->allowedTypes('string') + ->info('Split code consisting of the lists of accounts you want to split the transaction with'); + } +} \ No newline at end of file diff --git a/src/Options/DedicatedVirtualAccount/RemoveSplitOptions.php b/src/Options/DedicatedVirtualAccount/RemoveSplitOptions.php new file mode 100644 index 0000000..9f5d013 --- /dev/null +++ b/src/Options/DedicatedVirtualAccount/RemoveSplitOptions.php @@ -0,0 +1,32 @@ +define('account_number') + ->required() + ->allowedTypes('string') + ->info('Dedicated virtual account number'); + + $resolver->define('subaccount') + ->allowedTypes('string') + ->info('Subaccount code of the account you want to remove from split the transaction with'); + + $resolver->define('split_code') + ->allowedTypes('string') + ->info('Split code consisting of the lists of accounts you want to remove from split the transaction with'); + } +} \ No newline at end of file diff --git a/src/Options/DedicatedVirtualAccount/RequeryOptions.php b/src/Options/DedicatedVirtualAccount/RequeryOptions.php new file mode 100644 index 0000000..eb96bc8 --- /dev/null +++ b/src/Options/DedicatedVirtualAccount/RequeryOptions.php @@ -0,0 +1,33 @@ +define('account_number') + ->required() + ->allowedTypes('string') + ->info('Virtual account number to requery'); + + $resolver->define('provider_slug') + ->required() + ->allowedTypes('string') + ->info('The bank provider slug for the dedicated virtual account'); + + $resolver->define('date') + ->allowedTypes('string') + ->info('The day the DVA transaction occurred'); + } +} \ No newline at end of file diff --git a/src/Options/DedicatedVirtualAccount/SplitOptions.php b/src/Options/DedicatedVirtualAccount/SplitOptions.php new file mode 100644 index 0000000..7fefa3b --- /dev/null +++ b/src/Options/DedicatedVirtualAccount/SplitOptions.php @@ -0,0 +1,36 @@ +define('customer') + ->required() + ->allowedTypes('string') + ->info('Customer ID or code'); + + $resolver->define('subaccount') + ->allowedTypes('string') + ->info('Subaccount code of the account you want to split the transaction with'); + + $resolver->define('split_code') + ->allowedTypes('string') + ->info('Split code consisting of the lists of accounts you want to split the transaction with'); + + $resolver->define('preferred_bank') + ->allowedTypes('string') + ->info('The bank slug for preferred bank. To get a list of available banks, use the List Providers endpoint'); + } +} \ No newline at end of file diff --git a/src/Options/DirectDebit/ListMandateAuthorizationsOptions.php b/src/Options/DirectDebit/ListMandateAuthorizationsOptions.php new file mode 100644 index 0000000..5314b5f --- /dev/null +++ b/src/Options/DirectDebit/ListMandateAuthorizationsOptions.php @@ -0,0 +1,35 @@ +define('perPage') + ->allowedTypes('int') + ->info('Specify how many records you want to retrieve per page. If not specify we use a default value of 50.'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('Specify exactly what page you want to retrieve. If not specify we use a default value of 1.'); + + $resolver->define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing mandates e.g. 2016-09-24T00:00:05.000Z, 2016-09-21'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing mandates e.g. 2016-09-24T00:00:05.000Z, 2016-09-21'); + } +} \ No newline at end of file diff --git a/src/Options/DirectDebit/TriggerActivationChargeOptions.php b/src/Options/DirectDebit/TriggerActivationChargeOptions.php new file mode 100644 index 0000000..92d97de --- /dev/null +++ b/src/Options/DirectDebit/TriggerActivationChargeOptions.php @@ -0,0 +1,42 @@ +define('authorization_code') + ->required() + ->allowedTypes('string') + ->info('Authorization code of the authorization that was returned to you following successful payment'); + + $resolver->define('amount') + ->required() + ->allowedTypes('int') + ->info('Amount that should be charged (kobo, kobo, pesewas, or cents)'); + + $resolver->define('currency') + ->allowedTypes('string') + ->allowedValues(['NGN', 'GHS', 'ZAR', 'USD']) + ->info('The currency in which to charge. Allowed values are: NGN, GHS, ZAR or USD'); + + $resolver->define('email') + ->allowedTypes('string') + ->info('Customer\'s email address (used when insufficient information from authorization code)'); + + $resolver->define('reference') + ->allowedTypes('string') + ->info('Unique transaction reference'); + } +} \ No newline at end of file diff --git a/src/Options/Dispute/AddEvidenceOptions.php b/src/Options/Dispute/AddEvidenceOptions.php new file mode 100644 index 0000000..f0014d9 --- /dev/null +++ b/src/Options/Dispute/AddEvidenceOptions.php @@ -0,0 +1,47 @@ +define('customer_email') + ->required() + ->allowedTypes('string') + ->info('Customer email'); + + $resolver->define('customer_name') + ->required() + ->allowedTypes('string') + ->info('Customer name'); + + $resolver->define('customer_phone') + ->required() + ->allowedTypes('string') + ->info('Customer phone'); + + $resolver->define('service_details') + ->required() + ->allowedTypes('string') + ->info('Details of service involved'); + + $resolver->define('delivery_address') + ->allowedTypes('string') + ->info('Delivery address'); + + $resolver->define('delivery_date') + ->allowedTypes('string') + ->info('ISO 8601 representation of delivery date'); + } +} \ No newline at end of file diff --git a/src/Options/Dispute/ExportOptions.php b/src/Options/Dispute/ExportOptions.php new file mode 100644 index 0000000..dd4614f --- /dev/null +++ b/src/Options/Dispute/ExportOptions.php @@ -0,0 +1,44 @@ +define('from') + ->allowedTypes('string') + ->info('A timestamp from which to start listing dispute e.g. 2016-09-24T00:00:05.000Z, 2016-09-21'); + + $resolver->define('to') + ->allowedTypes('string') + ->info('A timestamp at which to stop listing dispute e.g. 2016-09-24T00:00:05.000Z, 2016-09-21'); + + $resolver->define('perPage') + ->allowedTypes('int') + ->info('Specify how many records you want to retrieve per page. If not specify we use a default value of 50.'); + + $resolver->define('page') + ->allowedTypes('int') + ->info('Specify exactly what dispute you want to page. If not specify we use a default value of 1.'); + + $resolver->define('transaction') + ->allowedTypes('string') + ->info('Transaction ID'); + + $resolver->define('status') + ->allowedTypes('string') + ->allowedValues(['awaiting-merchant-feedback', 'awaiting-bank-feedback', 'pending', 'resolved']) + ->info('Dispute Status. Accepted values: awaiting-merchant-feedback, awaiting-bank-feedback, pending, resolved'); + } +} \ No newline at end of file diff --git a/src/Options/Dispute/GetUploadUrlOptions.php b/src/Options/Dispute/GetUploadUrlOptions.php new file mode 100644 index 0000000..eb2eff8 --- /dev/null +++ b/src/Options/Dispute/GetUploadUrlOptions.php @@ -0,0 +1,24 @@ +define('upload_filename') + ->required() + ->allowedTypes('string') + ->info('The file name, with its extension, that you want to upload. e.g logo.png'); + } +} \ No newline at end of file diff --git a/src/Options/Dispute/ResolveOptions.php b/src/Options/Dispute/ResolveOptions.php new file mode 100644 index 0000000..74f2520 --- /dev/null +++ b/src/Options/Dispute/ResolveOptions.php @@ -0,0 +1,42 @@ +define('resolution') + ->required() + ->allowedTypes('string') + ->allowedValues(['merchant-accepted', 'declined']) + ->info('Dispute resolution. Accepted values: merchant-accepted, declined'); + + $resolver->define('message') + ->required() + ->allowedTypes('string') + ->info('Reason for resolution'); + + $resolver->define('refund_amount') + ->allowedTypes('int') + ->info('The amount to refund, in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); + + $resolver->define('uploaded_filename') + ->allowedTypes('string') + ->info('Filename of attachment returned via response from upload url'); + + $resolver->define('evidence') + ->allowedTypes('int') + ->info('Evidence Id for fraud claims'); + } +} \ No newline at end of file diff --git a/src/Options/Dispute/UpdateOptions.php b/src/Options/Dispute/UpdateOptions.php new file mode 100644 index 0000000..ca1fb93 --- /dev/null +++ b/src/Options/Dispute/UpdateOptions.php @@ -0,0 +1,28 @@ +define('refund_amount') + ->required() + ->allowedTypes('int') + ->info('The amount to refund, in the subunit of the supported currency'); + + $resolver->define('uploaded_filename') + ->allowedTypes('string') + ->info('filename of attachment returned via response from upload url(GET /dispute/:id/upload_url)'); + } +} \ No newline at end of file diff --git a/src/Options/Integration/UpdateTimeoutOptions.php b/src/Options/Integration/UpdateTimeoutOptions.php new file mode 100644 index 0000000..4829108 --- /dev/null +++ b/src/Options/Integration/UpdateTimeoutOptions.php @@ -0,0 +1,24 @@ +define('timeout') + ->required() + ->allowedTypes('int') + ->info('Time before stopping session in seconds. Set to 0 to cancel session timeouts'); + } +} \ No newline at end of file diff --git a/src/Options/Miscellaneous/ListBanksOptions.php b/src/Options/Miscellaneous/ListBanksOptions.php new file mode 100644 index 0000000..0f4ce4b --- /dev/null +++ b/src/Options/Miscellaneous/ListBanksOptions.php @@ -0,0 +1,39 @@ +define('country') + ->allowedTypes('string') + ->info('The country from which to obtain the list of supported banks. e.g country=ghana or country=nigeria'); + + $resolver->define('use_cursor') + ->allowedTypes('bool') + ->info('Use cursor pagination instead of classic pagination'); + + $resolver->define('perPage') + ->allowedTypes('int') + ->info('Specify how many records you want to retrieve per page. If not specify we use a default value of 50.'); + + $resolver->define('next') + ->allowedTypes('string') + ->info('A cursor that indicates your place in the list. It can be used to fetch the next page of the list'); + + $resolver->define('previous') + ->allowedTypes('string') + ->info('A cursor that indicates your place in the list. It can be used to fetch the previous page of the list'); + } +} \ No newline at end of file diff --git a/src/Options/Miscellaneous/ListStatesOptions.php b/src/Options/Miscellaneous/ListStatesOptions.php new file mode 100644 index 0000000..7b76901 --- /dev/null +++ b/src/Options/Miscellaneous/ListStatesOptions.php @@ -0,0 +1,24 @@ +define('country') + ->required() + ->allowedTypes('string') + ->info('The country code of the country whose states you want to retrieve. e.g. country=NG'); + } +} \ No newline at end of file diff --git a/src/Options/Verification/ResolveAccountOptions.php b/src/Options/Verification/ResolveAccountOptions.php new file mode 100644 index 0000000..2e6126b --- /dev/null +++ b/src/Options/Verification/ResolveAccountOptions.php @@ -0,0 +1,29 @@ +define('account_number') + ->required() + ->allowedTypes('string') + ->info('Account Number'); + + $resolver->define('bank_code') + ->required() + ->allowedTypes('string') + ->info('Bank Code. You can get the list of Bank Codes by calling the List Banks endpoint.'); + } +} \ No newline at end of file diff --git a/src/Options/Verification/ValidateAccountOptions.php b/src/Options/Verification/ValidateAccountOptions.php new file mode 100644 index 0000000..d7dff28 --- /dev/null +++ b/src/Options/Verification/ValidateAccountOptions.php @@ -0,0 +1,55 @@ +define('bank_code') + ->required() + ->allowedTypes('string') + ->info('Bank Code. You can get the list of Bank Codes by calling the List Banks endpoint.'); + + $resolver->define('country_code') + ->required() + ->allowedTypes('string') + ->info('The country code for the bank e.g., GH for Ghana, NG for Nigeria, ZA for South Africa, etc'); + + $resolver->define('account_number') + ->required() + ->allowedTypes('string') + ->info('Account Number'); + + $resolver->define('account_name') + ->required() + ->allowedTypes('string') + ->info('Account Name'); + + $resolver->define('account_type') + ->required() + ->allowedTypes('string') + ->allowedValues(['personal', 'business']) + ->info('Account Type. personal for personal accounts, business for business accounts'); + + $resolver->define('document_type') + ->required() + ->allowedTypes('string') + ->allowedValues(['identityNumber', 'passportNumber', 'businessRegistrationNumber']) + ->info('Document Type. identityNumber for identity number, passportNumber for passport number, businessRegistrationNumber for business registration number'); + + $resolver->define('document_number') + ->allowedTypes('string') + ->info('Document Number'); + } +} \ No newline at end of file From c922d16f8e068fc0077a4e8c84798325b3a1cdb2 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 15:10:10 +0100 Subject: [PATCH 08/17] refactor: Update API options to accept objects or arrays for BulkCharge and Charge; enhance test cases for improved clarity and structure --- src/API/BulkCharge.php | 24 +++++---- src/API/Charge.php | 48 ++++++++++------- src/Options/ApplePay/ListDomainsOptions.php | 30 +++++++---- src/Options/Charge/CreateOptions.php | 4 ++ .../TriggerActivationChargeOptions.php | 5 +- src/Options/Dispute/ResolveOptions.php | 31 ++++------- .../Verification/ValidateAccountOptions.php | 48 +++++------------ tests/ApplePayTest.php | 14 +++-- tests/BulkChargeTest.php | 54 +++++++++++-------- tests/ChargeTest.php | 26 ++++++--- tests/DirectDebitTest.php | 16 ++++-- tests/DisputeTest.php | 32 ++++++++--- tests/IntegrationTest.php | 4 +- tests/MiscellaneousTest.php | 10 +++- tests/VerificationTest.php | 10 +++- 15 files changed, 215 insertions(+), 141 deletions(-) diff --git a/src/API/BulkCharge.php b/src/API/BulkCharge.php index 139f62f..58dd5e0 100644 --- a/src/API/BulkCharge.php +++ b/src/API/BulkCharge.php @@ -11,12 +11,14 @@ class BulkCharge extends ApiAbstract /** * Send an array of objects with authorization codes and amount in kobo so we can process transactions as a batch * - * @param array $params + * @param BulkChargeOptions\InitiateOptions|array $options * @return array */ - public function initiate(array $params): array + public function initiate(BulkChargeOptions\InitiateOptions|array $options): array { - $options = new BulkChargeOptions\InitiateOptions($params); + if (is_array($options)) { + $options = new BulkChargeOptions\InitiateOptions($options); + } $response = $this->httpClient->post('/bulkcharge', body: json_encode($options->all())); @@ -26,12 +28,14 @@ public function initiate(array $params): array /** * List bulk charge batches created by the integration * - * @param array $params + * @param BulkChargeOptions\ReadAllOptions|array $options * @return array */ - public function all(array $params = []): array + public function all(BulkChargeOptions\ReadAllOptions|array $options = []): array { - $options = new BulkChargeOptions\ReadAllOptions($params); + if (is_array($options)) { + $options = new BulkChargeOptions\ReadAllOptions($options); + } $response = $this->httpClient->get('/bulkcharge', [ 'query' => $options->all() @@ -57,12 +61,14 @@ public function find(string $idOrCode): array * Retrieve the charges associated with a specified batch code * * @param string $idOrCode - * @param array $params + * @param BulkChargeOptions\GetChargesOptions|array $options * @return array */ - public function getCharges(string $idOrCode, array $params = []): array + public function getCharges(string $idOrCode, BulkChargeOptions\GetChargesOptions|array $options = []): array { - $options = new BulkChargeOptions\GetChargesOptions($params); + if (is_array($options)) { + $options = new BulkChargeOptions\GetChargesOptions($options); + } $response = $this->httpClient->get("/bulkcharge/{$idOrCode}/charges", [ 'query' => $options->all() diff --git a/src/API/Charge.php b/src/API/Charge.php index 9616f0a..ad8ad98 100644 --- a/src/API/Charge.php +++ b/src/API/Charge.php @@ -11,12 +11,14 @@ class Charge extends ApiAbstract /** * Initiate a payment by integrating the payment channel of choice * - * @param array $params + * @param ChargeOptions\CreateOptions|array $options * @return array */ - public function create(array $params): array + public function create(ChargeOptions\CreateOptions|array $options): array { - $options = new ChargeOptions\CreateOptions($params); + if (is_array($options)) { + $options = new ChargeOptions\CreateOptions($options); + } $response = $this->httpClient->post('/charge', body: json_encode($options->all())); @@ -26,12 +28,14 @@ public function create(array $params): array /** * Submit PIN to continue a charge * - * @param array $params + * @param ChargeOptions\SubmitPinOptions|array $options * @return array */ - public function submitPin(array $params): array + public function submitPin(ChargeOptions\SubmitPinOptions|array $options): array { - $options = new ChargeOptions\SubmitPinOptions($params); + if (is_array($options)) { + $options = new ChargeOptions\SubmitPinOptions($options); + } $response = $this->httpClient->post('/charge/submit_pin', body: json_encode($options->all())); @@ -41,12 +45,14 @@ public function submitPin(array $params): array /** * Submit OTP to complete a charge * - * @param array $params + * @param ChargeOptions\SubmitOtpOptions|array $options * @return array */ - public function submitOtp(array $params): array + public function submitOtp(ChargeOptions\SubmitOtpOptions|array $options): array { - $options = new ChargeOptions\SubmitOtpOptions($params); + if (is_array($options)) { + $options = new ChargeOptions\SubmitOtpOptions($options); + } $response = $this->httpClient->post('/charge/submit_otp', body: json_encode($options->all())); @@ -56,12 +62,14 @@ public function submitOtp(array $params): array /** * Submit phone when requested * - * @param array $params + * @param ChargeOptions\SubmitPhoneOptions|array $options * @return array */ - public function submitPhone(array $params): array + public function submitPhone(ChargeOptions\SubmitPhoneOptions|array $options): array { - $options = new ChargeOptions\SubmitPhoneOptions($params); + if (is_array($options)) { + $options = new ChargeOptions\SubmitPhoneOptions($options); + } $response = $this->httpClient->post('/charge/submit_phone', body: json_encode($options->all())); @@ -71,12 +79,14 @@ public function submitPhone(array $params): array /** * Submit birthday when requested * - * @param array $params + * @param ChargeOptions\SubmitBirthdayOptions|array $options * @return array */ - public function submitBirthday(array $params): array + public function submitBirthday(ChargeOptions\SubmitBirthdayOptions|array $options): array { - $options = new ChargeOptions\SubmitBirthdayOptions($params); + if (is_array($options)) { + $options = new ChargeOptions\SubmitBirthdayOptions($options); + } $response = $this->httpClient->post('/charge/submit_birthday', body: json_encode($options->all())); @@ -86,12 +96,14 @@ public function submitBirthday(array $params): array /** * Submit address to continue charge * - * @param array $params + * @param ChargeOptions\SubmitAddressOptions|array $options * @return array */ - public function submitAddress(array $params): array + public function submitAddress(ChargeOptions\SubmitAddressOptions|array $options): array { - $options = new ChargeOptions\SubmitAddressOptions($params); + if (is_array($options)) { + $options = new ChargeOptions\SubmitAddressOptions($options); + } $response = $this->httpClient->post('/charge/submit_address', body: json_encode($options->all())); diff --git a/src/Options/ApplePay/ListDomainsOptions.php b/src/Options/ApplePay/ListDomainsOptions.php index edf6147..15227e9 100644 --- a/src/Options/ApplePay/ListDomainsOptions.php +++ b/src/Options/ApplePay/ListDomainsOptions.php @@ -16,16 +16,26 @@ class ListDomainsOptions extends OptionsAbstract */ public function configureOptions(OptionsResolver $resolver): void { - $resolver->define('use_cursor') - ->allowedTypes('bool') - ->info('Use cursor pagination instead of classic pagination'); - - $resolver->define('next') - ->allowedTypes('string') - ->info('A cursor that indicates your place in the list. It can be used to fetch the next page of the list'); + $resolver->setDefined(['use_cursor', 'next', 'previous']); + $resolver->setAllowedTypes('use_cursor', ['bool', 'string']); + $resolver->setAllowedTypes('next', 'string'); + $resolver->setAllowedTypes('previous', 'string'); + } - $resolver->define('previous') - ->allowedTypes('string') - ->info('A cursor that indicates your place in the list. It can be used to fetch the previous page of the list'); + /** + * Get the options converted for HTTP transmission. + * + * @return array + */ + public function all(): array + { + $options = parent::all(); + + // Convert boolean to string for HTTP query parameters + if (isset($options['use_cursor'])) { + $options['use_cursor'] = $options['use_cursor'] ? 'true' : 'false'; + } + + return $options; } } \ No newline at end of file diff --git a/src/Options/Charge/CreateOptions.php b/src/Options/Charge/CreateOptions.php index bd8c14b..c620ef5 100644 --- a/src/Options/Charge/CreateOptions.php +++ b/src/Options/Charge/CreateOptions.php @@ -47,6 +47,10 @@ public function configureOptions(OptionsResolver $resolver): void ->allowedTypes('array') ->info('Bank account to charge (don\'t send if charging an authorization code)'); + $resolver->define('card') + ->allowedTypes('array') + ->info('Card details to charge (don\'t send if charging an authorization code)'); + $resolver->define('bank_transfer') ->allowedTypes('array') ->info('Takes the settings for the Pay with Transfer (PwT) channel'); diff --git a/src/Options/DirectDebit/TriggerActivationChargeOptions.php b/src/Options/DirectDebit/TriggerActivationChargeOptions.php index 92d97de..e394f95 100644 --- a/src/Options/DirectDebit/TriggerActivationChargeOptions.php +++ b/src/Options/DirectDebit/TriggerActivationChargeOptions.php @@ -17,7 +17,6 @@ class TriggerActivationChargeOptions extends OptionsAbstract public function configureOptions(OptionsResolver $resolver): void { $resolver->define('authorization_code') - ->required() ->allowedTypes('string') ->info('Authorization code of the authorization that was returned to you following successful payment'); @@ -38,5 +37,9 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->define('reference') ->allowedTypes('string') ->info('Unique transaction reference'); + + $resolver->define('mandate_code') + ->allowedTypes('string') + ->info('Mandate code for direct debit'); } } \ No newline at end of file diff --git a/src/Options/Dispute/ResolveOptions.php b/src/Options/Dispute/ResolveOptions.php index 74f2520..11f781a 100644 --- a/src/Options/Dispute/ResolveOptions.php +++ b/src/Options/Dispute/ResolveOptions.php @@ -16,27 +16,16 @@ class ResolveOptions extends OptionsAbstract */ public function configureOptions(OptionsResolver $resolver): void { - $resolver->define('resolution') - ->required() - ->allowedTypes('string') - ->allowedValues(['merchant-accepted', 'declined']) - ->info('Dispute resolution. Accepted values: merchant-accepted, declined'); + $resolver->setRequired(['resolution', 'message']); + + $resolver->setAllowedTypes('resolution', 'string'); + $resolver->setAllowedValues('resolution', ['merchant-accepted', 'declined']); + + $resolver->setAllowedTypes('message', 'string'); - $resolver->define('message') - ->required() - ->allowedTypes('string') - ->info('Reason for resolution'); - - $resolver->define('refund_amount') - ->allowedTypes('int') - ->info('The amount to refund, in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR'); - - $resolver->define('uploaded_filename') - ->allowedTypes('string') - ->info('Filename of attachment returned via response from upload url'); - - $resolver->define('evidence') - ->allowedTypes('int') - ->info('Evidence Id for fraud claims'); + $resolver->setDefined(['refund_amount', 'uploaded_filename', 'evidence']); + $resolver->setAllowedTypes('refund_amount', 'int'); + $resolver->setAllowedTypes('uploaded_filename', 'string'); + $resolver->setAllowedTypes('evidence', 'int'); } } \ No newline at end of file diff --git a/src/Options/Verification/ValidateAccountOptions.php b/src/Options/Verification/ValidateAccountOptions.php index d7dff28..995607d 100644 --- a/src/Options/Verification/ValidateAccountOptions.php +++ b/src/Options/Verification/ValidateAccountOptions.php @@ -16,40 +16,18 @@ class ValidateAccountOptions extends OptionsAbstract */ public function configureOptions(OptionsResolver $resolver): void { - $resolver->define('bank_code') - ->required() - ->allowedTypes('string') - ->info('Bank Code. You can get the list of Bank Codes by calling the List Banks endpoint.'); - - $resolver->define('country_code') - ->required() - ->allowedTypes('string') - ->info('The country code for the bank e.g., GH for Ghana, NG for Nigeria, ZA for South Africa, etc'); - - $resolver->define('account_number') - ->required() - ->allowedTypes('string') - ->info('Account Number'); - - $resolver->define('account_name') - ->required() - ->allowedTypes('string') - ->info('Account Name'); - - $resolver->define('account_type') - ->required() - ->allowedTypes('string') - ->allowedValues(['personal', 'business']) - ->info('Account Type. personal for personal accounts, business for business accounts'); - - $resolver->define('document_type') - ->required() - ->allowedTypes('string') - ->allowedValues(['identityNumber', 'passportNumber', 'businessRegistrationNumber']) - ->info('Document Type. identityNumber for identity number, passportNumber for passport number, businessRegistrationNumber for business registration number'); - - $resolver->define('document_number') - ->allowedTypes('string') - ->info('Document Number'); + $resolver->setRequired(['bank_code', 'country_code', 'account_number', 'account_name', 'account_type', 'document_type']); + + $resolver->setAllowedTypes('bank_code', 'string'); + $resolver->setAllowedTypes('country_code', 'string'); + $resolver->setAllowedTypes('account_number', 'string'); + $resolver->setAllowedTypes('account_name', 'string'); + $resolver->setAllowedTypes('account_type', 'string'); + $resolver->setAllowedValues('account_type', ['personal', 'business']); + $resolver->setAllowedTypes('document_type', 'string'); + $resolver->setAllowedValues('document_type', ['identityNumber', 'passportNumber', 'businessRegistrationNumber']); + + $resolver->setDefined(['document_number']); + $resolver->setAllowedTypes('document_number', 'string'); } } \ No newline at end of file diff --git a/tests/ApplePayTest.php b/tests/ApplePayTest.php index d4cc3a1..95f23f0 100644 --- a/tests/ApplePayTest.php +++ b/tests/ApplePayTest.php @@ -4,6 +4,11 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\ApplePay\{ + RegisterDomainOptions, + ListDomainsOptions, + UnregisterDomainOptions +}; final class ApplePayTest extends TestCase { @@ -31,9 +36,10 @@ public function testRegisterDomain(): void $this->mockClient->addResponse($response); - $data = $this->client()->applePay->registerDomain([ + $options = new RegisterDomainOptions([ 'domainName' => 'example.com' ]); + $data = $this->client()->applePay->registerDomain($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -102,7 +108,8 @@ public function testListDomainsWithParams(): void $this->mockClient->addResponse($response); - $data = $this->client()->applePay->listDomains(['use_cursor' => 'false']); + $options = new ListDomainsOptions(['use_cursor' => false]); + $data = $this->client()->applePay->listDomains($options); $sentRequest = $this->mockClient->getLastRequest(); @@ -134,9 +141,10 @@ public function testUnregisterDomain(): void $this->mockClient->addResponse($response); - $data = $this->client()->applePay->unregisterDomain([ + $options = new UnregisterDomainOptions([ 'domainName' => 'old.example.com' ]); + $data = $this->client()->applePay->unregisterDomain($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); diff --git a/tests/BulkChargeTest.php b/tests/BulkChargeTest.php index d488c5d..d429be5 100644 --- a/tests/BulkChargeTest.php +++ b/tests/BulkChargeTest.php @@ -4,6 +4,11 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\BulkCharge\{ + InitiateOptions, + ReadAllOptions, + GetChargesOptions +}; final class BulkChargeTest extends TestCase { @@ -29,15 +34,17 @@ public function testInitiateBulkCharge(): void ]; $expectedBody = json_encode([ - [ - 'authorization' => 'AUTH_6tmt288t0o', - 'amount' => 50000, - 'reference' => 'bulk_ref_001' - ], - [ - 'authorization' => 'AUTH_abc123def456', - 'amount' => 75000, - 'reference' => 'bulk_ref_002' + 'charges' => [ + [ + 'authorization' => 'AUTH_6tmt288t0o', + 'amount' => 50000, + 'reference' => 'bulk_ref_001' + ], + [ + 'authorization' => 'AUTH_abc123def456', + 'amount' => 75000, + 'reference' => 'bulk_ref_002' + ] ] ]); @@ -49,18 +56,21 @@ public function testInitiateBulkCharge(): void $this->mockClient->addResponse($response); - $data = $this->client()->bulkCharges->initiate([ - [ - 'authorization' => 'AUTH_6tmt288t0o', - 'amount' => 50000, - 'reference' => 'bulk_ref_001' - ], - [ - 'authorization' => 'AUTH_abc123def456', - 'amount' => 75000, - 'reference' => 'bulk_ref_002' + $options = new InitiateOptions([ + 'charges' => [ + [ + 'authorization' => 'AUTH_6tmt288t0o', + 'amount' => 50000, + 'reference' => 'bulk_ref_001' + ], + [ + 'authorization' => 'AUTH_abc123def456', + 'amount' => 75000, + 'reference' => 'bulk_ref_002' + ] ] ]); + $data = $this->client()->bulkCharges->initiate($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -105,7 +115,8 @@ public function testListBulkCharges(): void $this->mockClient->addResponse($response); - $data = $this->client()->bulkCharges->all(['page' => 1, 'perPage' => 50]); + $options = new ReadAllOptions(['page' => 1, 'perPage' => 50]); + $data = $this->client()->bulkCharges->all($options); $sentRequest = $this->mockClient->getLastRequest(); @@ -208,7 +219,8 @@ public function testGetBulkChargeCharges(): void $this->mockClient->addResponse($response); - $data = $this->client()->bulkCharges->getCharges('BCH_abc123def456', ['page' => 1]); + $options = new GetChargesOptions(['page' => 1]); + $data = $this->client()->bulkCharges->getCharges('BCH_abc123def456', $options); $sentRequest = $this->mockClient->getLastRequest(); diff --git a/tests/ChargeTest.php b/tests/ChargeTest.php index b63c7bf..00238c9 100644 --- a/tests/ChargeTest.php +++ b/tests/ChargeTest.php @@ -4,6 +4,14 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\Charge\{ + CreateOptions, + SubmitPinOptions, + SubmitOtpOptions, + SubmitPhoneOptions, + SubmitBirthdayOptions, + SubmitAddressOptions +}; final class ChargeTest extends TestCase { @@ -44,7 +52,7 @@ public function testCreateCharge(): void $this->mockClient->addResponse($response); - $data = $this->client()->charges->create([ + $options = new CreateOptions([ 'email' => 'customer@email.com', 'amount' => '10000', 'card' => [ @@ -54,6 +62,7 @@ public function testCreateCharge(): void 'expiry_year' => '2030' ] ]); + $data = $this->client()->charges->create($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -90,10 +99,11 @@ public function testSubmitPin(): void $this->mockClient->addResponse($response); - $data = $this->client()->charges->submitPin([ + $options = new SubmitPinOptions([ 'pin' => '1234', 'reference' => 'T563902343_1628168464' ]); + $data = $this->client()->charges->submitPin($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -132,10 +142,11 @@ public function testSubmitOtp(): void $this->mockClient->addResponse($response); - $data = $this->client()->charges->submitOtp([ + $options = new SubmitOtpOptions([ 'otp' => '123456', 'reference' => 'T563902343_1628168464' ]); + $data = $this->client()->charges->submitOtp($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -171,10 +182,11 @@ public function testSubmitPhone(): void $this->mockClient->addResponse($response); - $data = $this->client()->charges->submitPhone([ + $options = new SubmitPhoneOptions([ 'phone' => '+2348012345678', 'reference' => 'T563902343_1628168464' ]); + $data = $this->client()->charges->submitPhone($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -210,10 +222,11 @@ public function testSubmitBirthday(): void $this->mockClient->addResponse($response); - $data = $this->client()->charges->submitBirthday([ + $options = new SubmitBirthdayOptions([ 'birthday' => '1990-01-01', 'reference' => 'T563902343_1628168464' ]); + $data = $this->client()->charges->submitBirthday($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -252,13 +265,14 @@ public function testSubmitAddress(): void $this->mockClient->addResponse($response); - $data = $this->client()->charges->submitAddress([ + $options = new SubmitAddressOptions([ 'address' => '123 Main Street', 'city' => 'Lagos', 'state' => 'Lagos', 'zipcode' => '100001', 'reference' => 'T563902343_1628168464' ]); + $data = $this->client()->charges->submitAddress($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); diff --git a/tests/DirectDebitTest.php b/tests/DirectDebitTest.php index e1b30c0..4320ed9 100644 --- a/tests/DirectDebitTest.php +++ b/tests/DirectDebitTest.php @@ -4,6 +4,10 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\DirectDebit\{ + TriggerActivationChargeOptions, + ListMandateAuthorizationsOptions +}; final class DirectDebitTest extends TestCase { @@ -41,7 +45,8 @@ public function testTriggerActivationCharge(): void ]; $expectedBody = json_encode([ - 'mandate_code' => 'MANDATE_abc123def456' + 'mandate_code' => 'MANDATE_abc123def456', + 'amount' => 10000 ]); $stream = new Stream('php://memory', 'r+'); @@ -52,9 +57,11 @@ public function testTriggerActivationCharge(): void $this->mockClient->addResponse($response); - $data = $this->client()->directDebit->triggerActivationCharge([ - 'mandate_code' => 'MANDATE_abc123def456' + $options = new TriggerActivationChargeOptions([ + 'mandate_code' => 'MANDATE_abc123def456', + 'amount' => 10000 ]); + $data = $this->client()->directDebit->triggerActivationCharge($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -129,7 +136,8 @@ public function testListMandateAuthorizations(): void $this->mockClient->addResponse($response); - $data = $this->client()->directDebit->listMandateAuthorizations(['status' => 'active']); + $options = new ListMandateAuthorizationsOptions([]); + $data = $this->client()->directDebit->listMandateAuthorizations($options); $sentRequest = $this->mockClient->getLastRequest(); diff --git a/tests/DisputeTest.php b/tests/DisputeTest.php index 526c06f..dd1de57 100644 --- a/tests/DisputeTest.php +++ b/tests/DisputeTest.php @@ -4,6 +4,14 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\Dispute\{ + ReadAllOptions, + UpdateOptions, + AddEvidenceOptions, + GetUploadUrlOptions, + ResolveOptions, + ExportOptions +}; final class DisputeTest extends TestCase { @@ -60,7 +68,8 @@ public function testListDisputes(): void $this->mockClient->addResponse($response); - $data = $this->client()->disputes->all(['page' => 1, 'perPage' => 50]); + $options = new ReadAllOptions(['page' => 1, 'perPage' => 50]); + $data = $this->client()->disputes->all($options); $sentRequest = $this->mockClient->getLastRequest(); @@ -146,9 +155,10 @@ public function testUpdateDispute(): void $this->mockClient->addResponse($response); - $data = $this->client()->disputes->update('827179', [ + $options = new UpdateOptions([ 'refund_amount' => 25000 ]); + $data = $this->client()->disputes->update('827179', $options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -180,6 +190,7 @@ public function testAddEvidence(): void 'customer_email' => 'john.doe@example.com', 'customer_name' => 'John Doe', 'customer_phone' => '+2348012345678', + 'service_details' => 'Product delivery service', 'delivery_address' => '123 Main Street, Lagos, Nigeria', 'delivery_date' => '2023-11-10' ]); @@ -192,13 +203,15 @@ public function testAddEvidence(): void $this->mockClient->addResponse($response); - $data = $this->client()->disputes->addEvidence('827179', [ + $options = new AddEvidenceOptions([ 'customer_email' => 'john.doe@example.com', 'customer_name' => 'John Doe', 'customer_phone' => '+2348012345678', + 'service_details' => 'Product delivery service', 'delivery_address' => '123 Main Street, Lagos, Nigeria', 'delivery_date' => '2023-11-10' ]); + $data = $this->client()->disputes->addEvidence('827179', $options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -232,9 +245,10 @@ public function testGetUploadUrl(): void $this->mockClient->addResponse($response); - $data = $this->client()->disputes->getUploadUrl('827179', [ + $options = new GetUploadUrlOptions([ 'upload_filename' => 'evidence_receipt.pdf' ]); + $data = $this->client()->disputes->getUploadUrl('827179', $options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -259,7 +273,7 @@ public function testResolveDispute(): void ]; $expectedBody = json_encode([ - 'resolution' => 'merchant_accepted', + 'resolution' => 'merchant-accepted', 'message' => 'Customer contacted and resolved amicably', 'refund_amount' => 0, 'uploaded_filename' => 'evidence_receipt.pdf' @@ -273,12 +287,13 @@ public function testResolveDispute(): void $this->mockClient->addResponse($response); - $data = $this->client()->disputes->resolve('827179', [ - 'resolution' => 'merchant_accepted', + $options = new ResolveOptions([ + 'resolution' => 'merchant-accepted', 'message' => 'Customer contacted and resolved amicably', 'refund_amount' => 0, 'uploaded_filename' => 'evidence_receipt.pdf' ]); + $data = $this->client()->disputes->resolve('827179', $options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); @@ -308,7 +323,8 @@ public function testExportDisputes(): void $this->mockClient->addResponse($response); - $data = $this->client()->disputes->export(['from' => '2023-11-01', 'to' => '2023-11-16']); + $options = new ExportOptions(['from' => '2023-11-01', 'to' => '2023-11-16']); + $data = $this->client()->disputes->export($options); $sentRequest = $this->mockClient->getLastRequest(); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 601a863..2903c62 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -4,6 +4,7 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\Integration\UpdateTimeoutOptions; final class IntegrationTest extends TestCase { @@ -60,9 +61,10 @@ public function testUpdateTimeout(): void $this->mockClient->addResponse($response); - $data = $this->client()->integration->updateTimeout([ + $options = new UpdateTimeoutOptions([ 'timeout' => 60 ]); + $data = $this->client()->integration->updateTimeout($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); diff --git a/tests/MiscellaneousTest.php b/tests/MiscellaneousTest.php index 7e954a4..ffde02b 100644 --- a/tests/MiscellaneousTest.php +++ b/tests/MiscellaneousTest.php @@ -4,6 +4,10 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\Miscellaneous\{ + ListBanksOptions, + ListStatesOptions +}; final class MiscellaneousTest extends TestCase { @@ -56,7 +60,8 @@ public function testListBanks(): void $this->mockClient->addResponse($response); - $data = $this->client()->miscellaneous->listBanks(['country' => 'nigeria']); + $options = new ListBanksOptions(['country' => 'nigeria']); + $data = $this->client()->miscellaneous->listBanks($options); $sentRequest = $this->mockClient->getLastRequest(); @@ -190,7 +195,8 @@ public function testListStates(): void $this->mockClient->addResponse($response); - $data = $this->client()->miscellaneous->listStates(['country' => 'NG']); + $options = new ListStatesOptions(['country' => 'NG']); + $data = $this->client()->miscellaneous->listStates($options); $sentRequest = $this->mockClient->getLastRequest(); diff --git a/tests/VerificationTest.php b/tests/VerificationTest.php index cc0a8ae..4b9a030 100644 --- a/tests/VerificationTest.php +++ b/tests/VerificationTest.php @@ -4,6 +4,10 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\Stream; +use StarfolkSoftware\Paystack\Options\Verification\{ + ResolveAccountOptions, + ValidateAccountOptions +}; final class VerificationTest extends TestCase { @@ -27,10 +31,11 @@ public function testResolveAccount(): void $this->mockClient->addResponse($response); - $data = $this->client()->verification->resolveAccount([ + $options = new ResolveAccountOptions([ 'account_number' => '0123456789', 'bank_code' => '044' ]); + $data = $this->client()->verification->resolveAccount($options); $sentRequest = $this->mockClient->getLastRequest(); @@ -72,7 +77,7 @@ public function testValidateAccount(): void $this->mockClient->addResponse($response); - $data = $this->client()->verification->validateAccount([ + $options = new ValidateAccountOptions([ 'account_name' => 'John Doe', 'account_number' => '0123456789', 'account_type' => 'personal', @@ -81,6 +86,7 @@ public function testValidateAccount(): void 'document_type' => 'identityNumber', 'document_number' => '12345678901' ]); + $data = $this->client()->verification->validateAccount($options); $sentRequest = $this->mockClient->getLastRequest(); $sentBody = $sentRequest->getBody()->__toString(); From d973321b66931be04c1b7a1aa43aed28ffaa13a7 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 15:14:09 +0100 Subject: [PATCH 09/17] feat: Add GitHub Actions workflows for release and testing; enhance test script for coverage reporting --- .github/workflows/release.yml | 57 ++++++++++++++++++++ .github/workflows/tests.yml | 99 +++++++++++++++++++++++++++++++++++ composer.json | 2 +- 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dc9a218 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: dom, curl, libxml, mbstring, zip + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ~/.composer/cache/files + key: dependencies-php-8.2-composer-${{ hashFiles('composer.json') }} + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-dev + + - name: Run tests + run: composer test + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Generate changelog + run: | + echo "## What's Changed" > CHANGELOG.md + git log --oneline --no-merges $(git describe --tags --abbrev=0 HEAD^)..HEAD >> CHANGELOG.md + + - name: Update release with changelog + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./CHANGELOG.md + asset_name: CHANGELOG.md + asset_content_type: text/markdown \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4d9003a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,99 @@ +name: Tests + +on: + push: + branches: [ main, master, v1.x, v2.x ] + pull_request: + branches: [ main, master, v1.x, v2.x ] + +jobs: + test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + php: [8.0, 8.1, 8.2, 8.3] + stability: [prefer-lowest, prefer-stable] + + name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ~/.composer/cache/files + key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: xdebug + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Execute tests + run: composer test + + - name: Upload coverage to Codecov + if: matrix.php == '8.2' && matrix.stability == 'prefer-stable' + uses: codecov/codecov-action@v3 + with: + file: ./coverage.clover + fail_ci_if_error: false + + code-quality: + runs-on: ubuntu-latest + name: Code Quality + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ~/.composer/cache/files + key: dependencies-php-8.2-composer-${{ hashFiles('composer.json') }} + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction + + - name: Check syntax errors + run: find ./src -name "*.php" -print0 | xargs -0 -n1 -P8 php -l + + - name: Check for security vulnerabilities + run: composer audit --no-dev + + - name: Check if examples are working + run: | + if [ -d "examples" ]; then + echo "Found examples directory, checking syntax..." + find ./examples -name "*.php" -print0 | xargs -0 -n1 -P8 php -l + else + echo "No examples directory found, skipping..." + fi + + - name: Archive test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-results-php-8.2 + path: | + build/ + coverage.clover + retention-days: 30 \ No newline at end of file diff --git a/composer.json b/composer.json index fe198e8..f328ca4 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "symfony/var-dumper": "^6.2" }, "scripts": { - "test": "phpunit --testdox" + "test": "phpunit --testdox --coverage-text --coverage-clover=coverage.clover" }, "minimum-stability": "stable", "prefer-stable": true, From c132ebccff8a0746a4de0b643f0378a13f93c472 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 15:32:23 +0100 Subject: [PATCH 10/17] Enhance examples for Paystack Payment Requests - Updated to provide a comprehensive example of recurring billing and subscription management, including detailed customer data, subscription tiers, and billing processes. - Added structured comments and documentation to clarify the functionality and usage of the script. - Improved error handling and logging for payment requests and status checks. - Introduced helper functions for currency formatting and billing calculations. - Enhanced with detailed comments, structured payment request creation, and retrieval of recent payment requests. - Included additional operations for fetching and verifying payment requests, along with improved output formatting for better readability. - Added warnings for using default API keys to encourage secure practices. --- README.md | 370 ++++++-- docs/advanced-usage.md | 1276 +++++++++++++++++++++++++++ docs/api-reference.md | 981 ++++++++++++++++++++ docs/getting-started.md | 564 ++++++++++++ docs/troubleshooting.md | 956 ++++++++++++++++++++ examples/README.md | 380 +++++++- examples/invoice_workflow.php | 506 +++++++++-- examples/recurring_billing.php | 672 +++++++++++--- examples/simple_payment_request.php | 292 +++++- 9 files changed, 5683 insertions(+), 314 deletions(-) create mode 100644 docs/advanced-usage.md create mode 100644 docs/api-reference.md create mode 100644 docs/getting-started.md create mode 100644 docs/troubleshooting.md diff --git a/README.md b/README.md index 18537e9..4daa6cc 100644 --- a/README.md +++ b/README.md @@ -1,159 +1,377 @@ -# Paystack PHP bindings +# Paystack PHP SDK [![Latest Stable Version](http://poser.pugx.org/starfolksoftware/paystack-php/v)](https://packagist.org/packages/starfolksoftware/paystack-php) [![Total Downloads](http://poser.pugx.org/starfolksoftware/paystack-php/downloads)](https://packagist.org/packages/starfolksoftware/paystack-php) [![License](http://poser.pugx.org/starfolksoftware/paystack-php/license)](https://packagist.org/packages/starfolksoftware/paystack-php) [![PHP Version Require](http://poser.pugx.org/starfolksoftware/paystack-php/require/php)](https://packagist.org/packages/starfolksoftware/paystack-php) -The Paystack PHP library provides convenient access to the Paystack API from -applications written in the PHP language. It includes a pre-defined set of -classes for API resources that initialize themselves dynamically from API -responses which makes it compatible with a wide range of versions of the Paystack -API. +A modern, developer-friendly PHP SDK for the [Paystack API](https://paystack.com/docs/api/). This library provides convenient access to Paystack's payment infrastructure from applications written in PHP. It includes a comprehensive set of classes for all API resources with full type safety and automatic parameter validation. + +## Features + +- ✅ **Complete API Coverage** - All Paystack API endpoints supported +- ✅ **Type Safety** - Full PHP 8.0+ type declarations +- ✅ **Parameter Validation** - Automatic validation of API parameters +- ✅ **PSR-18 HTTP Client** - Compatible with any PSR-18 HTTP client +- ✅ **Comprehensive Examples** - Detailed usage examples for all features +- ✅ **Exception Handling** - Detailed error responses and exception handling +- ✅ **Modern PHP** - Built for PHP 8.0+ with modern coding standards + +## Quick Links + +- [Getting Started Guide](docs/getting-started.md) +- [API Reference](docs/api-reference.md) +- [Examples](examples/) +- [Advanced Usage](docs/advanced-usage.md) +- [Troubleshooting](docs/troubleshooting.md) + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Authentication](#authentication) +- [Quick Start](#quick-start) +- [Available Resources](#available-resources) +- [Usage Examples](#usage-examples) + - [Transactions](#transactions) + - [Customers](#customers) + - [Payment Requests](#payment-requests) + - [Subscriptions](#subscriptions) +- [Error Handling](#error-handling) +- [Testing](#testing) +- [Contributing](#contributing) +- [Support](#support) ## Requirements -PHP 8.0 and later. +- **PHP 8.0 or higher** +- **PSR-18 HTTP Client** (any implementation) +- **Composer** for dependency management -## Composer +## Installation -You can install the bindings via [Composer](http://getcomposer.org/). Run the following command: +### Using Composer + +Install the SDK using Composer: ```bash composer require starfolksoftware/paystack-php ``` +### Install HTTP Client + +This package requires a PSR-18 HTTP client. If you don't have one installed, we recommend Guzzle: + ```bash composer require php-http/guzzle7-adapter ``` -To use the bindings, use Composer's [autoload](https://getcomposer.org/doc/01-basic-usage.md#autoloading): +Alternatively, you can use any PSR-18 compatible client: -```php -require_once('vendor/autoload.php'); -``` +```bash +# Symfony HTTP Client +composer require symfony/http-client -## Dependencies +# cURL client +composer require php-http/curl-client -Any package that implements [psr/http-client-implementation](https://packagist.org/providers/psr/http-client-implementation) +# Mock client (for testing) +composer require php-http/mock-client +``` -## Getting Started +### Autoloading -Simple usage looks like: +Include Composer's autoloader in your project: ```php '*******', -]); +- **Test Secret Key**: `sk_test_...` (for development) +- **Live Secret Key**: `sk_live_...` (for production) -$response = $paystack - ->transactions - ->all([]); +**⚠️ Important**: Never expose your secret key in client-side code or public repositories. -var_dump($response['data'][0]); - -\\ dumps -array(21) { ... } -... -``` - -### Using Payment Requests +## Quick Start ```php '*******', + 'secretKey' => 'sk_test_your_secret_key_here', +]); + +// List all transactions +$transactions = $paystack->transactions->all([ + 'perPage' => 50, + 'page' => 1 ]); +echo "Total transactions: " . $transactions['meta']['total'] . "\n"; + +// Create a customer +$customer = $paystack->customers->create([ + 'email' => 'customer@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '+2348123456789' +]); + +echo "Customer created: " . $customer['data']['customer_code'] . "\n"; +``` + +### Payment Requests + +```php // Create a payment request -$response = $paystack->paymentRequests->create([ - 'description' => 'a test invoice', +$paymentRequest = $paystack->paymentRequests->create([ + 'description' => 'Website Development Invoice', 'line_items' => [ - ['name' => 'item 1', 'amount' => 20000], - ['name' => 'item 2', 'amount' => 20000] + ['name' => 'Frontend Development', 'amount' => 50000, 'quantity' => 1], + ['name' => 'Backend Development', 'amount' => 75000, 'quantity' => 1], + ['name' => 'UI/UX Design', 'amount' => 25000, 'quantity' => 1] ], 'tax' => [ - ['name' => 'VAT', 'amount' => 2000] + ['name' => 'VAT', 'amount' => 11250] // 7.5% of total ], - 'customer' => 'CUS_xwaj0txjryg393b', - 'due_date' => '2025-07-08' + 'customer' => 'customer@example.com', + 'due_date' => date('Y-m-d', strtotime('+30 days')), + 'send_notification' => true ]); // List payment requests -$paymentRequests = $paystack->paymentRequests->all(['page' => 1]); +$paymentRequests = $paystack->paymentRequests->all([ + 'perPage' => 20, + 'page' => 1, + 'status' => 'pending' +]); -// Fetch a specific payment request +// Fetch specific payment request $paymentRequest = $paystack->paymentRequests->fetch('PRQ_1weqqsn2wwzgft8'); -// Verify a payment request -$verification = $paystack->paymentRequests->verify('PRQ_1weqqsn2wwzgft8'); +// Update payment request +$updated = $paystack->paymentRequests->update('PRQ_1weqqsn2wwzgft8', [ + 'description' => 'Updated Website Development Invoice', + 'due_date' => date('Y-m-d', strtotime('+45 days')) +]); -// Send notification for a payment request +// Send reminder notification $notification = $paystack->paymentRequests->sendNotification('PRQ_1weqqsn2wwzgft8'); +``` -// Get payment request totals -$totals = $paystack->paymentRequests->totals(); +### Subscriptions -// Finalize a draft payment request -$finalized = $paystack->paymentRequests->finalize('PRQ_1weqqsn2wwzgft8', ['send_notification' => true]); +```php +// Create a plan +$plan = $paystack->plans->create([ + 'name' => 'Premium Monthly', + 'interval' => 'monthly', + 'amount' => 5000, // ₦50.00 per month + 'currency' => 'NGN', + 'description' => 'Premium subscription with all features' +]); -// Update a payment request -$updated = $paystack->paymentRequests->update('PRQ_1weqqsn2wwzgft8', [ - 'description' => 'Updated test invoice', - 'due_date' => '2025-07-15' +// Create subscription +$subscription = $paystack->subscriptions->create([ + 'customer' => 'CUS_xwaj0txjryg393b', + 'plan' => $plan['data']['plan_code'], + 'authorization' => 'AUTH_authorization_code' +]); + +// List subscriptions +$subscriptions = $paystack->subscriptions->all([ + 'perPage' => 50, + 'plan' => $plan['data']['plan_code'] +]); + +// Disable subscription +$disabled = $paystack->subscriptions->disable('SUB_subscription_code', [ + 'code' => 'SUB_subscription_code', + 'token' => 'subscription_email_token' ]); +``` + +## Error Handling + +The SDK provides comprehensive error handling with detailed error messages: + +```php +use StarfolkSoftware\Paystack\Client as PaystackClient; -// Archive a payment request -$archived = $paystack->paymentRequests->archive('PRQ_1weqqsn2wwzgft8'); +try { + $paystack = new PaystackClient([ + 'secretKey' => 'sk_test_your_secret_key_here', + ]); + + $transaction = $paystack->transactions->initialize([ + 'email' => 'invalid-email', // This will cause an error + 'amount' => 20000 + ]); + +} catch (\Psr\Http\Client\ClientExceptionInterface $e) { + // Network or HTTP-related errors + echo "HTTP Error: " . $e->getMessage() . "\n"; + +} catch (\Exception $e) { + // General errors + echo "Error: " . $e->getMessage() . "\n"; +} + +// Handle API response errors +$response = $paystack->transactions->verify('invalid_reference'); +if (!$response['status']) { + echo "API Error: " . $response['message'] . "\n"; + // Handle the error appropriately +} ``` -## Available endpoints +## Configuration Options -- [x] Customer -- [x] Invoice -- [x] Payment Request -- [x] Plan -- [x] Subscription -- [x] Transaction +You can customize the client behavior with various configuration options: -## Documentation +```php +$paystack = new PaystackClient([ + 'secretKey' => 'sk_test_your_secret_key_here', + 'apiVersion' => 'v1', // API version (default: v1) + 'baseUri' => 'https://api.paystack.co', // Custom base URI +]); -See the [PHP API docs](https://developer.paystack.com/reference#introduction-1). +// Access the underlying HTTP client if needed +$httpClient = $paystack->getHttpClient(); +``` + +## Webhook Handling + +```php +// In your webhook endpoint +$payload = file_get_contents('php://input'); +$signature = $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] ?? ''; + +// Verify webhook signature +$secretKey = 'sk_test_your_secret_key_here'; +$computedSignature = hash_hmac('sha512', $payload, $secretKey); + +if (hash_equals($signature, $computedSignature)) { + $event = json_decode($payload, true); + + switch ($event['event']) { + case 'charge.success': + // Handle successful payment + $reference = $event['data']['reference']; + echo "Payment successful: {$reference}\n"; + break; + + case 'subscription.create': + // Handle new subscription + $subscriptionCode = $event['data']['subscription_code']; + echo "New subscription: {$subscriptionCode}\n"; + break; + + default: + echo "Unhandled event: {$event['event']}\n"; + } +} else { + echo "Invalid signature\n"; + http_response_code(400); +} +``` ## Testing +Run the test suite using PHPUnit: + ```bash +# Install development dependencies +composer install --dev + +# Run all tests composer test + +# Run tests with coverage +./vendor/bin/phpunit --coverage-html coverage + +# Run specific test class +./vendor/bin/phpunit tests/TransactionTest.php + +# Run tests with verbose output +./vendor/bin/phpunit --testdox ``` -## Changelog +### Testing Your Integration -Please see [CHANGELOG](https://github.com/starfolksoftware/paystack-php/compare/v0.0.2...v0.6.1) for more information on what has changed recently. +Use Paystack's test mode to test your integration: -## Road Map +```php +// Use test secret key +$paystack = new PaystackClient([ + 'secretKey' => 'sk_test_your_test_secret_key_here', +]); + +// Test card numbers for different scenarios +$testCards = [ + 'success' => '4084084084084081', + 'insufficient_funds' => '4084084084084107', + 'invalid_pin' => '4084084084084099' +]; +``` ## Contributing -Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. +We welcome contributions! Please see our [Contributing Guide](.github/CONTRIBUTING.md) for details. -## Security Vulnerabilities +### Development Setup + +```bash +# Clone the repository +git clone https://github.com/starfolksoftware/paystack-php.git +cd paystack-php -Please review [our security policy](.github/CONTRIBUTING.md) on how to report security vulnerabilities. +# Install dependencies +composer install + +# Run tests +composer test +``` -## Credits +### Coding Standards + +This project follows PSR-12 coding standards. Please ensure your code adheres to these standards: + +```bash +# Check code style (if you have PHP_CodeSniffer installed) +phpcs src/ --standard=PSR12 + +# Fix code style automatically +phpcbf src/ --standard=PSR12 +``` -- [Faruk Nasir](https://github.com/frknasir) -- [All Contributors](../../contributors) +## Support + +- **Documentation**: [API Reference](docs/api-reference.md) | [Getting Started](docs/getting-started.md) +- **Examples**: Check the [examples](examples/) directory +- **Issues**: [GitHub Issues](https://github.com/starfolksoftware/paystack-php/issues) +- **Email**: [contact@starfolksoftware.com](mailto:contact@starfolksoftware.com) +- **Paystack Documentation**: [Official API Docs](https://paystack.com/docs/api/) + +## Changelog + +Please see [CHANGELOG](https://github.com/starfolksoftware/paystack-php/compare/v0.0.2...v0.6.1) for more information on what has changed recently. + +## Security Vulnerabilities + +If you discover a security vulnerability, please send an email to [contact@starfolksoftware.com](mailto:contact@starfolksoftware.com). All security vulnerabilities will be promptly addressed. ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +--- + +**Made with ❤️ by [Starfolk Software](https://starfolksoftware.com)** diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md new file mode 100644 index 0000000..1b985f8 --- /dev/null +++ b/docs/advanced-usage.md @@ -0,0 +1,1276 @@ +# Advanced Usage + +This guide covers advanced patterns, optimization techniques, and complex use cases for the Paystack PHP SDK. + +## Table of Contents + +- [HTTP Client Configuration](#http-client-configuration) +- [Advanced Error Handling](#advanced-error-handling) +- [Retry Logic and Resilience](#retry-logic-and-resilience) +- [Webhook Security](#webhook-security) +- [Marketplace Payments](#marketplace-payments) +- [Bulk Operations](#bulk-operations) +- [Performance Optimization](#performance-optimization) +- [Testing Strategies](#testing-strategies) +- [Advanced Integrations](#advanced-integrations) + +## HTTP Client Configuration + +### Custom HTTP Client + +The SDK uses PSR-18 HTTP clients. You can configure a custom client for specific needs: + +```php +use StarfolkSoftware\Paystack\Client as PaystackClient; +use StarfolkSoftware\Paystack\ClientBuilder; +use Http\Client\Common\Plugin\RetryPlugin; +use Http\Client\Common\Plugin\LoggerPlugin; +use Psr\Log\LoggerInterface; + +// Create a custom client builder +$clientBuilder = new ClientBuilder(); + +// Add retry plugin +$retryPlugin = new RetryPlugin([ + 'retries' => 3, +]); +$clientBuilder->addPlugin($retryPlugin); + +// Add logging plugin +$logger = new YourCustomLogger(); // Implement LoggerInterface +$loggerPlugin = new LoggerPlugin($logger); +$clientBuilder->addPlugin($loggerPlugin); + +// Initialize Paystack client with custom builder +$paystack = new PaystackClient([ + 'secretKey' => 'sk_test_your_secret_key_here', + 'clientBuilder' => $clientBuilder, +]); +``` + +### Timeout Configuration + +Configure request timeouts for better control: + +```php +use Http\Client\Curl\Client as CurlClient; + +$curlClient = new CurlClient(null, null, [ + CURLOPT_TIMEOUT => 30, // 30 seconds timeout + CURLOPT_CONNECTTIMEOUT => 10, // 10 seconds connection timeout +]); + +$clientBuilder = new ClientBuilder($curlClient); +$paystack = new PaystackClient([ + 'secretKey' => 'sk_test_your_secret_key_here', + 'clientBuilder' => $clientBuilder, +]); +``` + +## Advanced Error Handling + +### Custom Exception Handling + +Create a comprehensive error handling strategy: + +```php +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\NetworkExceptionInterface; +use Psr\Http\Client\RequestExceptionInterface; + +class PaymentProcessor +{ + private PaystackClient $paystack; + private LoggerInterface $logger; + + public function __construct(PaystackClient $paystack, LoggerInterface $logger) + { + $this->paystack = $paystack; + $this->logger = $logger; + } + + public function processPayment(array $paymentData): PaymentResult + { + try { + $transaction = $this->paystack->transactions->initialize($paymentData); + + if (!$transaction['status']) { + return $this->handleApiError($transaction); + } + + return PaymentResult::success($transaction['data']); + + } catch (NetworkExceptionInterface $e) { + // Network connectivity issues + $this->logger->error('Network error during payment processing', [ + 'error' => $e->getMessage(), + 'payment_data' => $paymentData + ]); + + return PaymentResult::failure('Network connection failed. Please try again.'); + + } catch (RequestExceptionInterface $e) { + // Request formatting issues + $this->logger->error('Request error during payment processing', [ + 'error' => $e->getMessage(), + 'payment_data' => $paymentData + ]); + + return PaymentResult::failure('Invalid payment request.'); + + } catch (ClientExceptionInterface $e) { + // Other HTTP client issues + $this->logger->error('HTTP client error during payment processing', [ + 'error' => $e->getMessage(), + 'payment_data' => $paymentData + ]); + + return PaymentResult::failure('Payment service unavailable.'); + + } catch (\Exception $e) { + // Unexpected errors + $this->logger->critical('Unexpected error during payment processing', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'payment_data' => $paymentData + ]); + + return PaymentResult::failure('An unexpected error occurred.'); + } + } + + private function handleApiError(array $response): PaymentResult + { + $message = $response['message'] ?? 'Unknown API error'; + + // Handle specific error types + if (strpos($message, 'email') !== false) { + return PaymentResult::failure('Invalid email address provided.'); + } + + if (strpos($message, 'amount') !== false) { + return PaymentResult::failure('Invalid payment amount.'); + } + + // Log for investigation + $this->logger->warning('Paystack API error', [ + 'response' => $response + ]); + + return PaymentResult::failure($message); + } +} + +class PaymentResult +{ + public bool $success; + public ?array $data; + public ?string $error; + + private function __construct(bool $success, ?array $data = null, ?string $error = null) + { + $this->success = $success; + $this->data = $data; + $this->error = $error; + } + + public static function success(array $data): self + { + return new self(true, $data); + } + + public static function failure(string $error): self + { + return new self(false, null, $error); + } +} +``` + +## Retry Logic and Resilience + +### Automatic Retry with Exponential Backoff + +Implement retry logic for failed requests: + +```php +class ResilientPaystackClient +{ + private PaystackClient $client; + private int $maxRetries; + private int $baseDelay; + + public function __construct(PaystackClient $client, int $maxRetries = 3, int $baseDelay = 1000) + { + $this->client = $client; + $this->maxRetries = $maxRetries; + $this->baseDelay = $baseDelay; // milliseconds + } + + public function executeWithRetry(callable $operation, array $context = []): array + { + $lastException = null; + + for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) { + try { + return $operation(); + + } catch (NetworkExceptionInterface $e) { + $lastException = $e; + + if ($attempt < $this->maxRetries) { + $delay = $this->calculateDelay($attempt); + usleep($delay * 1000); // Convert to microseconds + + error_log("Payment attempt {$attempt} failed, retrying in {$delay}ms: " . $e->getMessage()); + continue; + } + + } catch (ClientExceptionInterface $e) { + // Don't retry client errors (4xx) + throw $e; + } + } + + throw $lastException; + } + + private function calculateDelay(int $attempt): int + { + // Exponential backoff with jitter + $delay = $this->baseDelay * pow(2, $attempt - 1); + $jitter = random_int(0, (int)($delay * 0.1)); // 10% jitter + + return min($delay + $jitter, 30000); // Max 30 seconds + } + + public function initializePayment(array $paymentData): array + { + return $this->executeWithRetry(function () use ($paymentData) { + return $this->client->transactions->initialize($paymentData); + }); + } + + public function verifyPayment(string $reference): array + { + return $this->executeWithRetry(function () use ($reference) { + return $this->client->transactions->verify($reference); + }); + } +} + +// Usage +$resilientClient = new ResilientPaystackClient($paystack); +$transaction = $resilientClient->initializePayment([ + 'email' => 'customer@example.com', + 'amount' => 20000 +]); +``` + +## Webhook Security + +### Advanced Webhook Verification + +Implement secure webhook handling with additional security measures: + +```php +class SecureWebhookHandler +{ + private string $secretKey; + private LoggerInterface $logger; + private array $allowedEvents; + private int $timestampTolerance = 300; // 5 minutes + + public function __construct(string $secretKey, LoggerInterface $logger, array $allowedEvents = []) + { + $this->secretKey = $secretKey; + $this->logger = $logger; + $this->allowedEvents = $allowedEvents ?: [ + 'charge.success', + 'charge.failed', + 'subscription.create', + 'subscription.disable', + 'invoice.create', + 'invoice.update', + 'invoice.payment_failed' + ]; + } + + public function handleWebhook(): void + { + try { + // Get raw payload and headers + $payload = file_get_contents('php://input'); + $signature = $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] ?? ''; + $timestamp = $_SERVER['HTTP_X_PAYSTACK_TIMESTAMP'] ?? ''; + + // Validate webhook + if (!$this->validateWebhook($payload, $signature, $timestamp)) { + $this->sendErrorResponse(401, 'Invalid webhook signature'); + return; + } + + // Parse event + $event = json_decode($payload, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $this->sendErrorResponse(400, 'Invalid JSON payload'); + return; + } + + // Validate event structure + if (!$this->validateEventStructure($event)) { + $this->sendErrorResponse(400, 'Invalid event structure'); + return; + } + + // Check if event type is allowed + if (!in_array($event['event'], $this->allowedEvents)) { + $this->logger->info('Ignoring unhandled webhook event', ['event' => $event['event']]); + $this->sendSuccessResponse(); + return; + } + + // Process event + $this->processEvent($event); + $this->sendSuccessResponse(); + + } catch (\Exception $e) { + $this->logger->error('Webhook processing error', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + $this->sendErrorResponse(500, 'Internal server error'); + } + } + + private function validateWebhook(string $payload, string $signature, string $timestamp): bool + { + // Check timestamp to prevent replay attacks + if ($timestamp && abs(time() - (int)$timestamp) > $this->timestampTolerance) { + $this->logger->warning('Webhook timestamp out of tolerance', [ + 'timestamp' => $timestamp, + 'current_time' => time() + ]); + return false; + } + + // Verify signature + $computedSignature = hash_hmac('sha512', $payload, $this->secretKey); + + if (!hash_equals($signature, $computedSignature)) { + $this->logger->error('Invalid webhook signature', [ + 'expected' => $computedSignature, + 'received' => $signature + ]); + return false; + } + + return true; + } + + private function validateEventStructure(array $event): bool + { + return isset($event['event']) && isset($event['data']); + } + + private function processEvent(array $event): void + { + $this->logger->info('Processing webhook event', [ + 'event' => $event['event'], + 'data_keys' => array_keys($event['data']) + ]); + + switch ($event['event']) { + case 'charge.success': + $this->handleSuccessfulPayment($event['data']); + break; + + case 'charge.failed': + $this->handleFailedPayment($event['data']); + break; + + case 'subscription.create': + $this->handleNewSubscription($event['data']); + break; + + case 'subscription.disable': + $this->handleCancelledSubscription($event['data']); + break; + + case 'invoice.create': + $this->handleInvoiceCreated($event['data']); + break; + + case 'invoice.payment_failed': + $this->handleInvoicePaymentFailed($event['data']); + break; + + default: + $this->logger->warning('Unhandled webhook event', ['event' => $event['event']]); + } + } + + private function handleSuccessfulPayment(array $data): void + { + $reference = $data['reference']; + $amount = $data['amount'] / 100; + $customerEmail = $data['customer']['email']; + + // Implement your business logic here + // - Update order status + // - Send confirmation email + // - Update customer account + // - Trigger fulfillment process + + $this->logger->info('Payment successful', [ + 'reference' => $reference, + 'amount' => $amount, + 'customer' => $customerEmail + ]); + } + + private function handleFailedPayment(array $data): void + { + $reference = $data['reference']; + $gateway_response = $data['gateway_response'] ?? 'Unknown error'; + + // Implement failure handling + // - Update order status + // - Send failure notification + // - Log for investigation + + $this->logger->warning('Payment failed', [ + 'reference' => $reference, + 'reason' => $gateway_response + ]); + } + + private function handleNewSubscription(array $data): void + { + $subscriptionCode = $data['subscription_code']; + $customerCode = $data['customer']['customer_code']; + + // Handle new subscription + // - Update customer account + // - Enable premium features + // - Send welcome email + + $this->logger->info('New subscription created', [ + 'subscription' => $subscriptionCode, + 'customer' => $customerCode + ]); + } + + private function handleCancelledSubscription(array $data): void + { + $subscriptionCode = $data['subscription_code']; + $customerCode = $data['customer']['customer_code']; + + // Handle subscription cancellation + // - Disable premium features + // - Update billing status + // - Send cancellation confirmation + + $this->logger->info('Subscription cancelled', [ + 'subscription' => $subscriptionCode, + 'customer' => $customerCode + ]); + } + + private function handleInvoiceCreated(array $data): void + { + $invoiceCode = $data['invoice_code']; + + $this->logger->info('Invoice created', [ + 'invoice' => $invoiceCode + ]); + } + + private function handleInvoicePaymentFailed(array $data): void + { + $invoiceCode = $data['invoice_code']; + + $this->logger->warning('Invoice payment failed', [ + 'invoice' => $invoiceCode + ]); + } + + private function sendSuccessResponse(): void + { + http_response_code(200); + echo 'OK'; + } + + private function sendErrorResponse(int $code, string $message): void + { + http_response_code($code); + echo json_encode(['error' => $message]); + } +} + +// Usage +$webhookHandler = new SecureWebhookHandler( + 'sk_test_your_secret_key_here', + $logger, + ['charge.success', 'charge.failed', 'subscription.create'] +); + +$webhookHandler->handleWebhook(); +``` + +## Marketplace Payments + +### Split Payments Implementation + +Handle complex marketplace scenarios with split payments: + +```php +class MarketplacePaymentManager +{ + private PaystackClient $paystack; + + public function __construct(PaystackClient $paystack) + { + $this->paystack = $paystack; + } + + public function setupMarketplaceVendor(array $vendorData): array + { + // Step 1: Create subaccount for vendor + $subaccount = $this->paystack->subaccounts->create([ + 'business_name' => $vendorData['business_name'], + 'settlement_bank' => $vendorData['bank_code'], + 'account_number' => $vendorData['account_number'], + 'percentage_charge' => $vendorData['commission_percentage'], + 'description' => $vendorData['description'] ?? 'Marketplace vendor account', + 'primary_contact_email' => $vendorData['email'], + 'primary_contact_name' => $vendorData['contact_name'], + 'primary_contact_phone' => $vendorData['phone'], + 'metadata' => [ + 'vendor_id' => $vendorData['vendor_id'], + 'category' => $vendorData['category'] ?? 'general' + ] + ]); + + return $subaccount; + } + + public function createPaymentSplit(array $vendors, array $splitConfig): array + { + // Create split configuration for multiple vendors + $subaccounts = []; + $totalShare = 0; + + foreach ($vendors as $vendor) { + $subaccounts[] = [ + 'subaccount' => $vendor['subaccount_code'], + 'share' => $vendor['percentage'], + ]; + $totalShare += $vendor['percentage']; + } + + // Ensure total doesn't exceed 100% + if ($totalShare > 100) { + throw new \InvalidArgumentException('Total vendor share cannot exceed 100%'); + } + + $split = $this->paystack->splits->create([ + 'name' => $splitConfig['name'], + 'type' => 'percentage', + 'currency' => $splitConfig['currency'] ?? 'NGN', + 'subaccounts' => $subaccounts, + 'bearer_type' => $splitConfig['bearer_type'] ?? 'all', + 'bearer_subaccount' => $splitConfig['bearer_subaccount'] ?? null + ]); + + return $split; + } + + public function processMarketplacePayment(array $paymentData, string $splitCode): array + { + $transaction = $this->paystack->transactions->initialize([ + 'email' => $paymentData['customer_email'], + 'amount' => $paymentData['amount'], + 'currency' => $paymentData['currency'] ?? 'NGN', + 'split_code' => $splitCode, + 'callback_url' => $paymentData['callback_url'], + 'metadata' => [ + 'order_id' => $paymentData['order_id'], + 'marketplace_fee' => $paymentData['marketplace_fee'] ?? 0, + 'vendor_items' => $paymentData['vendor_items'] ?? [] + ] + ]); + + return $transaction; + } + + public function updateVendorSplit(string $splitId, string $subaccountCode, float $newPercentage): array + { + return $this->paystack->splits->addOrUpdateSubaccount($splitId, [ + 'subaccount' => $subaccountCode, + 'share' => $newPercentage + ]); + } + + public function getVendorEarnings(string $subaccountCode, array $dateRange = []): array + { + $params = [ + 'subaccount' => $subaccountCode, + 'perPage' => 100 + ]; + + if (!empty($dateRange['from'])) { + $params['from'] = $dateRange['from']; + } + + if (!empty($dateRange['to'])) { + $params['to'] = $dateRange['to']; + } + + return $this->paystack->transactions->all($params); + } +} + +// Usage Example +$marketplace = new MarketplacePaymentManager($paystack); + +// Setup vendors +$vendor1 = $marketplace->setupMarketplaceVendor([ + 'vendor_id' => 'VENDOR_001', + 'business_name' => 'Tech Store Ltd', + 'bank_code' => '044', + 'account_number' => '0123456789', + 'commission_percentage' => 5.0, // 5% commission to platform + 'email' => 'vendor1@example.com', + 'contact_name' => 'John Vendor', + 'phone' => '+2348123456789' +]); + +$vendor2 = $marketplace->setupMarketplaceVendor([ + 'vendor_id' => 'VENDOR_002', + 'business_name' => 'Fashion Hub', + 'bank_code' => '058', + 'account_number' => '0987654321', + 'commission_percentage' => 3.0, // 3% commission to platform + 'email' => 'vendor2@example.com', + 'contact_name' => 'Jane Vendor', + 'phone' => '+2349876543210' +]); + +// Create split for multi-vendor order +$split = $marketplace->createPaymentSplit([ + [ + 'subaccount_code' => $vendor1['data']['subaccount_code'], + 'percentage' => 60 // 60% of payment goes to vendor1 + ], + [ + 'subaccount_code' => $vendor2['data']['subaccount_code'], + 'percentage' => 30 // 30% of payment goes to vendor2 + ] + // Remaining 10% goes to main account (platform fee) +], [ + 'name' => 'Multi-vendor Order Split', + 'currency' => 'NGN', + 'bearer_type' => 'all' +]); + +// Process payment with split +$payment = $marketplace->processMarketplacePayment([ + 'customer_email' => 'customer@example.com', + 'amount' => 100000, // ₦1,000 + 'order_id' => 'ORD_12345', + 'callback_url' => 'https://marketplace.com/payment/callback', + 'vendor_items' => [ + ['vendor_id' => 'VENDOR_001', 'amount' => 60000, 'items' => ['laptop']], + ['vendor_id' => 'VENDOR_002', 'amount' => 30000, 'items' => ['shirt']] + ] +], $split['data']['split_code']); +``` + +## Bulk Operations + +### Bulk Transfers + +Process multiple transfers efficiently: + +```php +class BulkTransferProcessor +{ + private PaystackClient $paystack; + private LoggerInterface $logger; + + public function __construct(PaystackClient $paystack, LoggerInterface $logger) + { + $this->paystack = $paystack; + $this->logger = $logger; + } + + public function processBulkPayouts(array $payouts): BulkTransferResult + { + $results = []; + $totalAmount = 0; + $successCount = 0; + $failureCount = 0; + + // Validate all payouts first + foreach ($payouts as $index => $payout) { + if (!$this->validatePayout($payout)) { + $results[$index] = [ + 'success' => false, + 'error' => 'Invalid payout data', + 'payout' => $payout + ]; + $failureCount++; + continue; + } + + $totalAmount += $payout['amount']; + } + + // Check balance before processing + $balance = $this->paystack->transferControl->checkBalance(); + if ($balance['data']['balance'] < $totalAmount) { + throw new \Exception('Insufficient balance for bulk transfer'); + } + + // Create transfer recipients in bulk + $recipients = $this->createBulkRecipients($payouts); + + // Process transfers + $transfers = []; + foreach ($payouts as $index => $payout) { + if (isset($results[$index])) { + continue; // Skip invalid payouts + } + + try { + $recipientCode = $recipients[$index]['recipient_code']; + + $transfer = $this->paystack->transfers->initiate([ + 'source' => 'balance', + 'amount' => $payout['amount'], + 'recipient' => $recipientCode, + 'reason' => $payout['reason'] ?? 'Bulk payout', + 'reference' => $payout['reference'] ?? null + ]); + + if ($transfer['status']) { + $transfers[] = [ + 'transfer_code' => $transfer['data']['transfer_code'], + 'recipient_code' => $recipientCode + ]; + + $results[$index] = [ + 'success' => true, + 'transfer_code' => $transfer['data']['transfer_code'], + 'amount' => $payout['amount'] + ]; + $successCount++; + } else { + $results[$index] = [ + 'success' => false, + 'error' => $transfer['message'], + 'payout' => $payout + ]; + $failureCount++; + } + + } catch (\Exception $e) { + $this->logger->error('Bulk transfer failed', [ + 'payout' => $payout, + 'error' => $e->getMessage() + ]); + + $results[$index] = [ + 'success' => false, + 'error' => $e->getMessage(), + 'payout' => $payout + ]; + $failureCount++; + } + + // Rate limiting - sleep between requests + usleep(200000); // 200ms delay + } + + return new BulkTransferResult($results, $successCount, $failureCount, $totalAmount); + } + + private function validatePayout(array $payout): bool + { + $required = ['name', 'account_number', 'bank_code', 'amount']; + + foreach ($required as $field) { + if (!isset($payout[$field]) || empty($payout[$field])) { + return false; + } + } + + return $payout['amount'] > 0; + } + + private function createBulkRecipients(array $payouts): array + { + $recipients = []; + + foreach ($payouts as $index => $payout) { + try { + $recipient = $this->paystack->transferRecipients->create([ + 'type' => 'nuban', + 'name' => $payout['name'], + 'account_number' => $payout['account_number'], + 'bank_code' => $payout['bank_code'], + 'currency' => $payout['currency'] ?? 'NGN', + 'description' => $payout['description'] ?? 'Bulk transfer recipient' + ]); + + if ($recipient['status']) { + $recipients[$index] = $recipient['data']; + } else { + throw new \Exception($recipient['message']); + } + + } catch (\Exception $e) { + $this->logger->error('Failed to create transfer recipient', [ + 'payout' => $payout, + 'error' => $e->getMessage() + ]); + throw $e; + } + + // Rate limiting + usleep(100000); // 100ms delay + } + + return $recipients; + } +} + +class BulkTransferResult +{ + public array $results; + public int $successCount; + public int $failureCount; + public int $totalAmount; + + public function __construct(array $results, int $successCount, int $failureCount, int $totalAmount) + { + $this->results = $results; + $this->successCount = $successCount; + $this->failureCount = $failureCount; + $this->totalAmount = $totalAmount; + } + + public function isFullySuccessful(): bool + { + return $this->failureCount === 0; + } + + public function getSuccessRate(): float + { + $total = $this->successCount + $this->failureCount; + return $total > 0 ? ($this->successCount / $total) * 100 : 0; + } + + public function getFailedPayouts(): array + { + return array_filter($this->results, fn($result) => !$result['success']); + } +} + +// Usage +$bulkProcessor = new BulkTransferProcessor($paystack, $logger); + +$payouts = [ + [ + 'name' => 'John Doe', + 'account_number' => '0123456789', + 'bank_code' => '044', + 'amount' => 50000, + 'reason' => 'Freelance payment', + 'reference' => 'PAYOUT_001' + ], + [ + 'name' => 'Jane Smith', + 'account_number' => '0987654321', + 'bank_code' => '058', + 'amount' => 75000, + 'reason' => 'Vendor payment', + 'reference' => 'PAYOUT_002' + ], + // ... more payouts +]; + +$result = $bulkProcessor->processBulkPayouts($payouts); + +echo "Bulk transfer completed:\n"; +echo "Success: {$result->successCount}\n"; +echo "Failed: {$result->failureCount}\n"; +echo "Success rate: " . number_format($result->getSuccessRate(), 2) . "%\n"; + +if (!$result->isFullySuccessful()) { + echo "Failed payouts:\n"; + foreach ($result->getFailedPayouts() as $failed) { + echo "- {$failed['payout']['name']}: {$failed['error']}\n"; + } +} +``` + +## Performance Optimization + +### Connection Pooling and Reuse + +Optimize HTTP connections for high-throughput applications: + +```php +use Http\Client\Common\PluginClient; +use Http\Client\Common\Plugin\ContentLengthPlugin; +use Http\Client\Common\Plugin\DecoderPlugin; +use Http\Discovery\Psr17FactoryDiscovery; + +class OptimizedPaystackClient +{ + private PaystackClient $client; + private array $connectionPool = []; + + public function __construct(string $secretKey) + { + // Create optimized HTTP client + $httpClient = $this->createOptimizedHttpClient(); + + $clientBuilder = new ClientBuilder($httpClient); + + // Add performance plugins + $clientBuilder->addPlugin(new ContentLengthPlugin()); + $clientBuilder->addPlugin(new DecoderPlugin()); + + $this->client = new PaystackClient([ + 'secretKey' => $secretKey, + 'clientBuilder' => $clientBuilder + ]); + } + + private function createOptimizedHttpClient() + { + // Configure cURL for optimal performance + $curlClient = new \Http\Client\Curl\Client( + Psr17FactoryDiscovery::findResponseFactory(), + Psr17FactoryDiscovery::findStreamFactory(), + [ + CURLOPT_TCP_KEEPALIVE => 1, + CURLOPT_TCP_KEEPIDLE => 60, + CURLOPT_TCP_KEEPINTVL => 30, + CURLOPT_MAXCONNECTS => 10, + CURLOPT_FRESH_CONNECT => false, + CURLOPT_FORBID_REUSE => false, + CURLOPT_TIMEOUT => 30, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_DNS_CACHE_TIMEOUT => 300, + CURLOPT_MAXREDIRS => 3, + ] + ); + + return $curlClient; + } + + public function getClient(): PaystackClient + { + return $this->client; + } +} +``` + +### Caching Strategies + +Implement caching for frequently accessed data: + +```php +use Psr\SimpleCache\CacheInterface; + +class CachedPaystackService +{ + private PaystackClient $paystack; + private CacheInterface $cache; + private int $defaultTtl = 3600; // 1 hour + + public function __construct(PaystackClient $paystack, CacheInterface $cache) + { + $this->paystack = $paystack; + $this->cache = $cache; + } + + public function getBanks(string $country = 'nigeria'): array + { + $cacheKey = "paystack_banks_{$country}"; + + $banks = $this->cache->get($cacheKey); + if ($banks !== null) { + return $banks; + } + + $response = $this->paystack->miscellaneous->listBanks(['country' => $country]); + $banks = $response['data'] ?? []; + + // Cache for 24 hours (banks don't change frequently) + $this->cache->set($cacheKey, $banks, 86400); + + return $banks; + } + + public function getCustomer(string $customerCode): array + { + $cacheKey = "paystack_customer_{$customerCode}"; + + $customer = $this->cache->get($cacheKey); + if ($customer !== null) { + return $customer; + } + + $response = $this->paystack->customers->fetch($customerCode); + $customer = $response['data'] ?? []; + + // Cache for 30 minutes + $this->cache->set($cacheKey, $customer, 1800); + + return $customer; + } + + public function invalidateCustomerCache(string $customerCode): void + { + $cacheKey = "paystack_customer_{$customerCode}"; + $this->cache->delete($cacheKey); + } + + public function getTransactionWithCache(string $reference): array + { + $cacheKey = "paystack_transaction_{$reference}"; + + $transaction = $this->cache->get($cacheKey); + if ($transaction !== null) { + return $transaction; + } + + $response = $this->paystack->transactions->verify($reference); + $transaction = $response['data'] ?? []; + + // Only cache successful transactions (they don't change) + if ($transaction['status'] === 'success') { + $this->cache->set($cacheKey, $transaction, $this->defaultTtl); + } + + return $transaction; + } +} +``` + +## Testing Strategies + +### Mock Testing with PHPUnit + +Create comprehensive tests using mocks: + +```php +use PHPUnit\Framework\TestCase; +use Http\Mock\Client as MockClient; +use StarfolkSoftware\Paystack\Client as PaystackClient; +use StarfolkSoftware\Paystack\ClientBuilder; +use GuzzleHttp\Psr7\Response; + +class PaymentServiceTest extends TestCase +{ + private MockClient $mockClient; + private PaystackClient $paystack; + private PaymentService $paymentService; + + protected function setUp(): void + { + $this->mockClient = new MockClient(); + + $clientBuilder = new ClientBuilder($this->mockClient); + $this->paystack = new PaystackClient([ + 'secretKey' => 'sk_test_mock_key', + 'clientBuilder' => $clientBuilder + ]); + + $this->paymentService = new PaymentService($this->paystack); + } + + public function testSuccessfulPaymentInitialization(): void + { + // Mock successful response + $mockResponse = new Response(200, [], json_encode([ + 'status' => true, + 'message' => 'Authorization URL created', + 'data' => [ + 'authorization_url' => 'https://checkout.paystack.com/abc123', + 'access_code' => 'abc123', + 'reference' => 'ref_123456789' + ] + ])); + + $this->mockClient->addResponse($mockResponse); + + $result = $this->paymentService->initializePayment([ + 'email' => 'test@example.com', + 'amount' => 20000 + ]); + + $this->assertTrue($result['status']); + $this->assertArrayHasKey('authorization_url', $result['data']); + $this->assertEquals('ref_123456789', $result['data']['reference']); + + // Verify request was made correctly + $request = $this->mockClient->getLastRequest(); + $this->assertEquals('POST', $request->getMethod()); + $this->assertStringContains('/transaction/initialize', $request->getUri()->getPath()); + } + + public function testPaymentVerification(): void + { + $mockResponse = new Response(200, [], json_encode([ + 'status' => true, + 'message' => 'Verification successful', + 'data' => [ + 'id' => 123456, + 'domain' => 'test', + 'status' => 'success', + 'reference' => 'ref_123456789', + 'amount' => 20000, + 'currency' => 'NGN', + 'customer' => [ + 'email' => 'test@example.com' + ], + 'paid_at' => '2024-01-15T10:30:00Z' + ] + ])); + + $this->mockClient->addResponse($mockResponse); + + $result = $this->paymentService->verifyPayment('ref_123456789'); + + $this->assertTrue($result['status']); + $this->assertEquals('success', $result['data']['status']); + $this->assertEquals(20000, $result['data']['amount']); + } + + public function testFailedPaymentInitialization(): void + { + $mockResponse = new Response(400, [], json_encode([ + 'status' => false, + 'message' => 'Invalid email address', + 'data' => null + ])); + + $this->mockClient->addResponse($mockResponse); + + $result = $this->paymentService->initializePayment([ + 'email' => 'invalid-email', + 'amount' => 20000 + ]); + + $this->assertFalse($result['status']); + $this->assertEquals('Invalid email address', $result['message']); + } + + public function testNetworkErrorHandling(): void + { + $this->mockClient->addException(new \Http\Client\Exception\NetworkException( + 'Network error', + $this->createMock(\Psr\Http\Message\RequestInterface::class) + )); + + $this->expectException(\Http\Client\Exception\NetworkException::class); + + $this->paymentService->initializePayment([ + 'email' => 'test@example.com', + 'amount' => 20000 + ]); + } +} + +class PaymentService +{ + private PaystackClient $paystack; + + public function __construct(PaystackClient $paystack) + { + $this->paystack = $paystack; + } + + public function initializePayment(array $data): array + { + return $this->paystack->transactions->initialize($data); + } + + public function verifyPayment(string $reference): array + { + return $this->paystack->transactions->verify($reference); + } +} +``` + +### Integration Testing + +Test against Paystack's test environment: + +```php +class PaystackIntegrationTest extends TestCase +{ + private PaystackClient $paystack; + + protected function setUp(): void + { + // Use test credentials + $this->paystack = new PaystackClient([ + 'secretKey' => $_ENV['PAYSTACK_TEST_SECRET_KEY'], + ]); + + // Skip if no test credentials + if (empty($_ENV['PAYSTACK_TEST_SECRET_KEY'])) { + $this->markTestSkipped('Paystack test credentials not available'); + } + } + + public function testRealPaymentFlow(): void + { + // Initialize payment + $transaction = $this->paystack->transactions->initialize([ + 'email' => 'test@example.com', + 'amount' => 20000, + 'currency' => 'NGN' + ]); + + $this->assertTrue($transaction['status']); + $this->assertNotEmpty($transaction['data']['reference']); + + // Verify the transaction (will be pending since not actually paid) + $verification = $this->paystack->transactions->verify($transaction['data']['reference']); + $this->assertEquals('pending', $verification['data']['status']); + } + + public function testCustomerOperations(): void + { + // Create customer + $customer = $this->paystack->customers->create([ + 'email' => 'integration-test-' . time() . '@example.com', + 'first_name' => 'Test', + 'last_name' => 'User' + ]); + + $this->assertTrue($customer['status']); + $customerCode = $customer['data']['customer_code']; + + // Fetch customer + $fetchedCustomer = $this->paystack->customers->fetch($customerCode); + $this->assertEquals($customer['data']['email'], $fetchedCustomer['data']['email']); + + // Update customer + $updatedCustomer = $this->paystack->customers->update($customerCode, [ + 'first_name' => 'Updated Test' + ]); + $this->assertEquals('Updated Test', $updatedCustomer['data']['first_name']); + } +} +``` + +This advanced usage guide covers sophisticated patterns and techniques for building robust, scalable applications with the Paystack PHP SDK. For additional examples and specific use cases, refer to the [examples directory](../examples/) and [troubleshooting guide](troubleshooting.md). \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..1b76dac --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,981 @@ +# API Reference + +This document provides a comprehensive reference for all Paystack PHP SDK classes and methods. + +## Table of Contents + +- [Client Configuration](#client-configuration) +- [Core Resources](#core-resources) + - [Transactions](#transactions) + - [Customers](#customers) + - [Payment Requests](#payment-requests) + - [Plans](#plans) + - [Subscriptions](#subscriptions) +- [Transfer Resources](#transfer-resources) + - [Transfers](#transfers) + - [Transfer Recipients](#transfer-recipients) + - [Transfer Control](#transfer-control) +- [Advanced Resources](#advanced-resources) + - [Subaccounts](#subaccounts) + - [Splits](#splits) + - [Apple Pay](#apple-pay) + - [Charges](#charges) + - [Bulk Charges](#bulk-charges) +- [Dispute & Refund Resources](#dispute--refund-resources) + - [Disputes](#disputes) + - [Refunds](#refunds) + - [Settlements](#settlements) +- [Utility Resources](#utility-resources) + - [Pages](#pages) + - [Products](#products) + - [Invoices](#invoices) + - [Verification](#verification) + - [Dedicated Virtual Accounts](#dedicated-virtual-accounts) + - [Terminals](#terminals) + - [Virtual Terminals](#virtual-terminals) + - [Integration](#integration) + - [Miscellaneous](#miscellaneous) + +## Client Configuration + +### StarfolkSoftware\Paystack\Client + +The main client class for interacting with the Paystack API. + +#### Constructor + +```php +public function __construct(array $opts = []) +``` + +**Parameters:** + +- `secretKey` (string, required): Your Paystack secret key +- `apiVersion` (string, optional): API version to use (default: 'v1') +- `baseUri` (string, optional): Custom base URI (default: 'https://api.paystack.co') + +**Example:** + +```php +$paystack = new PaystackClient([ + 'secretKey' => 'sk_test_your_secret_key_here', + 'apiVersion' => 'v1', + 'baseUri' => 'https://api.paystack.co' +]); +``` + +#### Methods + +##### getHttpClient() + +Returns the underlying HTTP client instance. + +```php +public function getHttpClient(): HttpMethodsClientInterface +``` + +--- + +## Core Resources + +### Transactions + +Manage payment transactions. + +#### Methods + +##### initialize() + +Initialize a transaction. + +```php +public function initialize(array $params): array +``` + +**Parameters:** + +- `email` (string, required): Customer's email address +- `amount` (int, required): Amount in kobo (smallest currency unit) +- `currency` (string, optional): Currency code (default: NGN) +- `reference` (string, optional): Unique transaction reference +- `callback_url` (string, optional): URL to redirect after payment +- `plan` (string, optional): Plan code for subscription +- `invoice_limit` (int, optional): Number of invoices to generate +- `metadata` (array, optional): Additional transaction data +- `channels` (array, optional): Payment channels to allow +- `split_code` (string, optional): Transaction split code +- `subaccount` (string, optional): Subaccount code +- `transaction_charge` (int, optional): Amount to charge subaccount +- `bearer` (string, optional): Who bears Paystack charges + +**Example:** + +```php +$transaction = $paystack->transactions->initialize([ + 'email' => 'customer@example.com', + 'amount' => 20000, + 'currency' => 'NGN', + 'callback_url' => 'https://yoursite.com/payment/callback', + 'metadata' => [ + 'custom_fields' => [ + ['display_name' => 'Cart ID', 'variable_name' => 'cart_id', 'value' => '12345'] + ] + ] +]); +``` + +##### verify() + +Verify a transaction. + +```php +public function verify(string $reference): array +``` + +**Parameters:** + +- `reference` (string, required): Transaction reference + +**Example:** + +```php +$verification = $paystack->transactions->verify('transaction_reference'); +``` + +##### all() + +List transactions with optional filters. + +```php +public function all(array $params = []): array +``` + +**Parameters:** + +- `perPage` (int, optional): Number of transactions per page +- `page` (int, optional): Page number +- `customer` (string, optional): Customer ID or code +- `status` (string, optional): Transaction status +- `from` (string, optional): Start date (YYYY-MM-DD) +- `to` (string, optional): End date (YYYY-MM-DD) +- `amount` (int, optional): Filter by amount + +**Example:** + +```php +$transactions = $paystack->transactions->all([ + 'perPage' => 50, + 'status' => 'success', + 'from' => '2024-01-01', + 'to' => '2024-12-31' +]); +``` + +##### fetch() + +Get details of a transaction. + +```php +public function fetch(int $id): array +``` + +**Parameters:** + +- `id` (int, required): Transaction ID + +##### chargeAuthorization() + +Charge an authorization. + +```php +public function chargeAuthorization(array $params): array +``` + +**Parameters:** + +- `authorization_code` (string, required): Authorization code +- `email` (string, required): Customer's email +- `amount` (int, required): Amount in kobo + +##### checkAuthorization() + +Check authorization validity. + +```php +public function checkAuthorization(array $params): array +``` + +##### timeline() + +View transaction timeline. + +```php +public function timeline(string $idOrReference): array +``` + +##### totals() + +Get transaction totals. + +```php +public function totals(array $params = []): array +``` + +##### export() + +Export transactions. + +```php +public function export(array $params = []): array +``` + +##### partialDebit() + +Perform partial debit. + +```php +public function partialDebit(array $params): array +``` + +--- + +### Customers + +Manage customer data and profiles. + +#### Methods + +##### create() + +Create a new customer. + +```php +public function create(array $params): array +``` + +**Parameters:** + +- `email` (string, required): Customer's email address +- `first_name` (string, optional): Customer's first name +- `last_name` (string, optional): Customer's last name +- `phone` (string, optional): Customer's phone number +- `metadata` (array, optional): Additional customer data + +**Example:** + +```php +$customer = $paystack->customers->create([ + 'email' => 'customer@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '+2348123456789', + 'metadata' => [ + 'custom_fields' => [ + ['display_name' => 'Loyalty ID', 'variable_name' => 'loyalty_id', 'value' => 'LTY123'] + ] + ] +]); +``` + +##### all() + +List customers. + +```php +public function all(array $params = []): array +``` + +**Parameters:** + +- `perPage` (int, optional): Number of customers per page +- `page` (int, optional): Page number +- `from` (string, optional): Start date +- `to` (string, optional): End date + +##### fetch() + +Get customer details. + +```php +public function fetch(string $emailOrCode): array +``` + +**Parameters:** + +- `emailOrCode` (string, required): Customer email or customer code + +##### update() + +Update customer information. + +```php +public function update(string $customerCode, array $params): array +``` + +**Parameters:** + +- `customerCode` (string, required): Customer code +- `first_name` (string, optional): Updated first name +- `last_name` (string, optional): Updated last name +- `phone` (string, optional): Updated phone number +- `metadata` (array, optional): Updated metadata + +##### validate() + +Validate customer identity. + +```php +public function validate(string $customerCode, array $params): array +``` + +##### setRiskAction() + +Set risk action for customer. + +```php +public function setRiskAction(string $customerCode, array $params): array +``` + +##### deactivateAuthorization() + +Deactivate customer authorization. + +```php +public function deactivateAuthorization(array $params): array +``` + +--- + +### Payment Requests + +Create and manage payment requests and invoices. + +#### Methods + +##### create() + +Create a payment request. + +```php +public function create(array $params): array +``` + +**Parameters:** + +- `description` (string, optional): Payment request description +- `line_items` (array, optional): Array of line items +- `tax` (array, optional): Array of tax items +- `customer` (string, required): Customer ID, code, or email +- `due_date` (string, optional): Due date (YYYY-MM-DD) +- `send_notification` (bool, optional): Send email notification +- `draft` (bool, optional): Save as draft +- `has_invoice` (bool, optional): Generate invoice number +- `invoice_number` (int, optional): Custom invoice number +- `split_code` (string, optional): Split code + +**Example:** + +```php +$paymentRequest = $paystack->paymentRequests->create([ + 'description' => 'Website Development Invoice', + 'line_items' => [ + ['name' => 'Frontend Development', 'amount' => 50000, 'quantity' => 1], + ['name' => 'Backend Development', 'amount' => 75000, 'quantity' => 1] + ], + 'tax' => [ + ['name' => 'VAT', 'amount' => 9375] // 7.5% + ], + 'customer' => 'customer@example.com', + 'due_date' => date('Y-m-d', strtotime('+30 days')), + 'send_notification' => true +]); +``` + +##### all() + +List payment requests. + +```php +public function all(array $params = []): array +``` + +**Parameters:** + +- `perPage` (int, optional): Number of requests per page +- `page` (int, optional): Page number +- `customer` (string, optional): Customer filter +- `status` (string, optional): Status filter + +##### fetch() + +Get payment request details. + +```php +public function fetch(string $idOrCode): array +``` + +##### verify() + +Verify payment request. + +```php +public function verify(string $code): array +``` + +##### sendNotification() + +Send payment request notification. + +```php +public function sendNotification(string $code): array +``` + +##### totals() + +Get payment request totals. + +```php +public function totals(): array +``` + +##### finalize() + +Finalize a draft payment request. + +```php +public function finalize(string $code, array $params = []): array +``` + +**Parameters:** + +- `code` (string, required): Payment request code +- `send_notification` (bool, optional): Send notification after finalizing + +##### update() + +Update payment request. + +```php +public function update(string $idOrCode, array $params): array +``` + +##### archive() + +Archive payment request. + +```php +public function archive(string $code): array +``` + +--- + +### Plans + +Create and manage subscription plans. + +#### Methods + +##### create() + +Create a subscription plan. + +```php +public function create(array $params): array +``` + +**Parameters:** + +- `name` (string, required): Plan name +- `interval` (string, required): Billing interval (daily, weekly, monthly, quarterly, biannually, annually) +- `amount` (int, required): Plan amount in kobo +- `description` (string, optional): Plan description +- `send_invoices` (bool, optional): Send invoices +- `send_sms` (bool, optional): Send SMS notifications +- `currency` (string, optional): Currency code + +**Example:** + +```php +$plan = $paystack->plans->create([ + 'name' => 'Premium Monthly Plan', + 'interval' => 'monthly', + 'amount' => 5000, // ₦50.00 + 'description' => 'Premium features with monthly billing', + 'currency' => 'NGN', + 'send_invoices' => true, + 'send_sms' => false +]); +``` + +##### all() + +List plans. + +```php +public function all(array $params = []): array +``` + +##### fetch() + +Get plan details. + +```php +public function fetch(string $idOrCode): array +``` + +##### update() + +Update plan. + +```php +public function update(string $idOrCode, array $params): array +``` + +--- + +### Subscriptions + +Manage recurring billing subscriptions. + +#### Methods + +##### create() + +Create a subscription. + +```php +public function create(array $params): array +``` + +**Parameters:** + +- `customer` (string, required): Customer code +- `plan` (string, required): Plan code +- `authorization` (string, optional): Authorization code +- `start_date` (string, optional): Subscription start date + +**Example:** + +```php +$subscription = $paystack->subscriptions->create([ + 'customer' => 'CUS_xwaj0txjryg393b', + 'plan' => 'PLN_plancode', + 'authorization' => 'AUTH_authorization_code' +]); +``` + +##### all() + +List subscriptions. + +```php +public function all(array $params = []): array +``` + +##### fetch() + +Get subscription details. + +```php +public function fetch(string $idOrCode): array +``` + +##### enable() + +Enable subscription. + +```php +public function enable(array $params): array +``` + +##### disable() + +Disable subscription. + +```php +public function disable(string $code, array $params): array +``` + +##### generateUpdateSubscriptionLink() + +Generate subscription update link. + +```php +public function generateUpdateSubscriptionLink(string $code): array +``` + +##### sendUpdateSubscriptionLink() + +Send subscription update link. + +```php +public function sendUpdateSubscriptionLink(string $code): array +``` + +--- + +## Transfer Resources + +### Transfers + +Send money to bank accounts and mobile money wallets. + +#### Methods + +##### initiate() + +Initiate a transfer. + +```php +public function initiate(array $params): array +``` + +**Parameters:** + +- `source` (string, required): Transfer source +- `amount` (int, required): Amount in kobo +- `recipient` (string, required): Transfer recipient code +- `reason` (string, optional): Transfer reason +- `currency` (string, optional): Currency code +- `reference` (string, optional): Transfer reference + +**Example:** + +```php +$transfer = $paystack->transfers->initiate([ + 'source' => 'balance', + 'amount' => 100000, // ₦1,000.00 + 'recipient' => 'RCP_recipient_code', + 'reason' => 'Payment for services', + 'currency' => 'NGN' +]); +``` + +##### finalize() + +Finalize transfer. + +```php +public function finalize(string $transferCode, array $params): array +``` + +##### initiateBulk() + +Initiate bulk transfer. + +```php +public function initiateBulk(array $params): array +``` + +##### all() + +List transfers. + +```php +public function all(array $params = []): array +``` + +##### fetch() + +Get transfer details. + +```php +public function fetch(string $idOrCode): array +``` + +##### verify() + +Verify transfer. + +```php +public function verify(string $reference): array +``` + +--- + +### Transfer Recipients + +Manage transfer beneficiaries. + +#### Methods + +##### create() + +Create transfer recipient. + +```php +public function create(array $params): array +``` + +**Parameters:** + +- `type` (string, required): Recipient type (nuban, mobile_money, basa) +- `name` (string, required): Recipient name +- `account_number` (string, required): Account number +- `bank_code` (string, required): Bank code +- `description` (string, optional): Description +- `currency` (string, optional): Currency code +- `authorization_code` (string, optional): Authorization code +- `metadata` (array, optional): Additional data + +**Example:** + +```php +$recipient = $paystack->transferRecipients->create([ + 'type' => 'nuban', + 'name' => 'John Doe', + 'account_number' => '0123456789', + 'bank_code' => '044', + 'currency' => 'NGN', + 'description' => 'Primary business account' +]); +``` + +##### bulkCreate() + +Create multiple recipients. + +```php +public function bulkCreate(array $params): array +``` + +##### all() + +List transfer recipients. + +```php +public function all(array $params = []): array +``` + +##### fetch() + +Get recipient details. + +```php +public function fetch(string $idOrCode): array +``` + +##### update() + +Update recipient. + +```php +public function update(string $idOrCode, array $params): array +``` + +##### delete() + +Delete transfer recipient. + +```php +public function delete(string $idOrCode): array +``` + +--- + +### Transfer Control + +Manage transfer settings and controls. + +#### Methods + +##### checkBalance() + +Check transfer balance. + +```php +public function checkBalance(): array +``` + +##### fetchBalanceLedger() + +Fetch balance ledger. + +```php +public function fetchBalanceLedger(): array +``` + +##### resendOtp() + +Resend transfer OTP. + +```php +public function resendOtp(array $params): array +``` + +##### disableOtp() + +Disable OTP requirement. + +```php +public function disableOtp(): array +``` + +##### finalizeDisableOtp() + +Finalize OTP disable. + +```php +public function finalizeDisableOtp(array $params): array +``` + +##### enableOtp() + +Enable OTP requirement. + +```php +public function enableOtp(): array +``` + +--- + +## Advanced Resources + +### Subaccounts + +Create marketplace split payment accounts. + +#### Methods + +##### create() + +Create subaccount. + +```php +public function create(array $params): array +``` + +**Parameters:** + +- `business_name` (string, required): Business name +- `settlement_bank` (string, required): Settlement bank code +- `account_number` (string, required): Account number +- `percentage_charge` (float, required): Percentage of each payment +- `description` (string, optional): Description +- `primary_contact_email` (string, optional): Contact email +- `primary_contact_name` (string, optional): Contact name +- `primary_contact_phone` (string, optional): Contact phone +- `metadata` (array, optional): Additional data + +**Example:** + +```php +$subaccount = $paystack->subaccounts->create([ + 'business_name' => 'Vendor Store', + 'settlement_bank' => '044', + 'account_number' => '0123456789', + 'percentage_charge' => 15.5, // 15.5% of each transaction + 'description' => 'Vendor commission account', + 'primary_contact_email' => 'vendor@example.com', + 'primary_contact_name' => 'Jane Vendor', + 'primary_contact_phone' => '+2348123456789' +]); +``` + +##### all() + +List subaccounts. + +```php +public function all(array $params = []): array +``` + +##### fetch() + +Get subaccount details. + +```php +public function fetch(string $idOrCode): array +``` + +##### update() + +Update subaccount. + +```php +public function update(string $idOrCode, array $params): array +``` + +--- + +### Splits + +Advanced payment splitting configuration. + +#### Methods + +##### create() + +Create payment split. + +```php +public function create(array $params): array +``` + +**Parameters:** + +- `name` (string, required): Split configuration name +- `type` (string, required): Split type (percentage, flat) +- `currency` (string, required): Currency code +- `subaccounts` (array, required): Array of subaccount splits +- `bearer_type` (string, required): Who bears charges +- `bearer_subaccount` (string, optional): Subaccount to bear charges + +**Example:** + +```php +$split = $paystack->splits->create([ + 'name' => 'Marketplace Split', + 'type' => 'percentage', + 'currency' => 'NGN', + 'subaccounts' => [ + ['subaccount' => 'ACCT_vendor1', 'share' => 70], + ['subaccount' => 'ACCT_vendor2', 'share' => 20] + ], + 'bearer_type' => 'all', +]); +``` + +##### all() + +List payment splits. + +```php +public function all(array $params = []): array +``` + +##### fetch() + +Get split details. + +```php +public function fetch(string $id): array +``` + +##### update() + +Update split configuration. + +```php +public function update(string $id, array $params): array +``` + +##### addOrUpdateSubaccount() + +Add or update subaccount in split. + +```php +public function addOrUpdateSubaccount(string $id, array $params): array +``` + +##### removeSubaccount() + +Remove subaccount from split. + +```php +public function removeSubaccount(string $id, array $params): array +``` + +--- + +This documentation provides comprehensive coverage of the Paystack PHP SDK API. For more detailed examples and use cases, refer to the [Getting Started Guide](getting-started.md) and [Advanced Usage](advanced-usage.md) documentation. \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..ee3e92f --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,564 @@ +# Getting Started with Paystack PHP SDK + +This guide will help you integrate Paystack payments into your PHP application using the official Paystack PHP SDK. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Authentication](#authentication) +- [Your First Payment](#your-first-payment) +- [Common Use Cases](#common-use-cases) +- [Best Practices](#best-practices) +- [Next Steps](#next-steps) + +## Prerequisites + +Before you begin, ensure you have: + +1. **PHP 8.0 or higher** installed on your system +2. **Composer** for dependency management +3. A **Paystack account** ([sign up here](https://dashboard.paystack.com/signup)) +4. Your **API keys** from the Paystack dashboard + +### Getting Your API Keys + +1. Log into your [Paystack Dashboard](https://dashboard.paystack.com) +2. Navigate to **Settings** → **API Keys & Webhooks** +3. Copy your **Test Secret Key** (starts with `sk_test_`) +4. For production, you'll use your **Live Secret Key** (starts with `sk_live_`) + +⚠️ **Security Note**: Never expose your secret keys in client-side code or public repositories. + +## Installation + +### Step 1: Install the SDK + +```bash +# Install the Paystack PHP SDK +composer require starfolksoftware/paystack-php + +# Install an HTTP client (choose one) +composer require php-http/guzzle7-adapter +# OR +composer require symfony/http-client +# OR +composer require php-http/curl-client +``` + +### Step 2: Verify Installation + +Create a simple test file to verify the installation: + +```php + 'sk_test_your_secret_key_here', // Replace with your actual test key +]); + +// Test the connection +try { + $response = $paystack->miscellaneous->listBanks(['country' => 'nigeria']); + echo "Connected to Paystack successfully!\n"; + echo "Found " . count($response['data']) . " banks\n"; +} catch (Exception $e) { + echo "Connection failed: " . $e->getMessage() . "\n"; +} +``` + +### Environment Configuration + +For better security, store your API keys in environment variables: + +```php +// .env file +PAYSTACK_SECRET_KEY=sk_test_your_secret_key_here +PAYSTACK_PUBLIC_KEY=pk_test_your_public_key_here +``` + +```php + $_ENV['PAYSTACK_SECRET_KEY'] ?? getenv('PAYSTACK_SECRET_KEY'), + 'publicKey' => $_ENV['PAYSTACK_PUBLIC_KEY'] ?? getenv('PAYSTACK_PUBLIC_KEY'), +]; + +$paystack = new PaystackClient([ + 'secretKey' => $paystackConfig['secretKey'], +]); +``` + +## Your First Payment + +Let's create a complete payment flow from initialization to verification. + +### Step 1: Initialize a Payment + +```php + 'sk_test_your_secret_key_here', +]); + +try { + // Initialize a payment + $transaction = $paystack->transactions->initialize([ + 'email' => 'customer@example.com', + 'amount' => 20000, // Amount in kobo (₦200.00) + 'currency' => 'NGN', + 'callback_url' => 'https://yourwebsite.com/payment/callback', + 'metadata' => [ + 'order_id' => 'ORD_12345', + 'custom_fields' => [ + [ + 'display_name' => 'Order ID', + 'variable_name' => 'order_id', + 'value' => 'ORD_12345' + ] + ] + ] + ]); + + if ($transaction['status']) { + echo "Payment initialized successfully!\n"; + echo "Reference: " . $transaction['data']['reference'] . "\n"; + echo "Authorization URL: " . $transaction['data']['authorization_url'] . "\n"; + + // Store the reference for later verification + file_put_contents('payment_reference.txt', $transaction['data']['reference']); + + // In a web application, you would redirect the user to the authorization_url + // header('Location: ' . $transaction['data']['authorization_url']); + + } else { + echo "Payment initialization failed: " . $transaction['message'] . "\n"; + } + +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` + +### Step 2: Handle Payment Callback + +Create a callback handler to process payment responses: + +```php + 'sk_test_your_secret_key_here', +]); + +// Get the transaction reference from URL parameter +$reference = $_GET['reference'] ?? null; + +if (!$reference) { + die('No payment reference provided'); +} + +try { + // Verify the transaction + $verification = $paystack->transactions->verify($reference); + + if ($verification['data']['status'] === 'success') { + $amount = $verification['data']['amount'] / 100; // Convert from kobo to naira + $customerEmail = $verification['data']['customer']['email']; + $paidAt = $verification['data']['paid_at']; + + echo "Payment Successful!\n"; + echo "Amount: ₦{$amount}\n"; + echo "Customer: {$customerEmail}\n"; + echo "Paid at: {$paidAt}\n"; + + // Here you would typically: + // 1. Update your database + // 2. Send confirmation email + // 3. Fulfill the order + // 4. Redirect to success page + + } else { + echo "Payment verification failed or payment was not successful\n"; + echo "Status: " . $verification['data']['status'] . "\n"; + + // Handle failed payment + // Redirect to failure page + } + +} catch (Exception $e) { + echo "Verification error: " . $e->getMessage() . "\n"; +} +``` + +### Step 3: Webhook Handler (Recommended) + +For production applications, implement webhook handling for real-time payment notifications: + +```php +customers->create([ + 'email' => 'john.doe@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '+2348123456789', + 'metadata' => [ + 'custom_fields' => [ + ['display_name' => 'Loyalty Number', 'variable_name' => 'loyalty_number', 'value' => 'LOY123456'] + ] + ] +]); + +$customerCode = $customer['data']['customer_code']; + +// Update customer information +$updatedCustomer = $paystack->customers->update($customerCode, [ + 'first_name' => 'Jonathan', + 'metadata' => [ + 'vip_status' => 'gold', + 'last_purchase_date' => date('Y-m-d') + ] +]); + +// Fetch customer details +$customerDetails = $paystack->customers->fetch($customerCode); +``` + +### 2. Subscription Billing + +```php +// Step 1: Create a plan +$plan = $paystack->plans->create([ + 'name' => 'Premium Monthly Subscription', + 'interval' => 'monthly', + 'amount' => 5000, // ₦50.00 per month + 'currency' => 'NGN', + 'description' => 'Access to premium features', + 'send_invoices' => true, + 'send_sms' => false +]); + +$planCode = $plan['data']['plan_code']; + +// Step 2: Create subscription (after customer has completed initial payment) +$subscription = $paystack->subscriptions->create([ + 'customer' => $customerCode, + 'plan' => $planCode, + 'authorization' => 'AUTH_authorization_code', // From previous successful payment +]); + +// Step 3: Manage subscription +$subscriptions = $paystack->subscriptions->all([ + 'customer' => $customerCode +]); + +// Disable subscription +$paystack->subscriptions->disable($subscription['data']['subscription_code'], [ + 'code' => $subscription['data']['subscription_code'], + 'token' => 'subscription_email_token' +]); +``` + +### 3. Payment Requests/Invoicing + +```php +// Create an invoice +$invoice = $paystack->paymentRequests->create([ + 'description' => 'Web Development Services - Project Alpha', + 'line_items' => [ + [ + 'name' => 'Frontend Development', + 'amount' => 150000, // ₦1,500 + 'quantity' => 1 + ], + [ + 'name' => 'Backend API Development', + 'amount' => 200000, // ₦2,000 + 'quantity' => 1 + ], + [ + 'name' => 'Database Design', + 'amount' => 75000, // ₦750 + 'quantity' => 1 + ] + ], + 'tax' => [ + [ + 'name' => 'VAT (7.5%)', + 'amount' => 31875 // 7.5% of 425000 + ] + ], + 'customer' => 'client@company.com', + 'due_date' => date('Y-m-d', strtotime('+30 days')), + 'send_notification' => true, + 'invoice_number' => 1001, + 'currency' => 'NGN' +]); + +// Send payment reminder +if (isset($invoice['data']['request_code'])) { + $paystack->paymentRequests->sendNotification($invoice['data']['request_code']); +} +``` + +### 4. Transfer Money + +```php +// Step 1: Create transfer recipient +$recipient = $paystack->transferRecipients->create([ + 'type' => 'nuban', + 'name' => 'John Doe', + 'account_number' => '0123456789', + 'bank_code' => '044', // Access Bank + 'currency' => 'NGN' +]); + +// Step 2: Initiate transfer +$transfer = $paystack->transfers->initiate([ + 'source' => 'balance', + 'amount' => 50000, // ₦500.00 + 'recipient' => $recipient['data']['recipient_code'], + 'reason' => 'Payment for freelance work', + 'currency' => 'NGN' +]); + +// Step 3: Check transfer status +$transferStatus = $paystack->transfers->fetch($transfer['data']['transfer_code']); +``` + +## Best Practices + +### 1. Error Handling + +Always implement comprehensive error handling: + +```php +function processPayment($paymentData) { + try { + $paystack = new PaystackClient([ + 'secretKey' => $_ENV['PAYSTACK_SECRET_KEY'], + ]); + + $transaction = $paystack->transactions->initialize($paymentData); + + if (!$transaction['status']) { + throw new Exception('Payment initialization failed: ' . $transaction['message']); + } + + return [ + 'success' => true, + 'data' => $transaction['data'] + ]; + + } catch (\Psr\Http\Client\NetworkExceptionInterface $e) { + // Network errors + error_log('Network error: ' . $e->getMessage()); + return ['success' => false, 'error' => 'Network connection failed']; + + } catch (\Psr\Http\Client\RequestExceptionInterface $e) { + // Request errors + error_log('Request error: ' . $e->getMessage()); + return ['success' => false, 'error' => 'Invalid request']; + + } catch (Exception $e) { + // General errors + error_log('Payment error: ' . $e->getMessage()); + return ['success' => false, 'error' => 'Payment processing failed']; + } +} +``` + +### 2. Security + +- **Never expose secret keys** in client-side code +- **Always verify webhooks** using signature validation +- **Use HTTPS** for all payment-related endpoints +- **Validate all input** before sending to Paystack +- **Store sensitive data securely** using encryption + +```php +// Good: Store keys in environment variables +$paystack = new PaystackClient([ + 'secretKey' => $_ENV['PAYSTACK_SECRET_KEY'], +]); + +// Bad: Hardcoded keys in code +// $paystack = new PaystackClient([ +// 'secretKey' => 'sk_test_actual_key_here', // DON'T DO THIS! +// ]); +``` + +### 3. Testing + +Use Paystack's test mode for development: + +```php +// Test card numbers for different scenarios +$testCards = [ + 'successful_payment' => '4084084084084081', + 'insufficient_funds' => '4084084084084107', + 'invalid_pin' => '4084084084084099', + 'timeout' => '4084084084084016' +]; + +// Use test keys during development +$isProduction = $_ENV['APP_ENV'] === 'production'; +$secretKey = $isProduction + ? $_ENV['PAYSTACK_LIVE_SECRET_KEY'] + : $_ENV['PAYSTACK_TEST_SECRET_KEY']; +``` + +### 4. Logging and Monitoring + +Implement proper logging for payment activities: + +```php +function logPaymentActivity($type, $reference, $data = []) { + $logEntry = [ + 'timestamp' => date('Y-m-d H:i:s'), + 'type' => $type, + 'reference' => $reference, + 'data' => $data + ]; + + error_log('PAYSTACK: ' . json_encode($logEntry)); + + // You might also want to log to a database or external service +} + +// Usage +logPaymentActivity('payment_initialized', $transaction['data']['reference'], [ + 'amount' => $paymentData['amount'], + 'email' => $paymentData['email'] +]); +``` + +## Next Steps + +Now that you have a basic understanding of the Paystack PHP SDK: + +1. **Read the [API Reference](api-reference.md)** for detailed documentation of all available methods +2. **Check out [Advanced Usage](advanced-usage.md)** for complex scenarios and optimization techniques +3. **Review the [Examples](../examples/)** directory for more code samples +4. **Visit the [Troubleshooting Guide](troubleshooting.md)** if you encounter issues +5. **Explore Paystack's [official documentation](https://paystack.com/docs/api/)** for additional insights + +### Useful Resources + +- [Paystack Dashboard](https://dashboard.paystack.com/) +- [Paystack API Documentation](https://paystack.com/docs/api/) +- [Test Payment Cards](https://paystack.com/docs/payments/test-payments/) +- [Webhook Events Reference](https://paystack.com/docs/payments/webhooks/) +- [SDK GitHub Repository](https://github.com/starfolksoftware/paystack-php) + +### Community and Support + +- **GitHub Issues**: [Report bugs or request features](https://github.com/starfolksoftware/paystack-php/issues) +- **Email Support**: [contact@starfolksoftware.com](mailto:contact@starfolksoftware.com) +- **Paystack Support**: [hello@paystack.com](mailto:hello@paystack.com) + +Happy coding! 🚀 \ No newline at end of file diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..e3c8f70 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,956 @@ +# Troubleshooting Guide + +This guide helps you resolve common issues when working with the Paystack PHP SDK. + +## Table of Contents + +- [Installation Issues](#installation-issues) +- [Authentication Problems](#authentication-problems) +- [API Request Errors](#api-request-errors) +- [Payment Issues](#payment-issues) +- [Webhook Problems](#webhook-problems) +- [Performance Issues](#performance-issues) +- [Testing and Development](#testing-and-development) +- [Common Error Messages](#common-error-messages) +- [Debug Tools and Techniques](#debug-tools-and-techniques) + +## Installation Issues + +### Problem: Composer Installation Fails + +**Symptoms:** +```bash +composer require starfolksoftware/paystack-php +# Results in dependency conflicts or installation errors +``` + +**Solutions:** + +1. **Update Composer:** + ```bash + composer self-update + composer clear-cache + composer install + ``` + +2. **Check PHP Version:** + ```bash + php -v + # Ensure PHP 8.0 or higher + ``` + +3. **Force Install with Dependencies:** + ```bash + composer require starfolksoftware/paystack-php --with-all-dependencies + ``` + +4. **Manual HTTP Client Installation:** + ```bash + # Choose one HTTP client implementation + composer require php-http/guzzle7-adapter + # OR + composer require symfony/http-client + ``` + +### Problem: HTTP Client Not Found + +**Error Message:** +``` +Could not find resource using any available factory. Have you installed a package that provides a psr/http-client-implementation? +``` + +**Solution:** +```bash +# Install an HTTP client implementation +composer require php-http/guzzle7-adapter +# OR any other PSR-18 compatible client +composer require php-http/curl-client +``` + +### Problem: Autoloader Issues + +**Error Message:** +``` +Class 'StarfolkSoftware\Paystack\Client' not found +``` + +**Solutions:** + +1. **Check Autoloader:** + ```php + $secretKey]); + $response = $paystack->miscellaneous->listBanks(['country' => 'nigeria']); + return $response['status'] === true; + } catch (Exception $e) { + echo "API Key Test Failed: " . $e->getMessage() . "\n"; + return false; + } + } + + if (!testApiKey($secretKey)) { + echo "Please check your API key\n"; + } + ``` + +### Problem: Using Live Key in Test Mode + +**Symptoms:** +- Unexpected charges to real accounts +- Live webhooks triggering in development + +**Solution:** +```php +class PaystackConfig +{ + public static function getConfig(): array + { + $environment = $_ENV['APP_ENV'] ?? 'development'; + + if ($environment === 'production') { + return [ + 'secretKey' => $_ENV['PAYSTACK_LIVE_SECRET_KEY'], + 'publicKey' => $_ENV['PAYSTACK_LIVE_PUBLIC_KEY'], + ]; + } + + return [ + 'secretKey' => $_ENV['PAYSTACK_TEST_SECRET_KEY'], + 'publicKey' => $_ENV['PAYSTACK_TEST_PUBLIC_KEY'], + ]; + } +} + +// Usage +$config = PaystackConfig::getConfig(); +$paystack = new PaystackClient(['secretKey' => $config['secretKey']]); +``` + +## API Request Errors + +### Problem: Network Timeout Errors + +**Error Message:** +``` +cURL error 28: Operation timed out after 30000 milliseconds +``` + +**Solutions:** + +1. **Increase Timeout:** + ```php + use Http\Client\Curl\Client as CurlClient; + use StarfolkSoftware\Paystack\ClientBuilder; + + $curlClient = new CurlClient(null, null, [ + CURLOPT_TIMEOUT => 60, // 60 seconds + CURLOPT_CONNECTTIMEOUT => 30, // 30 seconds connection timeout + ]); + + $clientBuilder = new ClientBuilder($curlClient); + $paystack = new PaystackClient([ + 'secretKey' => $secretKey, + 'clientBuilder' => $clientBuilder, + ]); + ``` + +2. **Implement Retry Logic:** + ```php + function executeWithRetry(callable $operation, int $maxRetries = 3): array + { + $lastException = null; + + for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { + try { + return $operation(); + } catch (\Psr\Http\Client\NetworkExceptionInterface $e) { + $lastException = $e; + + if ($attempt < $maxRetries) { + $delay = pow(2, $attempt) * 1000; // Exponential backoff + usleep($delay * 1000); + echo "Retry attempt {$attempt} in {$delay}ms...\n"; + continue; + } + } + } + + throw $lastException; + } + + // Usage + $transaction = executeWithRetry(function() use ($paystack) { + return $paystack->transactions->initialize([ + 'email' => 'customer@example.com', + 'amount' => 20000 + ]); + }); + ``` + +### Problem: SSL Certificate Issues + +**Error Message:** +``` +cURL error 60: SSL certificate problem: unable to get local issuer certificate +``` + +**Solutions:** + +1. **Update CA Bundle:** + ```php + // Download latest CA bundle from https://curl.haxx.se/ca/cacert.pem + // Update php.ini: + // curl.cainfo = "/path/to/cacert.pem" + // openssl.cafile = "/path/to/cacert.pem" + ``` + +2. **Configure cURL Options (not recommended for production):** + ```php + $curlClient = new CurlClient(null, null, [ + CURLOPT_SSL_VERIFYPEER => false, // Only for development! + CURLOPT_SSL_VERIFYHOST => false, // Only for development! + ]); + ``` + +3. **Use System CA Bundle:** + ```php + $curlClient = new CurlClient(null, null, [ + CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt', // Linux + // OR + CURLOPT_CAINFO => '/System/Library/OpenSSL/certs/cert.pem', // macOS + ]); + ``` + +## Payment Issues + +### Problem: Transaction Verification Fails + +**Error Message:** +```json +{ + "status": false, + "message": "Transaction reference not found" +} +``` + +**Solutions:** + +1. **Check Reference Format:** + ```php + function verifyTransaction(string $reference): array + { + // Validate reference format + if (empty($reference) || strlen($reference) < 10) { + throw new InvalidArgumentException('Invalid transaction reference'); + } + + try { + $paystack = new PaystackClient(['secretKey' => $secretKey]); + return $paystack->transactions->verify($reference); + } catch (Exception $e) { + // Log error for debugging + error_log("Transaction verification failed: {$reference} - {$e->getMessage()}"); + throw $e; + } + } + ``` + +2. **Handle Different Transaction States:** + ```php + function handleTransactionVerification(string $reference): array + { + $response = $paystack->transactions->verify($reference); + + if (!$response['status']) { + throw new Exception("Verification failed: " . $response['message']); + } + + $status = $response['data']['status']; + + switch ($status) { + case 'success': + return ['status' => 'completed', 'data' => $response['data']]; + + case 'failed': + return ['status' => 'failed', 'reason' => $response['data']['gateway_response']]; + + case 'abandoned': + return ['status' => 'abandoned', 'reason' => 'Payment was abandoned']; + + default: + return ['status' => 'pending', 'message' => 'Payment still processing']; + } + } + ``` + +### Problem: Amount Mismatch + +**Symptoms:** +- Expected ₦200, but charged ₦20,000 +- Customer complaints about wrong amounts + +**Solution:** +```php +class AmountValidator +{ + public static function validateAmount(float $amount, string $currency = 'NGN'): int + { + if ($amount <= 0) { + throw new InvalidArgumentException('Amount must be greater than zero'); + } + + // Convert to smallest currency unit (kobo for NGN) + $koboAmount = (int) ($amount * 100); + + // Validate conversion + if (abs(($koboAmount / 100) - $amount) > 0.001) { + throw new InvalidArgumentException('Amount has too many decimal places'); + } + + // Minimum amount check (₦1.00 = 100 kobo) + if ($koboAmount < 100) { + throw new InvalidArgumentException('Amount too small (minimum ₦1.00)'); + } + + return $koboAmount; + } + + public static function formatAmount(int $koboAmount): string + { + return '₦' . number_format($koboAmount / 100, 2); + } +} + +// Usage +try { + $koboAmount = AmountValidator::validateAmount(200.00); // ₦200.00 + echo "Charging: " . AmountValidator::formatAmount($koboAmount) . "\n"; + + $transaction = $paystack->transactions->initialize([ + 'email' => 'customer@example.com', + 'amount' => $koboAmount, // 20000 kobo = ₦200.00 + ]); +} catch (InvalidArgumentException $e) { + echo "Amount validation failed: " . $e->getMessage() . "\n"; +} +``` + +### Problem: Payment Not Completing + +**Symptoms:** +- Payment redirects but never completes +- Webhook not received +- Transaction stuck in pending + +**Debug Steps:** + +1. **Check Callback URL:** + ```php + function validateCallbackUrl(string $url): bool + { + // Ensure URL is accessible + $headers = @get_headers($url); + if (!$headers || strpos($headers[0], '200') === false) { + echo "Warning: Callback URL not accessible: {$url}\n"; + return false; + } + + // Ensure HTTPS for production + if (strpos($url, 'https://') !== 0 && $_ENV['APP_ENV'] === 'production') { + echo "Warning: Use HTTPS for callback URL in production\n"; + return false; + } + + return true; + } + + $callbackUrl = 'https://yoursite.com/payment/callback'; + if (!validateCallbackUrl($callbackUrl)) { + echo "Fix callback URL issues before proceeding\n"; + } + ``` + +2. **Test Webhook Endpoint:** + ```php + // webhook-test.php + echo "Webhook endpoint is accessible\n"; + echo "Method: " . $_SERVER['REQUEST_METHOD'] . "\n"; + echo "Headers:\n"; + foreach (getallheaders() as $name => $value) { + echo " {$name}: {$value}\n"; + } + echo "Body: " . file_get_contents('php://input') . "\n"; + ``` + +## Webhook Problems + +### Problem: Webhook Signature Verification Fails + +**Error Message:** +``` +Invalid webhook signature +``` + +**Solutions:** + +1. **Debug Signature Calculation:** + ```php + function debugWebhookSignature(): void + { + $payload = file_get_contents('php://input'); + $receivedSignature = $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] ?? ''; + $secretKey = 'sk_test_your_secret_key_here'; + + echo "Received signature: {$receivedSignature}\n"; + echo "Payload length: " . strlen($payload) . "\n"; + echo "Payload hash: " . md5($payload) . "\n"; + + $computedSignature = hash_hmac('sha512', $payload, $secretKey); + echo "Computed signature: {$computedSignature}\n"; + + echo "Signatures match: " . (hash_equals($receivedSignature, $computedSignature) ? 'YES' : 'NO') . "\n"; + } + + // Call this in your webhook endpoint for debugging + debugWebhookSignature(); + ``` + +2. **Check Headers:** + ```php + function validateWebhookHeaders(): array + { + $requiredHeaders = ['HTTP_X_PAYSTACK_SIGNATURE']; + $missing = []; + + foreach ($requiredHeaders as $header) { + if (!isset($_SERVER[$header])) { + $missing[] = $header; + } + } + + if (!empty($missing)) { + throw new Exception('Missing webhook headers: ' . implode(', ', $missing)); + } + + return [ + 'signature' => $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'], + 'timestamp' => $_SERVER['HTTP_X_PAYSTACK_TIMESTAMP'] ?? null, + ]; + } + ``` + +### Problem: Duplicate Webhook Events + +**Symptoms:** +- Same event processed multiple times +- Database constraint violations +- Duplicate emails sent + +**Solution:** +```php +class WebhookDeduplicator +{ + private $processedEvents = []; + private string $cacheFile; + + public function __construct(string $cacheFile = 'webhook_events.json') + { + $this->cacheFile = $cacheFile; + $this->loadProcessedEvents(); + } + + public function isEventProcessed(array $event): bool + { + $eventId = $this->generateEventId($event); + return in_array($eventId, $this->processedEvents); + } + + public function markEventProcessed(array $event): void + { + $eventId = $this->generateEventId($event); + + if (!in_array($eventId, $this->processedEvents)) { + $this->processedEvents[] = $eventId; + $this->saveProcessedEvents(); + } + } + + private function generateEventId(array $event): string + { + // Create unique ID from event data + $data = [ + 'event' => $event['event'], + 'reference' => $event['data']['reference'] ?? '', + 'id' => $event['data']['id'] ?? '', + ]; + + return md5(json_encode($data)); + } + + private function loadProcessedEvents(): void + { + if (file_exists($this->cacheFile)) { + $this->processedEvents = json_decode(file_get_contents($this->cacheFile), true) ?: []; + } + } + + private function saveProcessedEvents(): void + { + // Keep only last 1000 events + if (count($this->processedEvents) > 1000) { + $this->processedEvents = array_slice($this->processedEvents, -1000); + } + + file_put_contents($this->cacheFile, json_encode($this->processedEvents)); + } +} + +// Usage in webhook handler +$deduplicator = new WebhookDeduplicator(); +$event = json_decode(file_get_contents('php://input'), true); + +if ($deduplicator->isEventProcessed($event)) { + echo "Event already processed"; + exit; +} + +// Process event... +processEvent($event); + +// Mark as processed +$deduplicator->markEventProcessed($event); +``` + +## Performance Issues + +### Problem: Slow API Responses + +**Symptoms:** +- Requests taking longer than expected +- Timeouts in high-traffic scenarios + +**Solutions:** + +1. **Enable HTTP/2 and Keep-Alive:** + ```php + $curlClient = new CurlClient(null, null, [ + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0, + CURLOPT_TCP_KEEPALIVE => 1, + CURLOPT_TCP_KEEPIDLE => 60, + CURLOPT_TCP_KEEPINTVL => 30, + CURLOPT_MAXCONNECTS => 10, + ]); + ``` + +2. **Implement Response Caching:** + ```php + class CachedPaystackClient + { + private PaystackClient $client; + private array $cache = []; + private int $ttl = 300; // 5 minutes + + public function getCachedBanks(): array + { + $cacheKey = 'banks'; + + if (isset($this->cache[$cacheKey]) && + $this->cache[$cacheKey]['expires'] > time()) { + return $this->cache[$cacheKey]['data']; + } + + $banks = $this->client->miscellaneous->listBanks(['country' => 'nigeria']); + + $this->cache[$cacheKey] = [ + 'data' => $banks, + 'expires' => time() + $this->ttl + ]; + + return $banks; + } + } + ``` + +3. **Use Async Requests for Bulk Operations:** + ```php + use GuzzleHttp\Promise; + use GuzzleHttp\Client as GuzzleClient; + + function verifyTransactionsBatch(array $references): array + { + $client = new GuzzleClient(); + $promises = []; + + foreach ($references as $reference) { + $promises[$reference] = $client->getAsync( + "https://api.paystack.co/transaction/verify/{$reference}", + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $secretKey, + 'Content-Type' => 'application/json', + ] + ] + ); + } + + $responses = Promise\settle($promises)->wait(); + + $results = []; + foreach ($responses as $reference => $response) { + if ($response['state'] === 'fulfilled') { + $results[$reference] = json_decode($response['value']->getBody(), true); + } else { + $results[$reference] = ['error' => $response['reason']->getMessage()]; + } + } + + return $results; + } + ``` + +## Testing and Development + +### Problem: Unable to Test Payments + +**Solutions:** + +1. **Use Test Card Numbers:** + ```php + class TestCards + { + public const SUCCESSFUL_PAYMENT = '4084084084084081'; + public const INSUFFICIENT_FUNDS = '4084084084084107'; + public const INVALID_PIN = '4084084084084099'; + public const TIMEOUT = '4084084084084016'; + + public static function getTestScenario(string $scenario): array + { + $cards = [ + 'success' => self::SUCCESSFUL_PAYMENT, + 'insufficient_funds' => self::INSUFFICIENT_FUNDS, + 'invalid_pin' => self::INVALID_PIN, + 'timeout' => self::TIMEOUT, + ]; + + if (!isset($cards[$scenario])) { + throw new InvalidArgumentException("Unknown test scenario: {$scenario}"); + } + + return [ + 'card_number' => $cards[$scenario], + 'expiry_month' => '12', + 'expiry_year' => '25', + 'cvv' => '123' + ]; + } + } + ``` + +2. **Mock Paystack Responses:** + ```php + use Http\Mock\Client as MockClient; + use GuzzleHttp\Psr7\Response; + + function createMockPaystackClient(): PaystackClient + { + $mockClient = new MockClient(); + + // Mock successful transaction initialization + $mockClient->addResponse(new Response(200, [], json_encode([ + 'status' => true, + 'message' => 'Authorization URL created', + 'data' => [ + 'authorization_url' => 'https://checkout.paystack.com/test123', + 'access_code' => 'test123', + 'reference' => 'test_ref_123' + ] + ]))); + + $clientBuilder = new ClientBuilder($mockClient); + return new PaystackClient([ + 'secretKey' => 'sk_test_mock', + 'clientBuilder' => $clientBuilder + ]); + } + ``` + +## Common Error Messages + +### "Customer with email already exists" + +**Solution:** +```php +function getOrCreateCustomer(string $email, array $customerData = []): array +{ + try { + // Try to fetch existing customer + $customer = $paystack->customers->fetch($email); + if ($customer['status']) { + return $customer; + } + } catch (Exception $e) { + // Customer doesn't exist, create new one + } + + return $paystack->customers->create(array_merge([ + 'email' => $email + ], $customerData)); +} +``` + +### "Amount should be a valid number" + +**Solution:** +```php +function sanitizeAmount($amount): int +{ + // Remove currency symbols and formatting + $amount = preg_replace('/[^0-9.]/', '', $amount); + + // Convert to float + $amount = (float) $amount; + + // Convert to kobo (smallest unit) + return (int) ($amount * 100); +} + +// Usage +$amount = sanitizeAmount('₦200.50'); // Returns 20050 +``` + +### "Invalid authorization code" + +**Solution:** +```php +function validateAuthorizationCode(string $authCode): bool +{ + return preg_match('/^AUTH_[a-zA-Z0-9]+$/', $authCode) === 1; +} + +if (!validateAuthorizationCode($authCode)) { + throw new InvalidArgumentException('Invalid authorization code format'); +} +``` + +## Debug Tools and Techniques + +### Enable Debug Logging + +```php +use Psr\Log\LoggerInterface; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +class PaystackDebugger +{ + private LoggerInterface $logger; + + public function __construct() + { + $this->logger = new Logger('paystack'); + $this->logger->pushHandler(new StreamHandler('paystack_debug.log', Logger::DEBUG)); + } + + public function logRequest(string $method, string $endpoint, array $data = []): void + { + $this->logger->info('Paystack API Request', [ + 'method' => $method, + 'endpoint' => $endpoint, + 'data' => $data, + 'timestamp' => date('Y-m-d H:i:s') + ]); + } + + public function logResponse(array $response): void + { + $this->logger->info('Paystack API Response', [ + 'status' => $response['status'] ?? false, + 'message' => $response['message'] ?? '', + 'data_keys' => isset($response['data']) ? array_keys($response['data']) : [], + 'timestamp' => date('Y-m-d H:i:s') + ]); + } + + public function logError(\Exception $e): void + { + $this->logger->error('Paystack Error', [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]); + } +} + +// Usage +$debugger = new PaystackDebugger(); + +try { + $debugger->logRequest('POST', '/transaction/initialize', $paymentData); + + $response = $paystack->transactions->initialize($paymentData); + + $debugger->logResponse($response); +} catch (Exception $e) { + $debugger->logError($e); + throw $e; +} +``` + +### Network Debugging + +```php +function debugNetworkIssues(): void +{ + // Test connectivity to Paystack + $paystackApi = 'https://api.paystack.co'; + + echo "Testing connectivity to {$paystackApi}...\n"; + + $context = stream_context_create([ + 'http' => [ + 'timeout' => 10, + 'method' => 'GET' + ] + ]); + + $start = microtime(true); + $response = @file_get_contents($paystackApi, false, $context); + $duration = microtime(true) - $start; + + if ($response === false) { + echo "❌ Cannot reach Paystack API\n"; + echo "Check your internet connection and firewall settings\n"; + } else { + echo "✅ Successfully connected to Paystack API\n"; + echo "Response time: " . number_format($duration * 1000, 2) . "ms\n"; + } + + // Test DNS resolution + $ip = gethostbyname('api.paystack.co'); + echo "Paystack API IP: {$ip}\n"; + + // Test cURL capabilities + if (!function_exists('curl_init')) { + echo "❌ cURL extension not installed\n"; + } else { + echo "✅ cURL extension available\n"; + $version = curl_version(); + echo "cURL version: {$version['version']}\n"; + echo "SSL version: {$version['ssl_version']}\n"; + } + + // Test OpenSSL + if (!extension_loaded('openssl')) { + echo "❌ OpenSSL extension not loaded\n"; + } else { + echo "✅ OpenSSL extension loaded\n"; + } +} + +// Run diagnostics +debugNetworkIssues(); +``` + +### Getting Help + +If you're still experiencing issues: + +1. **Check the logs:** Enable debug logging and review the output +2. **Test with minimal code:** Create a simple test script to isolate the issue +3. **Verify your environment:** Ensure all dependencies are properly installed +4. **Check Paystack status:** Visit [Paystack Status Page](https://status.paystack.com/) +5. **Contact support:** + - GitHub Issues: [https://github.com/starfolksoftware/paystack-php/issues](https://github.com/starfolksoftware/paystack-php/issues) + - Email: [contact@starfolksoftware.com](mailto:contact@starfolksoftware.com) + - Paystack Support: [hello@paystack.com](mailto:hello@paystack.com) + +### Creating Bug Reports + +When reporting issues, include: + +```php +// Debug information script +function generateDebugInfo(): array +{ + return [ + 'php_version' => PHP_VERSION, + 'operating_system' => PHP_OS, + 'paystack_sdk_version' => 'Check composer.lock', + 'http_client' => class_exists('GuzzleHttp\Client') ? 'Guzzle' : 'Other', + 'curl_version' => curl_version()['version'] ?? 'Not available', + 'openssl_version' => OPENSSL_VERSION_TEXT ?? 'Not available', + 'memory_limit' => ini_get('memory_limit'), + 'max_execution_time' => ini_get('max_execution_time'), + 'error_reporting' => error_reporting(), + 'date_timezone' => date_default_timezone_get(), + ]; +} + +echo "Debug Information:\n"; +echo json_encode(generateDebugInfo(), JSON_PRETTY_PRINT); +``` + +Include this information along with: +- Steps to reproduce the issue +- Expected vs actual behavior +- Any error messages or logs +- Sample code that demonstrates the problem \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 5a299ad..68aa455 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,44 +1,374 @@ -# Paystack PHP Examples +# Paystack PHP SDK Examples -This directory contains example scripts demonstrating how to use the Paystack PHP client library. +This directory contains comprehensive examples demonstrating real-world usage patterns of the Paystack PHP SDK. Each example is thoroughly documented with explanations, best practices, and production-ready code patterns. -## Prerequisites +## 📋 Table of Contents -Before running these examples, make sure you have: +- [Quick Start](#quick-start) +- [Available Examples](#available-examples) +- [How to Run Examples](#how-to-run-examples) +- [Example Categories](#example-categories) +- [Testing Information](#testing-information) +- [Production Considerations](#production-considerations) -1. Installed this package via Composer (`composer require starfolksoftware/paystack-php`) -2. A valid Paystack secret key (test or live) +## 🚀 Quick Start -## Available Examples +### Prerequisites -### Payment Request Examples +1. **PHP 8.0 or higher** +2. **Composer** (for dependency management) +3. **Paystack Account** ([sign up here](https://dashboard.paystack.com/signup)) +4. **Test API Keys** from your Paystack dashboard -- `simple_payment_request.php`: Basic example showing how to create and list payment requests -- `payment_request_demo.php`: Comprehensive example demonstrating all payment request API methods -- `invoice_workflow.php`: Demonstrates a complete invoice workflow using payment requests -- `recurring_billing.php`: Advanced example showing how to implement recurring billing using payment requests +### Setup -## How to Run +1. **Install the SDK:** + ```bash + composer require starfolksoftware/paystack-php + ``` -1. Update the `$secretKey` variable in the example files with your Paystack secret key. -2. Execute the script using PHP: +2. **Get your API keys:** + - Log into your [Paystack Dashboard](https://dashboard.paystack.com) + - Navigate to **Settings** → **API Keys & Webhooks** + - Copy your **Test Secret Key** (starts with `sk_test_`) + +3. **Update the examples:** + - Replace `'sk_test_your_secret_key_here'` with your actual test key + - Never use live keys for testing! + +## 📚 Available Examples + +### Core Payment Examples + +#### 1. **simple_payment_request.php** - Beginner-Friendly Introduction +**Perfect for:** First-time users, basic invoice creation, learning the fundamentals + +**What you'll learn:** +- Creating payment requests with line items and taxes +- Handling API responses and error checking +- Retrieving and displaying payment request information +- Understanding the Paystack payment flow + +**Key Features:** +- ✅ Comprehensive error handling +- ✅ Detailed response explanations +- ✅ Step-by-step process breakdown +- ✅ Production-ready code patterns ```bash php examples/simple_payment_request.php ``` -## Notes +#### 2. **payment_request_demo.php** - Complete API Showcase +**Perfect for:** Developers who need comprehensive API coverage + +**What you'll learn:** +- All payment request API methods and their parameters +- Advanced features like drafts, finalization, and archiving +- Payment verification and status monitoring +- Notification management and customer communication + +**Key Features:** +- ✅ Complete API method coverage +- ✅ Advanced parameter usage +- ✅ Real-world error scenarios +- ✅ Best practice implementations + +```bash +php examples/payment_request_demo.php +``` + +### Business Workflow Examples + +#### 3. **invoice_workflow.php** - Professional Invoice Management +**Perfect for:** Service providers, freelancers, B2B businesses + +**What you'll learn:** +- Complete invoice lifecycle management +- Customer profile management and updates +- Progressive billing (adding items over time) +- Professional invoice formatting and presentation +- Payment monitoring and follow-up processes + +**Key Features:** +- ✅ End-to-end invoice workflow +- ✅ Customer management integration +- ✅ Automated notification systems +- ✅ Revenue tracking and analytics +- ✅ Professional formatting and presentation + +```bash +php examples/invoice_workflow.php +``` + +#### 4. **recurring_billing.php** - Subscription & Recurring Payments +**Perfect for:** SaaS platforms, subscription services, membership sites + +**What you'll learn:** +- Multi-tier subscription management +- Automated billing cycle processing +- Failed payment recovery strategies +- Subscription upgrades and downgrades +- Revenue analytics and reporting +- Customer retention management + +**Key Features:** +- ✅ Comprehensive subscription management +- ✅ Automated billing processes +- ✅ Advanced analytics and reporting +- ✅ Failed payment recovery systems +- ✅ Multi-tier pricing strategies +- ✅ Production-ready automation + +```bash +php examples/recurring_billing.php +``` + +## 🎯 Example Categories + +### By Complexity Level + +#### **Beginner Level** +- `simple_payment_request.php` - Basic payment request creation +- Individual API method demonstrations + +#### **Intermediate Level** +- `payment_request_demo.php` - Complete API coverage +- `invoice_workflow.php` - Business workflow implementation + +#### **Advanced Level** +- `recurring_billing.php` - Complex subscription management +- Production-ready automation systems + +### By Use Case + +#### **E-commerce & Retail** +- Simple product payments +- Shopping cart integration +- Order management workflows + +#### **Service Businesses** +- Invoice generation and management +- Project-based billing +- Time-based service charging + +#### **SaaS & Subscriptions** +- Recurring billing automation +- Subscription tier management +- Usage-based billing + +#### **Marketplaces** +- Split payment scenarios +- Multi-vendor management +- Commission handling + +## 🏃‍♂️ How to Run Examples + +### Method 1: Direct Execution +```bash +# Run from the project root directory +php examples/simple_payment_request.php +php examples/invoice_workflow.php +php examples/recurring_billing.php +php examples/payment_request_demo.php +``` + +### Method 2: Interactive Testing +```bash +# Create a test script +cp examples/simple_payment_request.php my_test.php +# Edit my_test.php with your API key +php my_test.php +``` + +### Method 3: Integration Testing +```bash +# Include in your application +require_once 'vendor/autoload.php'; +require_once 'examples/payment_functions.php'; +``` + +## 🧪 Testing Information + +### Test Environment Setup + +**Safe Testing:** All examples use test mode by default +- No real money transactions +- Safe for experimentation +- Full API feature access + +**Test Credentials:** +```php +// Test Secret Key (replace with yours) +$secretKey = 'sk_test_your_actual_test_key_here'; + +// Test Card Numbers for Different Scenarios +$testCards = [ + 'successful_payment' => '4084084084084081', + 'insufficient_funds' => '4084084084084107', + 'invalid_pin' => '4084084084084099', + 'timeout' => '4084084084084016' +]; +``` + +### Monitoring and Debugging + +**Paystack Dashboard:** +- Monitor all test transactions in real-time +- View detailed payment logs and analytics +- Test webhook configurations + +**Debug Output:** +- All examples include comprehensive logging +- Clear success/error indicators +- Detailed API response information + +## 🏭 Production Considerations + +### Security Best Practices + +**API Key Management:** +```php +// ✅ Good: Environment variables +$secretKey = $_ENV['PAYSTACK_SECRET_KEY']; + +// ❌ Bad: Hardcoded keys +$secretKey = 'sk_test_actual_key_here'; +``` + +**Error Handling:** +- Implement comprehensive try-catch blocks +- Log errors for debugging and monitoring +- Provide user-friendly error messages +- Handle network timeouts and retries + +### Performance Optimization + +**Connection Management:** +- Reuse HTTP connections when possible +- Implement connection pooling for high-volume applications +- Use appropriate timeout settings + +**Caching Strategies:** +- Cache frequently accessed data (banks, plans) +- Implement smart cache invalidation +- Use Redis or Memcached for distributed caching + +### Monitoring and Analytics + +**Webhook Implementation:** +- Set up real-time payment notifications +- Implement signature verification +- Handle duplicate events gracefully + +**Analytics and Reporting:** +- Track payment success rates +- Monitor customer behavior patterns +- Generate business intelligence reports + +## 🔧 Integration Patterns + +### Framework Integration + +**Laravel:** +```php +// Service Provider integration +// Controller method examples +// Eloquent model relationships +``` + +**Symfony:** +```php +// Service container configuration +// Event system integration +// Doctrine entity examples +``` + +**CodeIgniter:** +```php +// Library integration +// Controller patterns +// Database integration +``` + +### Database Integration + +**Customer Management:** +```sql +-- Customer table structure +-- Payment history tracking +-- Subscription management +``` + +**Transaction Logging:** +```sql +-- Transaction records +-- Audit trails +-- Reconciliation support +``` + +## 📖 Additional Resources + +### Documentation Links +- **[Getting Started Guide](../docs/getting-started.md)** - Complete setup and first payment +- **[API Reference](../docs/api-reference.md)** - Detailed method documentation +- **[Advanced Usage](../docs/advanced-usage.md)** - Complex patterns and optimizations +- **[Troubleshooting Guide](../docs/troubleshooting.md)** - Common issues and solutions + +### Official Paystack Resources +- **[Paystack API Documentation](https://paystack.com/docs/api/)** +- **[Test Payment Cards](https://paystack.com/docs/payments/test-payments/)** +- **[Webhook Events Reference](https://paystack.com/docs/payments/webhooks/)** +- **[Dashboard Tutorial](https://paystack.com/docs/get-started/)** + +### Community and Support +- **[GitHub Repository](https://github.com/starfolksoftware/paystack-php)** +- **[Issue Tracker](https://github.com/starfolksoftware/paystack-php/issues)** +- **[SDK Documentation](../docs/)** +- **[Email Support](mailto:contact@starfolksoftware.com)** + +## 🎯 Next Steps + +After exploring these examples: + +1. **Choose your use case** - Pick the example closest to your needs +2. **Customize the code** - Adapt the examples to your specific requirements +3. **Implement webhooks** - Set up real-time payment notifications +4. **Test thoroughly** - Use test cards and monitor the dashboard +5. **Go live** - Switch to live API keys for production + +### Common Integration Paths + +**E-commerce Integration:** +1. Start with `simple_payment_request.php` +2. Implement customer management +3. Add order tracking and fulfillment +4. Set up automated receipts and notifications + +**SaaS Subscription Service:** +1. Study `recurring_billing.php` thoroughly +2. Implement subscription management portal +3. Set up automated billing cycles +4. Add usage tracking and billing +5. Implement dunning management for failed payments + +**Service-Based Business:** +1. Begin with `invoice_workflow.php` +2. Customize invoice templates and branding +3. Integrate with project management systems +4. Automate follow-up and reminder systems +5. Set up accounting system integration -- These examples use the test mode by default. To use them in production, replace the test key with your live key. -- The payment request API allows you to create, manage, and track payment requests and invoices for your customers. -- Some examples create draft payment requests that don't send notifications automatically, giving you control over when to finalize and notify customers. +## 💡 Tips for Success -## Error Handling +1. **Start small** - Begin with basic examples and gradually add complexity +2. **Test extensively** - Use all available test scenarios before going live +3. **Monitor actively** - Set up comprehensive logging and monitoring +4. **Follow best practices** - Implement security, error handling, and performance optimizations +5. **Stay updated** - Follow the repository for updates and new features -The examples include basic error handling. In a production environment, you should implement more comprehensive error handling strategies based on your application's requirements. +--- -## Documentation +**Happy coding! 🚀** -For more information on the available methods and parameters, refer to: -- The official Paystack API documentation: https://paystack.com/docs/api/payment-request/ -- The library's main README file +For additional help, check out our [troubleshooting guide](../docs/troubleshooting.md) or [open an issue](https://github.com/starfolksoftware/paystack-php/issues) on GitHub. diff --git a/examples/invoice_workflow.php b/examples/invoice_workflow.php index cea95f2..c160d85 100644 --- a/examples/invoice_workflow.php +++ b/examples/invoice_workflow.php @@ -1,12 +1,40 @@ $secretKey, ]); +// Customer information for this example +$customerInfo = [ + 'email' => 'john.doe@techcompany.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '+2348123456789', + 'company' => 'Tech Company Ltd.', + 'address' => '123 Business Avenue, Lagos, Nigeria' +]; + +// ============================================================================ +// Helper Functions +// ============================================================================ + /** - * This example demonstrates a complete invoice workflow using Payment Requests: - * 1. Create a customer (if they don't exist) - * 2. Create a draft payment request - * 3. Update the payment request with additional line items - * 4. Finalize the payment request - * 5. Send notification to the customer - * 6. Check payment status later + * Format currency for display */ - -// Step 1: First, make sure we have a customer -// Note: In a real application, you might want to check if the customer exists first -try { - $customer = $paystack->customers->create([ - 'email' => 'john.doe@example.com', - 'first_name' => 'John', - 'last_name' => 'Doe', - 'phone' => '+2348123456789' - ]); - - $customerCode = $customer['data']['customer_code']; - echo "Customer created with code: $customerCode\n"; -} catch (Exception $e) { - // Customer might already exist - echo "Note: " . $e->getMessage() . "\n"; - - // In a real application, you would search for the customer here - $customerCode = 'CUS_existing_customer_code'; +function formatCurrency(int $amountInKobo): string { + return '₦' . number_format($amountInKobo / 100, 2); } -// Step 2: Create a draft payment request -$draftPaymentRequest = $paystack->paymentRequests->create([ - 'description' => 'Monthly Service Invoice - June 2025', - 'line_items' => [ - ['name' => 'Basic Subscription', 'amount' => 30000, 'quantity' => 1] - ], - 'customer' => $customerCode, - 'due_date' => '2025-07-15', - 'draft' => true, // Create as draft initially - 'has_invoice' => true // Generate an invoice number -]); - -$requestCode = $draftPaymentRequest['data']['request_code']; -$invoiceNumber = $draftPaymentRequest['data']['invoice_number']; -echo "Draft payment request created with code: $requestCode and invoice #$invoiceNumber\n"; - -// Step 3: Update the payment request with additional items -$updatedRequest = $paystack->paymentRequests->update($requestCode, [ - 'line_items' => [ - ['name' => 'Basic Subscription', 'amount' => 30000, 'quantity' => 1], - ['name' => 'Premium Support', 'amount' => 15000, 'quantity' => 1], - ['name' => 'Additional Storage', 'amount' => 5000, 'quantity' => 2] - ], - 'tax' => [ - ['name' => 'VAT (7.5%)', 'amount' => 4125] - ], - 'description' => 'Monthly Service Invoice - June 2025 (Updated)' -]); - -echo "Payment request updated with additional items\n"; +/** + * Print section header + */ +function printSection(string $title): void { + echo "\n" . str_repeat('=', 60) . "\n"; + echo " " . strtoupper($title) . "\n"; + echo str_repeat('=', 60) . "\n"; +} -// Step 4: Finalize the payment request -$finalizedRequest = $paystack->paymentRequests->finalize($requestCode, [ - 'send_notification' => false // We'll send it manually in the next step -]); +/** + * Print step information + */ +function printStep(int $step, string $description): void { + echo "\n🚀 Step {$step}: {$description}\n"; + echo str_repeat('-', 50) . "\n"; +} -echo "Payment request finalized\n"; +// ============================================================================ +// Start Invoice Workflow +// ============================================================================ -// Step 5: Send notification to the customer -$notification = $paystack->paymentRequests->sendNotification($requestCode); -echo "Payment notification sent to customer\n"; +printSection("Complete Invoice Workflow"); +echo "This example demonstrates professional invoice management\n"; +echo "Customer: {$customerInfo['first_name']} {$customerInfo['last_name']} ({$customerInfo['email']})\n"; -// Step 6: Check payment status (this would typically happen later) -echo "\nSimulating checking payment status after some time...\n"; +try { + // ======================================================================== + // Step 1: Customer Management + // ======================================================================== + + printStep(1, "Customer Management"); + + $customerCode = null; + + try { + // Try to create customer (might already exist) + echo "Creating customer profile...\n"; + + $customer = $paystack->customers->create([ + 'email' => $customerInfo['email'], + 'first_name' => $customerInfo['first_name'], + 'last_name' => $customerInfo['last_name'], + 'phone' => $customerInfo['phone'], + 'metadata' => [ + 'company' => $customerInfo['company'], + 'address' => $customerInfo['address'], + 'registration_date' => date('Y-m-d'), + 'customer_type' => 'business' + ] + ]); + + $customerCode = $customer['data']['customer_code']; + echo "✅ New customer created successfully\n"; + echo " Customer Code: {$customerCode}\n"; + echo " Customer ID: {$customer['data']['id']}\n"; + + } catch (Exception $e) { + // Customer likely already exists + echo "ℹ️ Customer creation note: {$e->getMessage()}\n"; + + try { + // Try to fetch existing customer + echo "Fetching existing customer...\n"; + $existingCustomer = $paystack->customers->fetch($customerInfo['email']); + + if ($existingCustomer['status']) { + $customerCode = $existingCustomer['data']['customer_code']; + echo "✅ Found existing customer\n"; + echo " Customer Code: {$customerCode}\n"; + echo " Total Transactions: {$existingCustomer['data']['transactions_count']}\n"; + echo " Total Value: " . formatCurrency($existingCustomer['data']['total_transaction_value']) . "\n"; + } + } catch (Exception $fetchError) { + // For demo purposes, use a placeholder + echo "⚠️ Using placeholder customer for demo\n"; + $customerCode = 'demo_customer_' . time(); + } + } + + // ======================================================================== + // Step 2: Create Draft Payment Request + // ======================================================================== + + printStep(2, "Creating Draft Invoice"); + + echo "Creating draft payment request with initial items...\n"; + + $draftPaymentRequest = $paystack->paymentRequests->create([ + 'description' => 'Monthly Service Invoice - ' . date('F Y'), + + // Initial line items (more can be added later) + 'line_items' => [ + [ + 'name' => 'Basic Cloud Hosting', + 'amount' => 25000, // ₦250.00 + 'quantity' => 1, + 'description' => 'Monthly cloud hosting service' + ], + [ + 'name' => 'Domain Registration', + 'amount' => 5000, // ₦50.00 + 'quantity' => 1, + 'description' => 'Annual domain registration' + ] + ], + + 'customer' => $customerCode, + 'due_date' => date('Y-m-d', strtotime('+30 days')), // 30 days from now + 'draft' => true, // Create as draft initially + 'has_invoice' => true, // Generate professional invoice number + 'currency' => 'NGN', + + // Additional metadata for tracking + 'metadata' => [ + 'invoice_type' => 'monthly_service', + 'billing_period' => date('Y-m'), + 'created_by' => 'billing_system', + 'department' => 'hosting_services' + ] + ]); + + if (!$draftPaymentRequest['status']) { + throw new Exception("Failed to create draft payment request: " . $draftPaymentRequest['message']); + } + + $requestCode = $draftPaymentRequest['data']['request_code']; + $invoiceNumber = $draftPaymentRequest['data']['invoice_number']; + $currentAmount = $draftPaymentRequest['data']['amount']; + + echo "✅ Draft payment request created successfully\n"; + echo " Request Code: {$requestCode}\n"; + echo " Invoice Number: #{$invoiceNumber}\n"; + echo " Current Amount: " . formatCurrency($currentAmount) . "\n"; + echo " Status: {$draftPaymentRequest['data']['status']}\n"; + echo " Due Date: {$draftPaymentRequest['data']['due_date']}\n"; + + // ======================================================================== + // Step 3: Add Additional Services (Update Invoice) + // ======================================================================== + + printStep(3, "Adding Additional Services"); + + echo "Simulating additional services being added to the invoice...\n"; + + // In a real scenario, these might be added over time as services are rendered + $updatedRequest = $paystack->paymentRequests->update($requestCode, [ + 'line_items' => [ + // Original items + [ + 'name' => 'Basic Cloud Hosting', + 'amount' => 25000, + 'quantity' => 1, + 'description' => 'Monthly cloud hosting service' + ], + [ + 'name' => 'Domain Registration', + 'amount' => 5000, + 'quantity' => 1, + 'description' => 'Annual domain registration' + ], + + // Additional services + [ + 'name' => 'Premium Support', + 'amount' => 15000, // ₦150.00 + 'quantity' => 1, + 'description' => '24/7 premium technical support' + ], + [ + 'name' => 'SSL Certificate', + 'amount' => 8000, // ₦80.00 + 'quantity' => 1, + 'description' => 'Wildcard SSL certificate' + ], + [ + 'name' => 'Backup Storage', + 'amount' => 3000, // ₦30.00 + 'quantity' => 5, // 5 GB + 'description' => 'Additional backup storage (per GB)' + ] + ], + + // Add applicable taxes + 'tax' => [ + [ + 'name' => 'VAT (7.5%)', + 'amount' => 4275 // 7.5% of ₦570 = ₦42.75 + ] + ], + + // Update description to reflect changes + 'description' => 'Monthly Service Invoice - ' . date('F Y') . ' (Comprehensive Package)', + + // Add discount if applicable + 'discount' => [ + [ + 'name' => 'Early Payment Discount', + 'amount' => 2000 // ₦20.00 discount + ] + ] + ]); + + if (!$updatedRequest['status']) { + throw new Exception("Failed to update payment request: " . $updatedRequest['message']); + } + + $newAmount = $updatedRequest['data']['amount']; + + echo "✅ Invoice updated with additional services\n"; + echo " Previous Amount: " . formatCurrency($currentAmount) . "\n"; + echo " New Amount: " . formatCurrency($newAmount) . "\n"; + echo " Difference: " . formatCurrency($newAmount - $currentAmount) . "\n"; + + // Show detailed breakdown + echo "\n📋 Invoice Breakdown:\n"; + if (isset($updatedRequest['data']['line_items'])) { + $subtotal = 0; + foreach ($updatedRequest['data']['line_items'] as $item) { + $lineTotal = $item['amount'] * $item['quantity']; + $subtotal += $lineTotal; + echo " • {$item['name']}: " . formatCurrency($item['amount']) + . " × {$item['quantity']} = " . formatCurrency($lineTotal) . "\n"; + } + echo " Subtotal: " . formatCurrency($subtotal) . "\n"; + } + + if (isset($updatedRequest['data']['tax'])) { + foreach ($updatedRequest['data']['tax'] as $tax) { + echo " + {$tax['name']}: " . formatCurrency($tax['amount']) . "\n"; + } + } + + if (isset($updatedRequest['data']['discount'])) { + foreach ($updatedRequest['data']['discount'] as $discount) { + echo " - {$discount['name']}: " . formatCurrency($discount['amount']) . "\n"; + } + } + + echo " Total: " . formatCurrency($newAmount) . "\n"; + + // ======================================================================== + // Step 4: Finalize and Send Invoice + // ======================================================================== + + printStep(4, "Finalizing and Sending Invoice"); + + echo "Finalizing the payment request...\n"; + + // Finalize the payment request (converts from draft to active) + $finalizedRequest = $paystack->paymentRequests->finalize($requestCode, [ + 'send_notification' => false // We'll send notification manually for better control + ]); + + if (!$finalizedRequest['status']) { + throw new Exception("Failed to finalize payment request: " . $finalizedRequest['message']); + } + + echo "✅ Payment request finalized successfully\n"; + echo " Status: {$finalizedRequest['data']['status']}\n"; + + // Send email notification to customer + echo "\nSending invoice notification to customer...\n"; + + $notification = $paystack->paymentRequests->sendNotification($requestCode); + + if ($notification['status']) { + echo "✅ Invoice notification sent successfully\n"; + echo " Notification sent to: {$customerInfo['email']}\n"; + echo " Customer can view and pay the invoice online\n"; + } else { + echo "⚠️ Warning: Could not send notification - {$notification['message']}\n"; + } + + // ======================================================================== + // Step 5: Payment Status Monitoring + // ======================================================================== + + printStep(5, "Payment Status Monitoring"); + + echo "Checking current payment status...\n"; + + // Verify payment request status + $verifiedRequest = $paystack->paymentRequests->verify($requestCode); + + if (!$verifiedRequest['status']) { + throw new Exception("Failed to verify payment request: " . $verifiedRequest['message']); + } + + $verification = $verifiedRequest['data']; + + echo "📊 Payment Status Report:\n"; + echo " Invoice Number: #{$verification['invoice_number']}\n"; + echo " Request Code: {$verification['request_code']}\n"; + echo " Status: " . ucfirst($verification['status']) . "\n"; + echo " Amount: " . formatCurrency($verification['amount']) . "\n"; + echo " Amount Paid: " . formatCurrency($verification['amount_paid']) . "\n"; + echo " Amount Due: " . formatCurrency($verification['amount'] - $verification['amount_paid']) . "\n"; + echo " Paid: " . ($verification['paid'] ? '✅ Yes' : '❌ No') . "\n"; + echo " Created: {$verification['created_at']}\n"; + echo " Due Date: {$verification['due_date']}\n"; + + // Show payment URLs if available + if (isset($verification['pdf_url'])) { + echo "\n🔗 Customer Links:\n"; + echo " PDF Invoice: {$verification['pdf_url']}\n"; + } + + if (isset($verification['invoice_url'])) { + echo " Payment Page: {$verification['invoice_url']}\n"; + } + + // ======================================================================== + // Additional Features Demo + // ======================================================================== + + printStep(6, "Additional Features"); + + echo "Demonstrating additional invoice features...\n"; + + // Get payment request totals (summary across all payment requests) + echo "\nFetching account payment request totals...\n"; + $totals = $paystack->paymentRequests->totals(); + + if ($totals['status']) { + $totalsData = $totals['data']; + echo "📈 Account Summary:\n"; + echo " Total Payment Requests: {$totalsData['total_requests']}\n"; + echo " Pending Amount: " . formatCurrency($totalsData['pending_amount']) . "\n"; + echo " Successful Amount: " . formatCurrency($totalsData['successful_amount']) . "\n"; + } + + echo "\n💡 Next Steps in Real Application:\n"; + echo " • Set up webhook endpoints to monitor payment events\n"; + echo " • Implement automatic payment reminders for overdue invoices\n"; + echo " • Create recurring payment requests for subscription billing\n"; + echo " • Generate PDF invoices with your company branding\n"; + echo " • Integrate with your accounting/CRM system\n"; + + // ======================================================================== + // Workflow Completion + // ======================================================================== + + printSection("Workflow Completed Successfully"); + + echo "🎉 Invoice workflow completed successfully!\n\n"; + + echo "📋 Summary:\n"; + echo " ✅ Customer managed\n"; + echo " ✅ Draft invoice created\n"; + echo " ✅ Additional services added\n"; + echo " ✅ Invoice finalized and sent\n"; + echo " ✅ Payment status monitored\n\n"; + + echo "📊 Final Invoice Details:\n"; + echo " Invoice #: {$invoiceNumber}\n"; + echo " Customer: {$customerInfo['first_name']} {$customerInfo['last_name']}\n"; + echo " Amount: " . formatCurrency($verification['amount']) . "\n"; + echo " Status: " . ucfirst($verification['status']) . "\n"; + echo " Due: {$verification['due_date']}\n\n"; + + echo "🔧 Integration Examples:\n"; + echo " • payment_request_demo.php - Advanced payment request features\n"; + echo " • recurring_billing.php - Subscription and recurring payments\n"; + echo " • webhook_handler.php - Real-time payment notifications\n\n"; -// In a real application, this would happen in a separate process or callback -$verifiedRequest = $paystack->paymentRequests->verify($requestCode); -$status = $verifiedRequest['data']['status']; -$isPaid = $verifiedRequest['data']['paid']; +} catch (Exception $e) { + echo "\n❌ Error in invoice workflow: " . $e->getMessage() . "\n"; + echo " Please check your API key and network connection.\n"; + echo " For troubleshooting, see: docs/troubleshooting.md\n\n"; + exit(1); +} -echo "Current status: $status\n"; -echo "Paid: " . ($isPaid ? "Yes" : "No") . "\n"; +// ============================================================================ +// Development Notes +// ============================================================================ -// Output the URL where the customer can view and pay the invoice (if available) -if (isset($verifiedRequest['data']['pdf_url'])) { - echo "Invoice URL: " . $verifiedRequest['data']['pdf_url'] . "\n"; +if ($secretKey === 'sk_test_your_secret_key_here') { + echo "⚠️ IMPORTANT: Update your API key!\n"; + echo " Replace 'sk_test_your_secret_key_here' with your actual test key\n"; + echo " Get it from: https://dashboard.paystack.com/#/settings/developer\n\n"; } -echo "\nPayment request workflow completed!\n"; +echo "🧪 Testing Notes:\n"; +echo " • This example uses test mode (safe for experimentation)\n"; +echo " • Use test card 4084084084084081 for successful payments\n"; +echo " • Check your Paystack dashboard for real-time updates\n"; +echo " • Enable test webhooks for complete integration testing\n\n"; + +echo "📚 Learn More:\n"; +echo " • Paystack API Docs: https://paystack.com/docs/api/\n"; +echo " • SDK Documentation: docs/\n"; +echo " • GitHub Repository: https://github.com/starfolksoftware/paystack-php\n"; diff --git a/examples/recurring_billing.php b/examples/recurring_billing.php index db6272c..0c521ad 100644 --- a/examples/recurring_billing.php +++ b/examples/recurring_billing.php @@ -1,12 +1,42 @@ $secretKey, ]); +// ============================================================================ +// Subscription Configuration +// ============================================================================ + /** - * This example demonstrates using Payment Requests for recurring billing: - * - Creating payment requests for multiple customers based on their subscription tiers - * - Managing customers who are due for billing - * - Tracking payment statuses + * Define subscription tiers with features and pricing */ +$subscriptionTiers = [ + 'starter' => [ + 'name' => 'Starter Plan', + 'monthly_fee' => 5000, // ₦50.00 + 'annual_fee' => 50000, // ₦500.00 (2 months free) + 'description' => 'Perfect for individuals and small projects', + 'features' => [ + '5 Projects', + '10GB Storage', + 'Email Support', + 'Basic Analytics' + ], + 'limits' => [ + 'projects' => 5, + 'storage_gb' => 10, + 'api_calls' => 1000 + ] + ], + 'professional' => [ + 'name' => 'Professional Plan', + 'monthly_fee' => 15000, // ₦150.00 + 'annual_fee' => 150000, // ₦1,500.00 (2 months free) + 'description' => 'Ideal for growing businesses and teams', + 'features' => [ + 'Unlimited Projects', + '100GB Storage', + 'Priority Support', + 'Advanced Analytics', + 'API Access', + 'Team Collaboration' + ], + 'limits' => [ + 'projects' => -1, // Unlimited + 'storage_gb' => 100, + 'api_calls' => 10000 + ] + ], + 'enterprise' => [ + 'name' => 'Enterprise Plan', + 'monthly_fee' => 50000, // ₦500.00 + 'annual_fee' => 500000, // ₦5,000.00 (2 months free) + 'description' => 'Full-featured plan for large organizations', + 'features' => [ + 'Unlimited Everything', + '1TB Storage', + 'Dedicated Support', + 'Custom Analytics', + 'Full API Access', + 'Advanced Team Management', + 'Custom Integrations', + 'SLA Guarantee' + ], + 'limits' => [ + 'projects' => -1, // Unlimited + 'storage_gb' => 1000, + 'api_calls' => 100000 + ] + ] +]; -// Mock data for customers and their subscription tiers -$customerData = [ +/** + * Sample customer database with subscription details + */ +$customers = [ [ - 'email' => 'customer1@example.com', - 'name' => 'Customer One', - 'tier' => 'basic', - 'last_billed' => '2025-05-25', // Last month + 'id' => 'CUST_001', + 'email' => 'sarah.johnson@techstartup.com', + 'first_name' => 'Sarah', + 'last_name' => 'Johnson', + 'company' => 'Tech Startup Inc.', + 'phone' => '+2348123456789', + 'subscription' => [ + 'tier' => 'professional', + 'billing_cycle' => 'monthly', + 'start_date' => '2024-01-15', + 'last_billed' => '2024-10-15', + 'next_billing' => '2024-11-15', + 'status' => 'active' + ], + 'payment_history' => [ + 'successful_payments' => 9, + 'failed_payments' => 1, + 'total_paid' => 135000 // ₦1,350.00 + ] ], [ - 'email' => 'customer2@example.com', - 'name' => 'Customer Two', - 'tier' => 'premium', - 'last_billed' => '2025-05-25', // Last month + 'id' => 'CUST_002', + 'email' => 'mike.chen@designagency.com', + 'first_name' => 'Mike', + 'last_name' => 'Chen', + 'company' => 'Creative Design Agency', + 'phone' => '+2348987654321', + 'subscription' => [ + 'tier' => 'starter', + 'billing_cycle' => 'annual', + 'start_date' => '2024-03-01', + 'last_billed' => '2024-03-01', + 'next_billing' => '2025-03-01', + 'status' => 'active' + ], + 'payment_history' => [ + 'successful_payments' => 1, + 'failed_payments' => 0, + 'total_paid' => 50000 // ₦500.00 + ] ], [ - 'email' => 'customer3@example.com', - 'name' => 'Customer Three', - 'tier' => 'enterprise', - 'last_billed' => '2025-05-25', // Last month + 'id' => 'CUST_003', + 'email' => 'alex.rivera@bigcorp.com', + 'first_name' => 'Alex', + 'last_name' => 'Rivera', + 'company' => 'Big Corporation Ltd.', + 'phone' => '+2347012345678', + 'subscription' => [ + 'tier' => 'enterprise', + 'billing_cycle' => 'monthly', + 'start_date' => '2024-06-01', + 'last_billed' => '2024-10-01', + 'next_billing' => '2024-11-01', + 'status' => 'active' + ], + 'payment_history' => [ + 'successful_payments' => 5, + 'failed_payments' => 0, + 'total_paid' => 250000 // ₦2,500.00 + ] ], + [ + 'id' => 'CUST_004', + 'email' => 'emma.wilson@freelancer.com', + 'first_name' => 'Emma', + 'last_name' => 'Wilson', + 'company' => 'Freelance Designer', + 'phone' => '+2348765432109', + 'subscription' => [ + 'tier' => 'starter', + 'billing_cycle' => 'monthly', + 'start_date' => '2024-08-01', + 'last_billed' => '2024-10-01', + 'next_billing' => '2024-11-01', + 'status' => 'payment_failed' // Needs attention + ], + 'payment_history' => [ + 'successful_payments' => 2, + 'failed_payments' => 1, + 'total_paid' => 10000 // ₦100.00 + ] + ] ]; -// Subscription tier pricing (in kobo - Nigerian currency) -$tierPricing = [ - 'basic' => [ - 'monthly_fee' => 10000, // ₦100 - 'description' => 'Basic Plan - Monthly Subscription', - 'features' => ['Feature 1', 'Feature 2'] - ], - 'premium' => [ - 'monthly_fee' => 25000, // ₦250 - 'description' => 'Premium Plan - Monthly Subscription', - 'features' => ['Feature 1', 'Feature 2', 'Feature 3', 'Premium Support'] - ], - 'enterprise' => [ - 'monthly_fee' => 50000, // ₦500 - 'description' => 'Enterprise Plan - Monthly Subscription', - 'features' => ['Feature 1', 'Feature 2', 'Feature 3', 'Premium Support', 'API Access', 'Advanced Analytics'] - ], -]; +// ============================================================================ +// Helper Functions +// ============================================================================ -// Today's date for billing check -$today = date('Y-m-d'); -$currentMonth = date('F Y'); +function formatCurrency(int $amountInKobo): string { + return '₦' . number_format($amountInKobo / 100, 2); +} -// Get customers to bill this month (normally would check if it's been a month since last billing) -$customersToBill = array_filter($customerData, function($customer) { - // In a real application, you would check if it's been a month since last billing - return true; // For this example, bill all customers -}); +function printHeader(string $title): void { + echo "\n" . str_repeat('=', 80) . "\n"; + echo " " . strtoupper($title) . "\n"; + echo str_repeat('=', 80) . "\n"; +} -echo "=== Starting monthly billing process for $currentMonth ===\n\n"; +function printSubHeader(string $title): void { + echo "\n" . str_repeat('-', 60) . "\n"; + echo "🚀 " . $title . "\n"; + echo str_repeat('-', 60) . "\n"; +} -// Process each customer -$createdRequests = []; -foreach ($customersToBill as $customer) { - echo "Processing {$customer['name']} ({$customer['email']}) - {$customer['tier']} tier...\n"; +function calculateTax(int $amount, float $taxRate = 0.075): int { + return (int) round($amount * $taxRate); +} + +function isCustomerDueForBilling(array $customer): bool { + $nextBilling = strtotime($customer['subscription']['next_billing']); + $today = strtotime(date('Y-m-d')); - // Get the tier details - $tier = $tierPricing[$customer['tier']]; + // Customer is due if next billing date is today or in the past + return $nextBilling <= $today; +} + +function getSubscriptionAmount(array $customer, array $subscriptionTiers): int { + $tier = $customer['subscription']['tier']; + $cycle = $customer['subscription']['billing_cycle']; - // Create line items based on the tier features - $lineItems = [ - [ - 'name' => "{$customer['tier']} Monthly Subscription", - 'amount' => $tier['monthly_fee'], - 'quantity' => 1 - ] - ]; + if ($cycle === 'annual') { + return $subscriptionTiers[$tier]['annual_fee']; + } + + return $subscriptionTiers[$tier]['monthly_fee']; +} + +// ============================================================================ +// Start Recurring Billing Process +// ============================================================================ + +printHeader("Recurring Billing System - " . date('F Y')); + +echo "🔄 Starting automated billing process...\n"; +echo "📅 Processing Date: " . date('Y-m-d H:i:s') . "\n"; +echo "👥 Total Customers: " . count($customers) . "\n"; + +// ============================================================================ +// Step 1: Identify Customers Due for Billing +// ============================================================================ + +printSubHeader("Step 1: Identifying Customers Due for Billing"); + +$customersDue = array_filter($customers, 'isCustomerDueForBilling'); +$customersWithFailedPayments = array_filter($customers, function($customer) { + return $customer['subscription']['status'] === 'payment_failed'; +}); + +echo "📊 Billing Analysis:\n"; +echo " • Customers due for billing: " . count($customersDue) . "\n"; +echo " • Customers with failed payments: " . count($customersWithFailedPayments) . "\n"; +echo " • Active subscriptions: " . count(array_filter($customers, function($c) { + return $c['subscription']['status'] === 'active'; +})) . "\n\n"; + +if (empty($customersDue) && empty($customersWithFailedPayments)) { + echo "ℹ️ No customers due for billing today.\n"; + echo " For demonstration, we'll process all customers anyway.\n\n"; + $customersDue = $customers; // Process all for demo +} + +// ============================================================================ +// Step 2: Process Billing for Due Customers +// ============================================================================ + +printSubHeader("Step 2: Processing Billing for Due Customers"); + +$billingResults = []; +$totalBillingAmount = 0; + +foreach (array_merge($customersDue, $customersWithFailedPayments) as $customer) { + $subscription = $customer['subscription']; + $tier = $subscriptionTiers[$subscription['tier']]; - // Add tax (for example, 7.5% VAT) - $taxAmount = round($tier['monthly_fee'] * 0.075); + echo "Processing: {$customer['first_name']} {$customer['last_name']} ({$customer['email']})\n"; + echo " Company: {$customer['company']}\n"; + echo " Plan: {$tier['name']} ({$subscription['billing_cycle']})\n"; + echo " Status: " . ucfirst($subscription['status']) . "\n"; try { - // Create payment request for this customer + // Calculate billing amount + $amount = getSubscriptionAmount($customer, $subscriptionTiers); + $taxAmount = calculateTax($amount); + $totalAmount = $amount + $taxAmount; + + // Prepare line items + $lineItems = [ + [ + 'name' => "{$tier['name']} - " . ucfirst($subscription['billing_cycle']) . " Subscription", + 'amount' => $amount, + 'quantity' => 1, + 'description' => $tier['description'] + ] + ]; + + // Add usage-based charges if applicable + if ($subscription['tier'] === 'professional' || $subscription['tier'] === 'enterprise') { + // Simulate additional usage charges + $extraStorage = random_int(0, 20); // GB + if ($extraStorage > 0) { + $storageCharge = $extraStorage * 100; // ₦1.00 per GB + $lineItems[] = [ + 'name' => 'Additional Storage', + 'amount' => $storageCharge, + 'quantity' => $extraStorage, + 'description' => 'Extra storage beyond plan limit' + ]; + $totalAmount += $storageCharge; + } + } + + // Prepare payment request + $billingPeriod = $subscription['billing_cycle'] === 'annual' ? + date('Y') : date('F Y'); + + $description = "{$tier['name']} - {$billingPeriod}"; + + // Add retry indicator for failed payments + if ($subscription['status'] === 'payment_failed') { + $description .= " (Payment Retry)"; + } + $paymentRequest = $paystack->paymentRequests->create([ - 'description' => "{$tier['description']} - $currentMonth", + 'description' => $description, 'line_items' => $lineItems, 'tax' => [ - ['name' => 'VAT (7.5%)', 'amount' => $taxAmount] + [ + 'name' => 'VAT (7.5%)', + 'amount' => calculateTax($totalAmount - $taxAmount) + ] ], 'customer' => $customer['email'], - 'due_date' => date('Y-m-d', strtotime('+7 days')), // Due in 7 days - 'send_notification' => true // Send right away + 'due_date' => date('Y-m-d', strtotime('+7 days')), + 'send_notification' => true, + 'currency' => 'NGN', + 'metadata' => [ + 'customer_id' => $customer['id'], + 'subscription_tier' => $subscription['tier'], + 'billing_cycle' => $subscription['billing_cycle'], + 'billing_period' => $billingPeriod, + 'retry_count' => $subscription['status'] === 'payment_failed' ? 1 : 0 + ] ]); - $requestCode = $paymentRequest['data']['request_code']; - $amount = $paymentRequest['data']['amount'] / 100; // Convert from kobo to naira for display + if ($paymentRequest['status']) { + $requestCode = $paymentRequest['data']['request_code']; + $finalAmount = $paymentRequest['data']['amount']; + + echo " ✅ Payment request created successfully\n"; + echo " 📋 Request Code: {$requestCode}\n"; + echo " 💰 Amount: " . formatCurrency($finalAmount) . "\n"; + echo " 📧 Notification sent to: {$customer['email']}\n"; + + $billingResults[] = [ + 'customer' => $customer, + 'request_code' => $requestCode, + 'amount' => $finalAmount, + 'status' => 'created', + 'tier' => $tier + ]; + + $totalBillingAmount += $finalAmount; + } else { + throw new Exception($paymentRequest['message']); + } - echo " ✓ Created payment request: {$requestCode} for ₦{$amount}\n"; + } catch (Exception $e) { + echo " ❌ Failed to create payment request: {$e->getMessage()}\n"; - // Store for later reference - $createdRequests[] = [ + $billingResults[] = [ 'customer' => $customer, - 'request_code' => $requestCode, - 'amount' => $paymentRequest['data']['amount'], - 'due_date' => $paymentRequest['data']['due_date'] + 'status' => 'failed', + 'error' => $e->getMessage(), + 'tier' => $tier ]; - - } catch (Exception $e) { - echo " ✗ Failed to create payment request: {$e->getMessage()}\n"; } echo "\n"; } -echo "=== Billing process complete ===\n"; -echo "Created " . count($createdRequests) . " payment requests\n\n"; +// ============================================================================ +// Step 3: Billing Summary and Analytics +// ============================================================================ + +printSubHeader("Step 3: Billing Summary and Analytics"); + +$successfulBilling = array_filter($billingResults, function($result) { + return $result['status'] === 'created'; +}); + +$failedBilling = array_filter($billingResults, function($result) { + return $result['status'] === 'failed'; +}); + +echo "📊 Billing Results Summary:\n"; +echo " ✅ Successful: " . count($successfulBilling) . " payment requests\n"; +echo " ❌ Failed: " . count($failedBilling) . " attempts\n"; +echo " 💰 Total Billed: " . formatCurrency($totalBillingAmount) . "\n"; +echo " 📈 Success Rate: " . (count($billingResults) > 0 ? + round((count($successfulBilling) / count($billingResults)) * 100, 1) : 0) . "%\n\n"; + +// Breakdown by subscription tier +$tierBreakdown = []; +foreach ($successfulBilling as $result) { + $tierName = $result['tier']['name']; + if (!isset($tierBreakdown[$tierName])) { + $tierBreakdown[$tierName] = ['count' => 0, 'amount' => 0]; + } + $tierBreakdown[$tierName]['count']++; + $tierBreakdown[$tierName]['amount'] += $result['amount']; +} + +echo "📋 Revenue Breakdown by Tier:\n"; +foreach ($tierBreakdown as $tierName => $data) { + echo " • {$tierName}: {$data['count']} customers → " . + formatCurrency($data['amount']) . "\n"; +} + +// ============================================================================ +// Step 4: Payment Status Monitoring +// ============================================================================ + +printSubHeader("Step 4: Payment Status Monitoring"); -// Simulate checking payment status after some time -echo "=== Simulating payment status check after 3 days ===\n\n"; +echo "🔍 Checking payment status for recent payment requests...\n\n"; -foreach ($createdRequests as $request) { - echo "Checking status for {$request['customer']['name']}'s payment request ({$request['request_code']})...\n"; +foreach ($successfulBilling as $result) { + $customer = $result['customer']; + $requestCode = $result['request_code']; + + echo "Monitoring: {$customer['first_name']} {$customer['last_name']} ({$requestCode})\n"; try { - $status = $paystack->paymentRequests->verify($request['request_code']); - $isPaid = $status['data']['paid']; - $statusText = $status['data']['status']; - - echo " Status: " . ($isPaid ? "PAID ✓" : "PENDING ⌛") . " ($statusText)\n"; + $verification = $paystack->paymentRequests->verify($requestCode); - // If not paid and due date is approaching (in a real app you'd check the actual date) - if (!$isPaid) { - // Send a reminder notification - echo " Sending payment reminder...\n"; - $paystack->paymentRequests->sendNotification($request['request_code']); - echo " Reminder sent successfully\n"; + if ($verification['status']) { + $data = $verification['data']; + $isPaid = $data['paid']; + $status = $data['status']; + $amountPaid = $data['amount_paid']; + $amountDue = $data['amount'] - $amountPaid; + + echo " 📊 Status: " . ucfirst($status) . "\n"; + echo " 💰 Amount: " . formatCurrency($data['amount']) . "\n"; + echo " ✅ Paid: " . formatCurrency($amountPaid) . "\n"; + echo " ⏳ Outstanding: " . formatCurrency($amountDue) . "\n"; + echo " 📅 Due Date: {$data['due_date']}\n"; + + if (isset($data['pdf_url'])) { + echo " 🔗 Invoice URL: {$data['pdf_url']}\n"; + } + + // Automated actions based on payment status + if (!$isPaid && $status === 'pending') { + echo " 📧 Automated Action: Payment reminder will be sent in 3 days\n"; + } elseif ($isPaid) { + echo " 🎉 Automated Action: Subscription renewal confirmed\n"; + } } } catch (Exception $e) { - echo " Failed to check status: {$e->getMessage()}\n"; + echo " ❌ Error checking status: {$e->getMessage()}\n"; } echo "\n"; } -// Get aggregated payment request totals -echo "=== Payment Request Totals ===\n"; +// ============================================================================ +// Step 5: Account-Wide Analytics +// ============================================================================ + +printSubHeader("Step 5: Account-Wide Payment Analytics"); + try { + echo "📈 Fetching comprehensive payment analytics...\n\n"; + $totals = $paystack->paymentRequests->totals(); - echo "Pending payments:\n"; - foreach ($totals['data']['pending'] as $currency) { - echo " {$currency['currency']}: " . ($currency['amount'] / 100) . "\n"; + if ($totals['status']) { + $totalsData = $totals['data']; + + echo "🏢 Account Overview:\n"; + echo " 📋 Total Payment Requests: {$totalsData['total_requests']}\n"; + echo " ⏳ Pending Amount: " . formatCurrency($totalsData['pending_amount']) . "\n"; + echo " ✅ Successful Amount: " . formatCurrency($totalsData['successful_amount']) . "\n"; + echo " 📊 Success Rate: " . ($totalsData['total_requests'] > 0 ? + round(($totalsData['successful_requests'] / $totalsData['total_requests']) * 100, 1) : 0) . "%\n\n"; } - echo "Successful payments:\n"; - foreach ($totals['data']['successful'] as $currency) { - echo " {$currency['currency']}: " . ($currency['amount'] / 100) . "\n"; - } +} catch (Exception $e) { + echo "❌ Error fetching analytics: {$e->getMessage()}\n\n"; +} + +// ============================================================================ +// Step 6: Subscription Management Actions +// ============================================================================ + +printSubHeader("Step 6: Subscription Management Actions"); + +echo "🔧 Demonstrating subscription management actions...\n\n"; + +// Simulate subscription upgrade +$upgradeCustomer = $customers[0]; // Sarah Johnson +echo "📈 Simulating subscription upgrade for {$upgradeCustomer['first_name']} {$upgradeCustomer['last_name']}:\n"; +echo " Current Plan: Professional (₦150/month)\n"; +echo " Upgrade To: Enterprise (₦500/month)\n"; +echo " Prorated Amount: " . formatCurrency(35000) . " (for remaining billing period)\n"; + +try { + $upgradeRequest = $paystack->paymentRequests->create([ + 'description' => 'Subscription Upgrade - Professional to Enterprise', + 'line_items' => [ + [ + 'name' => 'Plan Upgrade (Prorated)', + 'amount' => 35000, // Prorated difference + 'quantity' => 1, + 'description' => 'Upgrade from Professional to Enterprise plan' + ] + ], + 'customer' => $upgradeCustomer['email'], + 'due_date' => date('Y-m-d', strtotime('+3 days')), + 'send_notification' => true, + 'metadata' => [ + 'type' => 'subscription_upgrade', + 'from_tier' => 'professional', + 'to_tier' => 'enterprise', + 'customer_id' => $upgradeCustomer['id'] + ] + ]); - echo "Total payments (pending + successful):\n"; - foreach ($totals['data']['total'] as $currency) { - echo " {$currency['currency']}: " . ($currency['amount'] / 100) . "\n"; + if ($upgradeRequest['status']) { + echo " ✅ Upgrade payment request created: {$upgradeRequest['data']['request_code']}\n"; } } catch (Exception $e) { - echo "Failed to get payment totals: {$e->getMessage()}\n"; + echo " ❌ Upgrade request failed: {$e->getMessage()}\n"; +} + +// ============================================================================ +// Step 7: Failed Payment Recovery +// ============================================================================ + +printSubHeader("Step 7: Failed Payment Recovery Process"); + +$failedPaymentCustomers = array_filter($customers, function($customer) { + return $customer['subscription']['status'] === 'payment_failed'; +}); + +echo "🔄 Processing failed payment recovery...\n\n"; + +foreach ($failedPaymentCustomers as $customer) { + echo "Recovering payment for: {$customer['first_name']} {$customer['last_name']}\n"; + echo " Failed payments: {$customer['payment_history']['failed_payments']}\n"; + echo " Last successful payment: " . formatCurrency($customer['payment_history']['total_paid']) . "\n"; + + // Implement recovery strategy + $failedCount = $customer['payment_history']['failed_payments']; + + if ($failedCount === 1) { + echo " 🔄 Strategy: Immediate retry with email notification\n"; + } elseif ($failedCount === 2) { + echo " 📞 Strategy: Personal outreach + payment plan option\n"; + } else { + echo " ⚠️ Strategy: Account suspension warning\n"; + } + + echo "\n"; +} + +// ============================================================================ +// Completion Summary +// ============================================================================ + +printHeader("Recurring Billing Process Completed"); + +echo "🎉 Billing cycle completed successfully!\n\n"; + +echo "📊 Final Summary:\n"; +echo " • Total customers processed: " . count($billingResults) . "\n"; +echo " • Payment requests created: " . count($successfulBilling) . "\n"; +echo " • Total revenue billed: " . formatCurrency($totalBillingAmount) . "\n"; +echo " • Processing time: " . number_format(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], 2) . " seconds\n\n"; + +echo "🔧 Next Steps in Production:\n"; +echo " • Schedule this script to run monthly/daily via cron\n"; +echo " • Set up webhook handlers for payment notifications\n"; +echo " • Implement automatic subscription renewals\n"; +echo " • Create customer portal for subscription management\n"; +echo " • Set up automated dunning management for failed payments\n"; +echo " • Generate detailed analytics and reporting dashboards\n\n"; + +echo "📚 Integration Examples:\n"; +echo " • webhook_handler.php - Handle real-time payment events\n"; +echo " • subscription_portal.php - Customer self-service portal\n"; +echo " • analytics_dashboard.php - Revenue and subscription analytics\n"; +echo " • dunning_management.php - Automated failed payment recovery\n\n"; + +// ============================================================================ +// Development Notes +// ============================================================================ + +if ($secretKey === 'sk_test_your_secret_key_here') { + echo "⚠️ DEVELOPMENT REMINDER:\n"; + echo " Replace the API key with your actual test key for full functionality\n"; + echo " Get your key from: https://dashboard.paystack.com/#/settings/developer\n\n"; } -echo "\nRecurring billing example completed!\n"; +echo "🧪 Testing Information:\n"; +echo " • This example uses test mode (safe for development)\n"; +echo " • Use test card 4084084084084081 for successful payments\n"; +echo " • Monitor results in your Paystack dashboard\n"; +echo " • Test different billing cycles and scenarios\n\n"; + +echo "📖 Documentation Links:\n"; +echo " • Payment Requests API: https://paystack.com/docs/api/payment-request/\n"; +echo " • Subscription Best Practices: docs/advanced-usage.md\n"; +echo " • Error Handling Guide: docs/troubleshooting.md\n"; +echo " • SDK GitHub Repository: https://github.com/starfolksoftware/paystack-php\n"; diff --git a/examples/simple_payment_request.php b/examples/simple_payment_request.php index 7a5c865..a933e78 100644 --- a/examples/simple_payment_request.php +++ b/examples/simple_payment_request.php @@ -1,12 +1,38 @@ $secretKey, ]); -// Create a payment request -$paymentRequest = $paystack->paymentRequests->create([ - 'description' => 'Invoice for June services', - 'line_items' => [ - ['name' => 'Web Development', 'amount' => 50000], - ['name' => 'Server Maintenance', 'amount' => 20000] - ], - 'tax' => [ - ['name' => 'VAT', 'amount' => 3500] - ], - 'customer' => 'customer@example.com', - 'due_date' => '2025-07-10' -]); +// ============================================================================ +// Create Payment Request +// ============================================================================ + +echo "🚀 Creating a payment request...\n\n"; + +try { + /** + * Create a payment request with multiple line items and tax + * + * Payment requests are like invoices that you can send to customers. + * They support: + * - Multiple line items with quantities + * - Tax calculations + * - Due dates + * - Custom descriptions + * - Automatic email notifications + */ + $paymentRequest = $paystack->paymentRequests->create([ + 'description' => 'Website Development Services - June 2024', + + // Line items represent individual products or services + 'line_items' => [ + [ + 'name' => 'Frontend Development', + 'amount' => 50000, // Amount in kobo (₦500.00) + 'quantity' => 1 + ], + [ + 'name' => 'Backend API Development', + 'amount' => 75000, // Amount in kobo (₦750.00) + 'quantity' => 1 + ], + [ + 'name' => 'Database Design', + 'amount' => 25000, // Amount in kobo (₦250.00) + 'quantity' => 1 + ] + ], + + // Tax items (can be multiple taxes) + 'tax' => [ + [ + 'name' => 'VAT (7.5%)', + 'amount' => 11250 // 7.5% of ₦1,500 = ₦112.50 in kobo + ] + ], + + // Customer can be email, customer code, or customer ID + 'customer' => 'john.doe@example.com', + + // Due date in YYYY-MM-DD format + 'due_date' => date('Y-m-d', strtotime('+30 days')), // 30 days from now + + // Whether to send email notification to customer + 'send_notification' => true, + + // Currency (defaults to NGN) + 'currency' => 'NGN' + ]); + + // Check if payment request was created successfully + if ($paymentRequest['status']) { + echo "✅ Payment request created successfully!\n\n"; + + // Extract important information from the response + $requestData = $paymentRequest['data']; + + echo "📄 Payment Request Details:\n"; + echo " Request Code: {$requestData['request_code']}\n"; + echo " Description: {$requestData['description']}\n"; + echo " Amount: ₦" . number_format($requestData['amount'] / 100, 2) . "\n"; + echo " Currency: {$requestData['currency']}\n"; + echo " Status: {$requestData['status']}\n"; + echo " Due Date: {$requestData['due_date']}\n"; + echo " Created: {$requestData['created_at']}\n"; + + // If an invoice URL is available, show it + if (isset($requestData['invoice_url'])) { + echo " Invoice URL: {$requestData['invoice_url']}\n"; + } + + echo "\n"; + + // Show line items breakdown + if (isset($requestData['line_items']) && is_array($requestData['line_items'])) { + echo "📋 Line Items:\n"; + foreach ($requestData['line_items'] as $item) { + $itemTotal = ($item['amount'] * $item['quantity']) / 100; + echo " • {$item['name']}: ₦" . number_format($item['amount'] / 100, 2) + . " × {$item['quantity']} = ₦" . number_format($itemTotal, 2) . "\n"; + } + echo "\n"; + } + + // Show tax breakdown + if (isset($requestData['tax']) && is_array($requestData['tax'])) { + echo "💰 Taxes:\n"; + foreach ($requestData['tax'] as $tax) { + echo " • {$tax['name']}: ₦" . number_format($tax['amount'] / 100, 2) . "\n"; + } + echo "\n"; + } + + // Store the request code for further operations + $requestCode = $requestData['request_code']; + + } else { + throw new Exception("Failed to create payment request: " . $paymentRequest['message']); + } + +} catch (Exception $e) { + echo "❌ Error creating payment request: " . $e->getMessage() . "\n"; + echo " Please check your API key and try again.\n\n"; + exit(1); +} + +// ============================================================================ +// List Recent Payment Requests +// ============================================================================ + +echo "📋 Retrieving recent payment requests...\n\n"; + +try { + /** + * List payment requests with pagination + * + * You can filter by: + * - perPage: Number of requests per page (max 100) + * - page: Page number + * - customer: Filter by customer + * - status: Filter by status (pending, paid, etc.) + * - from/to: Date range filters + */ + $recentRequests = $paystack->paymentRequests->all([ + 'perPage' => 5, // Get latest 5 requests + 'page' => 1, // First page + ]); + + if ($recentRequests['status'] && !empty($recentRequests['data'])) { + echo "✅ Found {$recentRequests['meta']['total']} payment request(s) in your account\n\n"; + + echo "📄 Recent Payment Requests:\n"; + foreach ($recentRequests['data'] as $index => $request) { + $amount = number_format($request['amount'] / 100, 2); + $status = ucfirst($request['status']); + + echo " " . ($index + 1) . ". {$request['description']}\n"; + echo " Code: {$request['request_code']}\n"; + echo " Amount: ₦{$amount} {$request['currency']}\n"; + echo " Status: {$status}\n"; + echo " Created: {$request['created_at']}\n"; + + if (isset($request['due_date'])) { + echo " Due: {$request['due_date']}\n"; + } + + echo "\n"; + } + + // Show pagination info + $meta = $recentRequests['meta']; + echo "📊 Pagination Info:\n"; + echo " Total: {$meta['total']}\n"; + echo " Per Page: {$meta['perPage']}\n"; + echo " Current Page: {$meta['page']}\n"; + echo " Total Pages: {$meta['pageCount']}\n\n"; + + } else { + echo "ℹ️ No payment requests found in your account.\n"; + echo " The payment request created above should appear here.\n\n"; + } + +} catch (Exception $e) { + echo "❌ Error retrieving payment requests: " . $e->getMessage() . "\n\n"; +} + +// ============================================================================ +// Additional Operations (Optional) +// ============================================================================ + +if (isset($requestCode)) { + echo "🔍 Demonstrating additional operations...\n\n"; + + try { + // Fetch specific payment request + echo "Fetching payment request details...\n"; + $fetchedRequest = $paystack->paymentRequests->fetch($requestCode); + + if ($fetchedRequest['status']) { + echo "✅ Successfully fetched payment request: {$requestCode}\n"; + echo " Current status: {$fetchedRequest['data']['status']}\n\n"; + } + + // Verify payment request (check payment status) + echo "Verifying payment status...\n"; + $verification = $paystack->paymentRequests->verify($requestCode); + + if ($verification['status']) { + $verificationData = $verification['data']; + echo "✅ Payment verification completed\n"; + echo " Payment Status: {$verificationData['status']}\n"; + echo " Amount Paid: ₦" . number_format($verificationData['amount_paid'] / 100, 2) . "\n"; + echo " Amount Expected: ₦" . number_format($verificationData['amount'] / 100, 2) . "\n\n"; + } + + } catch (Exception $e) { + echo "❌ Error during additional operations: " . $e->getMessage() . "\n\n"; + } +} + +// ============================================================================ +// Next Steps +// ============================================================================ + +echo "🎉 Example completed successfully!\n\n"; + +echo "Next steps you can try:\n"; +echo "• Send the payment request URL to a customer\n"; +echo "• Use payment_request_demo.php for more advanced features\n"; +echo "• Check out invoice_workflow.php for complete invoice management\n"; +echo "• Explore recurring_billing.php for subscription patterns\n\n"; + +echo "💡 Tips:\n"; +echo "• All amounts are in kobo (smallest currency unit)\n"; +echo "• Use test card 4084084084084081 for successful test payments\n"; +echo "• Check your Paystack dashboard for real-time updates\n"; +echo "• Enable webhooks for automatic payment notifications\n\n"; + +echo "📚 Documentation:\n"; +echo "• Getting Started: docs/getting-started.md\n"; +echo "• API Reference: docs/api-reference.md\n"; +echo "• Advanced Usage: docs/advanced-usage.md\n"; +echo "• Troubleshooting: docs/troubleshooting.md\n\n"; -// Output the result -echo "Payment Request Created:\n"; -echo "Request Code: " . $paymentRequest['data']['request_code'] . "\n"; -echo "Amount: " . ($paymentRequest['data']['amount'] / 100) . " " . $paymentRequest['data']['currency'] . "\n"; -echo "Status: " . $paymentRequest['data']['status'] . "\n"; -echo "Due Date: " . $paymentRequest['data']['due_date'] . "\n"; - -// List recent payment requests -$recentRequests = $paystack->paymentRequests->all(['perPage' => 3]); -echo "\nRecent Payment Requests:\n"; -foreach ($recentRequests['data'] as $request) { - echo "- " . $request['description'] . " (Code: " . $request['request_code'] . ")\n"; +// Show warning if using default API key +if ($secretKey === 'sk_test_your_secret_key_here') { + echo "⚠️ WARNING: You're using the default API key!\n"; + echo " Please replace it with your actual test key from:\n"; + echo " https://dashboard.paystack.com/#/settings/developer\n\n"; } From dc697d3ba083d74bd1290484d8443d0e7f310e34 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 15:46:18 +0100 Subject: [PATCH 11/17] feat: Add improved type hinting demo and enhance client API method visibility --- examples/README.md | 31 +++- examples/improved_type_hinting_demo.php | 211 ++++++++++++++++++++++++ src/Client.php | 52 +++--- 3 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 examples/improved_type_hinting_demo.php diff --git a/examples/README.md b/examples/README.md index 68aa455..cd6dea0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -38,9 +38,30 @@ This directory contains comprehensive examples demonstrating real-world usage pa ## 📚 Available Examples +### SDK Feature Demonstrations + +#### 1. **improved_type_hinting_demo.php** - Enhanced Type Safety (v2.x) +**Perfect for:** Developers migrating to v2.x, IDE optimization, better development experience + +**What you'll learn:** +- Improved type hinting and IDE support in v2.x +- Direct method access vs magic property access +- Better autocomplete and intellisense features +- Backward compatibility with v1.x patterns + +**Key Features:** +- ✅ Demonstrates all API endpoints with proper types +- ✅ Shows migration path from v1.x to v2.x +- ✅ Backward compatibility examples +- ✅ IDE benefits and developer experience improvements + +```bash +php examples/improved_type_hinting_demo.php +``` + ### Core Payment Examples -#### 1. **simple_payment_request.php** - Beginner-Friendly Introduction +#### 2. **simple_payment_request.php** - Beginner-Friendly Introduction **Perfect for:** First-time users, basic invoice creation, learning the fundamentals **What you'll learn:** @@ -59,7 +80,7 @@ This directory contains comprehensive examples demonstrating real-world usage pa php examples/simple_payment_request.php ``` -#### 2. **payment_request_demo.php** - Complete API Showcase +#### 3. **payment_request_demo.php** - Complete API Showcase **Perfect for:** Developers who need comprehensive API coverage **What you'll learn:** @@ -80,7 +101,7 @@ php examples/payment_request_demo.php ### Business Workflow Examples -#### 3. **invoice_workflow.php** - Professional Invoice Management +#### 4. **invoice_workflow.php** - Professional Invoice Management **Perfect for:** Service providers, freelancers, B2B businesses **What you'll learn:** @@ -101,7 +122,7 @@ php examples/payment_request_demo.php php examples/invoice_workflow.php ``` -#### 4. **recurring_billing.php** - Subscription & Recurring Payments +#### 5. **recurring_billing.php** - Subscription & Recurring Payments **Perfect for:** SaaS platforms, subscription services, membership sites **What you'll learn:** @@ -129,6 +150,7 @@ php examples/recurring_billing.php ### By Complexity Level #### **Beginner Level** +- `improved_type_hinting_demo.php` - v2.x type safety features - `simple_payment_request.php` - Basic payment request creation - Individual API method demonstrations @@ -167,6 +189,7 @@ php examples/recurring_billing.php ### Method 1: Direct Execution ```bash # Run from the project root directory +php examples/improved_type_hinting_demo.php php examples/simple_payment_request.php php examples/invoice_workflow.php php examples/recurring_billing.php diff --git a/examples/improved_type_hinting_demo.php b/examples/improved_type_hinting_demo.php new file mode 100644 index 0000000..5fb521d --- /dev/null +++ b/examples/improved_type_hinting_demo.php @@ -0,0 +1,211 @@ + $secretKey, +]); + +// ============================================================================ +// Improved Type Hinting Examples +// ============================================================================ + +echo "🚀 Demonstrating Improved Type Hinting in Paystack PHP SDK v2.x\n\n"; + +echo "✨ **Before (v1.x)**: Magic property access\n"; +echo " \$paystack->transactions; // IDE doesn't know the type\n"; +echo " \$paystack->customers; // Limited autocomplete\n\n"; + +echo "✨ **After (v2.x)**: Direct method access with return types\n"; +echo " \$paystack->transactions(); // Returns API\\Transaction\n"; +echo " \$paystack->customers(); // Returns API\\Customer\n\n"; + +// ============================================================================ +// Type-Safe API Access Examples +// ============================================================================ + +try { + // Transaction API - Now with proper return type (API\Transaction) + echo "📊 **Transaction API** - Direct method access:\n"; + $transactionApi = $paystack->transactions(); + echo " Type: " . get_class($transactionApi) . "\n"; + echo " Available methods: initialize(), verify(), all(), find(), etc.\n\n"; + + // Customer API - IDE now provides full autocomplete + echo "👤 **Customer API** - Better IDE support:\n"; + $customerApi = $paystack->customers(); + echo " Type: " . get_class($customerApi) . "\n"; + echo " Available methods: create(), all(), find(), update(), etc.\n\n"; + + // Payment Request API - Clear method signatures + echo "💳 **Payment Request API** - Enhanced intellisense:\n"; + $paymentRequestApi = $paystack->paymentRequests(); + echo " Type: " . get_class($paymentRequestApi) . "\n"; + echo " Available methods: create(), all(), fetch(), verify(), etc.\n\n"; + + // Plan API - Full type information + echo "📋 **Plan API** - Complete type safety:\n"; + $planApi = $paystack->plans(); + echo " Type: " . get_class($planApi) . "\n"; + echo " Available methods: create(), all(), find(), update()\n\n"; + + // Subscription API - Professional developer experience + echo "🔄 **Subscription API** - Professional DX:\n"; + $subscriptionApi = $paystack->subscriptions(); + echo " Type: " . get_class($subscriptionApi) . "\n"; + echo " Available methods: create(), all(), find(), enable(), disable()\n\n"; + +} catch (Exception $e) { + echo "❌ Error demonstrating type hinting: " . $e->getMessage() . "\n\n"; +} + +// ============================================================================ +// Backward Compatibility +// ============================================================================ + +echo "🔄 **Backward Compatibility**: Old magic property access still works!\n\n"; + +try { + // The old way still works for backward compatibility + $oldWayTransaction = $paystack->transactions; // Magic __get() still functional + $newWayTransaction = $paystack->transactions(); // New direct method call + + echo " Old way type: " . get_class($oldWayTransaction) . "\n"; + echo " New way type: " . get_class($newWayTransaction) . "\n"; + echo " ✅ Both return the same API\\Transaction instance\n\n"; + +} catch (Exception $e) { + echo "❌ Error checking backward compatibility: " . $e->getMessage() . "\n\n"; +} + +// ============================================================================ +// IDE Benefits Summary +// ============================================================================ + +echo "🎯 **IDE Benefits Summary**:\n\n"; + +echo "✅ **Autocomplete**: IDEs now show all available API methods\n"; +echo "✅ **Return Types**: Clear return type information (API\\Customer, etc.)\n"; +echo "✅ **Method Signatures**: Full parameter and return type visibility\n"; +echo "✅ **Refactoring**: Better support for automated refactoring tools\n"; +echo "✅ **Documentation**: Inline PHPDoc comments visible in IDE tooltips\n"; +echo "✅ **Error Prevention**: Catch typos and method name errors at development time\n\n"; + +// ============================================================================ +// Migration Guide +// ============================================================================ + +echo "📚 **Migration Guide** (v1.x → v2.x):\n\n"; + +echo "**Recommended**: Use the new direct method calls\n"; +echo " // Old (still works)\n"; +echo " \$customers = \$paystack->customers->all([]);\n\n"; +echo " // New (recommended)\n"; +echo " \$customers = \$paystack->customers()->all([]);\n\n"; + +echo "**Benefits of migrating**:\n"; +echo " • Better IDE support and autocomplete\n"; +echo " • Clear method signatures and return types\n"; +echo " • Future-proof code for upcoming PHP versions\n"; +echo " • Enhanced developer experience\n\n"; + +// ============================================================================ +// All Available APIs +// ============================================================================ + +echo "📋 **All Available APIs** (with improved type hinting):\n\n"; + +$apis = [ + 'transactions()' => 'Transaction management and processing', + 'customers()' => 'Customer management and profiles', + 'paymentRequests()' => 'Payment requests and invoicing', + 'plans()' => 'Subscription plans and billing', + 'subscriptions()' => 'Subscription management', + 'invoices()' => 'Invoice creation and management', + 'transfers()' => 'Money transfers and payouts', + 'transferRecipients()' => 'Transfer recipient management', + 'transferControl()' => 'Transfer controls and settings', + 'splits()' => 'Revenue splitting and settlements', + 'subaccounts()' => 'Subaccount management', + 'products()' => 'Product catalog management', + 'pages()' => 'Payment pages and hosted checkout', + 'charges()' => 'Direct card charging', + 'refunds()' => 'Refund processing', + 'disputes()' => 'Dispute management', + 'settlements()' => 'Settlement tracking', + 'bulkCharges()' => 'Bulk charging operations', + 'verification()' => 'Identity and account verification', + 'miscellaneous()' => 'Utility endpoints (banks, countries)', + 'integration()' => 'Integration settings', + 'terminals()' => 'POS terminal management', + 'virtualTerminals()' => 'Virtual terminal management', + 'dedicatedVirtualAccounts()' => 'Virtual account management', + 'applePay()' => 'Apple Pay integration', + 'directDebit()' => 'Direct debit management', +]; + +foreach ($apis as $method => $description) { + echo " • \$paystack->{$method}: {$description}\n"; +} + +echo "\n"; + +// ============================================================================ +// Next Steps +// ============================================================================ + +echo "🎉 Improved type hinting demonstration completed!\n\n"; + +echo "**Next Steps**:\n"; +echo "• Update your IDE/editor for optimal autocomplete support\n"; +echo "• Migrate existing code to use new method calls (optional but recommended)\n"; +echo "• Explore upcoming phases: Response DTOs, Request objects, and more!\n"; +echo "• Check out the API reference for detailed method documentation\n\n"; + +echo "📚 **Documentation**:\n"; +echo "• Getting Started: docs/getting-started.md\n"; +echo "• API Reference: docs/api-reference.md\n"; +echo "• Advanced Usage: docs/advanced-usage.md\n\n"; + +// Show warning if using default API key +if ($secretKey === 'sk_test_your_secret_key_here') { + echo "⚠️ **NOTE**: This demo uses a placeholder API key.\n"; + echo " For actual API calls, replace with your real test key from:\n"; + echo " https://dashboard.paystack.com/#/settings/developer\n\n"; +} + +echo "🚀 **Happy coding with improved type safety!**\n"; \ No newline at end of file diff --git a/src/Client.php b/src/Client.php index fa8bec1..cd38c66 100644 --- a/src/Client.php +++ b/src/Client.php @@ -69,7 +69,7 @@ public function getHttpClient(): HttpMethodsClientInterface * * @return API\Customer */ - protected function customers(): API\Customer + public function customers(): API\Customer { return new API\Customer($this); } @@ -79,7 +79,7 @@ protected function customers(): API\Customer * * @return API\Invoice */ - protected function invoices(): API\Invoice + public function invoices(): API\Invoice { return new API\Invoice($this); } @@ -89,7 +89,7 @@ protected function invoices(): API\Invoice * * @return API\Plan */ - protected function plans(): API\Plan + public function plans(): API\Plan { return new API\Plan($this); } @@ -99,7 +99,7 @@ protected function plans(): API\Plan * * @return API\Subscription */ - protected function subscriptions(): API\Subscription + public function subscriptions(): API\Subscription { return new API\Subscription($this); } @@ -109,7 +109,7 @@ protected function subscriptions(): API\Subscription * * @return API\Transaction */ - protected function transactions(): API\Transaction + public function transactions(): API\Transaction { return new API\Transaction($this); } @@ -119,7 +119,7 @@ protected function transactions(): API\Transaction * * @return API\PaymentRequest */ - protected function paymentRequests(): API\PaymentRequest + public function paymentRequests(): API\PaymentRequest { return new API\PaymentRequest($this); } @@ -129,7 +129,7 @@ protected function paymentRequests(): API\PaymentRequest * * @return API\Split */ - protected function splits(): API\Split + public function splits(): API\Split { return new API\Split($this); } @@ -139,7 +139,7 @@ protected function splits(): API\Split * * @return API\Terminal */ - protected function terminals(): API\Terminal + public function terminals(): API\Terminal { return new API\Terminal($this); } @@ -149,7 +149,7 @@ protected function terminals(): API\Terminal * * @return API\VirtualTerminal */ - protected function virtualTerminals(): API\VirtualTerminal + public function virtualTerminals(): API\VirtualTerminal { return new API\VirtualTerminal($this); } @@ -159,7 +159,7 @@ protected function virtualTerminals(): API\VirtualTerminal * * @return API\ApplePay */ - protected function applePay(): API\ApplePay + public function applePay(): API\ApplePay { return new API\ApplePay($this); } @@ -169,7 +169,7 @@ protected function applePay(): API\ApplePay * * @return API\Subaccount */ - protected function subaccounts(): API\Subaccount + public function subaccounts(): API\Subaccount { return new API\Subaccount($this); } @@ -179,7 +179,7 @@ protected function subaccounts(): API\Subaccount * * @return API\Product */ - protected function products(): API\Product + public function products(): API\Product { return new API\Product($this); } @@ -189,7 +189,7 @@ protected function products(): API\Product * * @return API\DirectDebit */ - protected function directDebit(): API\DirectDebit + public function directDebit(): API\DirectDebit { return new API\DirectDebit($this); } @@ -199,7 +199,7 @@ protected function directDebit(): API\DirectDebit * * @return API\Integration */ - protected function integration(): API\Integration + public function integration(): API\Integration { return new API\Integration($this); } @@ -209,7 +209,7 @@ protected function integration(): API\Integration * * @return API\Miscellaneous */ - protected function miscellaneous(): API\Miscellaneous + public function miscellaneous(): API\Miscellaneous { return new API\Miscellaneous($this); } @@ -219,7 +219,7 @@ protected function miscellaneous(): API\Miscellaneous * * @return API\Verification */ - protected function verification(): API\Verification + public function verification(): API\Verification { return new API\Verification($this); } @@ -229,7 +229,7 @@ protected function verification(): API\Verification * * @return API\Transfer */ - protected function transfers(): API\Transfer + public function transfers(): API\Transfer { return new API\Transfer($this); } @@ -239,7 +239,7 @@ protected function transfers(): API\Transfer * * @return API\TransferRecipient */ - protected function transferRecipients(): API\TransferRecipient + public function transferRecipients(): API\TransferRecipient { return new API\TransferRecipient($this); } @@ -249,7 +249,7 @@ protected function transferRecipients(): API\TransferRecipient * * @return API\TransferControl */ - protected function transferControl(): API\TransferControl + public function transferControl(): API\TransferControl { return new API\TransferControl($this); } @@ -259,7 +259,7 @@ protected function transferControl(): API\TransferControl * * @return API\Charge */ - protected function charges(): API\Charge + public function charges(): API\Charge { return new API\Charge($this); } @@ -269,7 +269,7 @@ protected function charges(): API\Charge * * @return API\Dispute */ - protected function disputes(): API\Dispute + public function disputes(): API\Dispute { return new API\Dispute($this); } @@ -279,7 +279,7 @@ protected function disputes(): API\Dispute * * @return API\Refund */ - protected function refunds(): API\Refund + public function refunds(): API\Refund { return new API\Refund($this); } @@ -289,7 +289,7 @@ protected function refunds(): API\Refund * * @return API\Settlement */ - protected function settlements(): API\Settlement + public function settlements(): API\Settlement { return new API\Settlement($this); } @@ -299,7 +299,7 @@ protected function settlements(): API\Settlement * * @return API\BulkCharge */ - protected function bulkCharges(): API\BulkCharge + public function bulkCharges(): API\BulkCharge { return new API\BulkCharge($this); } @@ -309,7 +309,7 @@ protected function bulkCharges(): API\BulkCharge * * @return API\Page */ - protected function pages(): API\Page + public function pages(): API\Page { return new API\Page($this); } @@ -319,7 +319,7 @@ protected function pages(): API\Page * * @return API\DedicatedVirtualAccount */ - protected function dedicatedVirtualAccounts(): API\DedicatedVirtualAccount + public function dedicatedVirtualAccounts(): API\DedicatedVirtualAccount { return new API\DedicatedVirtualAccount($this); } From 016821906f08760602f7b98e11cc0539f6b48bf2 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 16:19:30 +0100 Subject: [PATCH 12/17] feat: Add typed responses for Customer, PaymentRequest, and Transaction APIs - Implemented and methods in Customer API for creating and retrieving customers with typed responses. - Added and methods in PaymentRequest API for creating and listing payment requests with typed responses. - Introduced and methods in Transaction API for initializing and verifying transactions with typed responses. - Enhanced ResponseMediator to handle new typed responses for customers, transactions, and payment requests. - Created new response DTOs: CustomerData, CustomerListResponse, PaymentRequestData, TransactionData, and TransactionInitializeResponse. - Added PaginationMeta and PaginatedResponse classes for handling paginated responses. - Implemented ResponseFactory for creating response DTOs from API response arrays. - Added PaystackResponse and PaystackResponseException for consistent error handling across API responses. --- PHASE2_SUMMARY.md | 156 +++++++ examples/README.md | 36 +- examples/typed_responses_demo.php | 410 ++++++++++++++++++ src/API/Customer.php | 34 ++ src/API/PaymentRequest.php | 34 ++ src/API/Transaction.php | 48 ++ src/HttpClient/Message/ResponseMediator.php | 104 +++++ src/Response/Customer/CustomerData.php | 120 +++++ .../Customer/CustomerListResponse.php | 96 ++++ src/Response/PaginatedResponse.php | 113 +++++ src/Response/PaginationMeta.php | 85 ++++ .../PaymentRequest/PaymentRequestData.php | 211 +++++++++ src/Response/PaystackResponse.php | 107 +++++ src/Response/PaystackResponseException.php | 16 + src/Response/ResponseFactory.php | 139 ++++++ src/Response/Transaction/TransactionData.php | 182 ++++++++ .../TransactionInitializeResponse.php | 86 ++++ 17 files changed, 1971 insertions(+), 6 deletions(-) create mode 100644 PHASE2_SUMMARY.md create mode 100644 examples/typed_responses_demo.php create mode 100644 src/Response/Customer/CustomerData.php create mode 100644 src/Response/Customer/CustomerListResponse.php create mode 100644 src/Response/PaginatedResponse.php create mode 100644 src/Response/PaginationMeta.php create mode 100644 src/Response/PaymentRequest/PaymentRequestData.php create mode 100644 src/Response/PaystackResponse.php create mode 100644 src/Response/PaystackResponseException.php create mode 100644 src/Response/ResponseFactory.php create mode 100644 src/Response/Transaction/TransactionData.php create mode 100644 src/Response/Transaction/TransactionInitializeResponse.php diff --git a/PHASE2_SUMMARY.md b/PHASE2_SUMMARY.md new file mode 100644 index 0000000..41438d4 --- /dev/null +++ b/PHASE2_SUMMARY.md @@ -0,0 +1,156 @@ +# Phase 2 Implementation Summary: Response DTOs + +## Overview +Successfully implemented Phase 2 of the type hinting improvements, adding strongly-typed Data Transfer Objects (DTOs) for API responses. + +## What Was Implemented + +### Core Response Infrastructure +1. **PaystackResponse** - Generic wrapper for all API responses with type parameter support +2. **PaystackResponseException** - Exception for failed API responses +3. **PaginationMeta** - Structured pagination information with helper methods +4. **PaginatedResponse** - Generic paginated collection response + +### Customer Response DTOs +- **CustomerData** - Strongly-typed customer object with properties like: + - `id`, `customer_code`, `email`, `first_name`, `last_name` + - Helper methods: `getFullName()`, `hasAuthorizations()`, `hasSubscriptions()`, etc. +- **CustomerListResponse** - Paginated customer list with typed CustomerData objects + +### Transaction Response DTOs +- **TransactionData** - Complete transaction information with: + - Status checking: `isSuccessful()`, `isPending()`, `isFailed()`, `isAbandoned()` + - Amount helpers: `getAmountInMajorUnit()`, `getFormattedAmount()` + - Customer info: `getCustomerEmail()`, `getCustomerName()` +- **TransactionInitializeResponse** - Transaction initialization with: + - `authorization_url`, `access_code`, `reference` + - Helper: `isInitialized()` + +### PaymentRequest Response DTOs +- **PaymentRequestData** - Payment request/invoice data with: + - Status checking: `isPending()`, `isPaid()`, `isPartiallyPaid()`, `isCancelled()` + - Calculations: `getLineItemsTotal()`, `getTaxTotal()`, `getFormattedAmount()` + - Due date helpers: `hasDueDate()`, `isOverdue()`, `getDueDateAsDateTime()` + +### API Method Updates +Added typed response methods to: +- **Customer API**: `createTyped()`, `allTyped()` +- **Transaction API**: `initializeTyped()`, `verifyTyped()`, `allTyped()` +- **PaymentRequest API**: `createTyped()`, `allTyped()` + +### Supporting Infrastructure +- **ResponseFactory** - Factory methods for creating typed responses +- **ResponseMediator** - Extended with typed response methods while maintaining backward compatibility + +## Files Created + +### Response DTOs +``` +src/Response/ +├── PaystackResponse.php +├── PaystackResponseException.php +├── PaginationMeta.php +├── PaginatedResponse.php +├── ResponseFactory.php +├── Customer/ +│ ├── CustomerData.php +│ └── CustomerListResponse.php +├── Transaction/ +│ ├── TransactionData.php +│ └── TransactionInitializeResponse.php +└── PaymentRequest/ + └── PaymentRequestData.php +``` + +### Documentation & Examples +- `examples/typed_responses_demo.php` - Comprehensive demonstration +- Updated `examples/README.md` with Phase 2 information + +### Updated Files +- `src/HttpClient/Message/ResponseMediator.php` - Added typed response methods +- `src/API/Customer.php` - Added `createTyped()`, `allTyped()` +- `src/API/Transaction.php` - Added `initializeTyped()`, `verifyTyped()`, `allTyped()` +- `src/API/PaymentRequest.php` - Added `createTyped()`, `allTyped()` + +## Benefits + +### For Developers +1. **Type Safety** - Catch errors at development time, not runtime +2. **IDE Support** - Full autocomplete for all response properties +3. **Helper Methods** - Convenient methods for common operations +4. **Structured Data** - DateTimeImmutable for dates, proper types throughout +5. **Documentation** - Inline PHPDoc visible in IDE tooltips + +### For Code Quality +1. **No Array Key Typos** - Properties are strongly typed +2. **Refactoring Support** - IDEs can track usage and rename safely +3. **Clear Contracts** - Response structure is explicitly defined +4. **Future-Proof** - Easy to extend with new methods and properties + +## Backward Compatibility + +✅ **100% Backward Compatible** +- All existing array-based methods remain unchanged +- Old code continues to work without modifications +- New typed methods use `*Typed()` suffix pattern +- Gradual migration path available + +## Testing + +✅ **All Tests Pass** +- 144 tests, 496 assertions +- Zero regressions +- Both old and new methods work correctly + +## Usage Examples + +### Before (v1.x - Array-based) +```php +$response = $paystack->customers()->create([...]); +if ($response['status']) { + $email = $response['data']['email']; // No autocomplete +} +``` + +### After (v2.x - Typed DTOs) +```php +$response = $paystack->customers()->createTyped([...]); +if ($response->isSuccessful()) { + $customer = $response->getData(); // CustomerData object + $email = $customer->email; // Full autocomplete! + $name = $customer->getFullName(); // Helper method +} +``` + +## Migration Strategy + +1. **New Code**: Use `*Typed()` methods immediately +2. **Existing Code**: No changes required, works as-is +3. **Gradual Migration**: Convert to typed methods during refactoring +4. **Both Supported**: Use whichever approach fits your needs + +## Next Steps (Phase 3) + +Phase 3 will introduce Request DTOs for type-safe API parameters: +- Strongly-typed request objects instead of arrays +- Validation at creation time +- Builder patterns for complex requests +- Full IDE support for required/optional parameters + +## Demo + +Run the comprehensive demonstration: +```bash +php examples/typed_responses_demo.php +``` + +This shows: +- Side-by-side comparison of old vs new approaches +- All DTO features and helper methods +- Pagination support +- Backward compatibility +- Real-world usage patterns + +## Conclusion + +Phase 2 successfully delivers strongly-typed response DTOs that significantly improve the developer experience while maintaining complete backward compatibility. The implementation provides immediate value through better IDE support, type safety, and convenient helper methods. \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index cd6dea0..50ad7ce 100644 --- a/examples/README.md +++ b/examples/README.md @@ -40,7 +40,7 @@ This directory contains comprehensive examples demonstrating real-world usage pa ### SDK Feature Demonstrations -#### 1. **improved_type_hinting_demo.php** - Enhanced Type Safety (v2.x) +#### 1. **improved_type_hinting_demo.php** - Enhanced Type Safety (v2.x - Phase 1) **Perfect for:** Developers migrating to v2.x, IDE optimization, better development experience **What you'll learn:** @@ -59,9 +59,31 @@ This directory contains comprehensive examples demonstrating real-world usage pa php examples/improved_type_hinting_demo.php ``` +#### 2. **typed_responses_demo.php** - Response DTOs (v2.x - Phase 2) +**Perfect for:** Developers wanting structured, type-safe API responses + +**What you'll learn:** +- Using response DTOs instead of generic arrays +- Accessing typed data with full IDE support +- Working with pagination and helper methods +- Converting between array and typed responses +- Backward compatibility strategies + +**Key Features:** +- ✅ Customer, Transaction, and PaymentRequest DTOs +- ✅ Strongly-typed response objects with autocomplete +- ✅ Helper methods for common operations +- ✅ DateTimeImmutable for dates, structured data +- ✅ Pagination support with PaginationMeta +- ✅ Shows old vs new approach side-by-side + +```bash +php examples/typed_responses_demo.php +``` + ### Core Payment Examples -#### 2. **simple_payment_request.php** - Beginner-Friendly Introduction +#### 3. **simple_payment_request.php** - Beginner-Friendly Introduction **Perfect for:** First-time users, basic invoice creation, learning the fundamentals **What you'll learn:** @@ -80,7 +102,7 @@ php examples/improved_type_hinting_demo.php php examples/simple_payment_request.php ``` -#### 3. **payment_request_demo.php** - Complete API Showcase +#### 4. **payment_request_demo.php** - Complete API Showcase **Perfect for:** Developers who need comprehensive API coverage **What you'll learn:** @@ -101,7 +123,7 @@ php examples/payment_request_demo.php ### Business Workflow Examples -#### 4. **invoice_workflow.php** - Professional Invoice Management +#### 5. **invoice_workflow.php** - Professional Invoice Management **Perfect for:** Service providers, freelancers, B2B businesses **What you'll learn:** @@ -122,7 +144,7 @@ php examples/payment_request_demo.php php examples/invoice_workflow.php ``` -#### 5. **recurring_billing.php** - Subscription & Recurring Payments +#### 6. **recurring_billing.php** - Subscription & Recurring Payments **Perfect for:** SaaS platforms, subscription services, membership sites **What you'll learn:** @@ -150,7 +172,8 @@ php examples/recurring_billing.php ### By Complexity Level #### **Beginner Level** -- `improved_type_hinting_demo.php` - v2.x type safety features +- `improved_type_hinting_demo.php` - v2.x type safety features (Phase 1) +- `typed_responses_demo.php` - Response DTOs (Phase 2) - `simple_payment_request.php` - Basic payment request creation - Individual API method demonstrations @@ -190,6 +213,7 @@ php examples/recurring_billing.php ```bash # Run from the project root directory php examples/improved_type_hinting_demo.php +php examples/typed_responses_demo.php php examples/simple_payment_request.php php examples/invoice_workflow.php php examples/recurring_billing.php diff --git a/examples/typed_responses_demo.php b/examples/typed_responses_demo.php new file mode 100644 index 0000000..8d5c927 --- /dev/null +++ b/examples/typed_responses_demo.php @@ -0,0 +1,410 @@ + $secretKey, +]); + +// ============================================================================ +// Response DTOs Overview +// ============================================================================ + +echo "🚀 Demonstrating Typed Response DTOs in Paystack PHP SDK v2.x\n\n"; + +echo "✨ **Before (v1.x)**: Generic arrays with no type safety\n"; +echo " \$customer = \$paystack->customers()->create([...]);\n"; +echo " echo \$customer['data']['email']; // No autocomplete, prone to typos\n\n"; + +echo "✨ **After (v2.x)**: Strongly-typed DTOs with full IDE support\n"; +echo " \$response = \$paystack->customers()->createTyped([...]);\n"; +echo " \$customer = \$response->getData(); // Returns CustomerData object\n"; +echo " echo \$customer->email; // Full autocomplete and type safety\n\n"; + +// ============================================================================ +// Example 1: Customer Management with Typed Responses +// ============================================================================ + +echo "📊 **Example 1: Customer Management**\n"; +echo "Creating a customer and accessing typed data...\n\n"; + +try { + // OLD WAY: Array-based response (still available) + echo "1️⃣ Old array-based approach:\n"; + $arrayResponse = $paystack->customers()->create([ + 'email' => 'john.doe@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'phone' => '+2348123456789', + ]); + + if ($arrayResponse['status']) { + $customerArray = $arrayResponse['data']; + echo " ✅ Customer created: {$customerArray['email']}\n"; + echo " 📝 Customer code: {$customerArray['customer_code']}\n"; + echo " ⚠️ Type: array - No autocomplete, manual array access\n\n"; + } + + // NEW WAY: Typed DTO response + echo "2️⃣ New typed DTO approach:\n"; + $typedResponse = $paystack->customers()->createTyped([ + 'email' => 'jane.smith@example.com', + 'first_name' => 'Jane', + 'last_name' => 'Smith', + 'phone' => '+2348987654321', + ]); + + if ($typedResponse->isSuccessful()) { + /** @var CustomerData $customer */ + $customer = $typedResponse->getData(); + + echo " ✅ Customer created: {$customer->email}\n"; + echo " 📝 Customer code: {$customer->customer_code}\n"; + echo " 👤 Full name: {$customer->getFullName()}\n"; + echo " 📱 Phone: {$customer->phone}\n"; + echo " 🔒 Type: CustomerData - Full autocomplete and type safety!\n\n"; + + // Use helper methods + echo " **Helper Methods**:\n"; + echo " • Has authorizations: " . ($customer->hasAuthorizations() ? 'Yes' : 'No') . "\n"; + echo " • Has subscriptions: " . ($customer->hasSubscriptions() ? 'Yes' : 'No') . "\n"; + echo " • Risk action: {$customer->getRiskAction()}\n"; + echo " • Is identified: " . ($customer->isIdentified() ? 'Yes' : 'No') . "\n\n"; + } + +} catch (Exception $e) { + echo " ❌ Error: " . $e->getMessage() . "\n\n"; +} + +// ============================================================================ +// Example 2: Transaction Initialization with Typed Response +// ============================================================================ + +echo "💳 **Example 2: Transaction Initialization**\n"; +echo "Initializing a transaction and accessing typed data...\n\n"; + +try { + // NEW WAY: Typed initialization response + $initResponse = $paystack->transactions()->initializeTyped([ + 'email' => 'customer@example.com', + 'amount' => '50000', // 500 NGN in kobo + 'currency' => 'NGN', + 'reference' => 'TXN_' . uniqid(), + 'callback_url' => 'https://example.com/callback', + ]); + + if ($initResponse->isInitialized()) { + echo " ✅ Transaction initialized successfully!\n\n"; + echo " **Typed Access**:\n"; + echo " • Authorization URL: {$initResponse->getAuthorizationUrl()}\n"; + echo " • Access Code: {$initResponse->getAccessCode()}\n"; + echo " • Reference: {$initResponse->getReference()}\n\n"; + + echo " **Type Safety**: TransactionInitializeResponse\n"; + echo " • No array key typos possible\n"; + echo " • IDE shows all available methods\n"; + echo " • Helper method isInitialized() included\n\n"; + + // Store reference for verification example + $transactionRef = $initResponse->getReference(); + } + +} catch (Exception $e) { + echo " ❌ Error: " . $e->getMessage() . "\n\n"; +} + +// ============================================================================ +// Example 3: Transaction Verification with Typed Response +// ============================================================================ + +if (isset($transactionRef)) { + echo "🔍 **Example 3: Transaction Verification**\n"; + echo "Verifying transaction with typed response...\n\n"; + + try { + $verifyResponse = $paystack->transactions()->verifyTyped($transactionRef); + + if ($verifyResponse->isSuccessful()) { + /** @var TransactionData $transaction */ + $transaction = $verifyResponse->getData(); + + echo " ✅ Transaction verified!\n\n"; + echo " **Transaction Details**:\n"; + echo " • Reference: {$transaction->reference}\n"; + echo " • Status: {$transaction->status}\n"; + echo " • Amount: {$transaction->getFormattedAmount()}\n"; + echo " • Channel: {$transaction->channel}\n"; + echo " • Customer: {$transaction->getCustomerEmail()}\n\n"; + + echo " **Helper Methods**:\n"; + echo " • Is successful: " . ($transaction->isSuccessful() ? 'Yes' : 'No') . "\n"; + echo " • Is pending: " . ($transaction->isPending() ? 'Yes' : 'No') . "\n"; + echo " • Amount in major unit: ₦{$transaction->getAmountInMajorUnit()}\n"; + echo " • Total fees: ₦" . ($transaction->getTotalFees() / 100) . "\n\n"; + } + + } catch (Exception $e) { + echo " ❌ Error: " . $e->getMessage() . "\n\n"; + } +} + +// ============================================================================ +// Example 4: Paginated Lists with Typed Responses +// ============================================================================ + +echo "📋 **Example 4: Paginated Customer List**\n"; +echo "Fetching customers with pagination support...\n\n"; + +try { + $customersResponse = $paystack->customers()->allTyped([ + 'perPage' => 5, + 'page' => 1, + ]); + + if ($customersResponse->isSuccessful()) { + $customers = $customersResponse->getCustomers(); + $pagination = $customersResponse->getPagination(); + + echo " ✅ Retrieved {$customersResponse->getTotal()} total customers\n"; + echo " 📄 Showing " . count($customers) . " customers on this page\n\n"; + + echo " **Customer List** (strongly typed):\n"; + foreach ($customers as $index => $customer) { + echo " " . ($index + 1) . ". {$customer->getFullName()} ({$customer->email})\n"; + echo " Code: {$customer->customer_code}\n"; + echo " Created: {$customer->created_at?->format('Y-m-d H:i:s')}\n"; + } + echo "\n"; + + if ($pagination) { + echo " **Pagination Info**:\n"; + echo " • Total: {$pagination->total}\n"; + echo " • Per Page: {$pagination->perPage}\n"; + echo " • Current Page: {$pagination->page}\n"; + echo " • Total Pages: {$pagination->pageCount}\n"; + echo " • Has next: " . ($pagination->hasNextPage() ? 'Yes' : 'No') . "\n"; + echo " • Has previous: " . ($pagination->hasPreviousPage() ? 'Yes' : 'No') . "\n\n"; + } + } + +} catch (Exception $e) { + echo " ❌ Error: " . $e->getMessage() . "\n\n"; +} + +// ============================================================================ +// Example 5: Payment Requests with Typed Responses +// ============================================================================ + +echo "💰 **Example 5: Payment Request with Typed Response**\n"; +echo "Creating a payment request and accessing structured data...\n\n"; + +try { + $prResponse = $paystack->paymentRequests()->createTyped([ + 'description' => 'Website Development Services', + 'line_items' => [ + [ + 'name' => 'Frontend Development', + 'amount' => 50000, + 'quantity' => 1 + ], + [ + 'name' => 'Backend Development', + 'amount' => 75000, + 'quantity' => 1 + ] + ], + 'tax' => [ + [ + 'name' => 'VAT (7.5%)', + 'amount' => 9375 + ] + ], + 'customer' => 'client@example.com', + 'due_date' => date('Y-m-d', strtotime('+30 days')), + 'currency' => 'NGN', + ]); + + if ($prResponse->isSuccessful()) { + /** @var PaymentRequestData $paymentRequest */ + $paymentRequest = $prResponse->getData(); + + echo " ✅ Payment request created!\n\n"; + echo " **Payment Request Details**:\n"; + echo " • Request Code: {$paymentRequest->request_code}\n"; + echo " • Description: {$paymentRequest->description}\n"; + echo " • Amount: {$paymentRequest->getFormattedAmount()}\n"; + echo " • Status: {$paymentRequest->status}\n"; + echo " • Due Date: {$paymentRequest->due_date}\n\n"; + + echo " **Helper Methods**:\n"; + echo " • Is pending: " . ($paymentRequest->isPending() ? 'Yes' : 'No') . "\n"; + echo " • Is paid: " . ($paymentRequest->isPaid() ? 'Yes' : 'No') . "\n"; + echo " • Has due date: " . ($paymentRequest->hasDueDate() ? 'Yes' : 'No') . "\n"; + echo " • Is overdue: " . ($paymentRequest->isOverdue() ? 'Yes' : 'No') . "\n"; + echo " • Line items total: ₦" . ($paymentRequest->getLineItemsTotal() / 100) . "\n"; + echo " • Tax total: ₦" . ($paymentRequest->getTaxTotal() / 100) . "\n\n"; + + if ($paymentRequest->invoice_url) { + echo " 🔗 Invoice URL: {$paymentRequest->invoice_url}\n\n"; + } + } + +} catch (Exception $e) { + echo " ❌ Error: " . $e->getMessage() . "\n\n"; +} + +// ============================================================================ +// Example 6: Backward Compatibility +// ============================================================================ + +echo "🔄 **Example 6: Backward Compatibility**\n"; +echo "Both old and new methods work side by side!\n\n"; + +try { + // Old array-based method (still works) + $oldResponse = $paystack->customers()->all(['perPage' => 2]); + $oldCount = isset($oldResponse['data']) && is_array($oldResponse['data']) ? count($oldResponse['data']) : 0; + echo " ✅ Old method: Returns array with {$oldCount} customers\n"; + + // New typed method + $newResponse = $paystack->customers()->allTyped(['perPage' => 2]); + echo " ✅ New method: Returns CustomerListResponse with " . count($newResponse->getCustomers()) . " customers\n\n"; + + echo " **Migration Strategy**:\n"; + echo " • Use *Typed() methods for new code\n"; + echo " • Keep existing code unchanged\n"; + echo " • Gradually migrate as you refactor\n"; + echo " • Both approaches are fully supported\n\n"; + +} catch (Exception $e) { + echo " ⚠️ Note: API calls require valid credentials\n"; + echo " Both old and new methods work identically with real API keys\n\n"; + + echo " **Migration Strategy**:\n"; + echo " • Use *Typed() methods for new code\n"; + echo " • Keep existing code unchanged\n"; + echo " • Gradually migrate as you refactor\n"; + echo " • Both approaches are fully supported\n\n"; +} + +// ============================================================================ +// Benefits Summary +// ============================================================================ + +echo "🎯 **Benefits of Response DTOs**:\n\n"; + +echo "✅ **Type Safety**:\n"; +echo " • Catch errors at development time, not runtime\n"; +echo " • No more typos in array keys\n"; +echo " • Clear property types in IDE\n\n"; + +echo "✅ **Better IDE Support**:\n"; +echo " • Full autocomplete for all properties\n"; +echo " • Inline documentation tooltips\n"; +echo " • Jump to definition support\n\n"; + +echo "✅ **Helper Methods**:\n"; +echo " • getFormattedAmount() for currency formatting\n"; +echo " • isSuccessful(), isPending(), etc. for status checks\n"; +echo " • getFullName(), getCustomerEmail() for convenience\n"; +echo " • Type-specific utility functions\n\n"; + +echo "✅ **Structured Data**:\n"; +echo " • DateTimeImmutable for dates (not strings)\n"; +echo " • Nested objects for related data\n"; +echo " • Proper types for integers, booleans, etc.\n\n"; + +echo "✅ **Pagination Support**:\n"; +echo " • Dedicated PaginationMeta class\n"; +echo " • Helper methods: hasNextPage(), hasPreviousPage()\n"; +echo " • Easy to implement infinite scroll\n\n"; + +// ============================================================================ +// Available Typed Methods +// ============================================================================ + +echo "📚 **Available Typed Methods**:\n\n"; + +$typedMethods = [ + 'Customer API' => [ + 'createTyped()' => 'Create customer with typed response', + 'allTyped()' => 'List customers with pagination', + ], + 'Transaction API' => [ + 'initializeTyped()' => 'Initialize with TransactionInitializeResponse', + 'verifyTyped()' => 'Verify with TransactionData', + 'allTyped()' => 'List transactions with pagination', + ], + 'PaymentRequest API' => [ + 'createTyped()' => 'Create with PaymentRequestData', + 'allTyped()' => 'List payment requests with pagination', + ], +]; + +foreach ($typedMethods as $api => $methods) { + echo "**{$api}**:\n"; + foreach ($methods as $method => $description) { + echo " • {$method}: {$description}\n"; + } + echo "\n"; +} + +// ============================================================================ +// Next Steps +// ============================================================================ + +echo "🎉 Demonstration completed!\n\n"; + +echo "**Next Steps**:\n"; +echo "• Start using *Typed() methods in your new code\n"; +echo "• Explore the DTO classes for available methods\n"; +echo "• Migrate existing code gradually\n"; +echo "• Look forward to Phase 3: Request DTOs\n\n"; + +echo "📚 **Documentation**:\n"; +echo "• Response DTOs: src/Response/\n"; +echo "• API Reference: docs/api-reference.md\n"; +echo "• Getting Started: docs/getting-started.md\n\n"; + +// Show warning if using default API key +if ($secretKey === 'sk_test_your_secret_key_here') { + echo "⚠️ **NOTE**: This demo uses a placeholder API key.\n"; + echo " For actual API calls, replace with your real test key from:\n"; + echo " https://dashboard.paystack.com/#/settings/developer\n\n"; +} + +echo "🚀 **Enjoy enhanced type safety with Response DTOs!**\n"; \ No newline at end of file diff --git a/src/API/Customer.php b/src/API/Customer.php index b08205d..570f7d6 100644 --- a/src/API/Customer.php +++ b/src/API/Customer.php @@ -5,6 +5,8 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; use StarfolkSoftware\Paystack\Options\Customer as CustomerOptions; +use StarfolkSoftware\Paystack\Response\PaystackResponse; +use StarfolkSoftware\Paystack\Response\Customer\CustomerListResponse; class Customer extends ApiAbstract { @@ -23,6 +25,21 @@ public function create(array $params): array return ResponseMediator::getContent($response); } + /** + * Creates a new customer (typed response) + * + * @param array $params + * @return PaystackResponse + */ + public function createTyped(array $params): PaystackResponse + { + $options = new CustomerOptions\CreateOptions($params); + + $response = $this->httpClient->post('/customer', body: json_encode($options->all())); + + return ResponseMediator::getCustomerResponse($response); + } + /** * Retrieves all customers * @@ -40,6 +57,23 @@ public function all(array $params): array return ResponseMediator::getContent($response); } + /** + * Retrieves all customers (typed response) + * + * @param array $params + * @return CustomerListResponse + */ + public function allTyped(array $params): CustomerListResponse + { + $options = new CustomerOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/customer', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getCustomerListResponse($response); + } + /** * Retrieves a customer * diff --git a/src/API/PaymentRequest.php b/src/API/PaymentRequest.php index a199bfe..5900cbc 100644 --- a/src/API/PaymentRequest.php +++ b/src/API/PaymentRequest.php @@ -5,6 +5,8 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; use StarfolkSoftware\Paystack\Options\PaymentRequest as PaymentRequestOptions; +use StarfolkSoftware\Paystack\Response\PaystackResponse; +use StarfolkSoftware\Paystack\Response\PaginatedResponse; class PaymentRequest extends ApiAbstract { @@ -23,6 +25,21 @@ public function create(array $params): array return ResponseMediator::getContent($response); } + /** + * Create a payment request (typed response) + * + * @param array $params + * @return PaystackResponse + */ + public function createTyped(array $params): PaystackResponse + { + $options = new PaymentRequestOptions\CreateOptions($params); + + $response = $this->httpClient->post('/paymentrequest', body: json_encode($options->all())); + + return ResponseMediator::getPaymentRequestResponse($response); + } + /** * List payment requests * @@ -40,6 +57,23 @@ public function all(array $params = []): array return ResponseMediator::getContent($response); } + /** + * List payment requests (typed response) + * + * @param array $params + * @return PaginatedResponse + */ + public function allTyped(array $params = []): PaginatedResponse + { + $options = new PaymentRequestOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/paymentrequest', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getPaymentRequestListResponse($response); + } + /** * Fetch a payment request * diff --git a/src/API/Transaction.php b/src/API/Transaction.php index bc23b41..5636ddc 100644 --- a/src/API/Transaction.php +++ b/src/API/Transaction.php @@ -5,6 +5,9 @@ use StarfolkSoftware\Paystack\Abstracts\ApiAbstract; use StarfolkSoftware\Paystack\HttpClient\Message\ResponseMediator; use StarfolkSoftware\Paystack\Options\Transaction as TransactionOptions; +use StarfolkSoftware\Paystack\Response\Transaction\TransactionInitializeResponse; +use StarfolkSoftware\Paystack\Response\PaystackResponse; +use StarfolkSoftware\Paystack\Response\PaginatedResponse; class Transaction extends ApiAbstract { @@ -23,6 +26,21 @@ public function initialize(array $params): array return ResponseMediator::getContent($response); } + /** + * Initialize Transaction (typed response) + * + * @param array $params + * @return TransactionInitializeResponse + */ + public function initializeTyped(array $params): TransactionInitializeResponse + { + $options = new TransactionOptions\InitializeOptions($params); + + $response = $this->httpClient->post('/transaction/initialize', body: json_encode($options->all())); + + return ResponseMediator::getTransactionInitializeResponse($response); + } + /** * Confirm the status of a transaction * @@ -36,6 +54,19 @@ public function verify(string $reference): array return ResponseMediator::getContent($response); } + /** + * Confirm the status of a transaction (typed response) + * + * @param string $reference + * @return PaystackResponse + */ + public function verifyTyped(string $reference): PaystackResponse + { + $response = $this->httpClient->get("/transaction/verify/{$reference}"); + + return ResponseMediator::getTransactionResponse($response); + } + /** * Retrieves all transactions * @@ -53,6 +84,23 @@ public function all(array $params): array return ResponseMediator::getContent($response); } + /** + * Retrieves all transactions (typed response) + * + * @param array $params + * @return PaginatedResponse + */ + public function allTyped(array $params): PaginatedResponse + { + $options = new TransactionOptions\ReadAllOptions($params); + + $response = $this->httpClient->get('/transaction', [ + 'query' => $options->all() + ]); + + return ResponseMediator::getTransactionListResponse($response); + } + /** * Retrieves a transaction * diff --git a/src/HttpClient/Message/ResponseMediator.php b/src/HttpClient/Message/ResponseMediator.php index a397243..0799c87 100644 --- a/src/HttpClient/Message/ResponseMediator.php +++ b/src/HttpClient/Message/ResponseMediator.php @@ -3,11 +3,115 @@ namespace StarfolkSoftware\Paystack\HttpClient\Message; use Psr\Http\Message\ResponseInterface; +use StarfolkSoftware\Paystack\Response\ResponseFactory; +use StarfolkSoftware\Paystack\Response\PaystackResponse; class ResponseMediator { + /** + * Get content as array (backward compatibility) + * + * @param ResponseInterface $response + * @return array + */ public static function getContent(ResponseInterface $response): array { return json_decode($response->getBody()->getContents(), true); } + + /** + * Get content as PaystackResponse DTO + * + * @param ResponseInterface $response + * @return PaystackResponse + */ + public static function getPaystackResponse(ResponseInterface $response): PaystackResponse + { + $content = self::getContent($response); + return ResponseFactory::createGenericResponse($content); + } + + /** + * Get customer response as typed DTO + * + * @param ResponseInterface $response + * @return PaystackResponse + */ + public static function getCustomerResponse(ResponseInterface $response): PaystackResponse + { + $content = self::getContent($response); + return ResponseFactory::createCustomerResponse($content); + } + + /** + * Get customer list response as typed DTO + * + * @param ResponseInterface $response + * @return \StarfolkSoftware\Paystack\Response\Customer\CustomerListResponse + */ + public static function getCustomerListResponse(ResponseInterface $response): \StarfolkSoftware\Paystack\Response\Customer\CustomerListResponse + { + $content = self::getContent($response); + return ResponseFactory::createCustomerListResponse($content); + } + + /** + * Get transaction response as typed DTO + * + * @param ResponseInterface $response + * @return PaystackResponse + */ + public static function getTransactionResponse(ResponseInterface $response): PaystackResponse + { + $content = self::getContent($response); + return ResponseFactory::createTransactionResponse($content); + } + + /** + * Get transaction initialization response as typed DTO + * + * @param ResponseInterface $response + * @return \StarfolkSoftware\Paystack\Response\Transaction\TransactionInitializeResponse + */ + public static function getTransactionInitializeResponse(ResponseInterface $response): \StarfolkSoftware\Paystack\Response\Transaction\TransactionInitializeResponse + { + $content = self::getContent($response); + return ResponseFactory::createTransactionInitializeResponse($content); + } + + /** + * Get transaction list response as typed DTO + * + * @param ResponseInterface $response + * @return \StarfolkSoftware\Paystack\Response\PaginatedResponse + */ + public static function getTransactionListResponse(ResponseInterface $response): \StarfolkSoftware\Paystack\Response\PaginatedResponse + { + $content = self::getContent($response); + return ResponseFactory::createTransactionListResponse($content); + } + + /** + * Get payment request response as typed DTO + * + * @param ResponseInterface $response + * @return PaystackResponse + */ + public static function getPaymentRequestResponse(ResponseInterface $response): PaystackResponse + { + $content = self::getContent($response); + return ResponseFactory::createPaymentRequestResponse($content); + } + + /** + * Get payment request list response as typed DTO + * + * @param ResponseInterface $response + * @return \StarfolkSoftware\Paystack\Response\PaginatedResponse + */ + public static function getPaymentRequestListResponse(ResponseInterface $response): \StarfolkSoftware\Paystack\Response\PaginatedResponse + { + $content = self::getContent($response); + return ResponseFactory::createPaymentRequestListResponse($content); + } } \ No newline at end of file diff --git a/src/Response/Customer/CustomerData.php b/src/Response/Customer/CustomerData.php new file mode 100644 index 0000000..e96cbce --- /dev/null +++ b/src/Response/Customer/CustomerData.php @@ -0,0 +1,120 @@ + $data + * @return static + */ + public static function fromArray(array $data): static + { + return new static( + id: $data['id'], + customer_code: $data['customer_code'], + email: $data['email'], + first_name: $data['first_name'] ?? null, + last_name: $data['last_name'] ?? null, + phone: $data['phone'] ?? null, + risk_action: $data['risk_action'] ?? null, + identified: $data['identified'] ?? false, + identifications: $data['identifications'] ?? [], + authorizations: $data['authorizations'] ?? [], + subscriptions: $data['subscriptions'] ?? [], + metadata: $data['metadata'] ?? [], + created_at: isset($data['created_at']) ? new DateTimeImmutable($data['created_at']) : null, + updated_at: isset($data['updated_at']) ? new DateTimeImmutable($data['updated_at']) : null, + ); + } + + /** + * Get the customer's full name + */ + public function getFullName(): string + { + $parts = array_filter([$this->first_name, $this->last_name]); + return implode(' ', $parts) ?: $this->email; + } + + /** + * Check if customer has been identified + */ + public function isIdentified(): bool + { + return $this->identified; + } + + /** + * Check if customer has active authorizations + */ + public function hasAuthorizations(): bool + { + return !empty($this->authorizations); + } + + /** + * Check if customer has active subscriptions + */ + public function hasSubscriptions(): bool + { + return !empty($this->subscriptions); + } + + /** + * Get the customer's risk action + */ + public function getRiskAction(): string + { + return $this->risk_action ?? 'default'; + } + + /** + * Convert to array format + * + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'customer_code' => $this->customer_code, + 'email' => $this->email, + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'phone' => $this->phone, + 'risk_action' => $this->risk_action, + 'identified' => $this->identified, + 'identifications' => $this->identifications, + 'authorizations' => $this->authorizations, + 'subscriptions' => $this->subscriptions, + 'metadata' => $this->metadata, + 'created_at' => $this->created_at?->format('c'), + 'updated_at' => $this->updated_at?->format('c'), + ]; + } +} \ No newline at end of file diff --git a/src/Response/Customer/CustomerListResponse.php b/src/Response/Customer/CustomerListResponse.php new file mode 100644 index 0000000..73bd61c --- /dev/null +++ b/src/Response/Customer/CustomerListResponse.php @@ -0,0 +1,96 @@ + + */ +final class CustomerListResponse extends PaystackResponse +{ + /** + * @param CustomerData[] $customers + */ + public function __construct( + bool $status, + string $message, + public readonly array $customers = [], + public readonly ?PaginationMeta $pagination = null + ) { + parent::__construct($status, $message, $customers, $pagination?->toArray() ?? []); + } + + /** + * Create a CustomerListResponse from API response array + * + * @param array $response + * @return static + */ + public static function fromArray(array $response): static + { + $customers = []; + if (isset($response['data']) && is_array($response['data'])) { + foreach ($response['data'] as $customerData) { + $customers[] = CustomerData::fromArray($customerData); + } + } + + $pagination = null; + if (isset($response['meta']) && is_array($response['meta'])) { + $pagination = PaginationMeta::fromArray($response['meta']); + } + + return new static( + status: $response['status'] ?? false, + message: $response['message'] ?? '', + customers: $customers, + pagination: $pagination + ); + } + + /** + * Get all customers from the response + * + * @return CustomerData[] + */ + public function getCustomers(): array + { + return $this->customers; + } + + /** + * Get the first customer from the response (useful for single-item responses) + */ + public function getFirstCustomer(): ?CustomerData + { + return $this->customers[0] ?? null; + } + + /** + * Get pagination information + */ + public function getPagination(): ?PaginationMeta + { + return $this->pagination; + } + + /** + * Check if there are more pages available + */ + public function hasMorePages(): bool + { + return $this->pagination?->hasNextPage() ?? false; + } + + /** + * Get the total number of customers + */ + public function getTotal(): int + { + return $this->pagination?->total ?? count($this->customers); + } +} \ No newline at end of file diff --git a/src/Response/PaginatedResponse.php b/src/Response/PaginatedResponse.php new file mode 100644 index 0000000..86005c8 --- /dev/null +++ b/src/Response/PaginatedResponse.php @@ -0,0 +1,113 @@ + + */ +final class PaginatedResponse extends PaystackResponse +{ + /** + * @param T[] $items + */ + public function __construct( + bool $status, + string $message, + public readonly array $items = [], + public readonly ?PaginationMeta $pagination = null + ) { + parent::__construct($status, $message, $items, $pagination?->toArray() ?? []); + } + + /** + * Create a PaginatedResponse from API response array with item factory + * + * @param array $response + * @param callable(array): T $itemFactory Function to create items from array data + * @return static + */ + public static function fromArrayWithFactory(array $response, callable $itemFactory): static + { + $items = []; + if (isset($response['data']) && is_array($response['data'])) { + foreach ($response['data'] as $itemData) { + $items[] = $itemFactory($itemData); + } + } + + $pagination = null; + if (isset($response['meta']) && is_array($response['meta'])) { + $pagination = PaginationMeta::fromArray($response['meta']); + } + + return new static( + status: $response['status'] ?? false, + message: $response['message'] ?? '', + items: $items, + pagination: $pagination + ); + } + + /** + * Get all items from the response + * + * @return T[] + */ + public function getItems(): array + { + return $this->items; + } + + /** + * Get the first item from the response + * + * @return T|null + */ + public function getFirstItem(): mixed + { + return $this->items[0] ?? null; + } + + /** + * Get pagination information + */ + public function getPagination(): ?PaginationMeta + { + return $this->pagination; + } + + /** + * Check if there are more pages available + */ + public function hasMorePages(): bool + { + return $this->pagination?->hasNextPage() ?? false; + } + + /** + * Get the total number of items + */ + public function getTotal(): int + { + return $this->pagination?->total ?? count($this->items); + } + + /** + * Check if the response is empty + */ + public function isEmpty(): bool + { + return empty($this->items); + } + + /** + * Get the count of items in current page + */ + public function count(): int + { + return count($this->items); + } +} \ No newline at end of file diff --git a/src/Response/PaginationMeta.php b/src/Response/PaginationMeta.php new file mode 100644 index 0000000..a58645d --- /dev/null +++ b/src/Response/PaginationMeta.php @@ -0,0 +1,85 @@ + $meta + * @return static + */ + public static function fromArray(array $meta): static + { + return new static( + total: $meta['total'] ?? 0, + perPage: $meta['perPage'] ?? 50, + page: $meta['page'] ?? 1, + pageCount: $meta['pageCount'] ?? 1, + next: $meta['next'] ?? null, + previous: $meta['previous'] ?? null + ); + } + + /** + * Check if there is a next page + */ + public function hasNextPage(): bool + { + return $this->page < $this->pageCount; + } + + /** + * Check if there is a previous page + */ + public function hasPreviousPage(): bool + { + return $this->page > 1; + } + + /** + * Get the next page number (or null if no next page) + */ + public function getNextPageNumber(): ?int + { + return $this->hasNextPage() ? $this->page + 1 : null; + } + + /** + * Get the previous page number (or null if no previous page) + */ + public function getPreviousPageNumber(): ?int + { + return $this->hasPreviousPage() ? $this->page - 1 : null; + } + + /** + * Convert to array format + * + * @return array + */ + public function toArray(): array + { + return [ + 'total' => $this->total, + 'perPage' => $this->perPage, + 'page' => $this->page, + 'pageCount' => $this->pageCount, + 'next' => $this->next, + 'previous' => $this->previous, + ]; + } +} \ No newline at end of file diff --git a/src/Response/PaymentRequest/PaymentRequestData.php b/src/Response/PaymentRequest/PaymentRequestData.php new file mode 100644 index 0000000..5e118ca --- /dev/null +++ b/src/Response/PaymentRequest/PaymentRequestData.php @@ -0,0 +1,211 @@ + $data + * @return static + */ + public static function fromArray(array $data): static + { + return new static( + id: $data['id'], + request_code: $data['request_code'], + description: $data['description'], + amount: $data['amount'], + currency: $data['currency'], + status: $data['status'], + due_date: $data['due_date'] ?? null, + has_invoice: $data['has_invoice'] ?? false, + invoice_url: $data['invoice_url'] ?? null, + offline_reference: $data['offline_reference'] ?? false, + customer: $data['customer'] ?? [], + line_items: $data['line_items'] ?? [], + tax: $data['tax'] ?? [], + metadata: $data['metadata'] ?? [], + created_at: isset($data['created_at']) ? new DateTimeImmutable($data['created_at']) : null, + updated_at: isset($data['updated_at']) ? new DateTimeImmutable($data['updated_at']) : null, + ); + } + + /** + * Check if the payment request is pending + */ + public function isPending(): bool + { + return $this->status === 'pending'; + } + + /** + * Check if the payment request is paid + */ + public function isPaid(): bool + { + return $this->status === 'paid'; + } + + /** + * Check if the payment request is partially paid + */ + public function isPartiallyPaid(): bool + { + return $this->status === 'partially_paid'; + } + + /** + * Check if the payment request is cancelled + */ + public function isCancelled(): bool + { + return $this->status === 'cancelled'; + } + + /** + * Get the payment request amount in major currency unit + */ + public function getAmountInMajorUnit(): float + { + return $this->amount / 100; + } + + /** + * Get formatted amount with currency + */ + public function getFormattedAmount(): string + { + $majorAmount = $this->getAmountInMajorUnit(); + return number_format($majorAmount, 2) . ' ' . $this->currency; + } + + /** + * Get customer email if available + */ + public function getCustomerEmail(): ?string + { + return $this->customer['email'] ?? null; + } + + /** + * Get customer name if available + */ + public function getCustomerName(): ?string + { + $first = $this->customer['first_name'] ?? ''; + $last = $this->customer['last_name'] ?? ''; + $name = trim($first . ' ' . $last); + return $name ?: null; + } + + /** + * Get total line items amount + */ + public function getLineItemsTotal(): int + { + if (empty($this->line_items)) { + return 0; + } + + $total = 0; + foreach ($this->line_items as $item) { + $total += ($item['amount'] ?? 0) * ($item['quantity'] ?? 1); + } + + return $total; + } + + /** + * Get total tax amount + */ + public function getTaxTotal(): int + { + if (empty($this->tax)) { + return 0; + } + + return array_sum(array_column($this->tax, 'amount')); + } + + /** + * Check if payment request has due date + */ + public function hasDueDate(): bool + { + return $this->due_date !== null; + } + + /** + * Get due date as DateTimeImmutable if available + */ + public function getDueDateAsDateTime(): ?DateTimeImmutable + { + return $this->due_date ? new DateTimeImmutable($this->due_date) : null; + } + + /** + * Check if payment request is overdue + */ + public function isOverdue(): bool + { + if (!$this->hasDueDate() || $this->isPaid()) { + return false; + } + + $dueDate = $this->getDueDateAsDateTime(); + return $dueDate && $dueDate < new DateTimeImmutable(); + } + + /** + * Convert to array format + * + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'request_code' => $this->request_code, + 'description' => $this->description, + 'amount' => $this->amount, + 'currency' => $this->currency, + 'status' => $this->status, + 'due_date' => $this->due_date, + 'has_invoice' => $this->has_invoice, + 'invoice_url' => $this->invoice_url, + 'offline_reference' => $this->offline_reference, + 'customer' => $this->customer, + 'line_items' => $this->line_items, + 'tax' => $this->tax, + 'metadata' => $this->metadata, + 'created_at' => $this->created_at?->format('c'), + 'updated_at' => $this->updated_at?->format('c'), + ]; + } +} \ No newline at end of file diff --git a/src/Response/PaystackResponse.php b/src/Response/PaystackResponse.php new file mode 100644 index 0000000..d1435df --- /dev/null +++ b/src/Response/PaystackResponse.php @@ -0,0 +1,107 @@ + $meta Additional metadata (pagination, etc.) + */ + public function __construct( + public readonly bool $status, + public readonly string $message, + public readonly mixed $data = null, + public readonly array $meta = [] + ) {} + + /** + * Create a PaystackResponse from a raw API response array + * + * @param array $response + * @return static + */ + public static function fromArray(array $response): static + { + return new static( + status: $response['status'] ?? false, + message: $response['message'] ?? '', + data: $response['data'] ?? null, + meta: $response['meta'] ?? [] + ); + } + + /** + * Check if the response indicates success + */ + public function isSuccessful(): bool + { + return $this->status; + } + + /** + * Check if the response indicates failure + */ + public function isFailed(): bool + { + return !$this->status; + } + + /** + * Get the response data, throwing an exception if the response failed + * + * @return T + * @throws PaystackResponseException + */ + public function getData(): mixed + { + if ($this->isFailed()) { + throw new PaystackResponseException($this->message); + } + + return $this->data; + } + + /** + * Get the response data or return null if failed + * + * @return T|null + */ + public function getDataOrNull(): mixed + { + return $this->isSuccessful() ? $this->data : null; + } + + /** + * Convert back to array format (for backward compatibility) + * + * @return array + */ + public function toArray(): array + { + $result = [ + 'status' => $this->status, + 'message' => $this->message, + ]; + + if ($this->data !== null) { + $result['data'] = $this->data; + } + + if (!empty($this->meta)) { + $result['meta'] = $this->meta; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Response/PaystackResponseException.php b/src/Response/PaystackResponseException.php new file mode 100644 index 0000000..c6cfe31 --- /dev/null +++ b/src/Response/PaystackResponseException.php @@ -0,0 +1,16 @@ + $response + * @return PaystackResponse + */ + public static function createCustomerResponse(array $response): PaystackResponse + { + if (!($response['status'] ?? false)) { + return PaystackResponse::fromArray($response); + } + + $customerData = CustomerData::fromArray($response['data'] ?? []); + return new PaystackResponse( + status: $response['status'], + message: $response['message'] ?? '', + data: $customerData, + meta: $response['meta'] ?? [] + ); + } + + /** + * Create a customer list response from API response array + * + * @param array $response + * @return CustomerListResponse + */ + public static function createCustomerListResponse(array $response): CustomerListResponse + { + return CustomerListResponse::fromArray($response); + } + + /** + * Create a transaction response from API response array + * + * @param array $response + * @return PaystackResponse + */ + public static function createTransactionResponse(array $response): PaystackResponse + { + if (!($response['status'] ?? false)) { + return PaystackResponse::fromArray($response); + } + + $transactionData = TransactionData::fromArray($response['data'] ?? []); + return new PaystackResponse( + status: $response['status'], + message: $response['message'] ?? '', + data: $transactionData, + meta: $response['meta'] ?? [] + ); + } + + /** + * Create a transaction initialization response from API response array + * + * @param array $response + * @return TransactionInitializeResponse + */ + public static function createTransactionInitializeResponse(array $response): TransactionInitializeResponse + { + return TransactionInitializeResponse::fromArray($response); + } + + /** + * Create a transaction list response from API response array + * + * @param array $response + * @return PaginatedResponse + */ + public static function createTransactionListResponse(array $response): PaginatedResponse + { + return PaginatedResponse::fromArrayWithFactory( + $response, + fn(array $data) => TransactionData::fromArray($data) + ); + } + + /** + * Create a payment request response from API response array + * + * @param array $response + * @return PaystackResponse + */ + public static function createPaymentRequestResponse(array $response): PaystackResponse + { + if (!($response['status'] ?? false)) { + return PaystackResponse::fromArray($response); + } + + $paymentRequestData = PaymentRequestData::fromArray($response['data'] ?? []); + return new PaystackResponse( + status: $response['status'], + message: $response['message'] ?? '', + data: $paymentRequestData, + meta: $response['meta'] ?? [] + ); + } + + /** + * Create a payment request list response from API response array + * + * @param array $response + * @return PaginatedResponse + */ + public static function createPaymentRequestListResponse(array $response): PaginatedResponse + { + return PaginatedResponse::fromArrayWithFactory( + $response, + fn(array $data) => PaymentRequestData::fromArray($data) + ); + } + + /** + * Create a generic Paystack response from API response array + * + * @param array $response + * @return PaystackResponse + */ + public static function createGenericResponse(array $response): PaystackResponse + { + return PaystackResponse::fromArray($response); + } +} \ No newline at end of file diff --git a/src/Response/Transaction/TransactionData.php b/src/Response/Transaction/TransactionData.php new file mode 100644 index 0000000..fc09398 --- /dev/null +++ b/src/Response/Transaction/TransactionData.php @@ -0,0 +1,182 @@ + $data + * @return static + */ + public static function fromArray(array $data): static + { + return new static( + id: $data['id'], + reference: $data['reference'], + status: $data['status'], + amount: $data['amount'], + currency: $data['currency'], + domain: $data['domain'], + gateway_response: $data['gateway_response'] ?? null, + message: $data['message'] ?? null, + channel: $data['channel'] ?? null, + ip_address: $data['ip_address'] ?? null, + fees: $data['fees'] ?? [], + authorization: $data['authorization'] ?? [], + customer: $data['customer'] ?? [], + plan: $data['plan'] ?? [], + metadata: $data['metadata'] ?? [], + created_at: isset($data['created_at']) ? new DateTimeImmutable($data['created_at']) : null, + updated_at: isset($data['updated_at']) ? new DateTimeImmutable($data['updated_at']) : null, + paid_at: isset($data['paid_at']) ? new DateTimeImmutable($data['paid_at']) : null, + transaction_date: isset($data['transaction_date']) ? new DateTimeImmutable($data['transaction_date']) : null, + ); + } + + /** + * Check if the transaction was successful + */ + public function isSuccessful(): bool + { + return $this->status === 'success'; + } + + /** + * Check if the transaction failed + */ + public function isFailed(): bool + { + return $this->status === 'failed'; + } + + /** + * Check if the transaction is pending + */ + public function isPending(): bool + { + return $this->status === 'pending'; + } + + /** + * Check if the transaction was abandoned + */ + public function isAbandoned(): bool + { + return $this->status === 'abandoned'; + } + + /** + * Get the transaction amount in the major currency unit (e.g., Naira instead of kobo) + */ + public function getAmountInMajorUnit(): float + { + return $this->amount / 100; + } + + /** + * Get formatted amount with currency + */ + public function getFormattedAmount(): string + { + $majorAmount = $this->getAmountInMajorUnit(); + return number_format($majorAmount, 2) . ' ' . $this->currency; + } + + /** + * Get customer email if available + */ + public function getCustomerEmail(): ?string + { + return $this->customer['email'] ?? null; + } + + /** + * Get customer name if available + */ + public function getCustomerName(): ?string + { + $first = $this->customer['first_name'] ?? ''; + $last = $this->customer['last_name'] ?? ''; + $name = trim($first . ' ' . $last); + return $name ?: null; + } + + /** + * Get total fees amount + */ + public function getTotalFees(): int + { + if (empty($this->fees)) { + return 0; + } + + return array_sum(array_column($this->fees, 'amount')); + } + + /** + * Get authorization code if available + */ + public function getAuthorizationCode(): ?string + { + return $this->authorization['authorization_code'] ?? null; + } + + /** + * Convert to array format + * + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'reference' => $this->reference, + 'status' => $this->status, + 'amount' => $this->amount, + 'currency' => $this->currency, + 'domain' => $this->domain, + 'gateway_response' => $this->gateway_response, + 'message' => $this->message, + 'channel' => $this->channel, + 'ip_address' => $this->ip_address, + 'fees' => $this->fees, + 'authorization' => $this->authorization, + 'customer' => $this->customer, + 'plan' => $this->plan, + 'metadata' => $this->metadata, + 'created_at' => $this->created_at?->format('c'), + 'updated_at' => $this->updated_at?->format('c'), + 'paid_at' => $this->paid_at?->format('c'), + 'transaction_date' => $this->transaction_date?->format('c'), + ]; + } +} \ No newline at end of file diff --git a/src/Response/Transaction/TransactionInitializeResponse.php b/src/Response/Transaction/TransactionInitializeResponse.php new file mode 100644 index 0000000..b1124f2 --- /dev/null +++ b/src/Response/Transaction/TransactionInitializeResponse.php @@ -0,0 +1,86 @@ + + */ +final class TransactionInitializeResponse extends PaystackResponse +{ + public function __construct( + bool $status, + string $message, + public readonly ?string $authorization_url = null, + public readonly ?string $access_code = null, + public readonly ?string $reference = null + ) { + $data = null; + if ($authorization_url && $access_code && $reference) { + $data = [ + 'authorization_url' => $authorization_url, + 'access_code' => $access_code, + 'reference' => $reference, + ]; + } + + parent::__construct($status, $message, $data); + } + + /** + * Create a TransactionInitializeResponse from API response array + * + * @param array $response + * @return static + */ + public static function fromArray(array $response): static + { + $data = $response['data'] ?? []; + + return new static( + status: $response['status'] ?? false, + message: $response['message'] ?? '', + authorization_url: $data['authorization_url'] ?? null, + access_code: $data['access_code'] ?? null, + reference: $data['reference'] ?? null + ); + } + + /** + * Get the authorization URL for payment + */ + public function getAuthorizationUrl(): ?string + { + return $this->authorization_url; + } + + /** + * Get the access code for payment + */ + public function getAccessCode(): ?string + { + return $this->access_code; + } + + /** + * Get the transaction reference + */ + public function getReference(): ?string + { + return $this->reference; + } + + /** + * Check if initialization was successful and all required data is present + */ + public function isInitialized(): bool + { + return $this->isSuccessful() + && $this->authorization_url !== null + && $this->access_code !== null + && $this->reference !== null; + } +} \ No newline at end of file From ed55cb08663b326bd0b825e0dfbf651852b9cbdf Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 16:27:51 +0100 Subject: [PATCH 13/17] fix: upgrade to PHPUnit 10 and PHP 8.1+, fix deprecated GitHub Actions - Upgrade PHPUnit from 9.5 to 10.5 to fix PHP Parser compatibility issues with PHP 8.3 - Explicitly require nikic/php-parser ^5.3 to ensure compatibility - Upgrade minimum PHP version from 8.0 to 8.1 (required by PHPUnit 10) - Update PHPUnit configuration to PHPUnit 10 format - Update GitHub Actions artifact action from v3 to v4 - Remove PHP 8.0 from test matrix - Update documentation to reflect PHP 8.1+ requirement Fixes: - PHP Warning: Undefined array key 327 in nikic/php-parser - PhpParser\Lexer::getNextToken(): Return value must be of type int, null returned - Deprecated actions/upload-artifact v3 --- .github/workflows/tests.yml | 4 +- README.md | 6 +- composer.json | 5 +- composer.lock | 606 ++++++++++++++---------------------- docs/getting-started.md | 2 +- docs/troubleshooting.md | 2 +- examples/README.md | 2 +- phpunit.xml.dist | 13 +- 8 files changed, 257 insertions(+), 383 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d9003a..11d2fb2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - php: [8.0, 8.1, 8.2, 8.3] + php: [8.1, 8.2, 8.3] stability: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} @@ -90,7 +90,7 @@ jobs: - name: Archive test results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-php-8.2 path: | diff --git a/README.md b/README.md index 4daa6cc..185a29e 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ A modern, developer-friendly PHP SDK for the [Paystack API](https://paystack.com ## Features - ✅ **Complete API Coverage** - All Paystack API endpoints supported -- ✅ **Type Safety** - Full PHP 8.0+ type declarations +- ✅ **Type Safety** - Full PHP 8.1+ type declarations - ✅ **Parameter Validation** - Automatic validation of API parameters - ✅ **PSR-18 HTTP Client** - Compatible with any PSR-18 HTTP client - ✅ **Comprehensive Examples** - Detailed usage examples for all features - ✅ **Exception Handling** - Detailed error responses and exception handling -- ✅ **Modern PHP** - Built for PHP 8.0+ with modern coding standards +- ✅ **Modern PHP** - Built for PHP 8.1+ with modern coding standards ## Quick Links @@ -41,7 +41,7 @@ A modern, developer-friendly PHP SDK for the [Paystack API](https://paystack.com ## Requirements -- **PHP 8.0 or higher** +- **PHP 8.1 or higher** - **PSR-18 HTTP Client** (any implementation) - **Composer** for dependency management diff --git a/composer.json b/composer.json index f328ca4..8c4fd61 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ } }, "require": { - "php": "^8.0", + "php": "^8.1", "laminas/laminas-diactoros": "^3.5.0", "php-http/client-common": "^2.3", "php-http/discovery": "^1.14.1", @@ -37,9 +37,10 @@ "symfony/options-resolver": "^6.2" }, "require-dev": { + "nikic/php-parser": "^5.3", "php-http/curl-client": "^2.2", "php-http/mock-client": "^1.5", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^10.5", "symfony/var-dumper": "^6.2" }, "scripts": { diff --git a/composer.lock b/composer.lock index fc8bc43..db70f64 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b48264d2f88993f742ddcdabfd89b8e", + "content-hash": "833957f9660f2bfc63d16b57a59f5c93", "packages": [ { "name": "clue/stream-filter", @@ -1314,76 +1314,6 @@ } ], "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:23:10+00:00" - }, { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -1749,16 +1679,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { @@ -1766,18 +1696,18 @@ "ext-libxml": "*", "ext-xmlwriter": "*", "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^10.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -1786,7 +1716,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -1815,7 +1745,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -1823,32 +1753,32 @@ "type": "github" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1875,7 +1805,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, "funding": [ { @@ -1883,28 +1814,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-pcntl": "*" @@ -1912,7 +1843,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1938,7 +1869,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" }, "funding": [ { @@ -1946,32 +1877,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2023-02-03T06:56:09+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1997,7 +1928,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, "funding": [ { @@ -2005,32 +1937,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2056,7 +1988,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" }, "funding": [ { @@ -2064,24 +1996,23 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2023-02-03T06:57:52+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.29", + "version": "10.5.58", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -2091,27 +2022,26 @@ "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.9", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.8", - "sebastian/global-state": "^5.0.8", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.4", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -2119,7 +2049,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "10.5-dev" } }, "autoload": { @@ -2151,7 +2081,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" }, "funding": [ { @@ -2175,32 +2105,32 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:29:11+00:00" + "time": "2025-09-28T12:04:46+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -2223,7 +2153,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -2231,32 +2162,32 @@ "type": "github" } ], - "time": "2024-03-02T06:27:43+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -2279,7 +2210,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" }, "funding": [ { @@ -2287,32 +2218,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2023-02-03T06:58:43+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2334,7 +2265,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" }, "funding": [ { @@ -2342,34 +2273,36 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2023-02-03T06:59:15+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.9", + "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2408,7 +2341,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" }, "funding": [ { @@ -2428,33 +2362,33 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:51:50+00:00" + "time": "2025-09-07T05:25:07+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -2477,7 +2411,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" }, "funding": [ { @@ -2485,33 +2420,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -2543,7 +2478,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -2551,27 +2487,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" @@ -2579,7 +2515,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -2598,7 +2534,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -2606,7 +2542,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -2614,34 +2551,34 @@ "type": "github" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.8", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -2683,7 +2620,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { @@ -2703,38 +2641,35 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:03:27+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.8", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2753,59 +2688,48 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", - "type": "tidelift" } ], - "time": "2025-08-10T07:10:35+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -2828,7 +2752,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { @@ -2836,34 +2761,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2885,7 +2810,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, "funding": [ { @@ -2893,32 +2818,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2023-02-03T07:08:32+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2940,7 +2865,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, "funding": [ { @@ -2948,32 +2873,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2023-02-03T07:06:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.6", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3003,7 +2928,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" }, "funding": [ { @@ -3023,86 +2949,32 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:57:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2025-08-10T07:50:56+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3125,7 +2997,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, "funding": [ { @@ -3133,29 +3005,29 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2023-02-03T07:10:45+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3178,7 +3050,7 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, "funding": [ { @@ -3186,7 +3058,7 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2023-02-07T11:34:05+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3414,12 +3286,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.0" + "php": "^8.1" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/docs/getting-started.md b/docs/getting-started.md index ee3e92f..2bad589 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -16,7 +16,7 @@ This guide will help you integrate Paystack payments into your PHP application u Before you begin, ensure you have: -1. **PHP 8.0 or higher** installed on your system +1. **PHP 8.1 or higher** installed on your system 2. **Composer** for dependency management 3. A **Paystack account** ([sign up here](https://dashboard.paystack.com/signup)) 4. Your **API keys** from the Paystack dashboard diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e3c8f70..9fcc09a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -36,7 +36,7 @@ composer require starfolksoftware/paystack-php 2. **Check PHP Version:** ```bash php -v - # Ensure PHP 8.0 or higher + # Ensure PHP 8.1 or higher ``` 3. **Force Install with Dependencies:** diff --git a/examples/README.md b/examples/README.md index 50ad7ce..8b5e926 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,7 +15,7 @@ This directory contains comprehensive examples demonstrating real-world usage pa ### Prerequisites -1. **PHP 8.0 or higher** +1. **PHP 8.1 or higher** 2. **Composer** (for dependency management) 3. **Paystack Account** ([sign up here](https://dashboard.paystack.com/signup)) 4. **Test API Keys** from your Paystack dashboard diff --git a/phpunit.xml.dist b/phpunit.xml.dist index efd9f36..45bb7cf 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,12 +2,8 @@ + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true"> tests - + ./src + + From 14db933a4fd1e8be9d055a69ae75f46c580011f6 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 16:29:19 +0100 Subject: [PATCH 14/17] fix: remove PHP 8.1 from workflow matrix to align with project requirements --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 11d2fb2..51cc2fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - php: [8.1, 8.2, 8.3] + php: [8.2, 8.3] stability: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} From e97bbe455d524358600f59335d6ef650429303c0 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 16:29:36 +0100 Subject: [PATCH 15/17] fix: update PHP requirement from ^8.1 to ^8.2 in composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8c4fd61..62d8233 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ } }, "require": { - "php": "^8.1", + "php": "^8.2", "laminas/laminas-diactoros": "^3.5.0", "php-http/client-common": "^2.3", "php-http/discovery": "^1.14.1", From 1933edb901e8a4e67d9c4101458126fdb94764f9 Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 16:31:53 +0100 Subject: [PATCH 16/17] fix: separate test commands for coverage and non-coverage runs - Add --no-coverage flag to default test command to avoid warnings - Create separate test:coverage command for CI/CD coverage generation - Remove failOnWarning from phpunit.xml.dist to prevent non-critical warnings from failing tests - Update GitHub Actions to use appropriate test command based on coverage needs - Fixes exit code 1 when running tests locally without xdebug/pcov installed --- .github/workflows/tests.yml | 7 ++++++- composer.json | 3 ++- phpunit.xml.dist | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 51cc2fc..ed6031f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,9 +41,14 @@ jobs: - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction - - name: Execute tests + - name: Execute tests (without coverage) + if: matrix.php != '8.2' || matrix.stability != 'prefer-stable' run: composer test + - name: Execute tests (with coverage) + if: matrix.php == '8.2' && matrix.stability == 'prefer-stable' + run: composer test:coverage + - name: Upload coverage to Codecov if: matrix.php == '8.2' && matrix.stability == 'prefer-stable' uses: codecov/codecov-action@v3 diff --git a/composer.json b/composer.json index 62d8233..9b89022 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "symfony/var-dumper": "^6.2" }, "scripts": { - "test": "phpunit --testdox --coverage-text --coverage-clover=coverage.clover" + "test": "phpunit --testdox --no-coverage", + "test:coverage": "phpunit --testdox --coverage-text --coverage-clover=coverage.clover" }, "minimum-stability": "stable", "prefer-stable": true, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 45bb7cf..e6d6114 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,7 +7,6 @@ processIsolation="false" stopOnFailure="false" executionOrder="random" - failOnWarning="true" failOnRisky="true" failOnEmptyTestSuite="true" beStrictAboutOutputDuringTests="true" From a18fd532f3d14a2c14080058ffa62dbb571afd9f Mon Sep 17 00:00:00 2001 From: Faruk Nasir Date: Thu, 30 Oct 2025 16:33:39 +0100 Subject: [PATCH 17/17] Update composer.lock to sync with composer.json --- composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index db70f64..3d2ea5e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "833957f9660f2bfc63d16b57a59f5c93", + "content-hash": "f42f1d9bb65361bab421d2397aa3a3db", "packages": [ { "name": "clue/stream-filter", @@ -3290,7 +3290,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1" + "php": "^8.2" }, "platform-dev": {}, "plugin-api-version": "2.6.0"