Skip to content

Commit

Permalink
Create list licenses console command (#115)
Browse files Browse the repository at this point in the history
* Create list licenses console command

Lists license keys (full key or shorthand), and information like
product/variant, status, usage, and customer.

* wip

---------

Co-authored-by: Pauline Vos <[email protected]>
Co-authored-by: Dries Vints <[email protected]>
  • Loading branch information
3 people authored Jan 13, 2025
1 parent b201ffc commit 762aa1d
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ Command | Description
--- | ---
`php artisan lmsqueezy:products` | List all available products with their variants and prices
`php artisan lmsqueezy:products 12345` | List a specific product by its ID with its variants and prices
`php artisan lmsqueezy:licenses 12345` | List licenses generated for a given product ID
`php artisan lmsqueezy:licenses -p 3 -s 20` | List the paginated result of all generated licenses
`php artisan lmsqueezy:licenses --order=1234 --status=active` | List active licenses for a given order ID


## Checkouts
Expand Down
175 changes: 175 additions & 0 deletions src/Console/ListLicensesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

namespace LemonSqueezy\Laravel\Console;

use Illuminate\Console\Command;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use LemonSqueezy\Laravel\LemonSqueezy;

use function Laravel\Prompts\error;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\info;

class ListLicensesCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lmsqueezy:licenses
{product? : The ID of the product to list licenses for }
{--status= : The status of the license key}
{--order= : List licenses belonging to a specific order }
{--l|long : Display full license key instead of shorthand }
{--p|page=1 : Page to display }
{--s|size=100 : Items per page to display }
';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Lists all generated licenses.';

public function handle(): int
{
if (! $this->validate()) {
return Command::FAILURE;
}

$storeResponse = spin(fn () => $this->fetchStore(), '🍋 Fetching store information...');
$store = $storeResponse->json('data.attributes');

return $this->handleLicenses($store);
}

protected function validate(): bool
{
$arr = array_merge(
config('lemon-squeezy'),
['page' => $this->option('page')],
['size' => $this->option('size')]
);
$validator = Validator::make($arr, [
'api_key' => [
'required',
],
'store' => [
'required',
],
'page' => [
'nullable', 'numeric', 'min:1',
],
'size' => [
'nullable', 'numeric', 'min:1', 'max:100'
]
], [
'api_key.required' => 'Lemon Squeezy API key not set. You can add it to your .env file as LEMON_SQUEEZY_API_KEY.',
'store.required' => 'Lemon Squeezy store ID not set. You can add it to your .env file as LEMON_SQUEEZY_STORE.',
]);

if ($validator->passes()) {
return true;
}

$this->newLine();

foreach ($validator->errors()->all() as $error) {
error($error);
}

return false;
}

protected function fetchStore(): Response
{
return LemonSqueezy::api('GET', sprintf('stores/%s', config('lemon-squeezy.store')));
}

protected function handleLicenses(array $store): int
{
$licensesResponse = spin(
fn () => LemonSqueezy::api(
'GET',
sprintf('license-keys'),
[
'filter[store_id]' => config('lemon-squeezy.store'),
'page[size]' => (int)$this->option('size'),
'page[number]' => (int)$this->option('page'),
'filter[product_id]' => $this->argument('product'),
'filter[order_id]' => $this->option('order'),
'filter[status]' => $this->option('status'),
]
),
'🍋 Fetching licenses...',
);

$currPage = $licensesResponse->json('meta.page.currentPage');
$lastPage = $licensesResponse->json('meta.page.lastPage');

if ($lastPage > 1 && $currPage <= $lastPage) {
info(sprintf('Showing page %d of %d', $currPage, $lastPage));
}

$licenses = collect($licensesResponse->json('data'));
$licenses->each(function ($license) use ($licensesResponse, $store) {
$this->displayLicense($license, $this->option('long'));

$this->newLine();
});

return Command::SUCCESS;
}

private function displayStatus(array $license): string
{
$status = Arr::get($license, 'attributes.status_formatted');
$limit = Arr::get($license, 'attributes.activation_limit') ?? '0';
$usage = Arr::get($license, 'attributes.activation_usage') ?? '0';

return "${status} (${usage}/${limit})";
}

private function displayProductInfo(array $license): void
{
$productId = Arr::get($license, 'attributes.product_id');
$variantId = Arr::get($license, 'attributes.variant_id') ?? 'None';

$this->components->twoColumnDetail(
'<fg=gray>Product:Variant</>',
"<fg=gray>${productId}:${variantId}</>"
);
}

private function displayCustomer(array $license): void
{
$customerName = Arr::get($license, 'attributes.user_name');
$customerEmail = Arr::get($license, 'attributes.user_email');

$this->components->twoColumnDetail(
'<fg=gray>Customer</>',
"<fg=gray>${customerName} [${customerEmail}]</>"
);
}

protected function displayLicense(array $license, bool $long): void
{
$key = Arr::get($license, $long ? 'attributes.key' : 'attributes.key_short');
$orderId = Arr::get($license, 'attributes.order_id');

$this->components->twoColumnDetail(
sprintf('<fg=green;options=bold>%s</>', $key),
$this->displayStatus($license)
);
$this->displayProductInfo($license);
$this->displayCustomer($license);
$this->components->twoColumnDetail(
'<fg=gray>Order ID</>',
"<fg=gray>${orderId}</>"
);
}
}
2 changes: 2 additions & 0 deletions src/LemonSqueezyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use LemonSqueezy\Laravel\Console\ListenCommand;
use LemonSqueezy\Laravel\Console\ListLicensesCommand;
use LemonSqueezy\Laravel\Console\ListProductsCommand;
use LemonSqueezy\Laravel\Http\Controllers\WebhookController;

