100 lines
3.3 KiB
PHP
100 lines
3.3 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\PlanFamily;
|
|
use App\Models\PlanVersion;
|
|
use App\Models\Subscription;
|
|
use Illuminate\Console\Command;
|
|
|
|
/**
|
|
* Tells the owner whether the catalogue is in a state that can actually sell.
|
|
*
|
|
* Everything here is computed at read time, which means a broken window or a
|
|
* missing price does not announce itself until a customer hits it. This is the
|
|
* thing to run after editing plans — before finding out from a failed checkout.
|
|
*/
|
|
class CheckPlanCatalogue extends Command
|
|
{
|
|
protected $signature = 'plans:check';
|
|
|
|
protected $description = 'Check the plan catalogue for overlaps, gaps and missing prices';
|
|
|
|
public function handle(): int
|
|
{
|
|
$currency = Subscription::catalogueCurrency();
|
|
$problems = [];
|
|
$now = now();
|
|
|
|
$families = PlanFamily::query()->with('versions.prices')->orderBy('tier')->get();
|
|
|
|
if ($families->isEmpty()) {
|
|
$this->error('The catalogue is empty. Nothing can be sold.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
foreach ($families as $family) {
|
|
$live = $family->versions->filter(fn (PlanVersion $v) => $v->isAvailableAt($now));
|
|
|
|
if ($live->count() > 1) {
|
|
$problems[] = "{$family->key}: {$live->count()} versions on sale at once (".
|
|
$live->pluck('version')->implode(', ').') — overlapping windows.';
|
|
}
|
|
|
|
if ($family->sales_enabled && $live->isEmpty()) {
|
|
$problems[] = "{$family->key}: on sale, but no version is available right now.";
|
|
}
|
|
|
|
foreach ($family->versions as $version) {
|
|
if (! $version->isPublished()) {
|
|
continue;
|
|
}
|
|
|
|
foreach ([Subscription::TERM_MONTHLY, Subscription::TERM_YEARLY] as $term) {
|
|
$priced = $version->prices
|
|
->first(fn ($p) => $p->term === $term && $p->currency === $currency);
|
|
|
|
if ($priced === null) {
|
|
$problems[] = "{$family->key} v{$version->version}: no {$term} price in {$currency}.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A contract that cannot say which version it was sold under has lost
|
|
// its provenance, which is the one thing the version table is for.
|
|
$orphaned = Subscription::query()->whereNull('plan_version_id')->count();
|
|
|
|
if ($orphaned > 0) {
|
|
$problems[] = "{$orphaned} subscription(s) have no plan version recorded.";
|
|
}
|
|
|
|
foreach ($families as $family) {
|
|
$state = $family->sales_enabled ? 'on sale' : 'withdrawn';
|
|
$version = $family->versions->first(fn (PlanVersion $v) => $v->isAvailableAt($now));
|
|
$this->line(sprintf(
|
|
' %-12s tier %d %-10s %s',
|
|
$family->key,
|
|
$family->tier,
|
|
$state,
|
|
$version !== null ? "v{$version->version}" : '—',
|
|
));
|
|
}
|
|
|
|
if ($problems === []) {
|
|
$this->newLine();
|
|
$this->info('Catalogue is consistent.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->newLine();
|
|
foreach ($problems as $problem) {
|
|
$this->error(' '.$problem);
|
|
}
|
|
|
|
return self::FAILURE;
|
|
}
|
|
}
|