-
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create list licenses console command (#115)
* 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
1 parent
b201ffc
commit 762aa1d
Showing
4 changed files
with
339 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}</>" | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |