From 147ae9efcb66795954f546b7cd57f588c51fd58d Mon Sep 17 00:00:00 2001 From: Myckhel Date: Mon, 6 Jul 2026 21:44:44 +0100 Subject: [PATCH 1/4] feat: add support for Terminal, Virtual Terminal, and Order API integrations with associated controllers and tests --- readme.md | 291 +++++++++++++++++- src/Http/Controllers/OrderController.php | 15 + src/Http/Controllers/TerminalController.php | 20 ++ .../Controllers/VirtualTerminalController.php | 20 ++ src/Support/Order.php | 64 ++++ src/Support/Terminal.php | 95 ++++++ src/Support/VirtualTerminal.php | 85 +++++ src/routes.php | 29 ++ tests/RoutesTest.php | 21 ++ tests/Support/OrderTest.php | 65 ++++ tests/Support/TerminalTest.php | 103 +++++++ tests/Support/VirtualTerminalTest.php | 85 +++++ 12 files changed, 888 insertions(+), 5 deletions(-) create mode 100644 src/Http/Controllers/OrderController.php create mode 100644 src/Http/Controllers/TerminalController.php create mode 100644 src/Http/Controllers/VirtualTerminalController.php create mode 100644 src/Support/Order.php create mode 100644 src/Support/Terminal.php create mode 100644 src/Support/VirtualTerminal.php create mode 100644 tests/Support/OrderTest.php create mode 100644 tests/Support/TerminalTest.php create mode 100644 tests/Support/VirtualTerminalTest.php diff --git a/readme.md b/readme.md index 5984bbb..1175178 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ Laravel wrapper for the [Paystack API](https://paystack.com/docs/), built for di - Covers a broad set of Paystack endpoints (transactions, customers, transfers, plans, subscriptions, disputes, refunds, and more). - Optional built-in HTTP routes for quick API proxying from your Laravel app. - Built-in webhook route with signature validation and event dispatching. -- Compatible with Laravel `10`, `11`, and `12`. +- Compatible with Laravel `10`, `11`, `12`, and `13`. ## Installation @@ -78,6 +78,241 @@ $customer = Customer::create([ ]); ``` +## Developer Integration Scenarios + +Here are common design patterns for using this package across different parts of a Laravel application. + +### 1. Controllers (Checkout & Verification) + +Controllers should initiate payments and handle callback verification. When an API call fails, the package automatically calls Laravel's `abort()`, throwing an `Symfony\Component\HttpKernel\Exception\HttpException` which is caught by Laravel's global exception handler. + +```php +namespace App\Http\Controllers; + +use App\Models\Order; +use Binkode\Paystack\Support\Transaction; +use Illuminate\Http\Request; + +class PaymentController extends Controller +{ + /** + * Step 1: Initialize checkout and redirect to Paystack + */ + public function checkout(Order $order) + { + // Paystack amount is in kobo (e.g. 5,000 NGN = 500000 kobo) + $amountInKobo = $order->total_amount * 100; + + $response = Transaction::initialize([ + 'email' => auth()->user()->email, + 'amount' => $amountInKobo, + 'reference' => 'ORD-' . $order->id . '-' . time(), + 'callback_url' => route('payment.callback'), + 'metadata' => [ + 'order_id' => $order->id, + ], + ]); + + if (isset($response['status']) && $response['status'] === true) { + // Save reference to the order + $order->update([ + 'payment_reference' => $response['data']['reference'], + 'status' => 'pending', + ]); + + // Redirect user to the Paystack checkout page + return redirect($response['data']['authorization_url']); + } + + return back()->with('error', 'Unable to initialize transaction with Paystack.'); + } + + /** + * Step 2: Handle user redirection back from Paystack (Callback) + */ + public function callback(Request $request) + { + $reference = $request->query('reference'); + + if (!$reference) { + return redirect()->route('dashboard')->with('error', 'No reference returned.'); + } + + $response = Transaction::verify($reference); + + if (isset($response['data']['status']) && $response['data']['status'] === 'success') { + $order = Order::where('payment_reference', $reference)->firstOrFail(); + + // Avoid double processing (idempotency check) + if ($order->status !== 'completed') { + $order->update(['status' => 'completed']); + // Trigger any order success events / mailers here + } + + return redirect()->route('orders.show', $order)->with('success', 'Payment successful!'); + } + + return redirect()->route('dashboard')->with('error', 'Payment verification failed.'); + } +} +``` + +### 2. Service Classes (Business Logic Isolation) + +For larger applications, abstract Paystack calls into a service layer to keep controllers clean. This is especially useful for managing complex customer profiles, plans, or subscriptions. + +```php +namespace App\Services; + +use App\Models\User; +use Binkode\Paystack\Support\Customer; +use Binkode\Paystack\Support\Subscription; + +class BillingService +{ + /** + * Ensure a user has a Paystack customer account, then subscribe them to a plan. + */ + public function subscribeUserToPlan(User $user, string $planCode): array + { + // 1. Ensure user has a Paystack customer code + if (!$user->paystack_customer_code) { + $customerRes = Customer::create([ + 'email' => $user->email, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'phone' => $user->phone, + ]); + + if (isset($customerRes['data']['customer_code'])) { + $user->update([ + 'paystack_customer_code' => $customerRes['data']['customer_code'], + ]); + } + } + + // 2. Create the subscription on Paystack + $subscriptionRes = Subscription::create([ + 'customer' => $user->paystack_customer_code, + 'plan' => $planCode, + ]); + + if (isset($subscriptionRes['status']) && $subscriptionRes['status'] === true) { + $user->update([ + 'subscription_code' => $subscriptionRes['data']['subscription_code'], + 'subscription_status' => 'active', + 'subscribed_at' => now(), + ]); + } + + return $subscriptionRes; + } +} +``` + +### 3. Queued Jobs (Background Processing) + +When interacting with the Paystack API inside queued jobs (e.g. processing bulk transfers or validating statuses in the background), network errors or rate limits (`429 Too Many Requests`) can occur. + +You should design your jobs to handle these failures gracefully and support retries: + +```php +namespace App\Jobs; + +use App\Models\TransferRequest; +use Binkode\Paystack\Support\Transfer; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpKernel\Exception\HttpException; + +class ProcessPayoutJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + /** + * The number of times the job may be attempted. + */ + public int $tries = 3; + + /** + * The number of seconds to wait before retrying the job. + */ + public int $backoff = 60; + + protected TransferRequest $payout; + + public function __construct(TransferRequest $payout) + { + $this->payout = $payout; + } + + public function handle(): void + { + // Don't re-process completed payouts + if ($this->payout->status === 'processed') { + return; + } + + try { + $response = Transfer::initiate([ + 'source' => 'balance', + 'amount' => $this->payout->amount * 100, // in kobo + 'recipient' => $this->payout->recipient_code, + 'reason' => "Payout for Request #{$this->payout->id}", + 'reference' => 'PAY-' . $this->payout->id . '-' . time(), + ]); + + if (isset($response['status']) && $response['status'] === true) { + $this->payout->update([ + 'transfer_code' => $response['data']['transfer_code'], + 'status' => 'processing', + ]); + } + } catch (HttpException $e) { + // Log the API failure + Log::error("Paystack API Payout Failure: " . $e->getMessage(), [ + 'payout_id' => $this->payout->id, + 'status_code' => $e->getStatusCode() + ]); + + // If it's a server error (5xx) or rate limit (429), retry the job + if ($e->getStatusCode() >= 500 || $e->getStatusCode() === 429) { + $this->release($this->backoff); + return; + } + + // For client errors (400, 401, 403, 404), fail the job as retries won't help + $this->payout->update(['status' => 'failed', 'error_log' => $e->getMessage()]); + $this->fail($e); + } + } +} +``` + +### 4. Error Handling + +Because the package uses Laravel's HTTP client under the hood, any failed response (status codes `4xx` and `5xx`) automatically throws a `Symfony\Component\HttpKernel\Exception\HttpException` via `abort($res->status(), ...)`. + +You can catch this in your application code for fine-grained error handling: + +```php +use Binkode\Paystack\Support\Transaction; +use Symfony\Component\HttpKernel\Exception\HttpException; + +try { + $verify = Transaction::verify("non_existent_ref"); +} catch (HttpException $e) { + $statusCode = $e->getStatusCode(); // e.g. 404 + $errorMessage = $e->getMessage(); // Message returned from Paystack + + // Handle error accordingly +} +``` + ## Available Support Classes - `ApplePay` @@ -89,6 +324,7 @@ $customer = Customer::create([ - `Dispute` - `Invoice` - `Miscellaneous` +- `Order` - `Page` - `Plan` - `Product` @@ -98,10 +334,12 @@ $customer = Customer::create([ - `Split` - `SubAccount` - `Subscription` +- `Terminal` - `Transaction` - `Transfer` - `TransferControl` - `Verification` +- `VirtualTerminal` See class methods in `src/Support/*`. @@ -137,16 +375,59 @@ Example listener: ```php use Binkode\Paystack\Events\Hook; +use App\Models\Order; +use App\Models\TransferRequest; use Illuminate\Support\Facades\Log; class PaystackWebhookListener { public function handle(Hook $event): void { - Log::info("Paystack webhook received", [ - "event" => $event->event["event"] ?? null, - "payload" => $event->event, - ]); + $payload = $event->event; + $eventType = $payload['event'] ?? null; + $data = $payload['data'] ?? []; + + Log::info("Paystack webhook received: {$eventType}"); + + switch ($eventType) { + case 'charge.success': + $reference = $data['reference'] ?? null; + if ($reference) { + $order = Order::where('payment_reference', $reference)->first(); + if ($order && $order->status !== 'completed') { + $order->update(['status' => 'completed']); + } + } + break; + + case 'transfer.success': + $transferCode = $data['transfer_code'] ?? null; + if ($transferCode) { + $payout = TransferRequest::where('transfer_code', $transferCode)->first(); + if ($payout) { + $payout->update(['status' => 'processed']); + } + } + break; + + case 'transfer.failed': + case 'transfer.reversed': + $transferCode = $data['transfer_code'] ?? null; + if ($transferCode) { + $payout = TransferRequest::where('transfer_code', $transferCode)->first(); + if ($payout) { + $payout->update([ + 'status' => 'failed', + 'error_log' => $data['reason'] ?? 'Transfer failed or was reversed.', + ]); + } + } + break; + + default: + Log::warning("Unhandled Paystack event: {$eventType}"); + break; + } } } ``` diff --git a/src/Http/Controllers/OrderController.php b/src/Http/Controllers/OrderController.php new file mode 100644 index 0000000..d038991 --- /dev/null +++ b/src/Http/Controllers/OrderController.php @@ -0,0 +1,15 @@ +all()) + : Order::$method(request()->all()); + } +} diff --git a/src/Http/Controllers/TerminalController.php b/src/Http/Controllers/TerminalController.php new file mode 100644 index 0000000..aabec0a --- /dev/null +++ b/src/Http/Controllers/TerminalController.php @@ -0,0 +1,20 @@ +all()); + } + + function __call($method, $args) + { + return $args + ? Terminal::$method($args[0], request()->all()) + : Terminal::$method(request()->all()); + } +} diff --git a/src/Http/Controllers/VirtualTerminalController.php b/src/Http/Controllers/VirtualTerminalController.php new file mode 100644 index 0000000..5b2b04c --- /dev/null +++ b/src/Http/Controllers/VirtualTerminalController.php @@ -0,0 +1,20 @@ +all()); + } + + function __call($method, $args) + { + return $args + ? VirtualTerminal::$method($args[0], request()->all()) + : VirtualTerminal::$method(request()->all()); + } +} diff --git a/src/Support/Order.php b/src/Support/Order.php new file mode 100644 index 0000000..cf65f3f --- /dev/null +++ b/src/Support/Order.php @@ -0,0 +1,64 @@ + 'miscellaneous,listProviders', 'get,country' => 'miscellaneous,listCountries', 'get,address_verification/states' => 'miscellaneous,listStates', + // terminal + 'get,terminal' => 'terminal,list', + 'get,terminal/{terminal}' => 'terminal,fetch', + 'put,terminal/{terminal}' => 'terminal,update', + 'get,terminal/{terminal}/presence' => 'terminal,fetchPresence', + 'post,terminal/{terminal}/event' => 'terminal,sendEvent', + 'get,terminal/{terminal}/event/{event}' => 'terminal,fetchEventStatus', + 'post,terminal/commission_device' => 'terminal,commission', + 'post,terminal/decommission_device' => 'terminal,decommission', + // virtual terminal + 'post,virtual_terminal' => 'virtualterminal,create', + 'get,virtual_terminal' => 'virtualterminal,list', + 'get,virtual_terminal/{virtual_terminal}' => 'virtualterminal,fetch', + 'put,virtual_terminal/{virtual_terminal}' => 'virtualterminal,update', + 'post,virtual_terminal/{virtual_terminal}/deactivate' => 'virtualterminal,deactivate', + 'post,virtual_terminal/{virtual_terminal}/destinations' => 'virtualterminal,assignDestination', + 'delete,virtual_terminal/{virtual_terminal}/destinations/{destination_id}' => 'virtualterminal,unassignDestination', + // order + 'post,order' => 'order,create', + 'get,order' => 'order,list', + 'get,order/{order}' => 'order,fetch', + 'get,order/product/{product}' => 'order,fetchProductOrders', + 'get,order/validate/{order}' => 'order,validate', ]; $controls = [ @@ -199,6 +225,9 @@ 'refund' => RefundController::class, 'verification' => VerificationController::class, 'miscellaneous' => MiscellaneousController::class, + 'terminal' => TerminalController::class, + 'virtualterminal' => VirtualTerminalController::class, + 'order' => OrderController::class, ]; collect($routes)->map(function ($route, $index) use ($controls) { diff --git a/tests/RoutesTest.php b/tests/RoutesTest.php index 94fa48b..ff7a36a 100644 --- a/tests/RoutesTest.php +++ b/tests/RoutesTest.php @@ -25,6 +25,27 @@ public function test_hook_route_is_registered(): void $this->assertContains('POST', $route->methods()); } + public function test_terminal_routes_are_registered(): void + { + $route = $this->findRouteByAction(\Binkode\Paystack\Http\Controllers\TerminalController::class . '@list'); + $this->assertInstanceOf(Route::class, $route); + $this->assertContains('GET', $route->methods()); + } + + public function test_virtual_terminal_routes_are_registered(): void + { + $route = $this->findRouteByAction(\Binkode\Paystack\Http\Controllers\VirtualTerminalController::class . '@list'); + $this->assertInstanceOf(Route::class, $route); + $this->assertContains('GET', $route->methods()); + } + + public function test_order_routes_are_registered(): void + { + $route = $this->findRouteByAction(\Binkode\Paystack\Http\Controllers\OrderController::class . '@list'); + $this->assertInstanceOf(Route::class, $route); + $this->assertContains('GET', $route->methods()); + } + private function findRouteByAction(string $action): ?Route { foreach ($this->app['router']->getRoutes() as $route) { diff --git a/tests/Support/OrderTest.php b/tests/Support/OrderTest.php new file mode 100644 index 0000000..627cc14 --- /dev/null +++ b/tests/Support/OrderTest.php @@ -0,0 +1,65 @@ +set('paystack.secret_key', 'sk_test_mockkey'); + } + + public function test_create_order() + { + Http::fake([ + 'https://api.paystack.co/order' => Http::response(['status' => true], 200) + ]); + + $response = Order::create(['product' => 'prod_123']); + $this->assertTrue($response['status']); + } + + public function test_list_orders() + { + Http::fake([ + 'https://api.paystack.co/order' => Http::response(['status' => true], 200) + ]); + + $response = Order::list(); + $this->assertTrue($response['status']); + } + + public function test_fetch_order() + { + Http::fake([ + 'https://api.paystack.co/order/ord_123' => Http::response(['status' => true], 200) + ]); + + $response = Order::fetch('ord_123'); + $this->assertTrue($response['status']); + } + + public function test_fetch_product_orders() + { + Http::fake([ + 'https://api.paystack.co/order/product/prod_123' => Http::response(['status' => true], 200) + ]); + + $response = Order::fetchProductOrders('prod_123'); + $this->assertTrue($response['status']); + } + + public function test_validate_order() + { + Http::fake([ + 'https://api.paystack.co/order/validate/ord_123' => Http::response(['status' => true], 200) + ]); + + $response = Order::validate('ord_123'); + $this->assertTrue($response['status']); + } +} diff --git a/tests/Support/TerminalTest.php b/tests/Support/TerminalTest.php new file mode 100644 index 0000000..00d7a48 --- /dev/null +++ b/tests/Support/TerminalTest.php @@ -0,0 +1,103 @@ +set('paystack.secret_key', 'sk_test_mockkey'); + } + + public function test_list_terminals() + { + Http::fake([ + 'https://api.paystack.co/terminal*' => Http::response(['status' => true, 'data' => []], 200) + ]); + + $response = Terminal::list(); + $this->assertTrue($response['status']); + Http::assertSent(function ($request) { + return $request->url() === 'https://api.paystack.co/terminal' && $request->method() === 'GET'; + }); + } + + public function test_fetch_terminal() + { + Http::fake([ + 'https://api.paystack.co/terminal/term_123' => Http::response(['status' => true], 200) + ]); + + $response = Terminal::fetch('term_123'); + $this->assertTrue($response['status']); + } + + public function test_update_terminal() + { + Http::fake([ + 'https://api.paystack.co/terminal/term_123' => Http::response(['status' => true], 200) + ]); + + $response = Terminal::update('term_123', ['name' => 'New Name']); + $this->assertTrue($response['status']); + Http::assertSent(function ($request) { + return $request->url() === 'https://api.paystack.co/terminal/term_123' && + $request->method() === 'PUT' && + $request['name'] === 'New Name'; + }); + } + + public function test_fetch_presence() + { + Http::fake([ + 'https://api.paystack.co/terminal/term_123/presence' => Http::response(['status' => true], 200) + ]); + + $response = Terminal::fetchPresence('term_123'); + $this->assertTrue($response['status']); + } + + public function test_send_event() + { + Http::fake([ + 'https://api.paystack.co/terminal/term_123/event' => Http::response(['status' => true], 200) + ]); + + $response = Terminal::sendEvent('term_123', ['type' => 'invoice']); + $this->assertTrue($response['status']); + } + + public function test_fetch_event_status() + { + Http::fake([ + 'https://api.paystack.co/terminal/term_123/event/evt_123' => Http::response(['status' => true], 200) + ]); + + $response = Terminal::fetchEventStatus('term_123', 'evt_123'); + $this->assertTrue($response['status']); + } + + public function test_commission() + { + Http::fake([ + 'https://api.paystack.co/terminal/commission_device' => Http::response(['status' => true], 200) + ]); + + $response = Terminal::commission(['serial_number' => '12345']); + $this->assertTrue($response['status']); + } + + public function test_decommission() + { + Http::fake([ + 'https://api.paystack.co/terminal/decommission_device' => Http::response(['status' => true], 200) + ]); + + $response = Terminal::decommission(['serial_number' => '12345']); + $this->assertTrue($response['status']); + } +} diff --git a/tests/Support/VirtualTerminalTest.php b/tests/Support/VirtualTerminalTest.php new file mode 100644 index 0000000..11c38fd --- /dev/null +++ b/tests/Support/VirtualTerminalTest.php @@ -0,0 +1,85 @@ +set('paystack.secret_key', 'sk_test_mockkey'); + } + + public function test_create_virtual_terminal() + { + Http::fake([ + 'https://api.paystack.co/virtual_terminal' => Http::response(['status' => true], 200) + ]); + + $response = VirtualTerminal::create(['title' => 'Test VT']); + $this->assertTrue($response['status']); + } + + public function test_list_virtual_terminals() + { + Http::fake([ + 'https://api.paystack.co/virtual_terminal' => Http::response(['status' => true], 200) + ]); + + $response = VirtualTerminal::list(); + $this->assertTrue($response['status']); + } + + public function test_fetch_virtual_terminal() + { + Http::fake([ + 'https://api.paystack.co/virtual_terminal/vt_123' => Http::response(['status' => true], 200) + ]); + + $response = VirtualTerminal::fetch('vt_123'); + $this->assertTrue($response['status']); + } + + public function test_update_virtual_terminal() + { + Http::fake([ + 'https://api.paystack.co/virtual_terminal/vt_123' => Http::response(['status' => true], 200) + ]); + + $response = VirtualTerminal::update('vt_123', ['title' => 'Updated VT']); + $this->assertTrue($response['status']); + } + + public function test_deactivate_virtual_terminal() + { + Http::fake([ + 'https://api.paystack.co/virtual_terminal/vt_123/deactivate' => Http::response(['status' => true], 200) + ]); + + $response = VirtualTerminal::deactivate('vt_123'); + $this->assertTrue($response['status']); + } + + public function test_assign_destination() + { + Http::fake([ + 'https://api.paystack.co/virtual_terminal/vt_123/destinations' => Http::response(['status' => true], 200) + ]); + + $response = VirtualTerminal::assignDestination('vt_123', ['destination' => 'Test']); + $this->assertTrue($response['status']); + } + + public function test_unassign_destination() + { + Http::fake([ + 'https://api.paystack.co/virtual_terminal/vt_123/destinations/dest_123' => Http::response(['status' => true], 200) + ]); + + $response = VirtualTerminal::unassignDestination('vt_123', 'dest_123'); + $this->assertTrue($response['status']); + } +} From fb636d5c1046b831beba0c33c37e28596438b3b4 Mon Sep 17 00:00:00 2001 From: Myckhel Date: Mon, 6 Jul 2026 21:51:10 +0100 Subject: [PATCH 2/4] chore: add guzzlehttp/guzzle dependency and update composer lockfile --- composer.json | 3 ++- composer.lock | 17 +++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index c8f049e..6a210aa 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ ], "require": { "php": "^8.1", - "illuminate/support": "^10.0|^11.0|^12.0|^13.0" + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "guzzlehttp/guzzle": "^7.2" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 0c5b2a8..810f3c8 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": "c4b1b29255fba7ddc25f04f17922a458", + "content-hash": "da58b34f8e49678e65193c357bf61188", "packages": [ { "name": "brick/math", @@ -643,16 +643,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.10.6", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "e7412b3180912c01650cc66647f18c1d1cbe9b94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/e7412b3180912c01650cc66647f18c1d1cbe9b94", + "reference": "e7412b3180912c01650cc66647f18c1d1cbe9b94", "shasum": "" }, "require": { @@ -670,8 +670,9 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.4", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -749,7 +750,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.10.6" }, "funding": [ { @@ -765,7 +766,7 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-06-01T13:06:22+00:00" }, { "name": "guzzlehttp/promises", From ec506e595682d63813c941306e39935b63f46c6c Mon Sep 17 00:00:00 2001 From: Myckhel Date: Mon, 6 Jul 2026 23:45:29 +0100 Subject: [PATCH 3/4] refactor: update order validation API endpoint and add project documentation files --- src/Support/Order.php | 2 +- src/routes.php | 2 +- tests/Support/OrderTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Support/Order.php b/src/Support/Order.php index cf65f3f..77594bd 100644 --- a/src/Support/Order.php +++ b/src/Support/Order.php @@ -59,6 +59,6 @@ static function fetchProductOrders($product, $params = []) */ static function validate($order, $params = []) { - return self::get("/order/validate/$order", $params); + return self::get("/order/$order/validate", $params); } } diff --git a/src/routes.php b/src/routes.php index aca430f..b9f6a6b 100644 --- a/src/routes.php +++ b/src/routes.php @@ -199,7 +199,7 @@ 'get,order' => 'order,list', 'get,order/{order}' => 'order,fetch', 'get,order/product/{product}' => 'order,fetchProductOrders', - 'get,order/validate/{order}' => 'order,validate', + 'get,order/{order}/validate' => 'order,validate', ]; $controls = [ diff --git a/tests/Support/OrderTest.php b/tests/Support/OrderTest.php index 627cc14..0c468fb 100644 --- a/tests/Support/OrderTest.php +++ b/tests/Support/OrderTest.php @@ -56,7 +56,7 @@ public function test_fetch_product_orders() public function test_validate_order() { Http::fake([ - 'https://api.paystack.co/order/validate/ord_123' => Http::response(['status' => true], 200) + 'https://api.paystack.co/order/ord_123/validate' => Http::response(['status' => true], 200) ]); $response = Order::validate('ord_123'); From 5c8e1cf9dda822bd2c9fcc9cce78fb08d8257dc9 Mon Sep 17 00:00:00 2001 From: Michael Ishola Date: Mon, 6 Jul 2026 23:49:16 +0100 Subject: [PATCH 4/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 1175178..71da3b3 100644 --- a/readme.md +++ b/readme.md @@ -84,7 +84,7 @@ Here are common design patterns for using this package across different parts of ### 1. Controllers (Checkout & Verification) -Controllers should initiate payments and handle callback verification. When an API call fails, the package automatically calls Laravel's `abort()`, throwing an `Symfony\Component\HttpKernel\Exception\HttpException` which is caught by Laravel's global exception handler. +Controllers should initiate payments and handle callback verification. When an API call fails, the package automatically calls Laravel's `abort()`, throwing a `Symfony\Component\HttpKernel\Exception\HttpException` which is caught by Laravel's global exception handler. ```php namespace App\Http\Controllers;