feat: use Saloon for LemonSqueezy integration

This commit is contained in:
Phan An 2024-03-22 14:40:52 +01:00
parent 06dfe8a1db
commit 1e0637b0bf
7 changed files with 151 additions and 47 deletions

View file

@ -3,20 +3,18 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception; use Exception;
use GuzzleHttp\Exception\ClientException; use Saloon\Exceptions\Request\RequestException;
use Throwable; use Throwable;
class FailedToActivateLicenseException extends Exception final class FailedToActivateLicenseException extends Exception
{ {
public static function fromThrowable(Throwable $e): self public static function fromThrowable(Throwable $e): self
{ {
return new static($e->getMessage(), $e->getCode(), $e); return new self($e->getMessage(), $e->getCode(), $e);
} }
public static function fromClientException(ClientException $e): self public static function fromRequestException(RequestException $e): self
{ {
$response = $e->getResponse(); return new self(object_get($e->getResponse()->object(), 'error'), $e->getStatus());
return new static(json_decode($response->getBody())->error, $response->getStatusCode());
} }
} }

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Integrations\LemonSqueezy;
use Saloon\Http\Connector;
use Saloon\Traits\Plugins\AcceptsJson;
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
class LemonSqueezyConnector extends Connector
{
use AcceptsJson;
use AlwaysThrowOnErrors;
public function resolveBaseUrl(): string
{
return 'https://api.lemonsqueezy.com/v1/';
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Integrations\LemonSqueezy\Requests;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasFormBody;
class ActivateLicenseRequest extends Request implements HasBody
{
use HasFormBody;
protected Method $method = Method::POST;
public function __construct(private string $key)
{
}
public function resolveEndpoint(): string
{
return '/licenses/activate';
}
/** @return array<mixed> */
protected function defaultBody(): array
{
return [
'license_key' => $this->key,
'instance_name' => 'Koel Plus',
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Integrations\LemonSqueezy\Requests;
use App\Models\License;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasFormBody;
class DeactivateLicenseRequest extends Request implements HasBody
{
use HasFormBody;
protected Method $method = Method::POST;
public function __construct(private License $license)
{
}
public function resolveEndpoint(): string
{
return '/licenses/deactivate';
}
/** @return array<mixed> */
protected function defaultBody(): array
{
return [
'license_key' => $this->license->key,
'instance_id' => $this->license->instance->id,
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Integrations\LemonSqueezy\Requests;
use App\Models\License;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasFormBody;
class ValidateLicenseRequest extends Request implements HasBody
{
use HasFormBody;
protected Method $method = Method::POST;
public function __construct(private License $license)
{
}
public function resolveEndpoint(): string
{
return '/licenses/validate';
}
/** @return array<mixed> */
protected function defaultBody(): array
{
return [
'license_key' => $this->license->key,
'instance_id' => $this->license->instance->id,
];
}
}

View file

@ -2,8 +2,6 @@
namespace App\Providers; namespace App\Providers;
use App\Services\ApiClients\ApiClient;
use App\Services\ApiClients\LemonSqueezyApiClient;
use App\Services\Contracts\MusicEncyclopedia; use App\Services\Contracts\MusicEncyclopedia;
use App\Services\LastfmService; use App\Services\LastfmService;
use App\Services\License\Contracts\LicenseServiceInterface; use App\Services\License\Contracts\LicenseServiceInterface;
@ -47,7 +45,6 @@ class AppServiceProvider extends ServiceProvider
}); });
$this->app->bind(LicenseServiceInterface::class, LicenseService::class); $this->app->bind(LicenseServiceInterface::class, LicenseService::class);
$this->app->bind(ApiClient::class, LemonSqueezyApiClient::class);
$this->app->when(LicenseService::class) $this->app->when(LicenseService::class)
->needs('$hashSalt') ->needs('$hashSalt')

View file

@ -3,43 +3,43 @@
namespace App\Services; namespace App\Services;
use App\Exceptions\FailedToActivateLicenseException; use App\Exceptions\FailedToActivateLicenseException;
use App\Http\Integrations\LemonSqueezy\LemonSqueezyConnector;
use App\Http\Integrations\LemonSqueezy\Requests\ActivateLicenseRequest;
use App\Http\Integrations\LemonSqueezy\Requests\DeactivateLicenseRequest;
use App\Http\Integrations\LemonSqueezy\Requests\ValidateLicenseRequest;
use App\Models\License; use App\Models\License;
use App\Services\ApiClients\ApiClient;
use App\Services\License\Contracts\LicenseServiceInterface; use App\Services\License\Contracts\LicenseServiceInterface;
use App\Values\LicenseInstance; use App\Values\LicenseInstance;
use App\Values\LicenseMeta; use App\Values\LicenseMeta;
use App\Values\LicenseStatus; use App\Values\LicenseStatus;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Saloon\Exceptions\Request\RequestException;
use Throwable; use Throwable;
class LicenseService implements LicenseServiceInterface class LicenseService implements LicenseServiceInterface
{ {
public function __construct(private ApiClient $client, private string $hashSalt) public function __construct(private LemonSqueezyConnector $connector, private string $hashSalt)
{ {
} }
public function activate(string $key): License public function activate(string $key): License
{ {
try { try {
$response = $this->client->post('licenses/activate', [ $result = $this->connector->send(new ActivateLicenseRequest($key))->object();
'license_key' => $key,
'instance_name' => 'Koel Plus',
]);
if ($response->meta->store_id !== config('lemonsqueezy.store_id')) { if ($result->meta->store_id !== config('lemonsqueezy.store_id')) {
throw new FailedToActivateLicenseException('This license key is not from Koels official store.'); throw new FailedToActivateLicenseException('This license key is not from Koels official store.');
} }
$license = $this->updateOrCreateLicenseFromApiResponse($response); $license = $this->updateOrCreateLicenseFromApiResponseBody($result);
$this->cacheStatus(LicenseStatus::valid($license)); $this->cacheStatus(LicenseStatus::valid($license));
return $license; return $license;
} catch (ClientException $e) { } catch (RequestException $e) {
throw FailedToActivateLicenseException::fromClientException($e); throw FailedToActivateLicenseException::fromRequestException($e);
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error($e); Log::error($e);
throw FailedToActivateLicenseException::fromThrowable($e); throw FailedToActivateLicenseException::fromThrowable($e);
@ -49,23 +49,20 @@ class LicenseService implements LicenseServiceInterface
public function deactivate(License $license): void public function deactivate(License $license): void
{ {
try { try {
$response = $this->client->post('licenses/deactivate', [ $result = $this->connector->send(new DeactivateLicenseRequest($license))->object();
'license_key' => $license->key,
'instance_id' => $license->instance->id,
]);
if ($response->deactivated) { if ($result->deactivated) {
self::deleteLicense($license); self::deleteLicense($license);
} }
} catch (ClientException $e) { } catch (RequestException $e) {
if ($e->getResponse()->getStatusCode() === Response::HTTP_NOT_FOUND) { if ($e->getStatus() === Response::HTTP_NOT_FOUND) {
// The instance ID was not found. The license record must be a leftover from an erroneous attempt. // The instance ID was not found. The license record must be a leftover from an erroneous attempt.
self::deleteLicense($license); self::deleteLicense($license);
return; return;
} }
throw FailedToActivateLicenseException::fromClientException($e); throw FailedToActivateLicenseException::fromRequestException($e);
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error($e); Log::error($e);
throw $e; throw $e;
@ -86,19 +83,12 @@ class LicenseService implements LicenseServiceInterface
} }
try { try {
$response = $this->client->post('licenses/validate', [ $result = $this->connector->send(new ValidateLicenseRequest($license))->object();
'license_key' => $license->key, $updatedLicense = $this->updateOrCreateLicenseFromApiResponseBody($result);
'instance_id' => $license->instance->id,
]);
$updatedLicense = $this->updateOrCreateLicenseFromApiResponse($response);
return self::cacheStatus(LicenseStatus::valid($updatedLicense)); return self::cacheStatus(LicenseStatus::valid($updatedLicense));
} catch (ClientException $e) { } catch (RequestException $e) {
Log::error($e); if ($e->getStatus() === Response::HTTP_BAD_REQUEST || $e->getStatus() === Response::HTTP_NOT_FOUND) {
$statusCode = $e->getResponse()->getStatusCode();
if ($statusCode === Response::HTTP_BAD_REQUEST || $statusCode === Response::HTTP_NOT_FOUND) {
return self::cacheStatus(LicenseStatus::invalid($license)); return self::cacheStatus(LicenseStatus::invalid($license));
} }
@ -114,16 +104,16 @@ class LicenseService implements LicenseServiceInterface
} }
/** @noinspection PhpIncompatibleReturnTypeInspection */ /** @noinspection PhpIncompatibleReturnTypeInspection */
private function updateOrCreateLicenseFromApiResponse(object $response): License private function updateOrCreateLicenseFromApiResponseBody(object $body): License
{ {
return License::query()->updateOrCreate([ return License::query()->updateOrCreate([
'hash' => sha1($response->license_key->key . $this->hashSalt), 'hash' => sha1($body->license_key->key . $this->hashSalt),
], [ ], [
'key' => $response->license_key->key, 'key' => $body->license_key->key,
'instance' => LicenseInstance::fromJsonObject($response->instance), 'instance' => LicenseInstance::fromJsonObject($body->instance),
'meta' => LicenseMeta::fromJsonObject($response->meta), 'meta' => LicenseMeta::fromJsonObject($body->meta),
'created_at' => $response->license_key->created_at, 'created_at' => $body->license_key->created_at,
'expires_at' => $response->license_key->expires_at, 'expires_at' => $body->license_key->expires_at,
]); ]);
} }