Expand Down Expand Up @@ -87,6 +88,7 @@ protected function bootCommands(): void
if ($this->app->runningInConsole()) {
$this->commands([
ListenCommand::class,
ListLicensesCommand::class,
ListProductsCommand::class,
]);
}
Expand Down
159 changes: 159 additions & 0 deletions tests/Feature/Commands/ListLicensesCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

use Illuminate\Support\Facades\Http;
use LemonSqueezy\Laravel\LemonSqueezy;

beforeEach(function () {
Http::fake([
LemonSqueezy::API.'/stores/fake' => Http::response([
'data' => [
'id' => 'fake',
'attributes' => [
'name' => 'Fake Store',
'currency' => 'EUR',
],
],
]),
LemonSqueezy::API.'/stores/other' => Http::response([
'data' => [
'id' => 'other',
'attributes' => [
'name' => 'Other Fake Store',
'currency' => 'EUR',
],
],
]),
LemonSqueezy::API.'/license-keys?*' => Http::response([
'data' => [
[
'id' => '1',
'attributes' => [
'key' => '0766391F-31BA-4508-8528-887A901C6262',
'key_short' => 'XXXX-887A901C6262',
'activation_limit' => '10',
'activation_usage' => '2',
'product_id' => '12345',
'order_id' => 'foo',
'variant_id' => '67890',
'status' => 'active',
'user_name' => 'John Doe',
'user_email' => '[email protected]',
],
],
[
'id' => '2',
'attributes' => [
'key' => '42FF3EE3-D03B-48DC-8C2F-F447677BF33F',
'key_short' => 'XXXX-F447677BF33F',
'activation_limit' => '6',
'activation_usage' => '0',
'product_id' => '12345',
'order_id' => 'bar',
'status' => 'active',
'user_name' => 'Jane Doe',
'user_email' => '[email protected]',
],
],
[
'id' => '3',
'attributes' => [
'key' => '9A631ABD-7F1D-4890-B211-1280B8A52A1F',
'key_short' => 'XXXX-1280B8A52A1F',
'activation_usage' => '0',
'product_id' => '67890',
'variant_id' => '12345',
'order_id' => 'foo',
'status' => 'disabled',
'user_name' => 'John Doe',
'user_email' => '[email protected]',
],
]
],
]),
]);
});

it('can list licenses', function () {
$this->artisan('lmsqueezy:licenses')

// First License
->expectsOutputToContain('XXXX-887A901C6262')
->expectsOutputToContain('12345:67890')
->expectsOutputToContain('John Doe [[email protected]]')

// Second License
->expectsOutputToContain('XXXX-F447677BF33F')
->expectsOutputToContain('12345:None')
->expectsOutputToContain('Jane Doe [[email protected]]')

// Third License
->expectsOutputToContain('XXXX-1280B8A52A1F')
->expectsOutputToContain('67890:12345')

->assertSuccessful();
});

it('can query licenses for a specific status', function () {
$this->artisan('lmsqueezy:licenses', ['--status' => 'disabled'])
->expectsOutputToContain('XXXX-1280B8A52A1F')
->expectsOutputToContain('XXXX-887A901C6262')
->doesntExpectOutput('XXXX-F447677BF33F')

->assertSuccessful();
});

it('can query licenses for a specific product', function () {
$this->artisan('lmsqueezy:licenses', ['product' => '12345'])
->doesntExpectOutput('XXXX-1280B8A52A1F')
->expectsOutputToContain('XXXX-887A901C6262')
->expectsOutputToContain('XXXX-F447677BF33F')

->assertSuccessful();
});

it('can query licenses for a specific order', function () {
$this->artisan('lmsqueezy:licenses', ['--order' => 'bar'])
->doesntExpectOutput('XXXX-1280B8A52A1F')
->doesntExpectOutput('XXXX-887A901C6262')
->expectsOutputToContain('XXXX-F447677BF33F')

->assertSuccessful();
});

it('can display the full license key', function () {
$this->artisan('lmsqueezy:licenses', ['-l' => true])
->expectsOutputToContain('9A631ABD-7F1D-4890-B211-1280B8A52A1F')
->doesntExpectOutput('XXXX-1280B8A52A1F')

->assertSuccessful();
});

it('can display the requested page', function () {
$this->artisan('lmsqueezy:licenses', ['-p' => '2', '-s' => '2'])
->doesntExpectOutput('XXXX-887A901C6262')
->doesntExpectOutput('XXXX-F447677BF33F')
->expectsOutputToContain('XXXX-1280B8A52A1F')

->assertSuccessful();
});

it('fails when api key is missing', function () {
config()->set('lemon-squeezy.api_key', null);

$this->artisan('lmsqueezy:licenses')
->expectsOutputToContain('Lemon Squeezy API key not set. You can add it to your .env file as LEMON_SQUEEZY_API_KEY.');
});

it('fails when store is missing', function () {
config()->set('lemon-squeezy.store', null);

$this->artisan('lmsqueezy:licenses')
->expectsOutputToContain('Lemon Squeezy store ID not set. You can add it to your .env file as LEMON_SQUEEZY_STORE.');
});

it('returns correct products based on the store id', function () {
config()->set('lemon-squeezy.store', 'other');

$this->artisan('lmsqueezy:licenses')
->doesntExpectOutput('XXXX-887A901C6262');
});

0 comments on commit 762aa1d

Please sign in to comment.