authorize('customers.grant_plan'); // modals are reachable without the route middleware $customer = Customer::query()->where('uuid', $uuid)->firstOrFail(); $this->customerUuid = $uuid; $this->customerName = (string) $customer->name; $this->datacenter = (string) (Datacenter::query()->active()->orderBy('code')->value('code') ?? ''); $subscription = Subscription::query() ->where('customer_id', $customer->id) ->where('status', 'active') ->latest('id') ->first(); $this->hasSubscription = $subscription !== null; $this->subscriptionId = $subscription?->id; // A customer with nothing yet can only be given a package — there is // no contract to hang a module off. $this->kind = $this->hasSubscription ? 'addon' : 'package'; } /** Today's price for whatever is picked, so a discount reads as one. */ public function cataloguePriceCents(): ?int { try { if ($this->kind === 'package' && $this->plan !== '') { return app(PlanCatalogue::class)->currentVersion($this->plan)->priceFor($this->term)?->amount_cents; } if ($this->kind === 'addon' && $this->addonKey !== '') { return app(AddonCatalogue::class)->priceCents($this->addonKey); } } catch (Throwable) { return null; } return null; } public function grant(): void { $this->authorize('customers.grant_plan'); $operator = auth('operator')->user(); abort_if($operator === null, 403); $customer = Customer::query()->where('uuid', $this->customerUuid)->firstOrFail(); if ($this->kind === 'package') { $this->grantPackage($customer, $operator); } else { $this->grantAddon($customer, $operator); } } private function grantPackage(Customer $customer, $operator): void { $priceRule = function (string $attribute, mixed $value, \Closure $fail): void { if (Money::toCents((string) $value) === null) { $fail(__('admin.grant.price_invalid')); } }; $data = $this->validate([ 'plan' => ['required', Rule::in(array_keys(app(PlanCatalogue::class)->sellable()))], 'term' => ['required', Rule::in([Subscription::TERM_MONTHLY, Subscription::TERM_YEARLY])], 'datacenter' => ['required', Rule::exists('datacenters', 'code')->where('active', true)], 'priceEuros' => ['required', 'string', $priceRule], 'bonusQuotaGb' => 'nullable|integer|min:0|max:1000000', 'bonusSeats' => 'nullable|integer|min:0|max:100000', 'bonusTrafficGb' => 'nullable|integer|min:0|max:10000000', 'note' => 'nullable|string|max:1000', ]); try { app(GrantSubscription::class)( customer: $customer, grantedBy: $operator, plan: $data['plan'], term: $data['term'], datacenter: $data['datacenter'], priceCents: Money::toCents($data['priceEuros']), bonus: array_filter([ 'quota_gb' => $data['bonusQuotaGb'] !== null && $data['bonusQuotaGb'] !== '' ? (int) $data['bonusQuotaGb'] : null, 'seats' => $data['bonusSeats'] !== null && $data['bonusSeats'] !== '' ? (int) $data['bonusSeats'] : null, 'traffic_gb' => $data['bonusTrafficGb'] !== null && $data['bonusTrafficGb'] !== '' ? (int) $data['bonusTrafficGb'] : null, ], fn ($value) => $value !== null), note: $data['note'] !== null && $data['note'] !== '' ? $data['note'] : null, until: $this->parseUntil($this->until), ); } catch (RuntimeException $e) { $this->addError('priceEuros', $e->getMessage()); return; } $this->dispatch('notify', message: __('admin.grant.granted')); $this->closeModal(); } private function grantAddon(Customer $customer, $operator): void { $priceRule = function (string $attribute, mixed $value, \Closure $fail): void { if (Money::toCents((string) $value) === null) { $fail(__('admin.grant.price_invalid')); } }; $data = $this->validate([ 'addonKey' => ['required', Rule::in(array_keys((array) config('provisioning.addons')))], 'quantity' => 'required|integer|min:1|max:100', 'priceEuros' => ['required', 'string', $priceRule], 'note' => 'nullable|string|max:1000', ]); // Re-resolved and re-checked, never trusted from the hydrated // property: a forged request must not be able to book a module onto // someone else's contract. $subscription = Subscription::query() ->where('customer_id', $customer->id) ->whereKey($this->subscriptionId) ->first(); if ($subscription === null) { $this->addError('addonKey', __('admin.grant.no_subscription')); return; } try { app(GrantAddon::class)( subscription: $subscription, grantedBy: $operator, addonKey: $data['addonKey'], priceCents: Money::toCents($data['priceEuros']), quantity: $data['quantity'], note: $data['note'] !== null && $data['note'] !== '' ? $data['note'] : null, until: $this->parseUntil($this->until), ); } catch (RuntimeException $e) { $this->addError('priceEuros', $e->getMessage()); return; } $this->dispatch('notify', message: __('admin.grant.granted')); $this->closeModal(); } /** * A date, read in the operator's own zone — the same pairing LocalTime * keeps for a datetime-local field, minus the time of day this form does * not ask for. "Until 15 August" means through the end of that day where * the operator is sitting, not UTC midnight. */ private function parseUntil(string $value): ?Carbon { if (trim($value) === '') { return null; } try { return Carbon::parse($value, config('app.display_timezone'))->endOfDay()->utc(); } catch (Throwable) { return null; } } /** @return array> */ private function existingGrants(int $customerId): array { $fromSubscriptions = Subscription::query() ->where('customer_id', $customerId) ->whereNotNull('granted_at') ->with('grantedBy') ->get() ->map(fn (Subscription $s) => [ 'kind' => 'package', 'label' => __('billing.plan.'.$s->plan), 'price_cents' => $s->price_cents, 'catalogue_price_cents' => $s->catalogue_price_cents, 'granted_by' => $s->grantedBy?->name ?? '—', 'granted_at' => $s->granted_at, 'granted_until' => $s->granted_until, 'note' => $s->grant_note, ]); $fromAddons = SubscriptionAddon::query() ->whereHas('subscription', fn ($q) => $q->where('customer_id', $customerId)) ->whereNotNull('granted_at') ->with('grantedBy') ->get() ->map(fn (SubscriptionAddon $a) => [ 'kind' => 'addon', 'label' => __('billing.addon.'.$a->addon_key.'.name'), 'price_cents' => $a->price_cents, 'catalogue_price_cents' => $a->catalogue_price_cents, 'granted_by' => $a->grantedBy?->name ?? '—', 'granted_at' => $a->granted_at, 'granted_until' => $a->granted_until, 'note' => $a->grant_note, ]); return $fromSubscriptions->concat($fromAddons) ->sortByDesc('granted_at') ->values() ->all(); } public function render() { // Read again rather than off the mounted properties: a second grant // can have happened while this modal sat open (EditDatacenter's // pattern), and the existing-grants list has to show it. $customer = Customer::query()->where('uuid', $this->customerUuid)->firstOrFail(); return view('livewire.admin.grant-plan', [ 'plans' => app(PlanCatalogue::class)->sellable(), 'addons' => (array) config('provisioning.addons'), 'datacenters' => Datacenter::query()->active()->orderBy('code')->pluck('name', 'code'), 'cataloguePriceCents' => $this->cataloguePriceCents(), 'grants' => $this->existingGrants($customer->id), ]); } }