98 lines
3.5 KiB
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;
|
|
}
|
|
}
|