term, [Subscription::TERM_MONTHLY, Subscription::TERM_YEARLY], true)) { $this->term = Subscription::TERM_MONTHLY; } } public function render() { $user = Auth::user(); $customer = $user === null ? null : Customer::query()->where('email', $user->email)->first(); // Someone who already has a contract does not buy a second one by // accident: changing package is a plan change, which prorates and keeps // their data. Same rule as the order page, checked again here because a // link into this page skips that one. $contract = $customer === null ? null : Subscription::query() ->where('customer_id', $customer->id) ->whereIn('status', ['active', 'past_due']) ->first(); $tax = TaxTreatment::for($customer); // NOT called `plan` in the view: the public property of that name is a // key, a string, and Livewire merges its properties into the view data — // so a package array under the same name is shadowed by it and every // `$plan['name']` in the template reads an offset off a string. $package = $this->plan === '' ? null : $this->planOf($this->plan); return view('livewire.checkout', [ 'package' => $package, 'contract' => $contract, 'tax' => $tax, 'lines' => $package === null ? [] : $this->lines($package, $tax), 'vat' => rtrim(rtrim(number_format(CompanyProfile::taxRate(), 1, ',', '.'), '0'), ','), ]); } /** * The chosen package as the shop describes it, or null when the key names * nothing on sale — a URL is a string somebody can retype. * * @return array|null */ private function planOf(string $key): ?array { try { $sellable = app(PlanCatalogue::class)->sellable(); } catch (Throwable) { // The catalogue fails loudly by design; a customer's page is not // the place to argue about it. No package, one sentence. return null; } if (! array_key_exists($key, $sellable)) { return null; } $plan = $sellable[$key]; return $plan + [ 'key' => $key, 'delivery' => app(HostCapacity::class)->deliveryFor((int) ($plan['disk_gb'] ?? 0)), ]; } /** * What will be charged, line by line, in the order an invoice states it. * * Net, tax and gross come from TaxTreatment rather than from a * multiplication here: a business with a valid EU VAT number outside Austria * pays no Austrian tax at all, and a summary that "adds 20 %" would quote * that customer a total they will never be charged. * * @param array $plan * @return array */ private function lines(array $plan, TaxTreatment $tax): array { $yearly = $this->term === Subscription::TERM_YEARLY; $recurringNet = (int) ($yearly ? $plan['yearly_price_cents'] : $plan['price_cents']); $recurringGross = $tax->chargeCents($recurringNet); $setupGross = SetupFee::chargedCents($tax); $setupNet = SetupFee::netCents(); return [ 'currency' => (string) $plan['currency'], 'recurring_net' => $recurringNet, 'recurring_gross' => $recurringGross, 'setup_net' => $setupNet, 'setup_gross' => $setupGross, // What leaves the account on the first charge: the term plus the // one-off. Every figure afterwards is the term alone. 'today_net' => $recurringNet + $setupNet, 'today_gross' => $recurringGross + $setupGross, 'today_tax' => ($recurringGross + $setupGross) - ($recurringNet + $setupNet), 'yearly' => $yearly, 'free_months' => (int) ($plan['free_months'] ?? 0), // Only on the yearly view, and only when there is something to // compare against: twelve monthly charges at the monthly price. 'twelve_months_gross' => $tax->chargeCents((int) $plan['price_cents']) * 12, 'per_month_gross' => $yearly ? intdiv($recurringGross, 12) : $recurringGross, ]; } }