121 lines
4.4 KiB
PHP
121 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Livewire\Concerns\ResolvesCustomer;
|
|
use App\Models\Order;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Component;
|
|
|
|
#[Layout('layouts.portal-app')]
|
|
class Billing extends Component
|
|
{
|
|
use ResolvesCustomer;
|
|
|
|
/**
|
|
* Record a purchase intent (order). Fulfillment (Stripe checkout + resize) is
|
|
* mocked for now — the order is created with status 'pending'.
|
|
*/
|
|
public function purchase(string $type, ?string $key = null): void
|
|
{
|
|
$customer = $this->requireCustomer();
|
|
if ($customer === null) {
|
|
return;
|
|
}
|
|
|
|
$instance = $customer->instances()->latest('id')->first();
|
|
$currentPlan = $instance?->plan ?? 'start';
|
|
$plans = (array) config('provisioning.plans');
|
|
$addons = (array) config('provisioning.addons');
|
|
|
|
[$plan, $amount, $addonKey] = match ($type) {
|
|
'upgrade' => [$key, (int) ($plans[$key]['price_cents'] ?? 0), null],
|
|
'storage' => [$currentPlan, (int) config('provisioning.storage_addon.price_cents', 0), null],
|
|
'traffic' => [$currentPlan, (int) config('provisioning.traffic.addon.price_cents', 0), null],
|
|
'addon' => [$currentPlan, (int) ($addons[$key]['price_cents'] ?? 0), $key],
|
|
default => [null, 0, null],
|
|
};
|
|
|
|
// Guard against invalid keys / non-upgrades.
|
|
$valid = match ($type) {
|
|
'upgrade' => isset($plans[$key]) && ($plans[$key]['price_cents'] ?? 0) > ($plans[$currentPlan]['price_cents'] ?? 0),
|
|
'storage' => true,
|
|
// Always available: running out of traffic is exactly when someone
|
|
// needs to be able to buy more, whatever plan they are on.
|
|
'traffic' => true,
|
|
'addon' => isset($addons[$key]),
|
|
default => false,
|
|
};
|
|
if (! $valid || $plan === null) {
|
|
return;
|
|
}
|
|
|
|
$datacenter = $instance?->host?->datacenter
|
|
?? $customer->orders()->latest('id')->value('datacenter')
|
|
?? 'fsn';
|
|
|
|
// A cart cannot hold two plan changes: they contradict each other and
|
|
// no checkout could resolve which one the customer meant. Choosing
|
|
// another simply replaces the pending one — add-ons still stack.
|
|
if ($type === 'upgrade') {
|
|
$replaced = Order::query()
|
|
->where('customer_id', $customer->id)
|
|
->where('status', 'pending')
|
|
->where('type', 'upgrade')
|
|
->delete();
|
|
|
|
if ($replaced > 0) {
|
|
$this->dispatch('notify', message: __('billing.cart.plan_replaced'));
|
|
}
|
|
}
|
|
|
|
Order::create([
|
|
'customer_id' => $customer->id,
|
|
'plan' => $plan,
|
|
'type' => $type,
|
|
'addon_key' => $addonKey,
|
|
'amount_cents' => $amount,
|
|
'currency' => 'EUR',
|
|
'datacenter' => $datacenter,
|
|
'status' => 'pending',
|
|
]);
|
|
|
|
$this->dispatch('notify', message: __('billing.purchased'));
|
|
}
|
|
|
|
#[\Livewire\Attributes\On('order-removed')]
|
|
public function orderRemoved(): void
|
|
{
|
|
$this->dispatch('notify', message: __('billing.cart.removed'));
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$customer = $this->customer();
|
|
$instance = $customer?->instances()->latest('id')->first();
|
|
$plans = (array) config('provisioning.plans');
|
|
$currentKey = $instance?->plan ?? 'start';
|
|
$currentPrice = (int) ($plans[$currentKey]['price_cents'] ?? 0);
|
|
|
|
$upgrades = collect($plans)
|
|
->filter(fn ($p, $k) => (int) ($p['price_cents'] ?? 0) > $currentPrice)
|
|
->keys()->all();
|
|
|
|
return view('livewire.billing', [
|
|
'currentKey' => $currentKey,
|
|
'current' => $plans[$currentKey] ?? [],
|
|
'instance' => $instance,
|
|
'plans' => $plans,
|
|
'features' => (array) config('provisioning.plan_features'),
|
|
'upgrades' => $upgrades,
|
|
'storage' => (array) config('provisioning.storage_addon'),
|
|
'trafficAddon' => (array) config('provisioning.traffic.addon'),
|
|
'trafficMeter' => $instance !== null ? \App\Services\Traffic\TrafficMeter::for($instance) : null,
|
|
'addons' => (array) config('provisioning.addons'),
|
|
'pending' => $customer
|
|
? $customer->orders()->where('status', 'pending')->latest('id')->get()
|
|
: collect(),
|
|
]);
|
|
}
|
|
}
|