Skip to content

Commit

Permalink
Add command to list products and variants (#80)
Browse files Browse the repository at this point in the history
* Add command to list products and variants

* wip

* Fix code styling

* apply Lemon Squeezy sort to rendered result

* wip

* tests: add tests to required configs

* wip

---------

Co-authored-by: heyjorgedev <[email protected]>
Co-authored-by: Dinis Esteves <[email protected]>
Co-authored-by: Dries Vints <[email protected]>
  • Loading branch information
4 people authored Jul 3, 2024
1 parent 940bd53 commit dfc6b10
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,16 @@ public function register(): void

Now you'll rely on your own migrations rather than the package one. Please note though that you're now responsible as well for keeping these in sync withe package one manually whenever you upgrade the package.

## Commands

Below you'll find a list of commands you can run to retrieve info from Lemon Squeezy:

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


## Checkouts

With this package, you can easily create checkouts for your customers.
Expand Down
188 changes: 188 additions & 0 deletions src/Console/ListProductsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?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;

class ListProductsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lmsqueezy:products
{product? : The ID of the product to list variants for.}
';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Lists all products and their variants.';

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

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

$productId = $this->argument('product');

if ($productId) {
return $this->handleProduct($store, $productId);
}

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

protected function validate(): bool
{
$validator = Validator::make(config('lemon-squeezy'), [
'api_key' => [
'required',
],
'store' => [
'required',
],
], [
'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 handleProduct(array $store, string $productId): int
{
$response = spin(
fn () => LemonSqueezy::api(
'GET',
sprintf('products/%s', $productId),
['include' => 'variants']
),
'🍋 Fetching product information...'
);

$product = $response->json('data');

$this->newLine();
$this->displayTitle();
$this->newLine();

$this->displayProduct($product);

$variants = collect($response->json('included'))
->filter(fn ($item) => $item['type'] === 'variants')
->sortBy('sort');

$variants->each(fn (array $variant) => $this->displayVariant(
$variant,
Arr::get($store, 'currency'),
$variants->count() > 1
));

$this->newLine();

return Command::SUCCESS;
}

protected function handleProducts(array $store): int
{
$productsResponse = spin(
fn () => LemonSqueezy::api(
'GET',
'products',
[
'include' => 'variants',
'filter[store_id]' => config('lemon-squeezy.store_id'),
'page[size]' => 100,
]
),
'🍋 Fetching products information...',
);

$products = collect($productsResponse->json('data'));

$this->newLine();
$this->displayTitle();
$this->newLine();

$products->each(function ($product) use ($productsResponse, $store) {
$this->displayProduct($product);

$variantIds = collect(Arr::get($product, 'relationships.variants.data'))->pluck('id');
$variants = collect($productsResponse->json('included'))
->filter(fn ($item) => $item['type'] === 'variants')
->filter(fn ($item) => $variantIds->contains($item['id']))
->sortBy('sort');

$variants->each(fn ($variant) => $this->displayVariant(
$variant,
Arr::get($store, 'currency'),
$variants->count() > 1
));

$this->newLine();
});

return Command::SUCCESS;
}

protected function displayTitle(): void
{
$this->components->twoColumnDetail('<fg=gray>Product/Variant</>', '<fg=gray>ID</>');
}

protected function displayProduct(array $product): void
{
$this->components->twoColumnDetail(
sprintf('<fg=green;options=bold>%s</>', Arr::get($product, 'attributes.name')),
Arr::get($product, 'id')
);
}

protected function displayVariant(array $variant, string $currency, bool $hidePending = false): void
{
if (Arr::get($variant, 'attributes.status') === 'pending' && $hidePending) {
return;
}

$name = Arr::get($variant, 'attributes.name');

$price = LemonSqueezy::formatAmount(
Arr::get($variant, 'attributes.price'),
$currency,
);

$id = Arr::get($variant, 'id');

$this->components->twoColumnDetail(sprintf('%s <fg=gray>%s</>', $name, $price), $id);
}
}
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\ListProductsCommand;
use LemonSqueezy\Laravel\Http\Controllers\WebhookController;

class LemonSqueezyServiceProvider extends ServiceProvider
Expand Down Expand Up @@ -86,6 +87,7 @@ protected function bootCommands(): void
if ($this->app->runningInConsole()) {
$this->commands([
ListenCommand::class,
ListProductsCommand::class,
]);
}
}
Expand Down
136 changes: 136 additions & 0 deletions tests/Feature/Commands/ListProductsCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?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.'/products/fake?*' => Http::response([
'data' => [
'id' => 'fake',
'attributes' => [
'name' => 'Fake Product',
],
'relationships' => [
'variants' => [
'data' => [
['id' => '123'],
],
],
],
],
'included' => [
[
'id' => '123',
'type' => 'variants',
'attributes' => [
'name' => 'Fake Variant',
'price' => 999,
],
],
],
]),
LemonSqueezy::API.'/products?*' => Http::response([
'data' => [
[
'id' => '1',
'attributes' => [
'name' => 'Pro',
],
'relationships' => [
'variants' => [
'data' => [
['id' => '123'],
],
],
],
],
[
'id' => '2',
'attributes' => [
'name' => 'Test',
],
'relationships' => [
'variants' => [
'data' => [
['id' => '321'],
['id' => '456'],
],
],
],
],
],
'included' => [
[
'id' => '123',
'type' => 'variants',
'attributes' => [
'name' => 'Default',
'price' => 999,
],
],
[
'id' => '321',
'type' => 'variants',
'attributes' => [
'name' => 'Monthly',
'price' => 929,
],
],
[
'id' => '456',
'type' => 'variants',
'attributes' => [
'name' => 'Yearly',
'price' => 939,
],
],
],
]),
]);
});

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

// First Product
->expectsOutputToContain('Pro')
->expectsOutputToContain('Default €9.99')

// Second Product
->expectsOutputToContain('Test')
->expectsOutputToContain('Monthly €9.29')
->expectsOutputToContain('Yearly €9.39')

->assertSuccessful();
});

it('can query a specific product', function () {
$this->artisan('lmsqueezy:products', ['product' => 'fake'])
->expectsOutputToContain('Fake Product')
->expectsOutputToContain('Fake Variant €9.99')
->assertSuccessful();
});

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

$this->artisan('lmsqueezy:products')
->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:products')
->expectsOutputToContain('Lemon Squeezy store ID not set. You can add it to your .env file as LEMON_SQUEEZY_STORE.');
});

0 comments on commit dfc6b10

Please sign in to comment.