diff --git a/app/Actions/Licenses/DeleteLicense.php b/app/Actions/Licenses/DeleteLicense.php index 51e5cf0f..b591999b 100644 --- a/app/Actions/Licenses/DeleteLicense.php +++ b/app/Actions/Licenses/DeleteLicense.php @@ -7,17 +7,15 @@ class DeleteLicense { - public function __construct( - protected Anystack $anystack - ) {} - /** * Handle the deletion of a license. */ public function handle(License $license, bool $deleteFromAnystack = true): bool { if ($deleteFromAnystack) { - $this->anystack->deleteLicense($license->anystack_product_id, $license->anystack_id); + Anystack::api() + ->license($license->anystack_id, $license->anystack_product_id) + ->delete(); } return $license->delete(); diff --git a/app/Actions/Licenses/SuspendLicense.php b/app/Actions/Licenses/SuspendLicense.php index 4241712a..8fe4383a 100644 --- a/app/Actions/Licenses/SuspendLicense.php +++ b/app/Actions/Licenses/SuspendLicense.php @@ -7,16 +7,14 @@ class SuspendLicense { - public function __construct( - protected Anystack $anystack - ) {} - /** * Handle the suspension of a license. */ public function handle(License $license): License { - $this->anystack->suspendLicense($license->anystack_product_id, $license->anystack_id); + Anystack::api() + ->license($license->anystack_id, $license->anystack_product_id) + ->suspend(); $license->update([ 'is_suspended' => true, diff --git a/app/Filament/Resources/LicenseResource/Pages/EditLicense.php b/app/Filament/Resources/LicenseResource/Pages/EditLicense.php index 53f875df..11467a8d 100644 --- a/app/Filament/Resources/LicenseResource/Pages/EditLicense.php +++ b/app/Filament/Resources/LicenseResource/Pages/EditLicense.php @@ -35,7 +35,10 @@ protected function getHeaderActions(): array ->visible(fn () => filled($this->record->anystack_id)) ->action(function () { try { - $response = app(Anystack::class)->getLicense($this->record->anystack_product_id, $this->record->anystack_id); + $response = Anystack::api() + ->license($this->record->anystack_id, $this->record->anystack_product_id) + ->retrieve(); + dispatch_sync(new UpsertLicenseFromAnystackLicense($response->json('data'))); Notification::make() diff --git a/app/Filament/Resources/LicenseResource/Pages/ListLicenses.php b/app/Filament/Resources/LicenseResource/Pages/ListLicenses.php index f7777c5b..d280d8cf 100644 --- a/app/Filament/Resources/LicenseResource/Pages/ListLicenses.php +++ b/app/Filament/Resources/LicenseResource/Pages/ListLicenses.php @@ -41,8 +41,10 @@ protected function getHeaderActions(): array ]) ->action(function (array $data) { try { - $productId = Subscription::Mini->anystackProductId(); - $response = app(Anystack::class)->getLicense($productId, $data['anystack_id']); + $response = Anystack::api() + ->license($data['anystack_id'], Subscription::Mini->anystackProductId()) // any plan's product id will work + ->retrieve(); + $licenseData = $response->json('data'); dispatch_sync(new UpsertLicenseFromAnystackLicense($licenseData)); diff --git a/app/Filament/Resources/SubscriptionItemResource/Pages/ViewSubscriptionItem.php b/app/Filament/Resources/SubscriptionItemResource/Pages/ViewSubscriptionItem.php index 6cceb0de..833eeed0 100644 --- a/app/Filament/Resources/SubscriptionItemResource/Pages/ViewSubscriptionItem.php +++ b/app/Filament/Resources/SubscriptionItemResource/Pages/ViewSubscriptionItem.php @@ -42,8 +42,10 @@ protected function getHeaderActions(): array ]) ->action(function (array $data) { try { - $productId = Subscription::Mini->anystackProductId(); - $response = app(Anystack::class)->getLicense($productId, $data['anystack_id']); + $response = Anystack::api() + ->license($data['anystack_id'], Subscription::Mini->anystackProductId()) // any plan's product id will work + ->retrieve(); + $licenseData = $response->json('data'); dispatch_sync(new UpsertLicenseFromAnystackLicense($licenseData)); diff --git a/app/Services/Anystack/Anystack.php b/app/Services/Anystack/Anystack.php index a6e4010f..efa2ce99 100644 --- a/app/Services/Anystack/Anystack.php +++ b/app/Services/Anystack/Anystack.php @@ -2,69 +2,34 @@ namespace App\Services\Anystack; -use Illuminate\Http\Client\PendingRequest; -use Illuminate\Http\Client\Response; -use Illuminate\Support\Facades\Http; +use App\Models\User; +use Illuminate\Http\Client\HttpClientException; -class Anystack +final class Anystack { /** * Create a new Anystack API client. */ - public function client(): PendingRequest + public static function api(?string $apiKey = null): AnystackClient { - return Http::withToken(config('services.anystack.key')) - ->acceptJson() - ->asJson(); - } + if (! ($apiKey ??= config('services.anystack.key'))) { + throw new HttpClientException('Anystack API key is not configured.'); + } - /** - * Suspend a license on AnyStack. - * - * @param string $productId The AnyStack product ID - * @param string $licenseId The AnyStack license ID - * @return Response The API response - * - * @throws \Illuminate\Http\Client\RequestException If the request fails - */ - public function suspendLicense(string $productId, string $licenseId): Response - { - return $this->client() - ->patch("https://api.anystack.sh/v1/products/{$productId}/licenses/{$licenseId}", [ - 'suspended' => true, - ]) - ->throw(); + return app(AnystackClient::class, ['apiKey' => $apiKey]); } - /** - * Delete a license on AnyStack. - * - * @param string $productId The AnyStack product ID - * @param string $licenseId The AnyStack license ID - * @return Response The API response - * - * @throws \Illuminate\Http\Client\RequestException If the request fails - */ - public function deleteLicense(string $productId, string $licenseId): Response + public static function findContact(string $contactUuid): ?User { - return $this->client() - ->delete("https://api.anystack.sh/v1/products/{$productId}/licenses/{$licenseId}") - ->throw(); + return User::query() + ->where('anystack_contact_id', $contactUuid) + ->first(); } - /** - * Retrieve a license from AnyStack. - * - * @param string $productId The AnyStack product ID - * @param string $licenseId The AnyStack license ID - * @return Response The API response - * - * @throws \Illuminate\Http\Client\RequestException If the request fails - */ - public function getLicense(string $productId, string $licenseId): Response + public static function findContactOrFail(string $contactUuid): User { - return $this->client() - ->get("https://api.anystack.sh/v1/products/{$productId}/licenses/{$licenseId}") - ->throw(); + return User::query() + ->where('anystack_contact_id', $contactUuid) + ->firstOrFail(); } } diff --git a/app/Services/Anystack/AnystackClient.php b/app/Services/Anystack/AnystackClient.php new file mode 100644 index 00000000..059e055d --- /dev/null +++ b/app/Services/Anystack/AnystackClient.php @@ -0,0 +1,49 @@ +prepareRequest()); + } + + /** + * Get the licenses resource for a product on Anystack. + */ + public function licenses(?string $productId = null): Licenses + { + $productId ??= Subscription::Mini->anystackProductId(); + + return new Licenses($this->prepareRequest(), $productId); + } + + public function license(string $id, ?string $productId = null): License + { + return $this->licenses($productId)->id($id); + } + + /** + * Create a new Anystack API client. + */ + public function prepareRequest(): PendingRequest + { + return Http::withToken($this->apiKey) + ->baseUrl('https://api.anystack.sh/v1/') + ->acceptJson() + ->asJson() + ->throw(); + } +} diff --git a/app/Services/Anystack/Resources/License.php b/app/Services/Anystack/Resources/License.php new file mode 100644 index 00000000..48ab8f90 --- /dev/null +++ b/app/Services/Anystack/Resources/License.php @@ -0,0 +1,47 @@ +client->get($this->baseUrl()); + } + + public function update(array $data): Response + { + return $this->client->patch($this->baseUrl(), $data); + } + + public function suspend(bool $suspend = true): Response + { + return $this->client->patch($this->baseUrl(), [ + 'suspended' => $suspend, + ]); + } + + public function renew(): Response + { + return $this->client->patch("{$this->baseUrl()}/renew"); + } + + public function delete(): Response + { + return $this->client->delete($this->baseUrl()); + } + + private function baseUrl(): string + { + return "products/{$this->productId}/licenses/{$this->licenseId}"; + } +} diff --git a/app/Services/Anystack/Resources/Licenses.php b/app/Services/Anystack/Resources/Licenses.php new file mode 100644 index 00000000..8dfdb6b5 --- /dev/null +++ b/app/Services/Anystack/Resources/Licenses.php @@ -0,0 +1,40 @@ +client->get($this->url(), [ + 'page' => $page, + ]); + } + + public function create(array $data): Response + { + return $this->client->post($this->url(), $data); + } + + public function id(string $licenseId): License + { + return new License( + $this->client, + $this->productId, + $licenseId, + ); + } + + private function url(): string + { + return "products/{$this->productId}/licenses"; + } +} diff --git a/app/Services/Anystack/Resources/Product.php b/app/Services/Anystack/Resources/Product.php new file mode 100644 index 00000000..873202f2 --- /dev/null +++ b/app/Services/Anystack/Resources/Product.php @@ -0,0 +1,29 @@ +client, $this->productId); + } + + public function retrieve(): Response + { + return $this->client->get($this->baseUrl()); + } + + private function baseUrl(): string + { + return "products/{$this->productId}"; + } +} diff --git a/app/Services/Anystack/Resources/Products.php b/app/Services/Anystack/Resources/Products.php new file mode 100644 index 00000000..c6ad058f --- /dev/null +++ b/app/Services/Anystack/Resources/Products.php @@ -0,0 +1,33 @@ +client->get($this->url(), [ + 'page' => $page, + ]); + } + + public function id(string $productId): Product + { + return new Product( + $this->client, + $productId, + ); + } + + private function url(): string + { + return 'products'; + } +} diff --git a/tests/Feature/Services/AnystackTest.php b/tests/Feature/Services/AnystackTest.php deleted file mode 100644 index c90de971..00000000 --- a/tests/Feature/Services/AnystackTest.php +++ /dev/null @@ -1,53 +0,0 @@ - Http::response([ - 'data' => [ - 'id' => 'license-123', - 'suspended' => true, - ], - ], 200), - ]); - - $anystack = new Anystack; - $response = $anystack->suspendLicense('product-123', 'license-123'); - - $this->assertEquals(200, $response->status()); - $this->assertEquals('license-123', $response->json('data.id')); - $this->assertTrue($response->json('data.suspended')); - - Http::assertSent(function ($request) { - return str_contains($request->url(), '/products/product-123/licenses/license-123') && - $request->method() === 'PATCH' && - $request->data() === ['suspended' => true]; - }); - } - - #[Test] - public function it_throws_exception_when_api_call_fails() - { - Http::fake([ - 'https://api.anystack.sh/v1/products/*/licenses/*' => Http::response([], 500), - ]); - - $this->expectException(\Illuminate\Http\Client\RequestException::class); - - $anystack = new Anystack; - $anystack->suspendLicense('product-123', 'license-123'); - } -}