118 lines
4.2 KiB
PHP
118 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Admin;
|
|
|
|
use App\Models\PlanFamily;
|
|
use App\Services\Billing\PlanCatalogue;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\Validate;
|
|
use Livewire\Component;
|
|
|
|
/**
|
|
* The plan lines we sell, and whether each is currently on sale.
|
|
*
|
|
* Deliberately shallow: a family is a name and a rank, and everything that can
|
|
* be got wrong — capabilities, prices, windows — lives one level down on the
|
|
* versions, where publishing makes it permanent.
|
|
*/
|
|
#[Layout('layouts.admin')]
|
|
class Plans extends Component
|
|
{
|
|
#[Validate(['required', 'string', 'max:32', 'regex:/^[a-z][a-z0-9_]*$/', 'unique:plan_families,key'])]
|
|
public string $key = '';
|
|
|
|
#[Validate('required|string|max:255')]
|
|
public string $name = '';
|
|
|
|
#[Validate('required|integer|min:0|max:255')]
|
|
public int $tier = 1;
|
|
|
|
public function mount(): void
|
|
{
|
|
// The page itself, not only its buttons. Hiding the nav entry and
|
|
// guarding the mutations still leaves the whole catalogue — prices,
|
|
// drafts, unreleased plans — readable to any operator who types the URL.
|
|
$this->authorize('plans.manage');
|
|
}
|
|
|
|
public function save(): void
|
|
{
|
|
$this->authorize('plans.manage');
|
|
|
|
$this->key = strtolower(trim($this->key));
|
|
|
|
$data = $this->validate([
|
|
// The key is permanent once created — orders, instances and every
|
|
// contract snapshot store it as a string — so it has to be a plain
|
|
// identifier, not something anyone will want to prettify later.
|
|
'key' => ['required', 'string', 'max:32', 'regex:/^[a-z][a-z0-9_]*$/', 'unique:plan_families,key'],
|
|
'name' => 'required|string|max:255',
|
|
'tier' => 'required|integer|min:0|max:255',
|
|
]);
|
|
|
|
PlanFamily::create([
|
|
'key' => $data['key'],
|
|
'name' => $data['name'],
|
|
'tier' => $data['tier'],
|
|
// Nothing to sell until a version is published, so it starts able
|
|
// to sell and simply has nothing available.
|
|
'sales_enabled' => true,
|
|
]);
|
|
|
|
$this->reset('key', 'name', 'tier');
|
|
$this->tier = 1;
|
|
$this->dispatch('notify', message: __('plans.created'));
|
|
}
|
|
|
|
/**
|
|
* The kill switch: stop selling this plan now, without touching a window.
|
|
*
|
|
* Existing customers keep their contract and their machine — this only
|
|
* decides whether anyone new can buy it.
|
|
*/
|
|
public function toggleSales(string $uuid): void
|
|
{
|
|
$this->authorize('plans.manage');
|
|
|
|
$family = PlanFamily::query()->where('uuid', $uuid)->first();
|
|
$family?->update(['sales_enabled' => ! $family->sales_enabled]);
|
|
|
|
$this->dispatch('notify', message: __($family?->sales_enabled ? 'plans.now_selling' : 'plans.withdrawn'));
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$now = now();
|
|
|
|
$families = PlanFamily::query()
|
|
->with(['versions.prices'])
|
|
->withCount('versions')
|
|
->orderBy('tier')
|
|
->get()
|
|
->map(fn (PlanFamily $family) => [
|
|
'uuid' => $family->uuid,
|
|
'key' => $family->key,
|
|
'name' => $family->name,
|
|
'tier' => $family->tier,
|
|
'sales_enabled' => $family->sales_enabled,
|
|
'is_recommended' => $family->is_recommended,
|
|
'versions_count' => $family->versions_count,
|
|
'live' => $family->versions->first(fn ($v) => $v->isAvailableAt($now)),
|
|
// What is queued up next, so a scheduled launch is visible
|
|
// before a customer discovers it.
|
|
'next' => $family->versions
|
|
->filter(fn ($v) => $v->isPublished() && $v->available_from->greaterThan($now))
|
|
->sortBy('available_from')
|
|
->first(),
|
|
'drafts' => $family->versions->filter(fn ($v) => ! $v->isPublished())->count(),
|
|
]);
|
|
|
|
return view('livewire.admin.plans', [
|
|
'families' => $families,
|
|
// The shop's own answer, so this page cannot disagree with what a
|
|
// customer actually sees.
|
|
'sellable' => array_keys(app(PlanCatalogue::class)->sellable()),
|
|
]);
|
|
}
|
|
}
|