CluPilotCloud/app/Livewire/Order.php

98 lines
3.5 KiB
PHP

<?php
namespace App\Livewire;
use App\Models\Customer;
use App\Models\Subscription;
use App\Services\Billing\PlanCatalogue;
use App\Services\Provisioning\HostCapacity;
use App\Support\CompanyProfile;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Throwable;
/**
* Choose a package and pay for it, without anybody writing an email.
*
* The website used to end in "anfragen" — a mailto: link and a promise to reply
* the same working day. That is an afternoon of somebody's time per customer,
* for a product whose whole point is that the machine does the work.
*
* The prices here are the catalogue's, gross, exactly as the public sheet shows
* them: a customer who was quoted 58,80 € must not meet 49 € on the page where
* they press the button, nor the other way round. The delivery promise is the
* estate's own answer (HostCapacity), so nobody is told "sofort" while a server
* still has to be bought.
*
* Nothing is charged here. The button opens a Stripe hosted checkout; the
* purchase becomes real on the webhook, which is the only place allowed to
* believe a payment happened.
*/
#[Layout('layouts.portal')]
class Order extends Component
{
public function render()
{
$user = Auth::user();
$customer = $user === null ? null : Customer::query()->where('email', $user->email)->first();
// Somebody already on a contract is not buying a second cloud from
// this page — changing package is a plan change, which prorates and
// keeps their data where it is.
$contract = $customer === null ? null : Subscription::query()
->where('customer_id', $customer->id)
->whereIn('status', ['active', 'past_due'])
->first();
return view('livewire.order', [
'plans' => $this->plans(),
'contract' => $contract,
'vat' => rtrim(rtrim(number_format(CompanyProfile::taxRate(), 1, ',', '.'), '0'), ','),
'setup' => CompanyProfile::setupFeeCents(),
]);
}
/**
* The packages on sale, in the words and figures the public sheet uses.
*
* @return array<int, array<string, mixed>>
*/
private function plans(): array
{
try {
$sellable = app(PlanCatalogue::class)->sellable();
} catch (Throwable) {
// The catalogue fails loudly by design — an overlap must not decide
// a customer's terms by row order. A portal page is not the place
// to argue about it: no list, one sentence, and the operator's
// console is already shouting.
return [];
}
$capacity = app(HostCapacity::class);
$rate = CompanyProfile::taxRate();
$plans = [];
foreach ($sellable as $key => $plan) {
$net = (int) $plan['price_cents'];
$plans[] = [
'key' => $key,
'name' => (string) $plan['name'],
'audience' => (string) ($plan['audience'] ?? ''),
'gross' => (int) round($net * (1 + $rate / 100)),
'net' => $net,
'currency' => (string) $plan['currency'],
'quota_gb' => (int) $plan['quota_gb'],
'traffic_gb' => (int) $plan['traffic_gb'],
'seats' => (int) $plan['seats'],
'recommended' => (bool) ($plan['recommended'] ?? false),
'delivery' => $capacity->deliveryFor((int) ($plan['disk_gb'] ?? 0)),
];
}
return $plans;
}
}