CluPilotCloud/app/Livewire/Billing.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(),
]);
}
}