where('status', 'active') ->when($datacenter !== null, fn ($q) => $q->where('datacenter', $datacenter)) ->get() ->map(fn (Host $host) => $host->availableGb()) ->max() ?? 0; } /** * Sellable packages that no host could currently take. * * Keyed by plan key, valued by what it needs and what the roomiest host has * left — an operator deciding whether to buy a machine needs the shortfall, * not a yes/no. * * @return array */ public function unplaceablePlans(?string $datacenter = null): array { try { $sellable = app(PlanCatalogue::class)->sellable(); } catch (Throwable) { // A catalogue that cannot be read is its own alarm elsewhere. This // is a capacity warning; it does not get to be the thing that takes // the console's overview page down. return []; } // No host at all is not a capacity problem — it is an installation // nobody has onboarded a machine into yet, and it has its own notices. // Without this, a fresh console greeted the operator with one warning // per sellable package before they had done anything at all. $hasHost = Host::query() ->where('status', 'active') ->when($datacenter !== null, fn ($q) => $q->where('datacenter', $datacenter)) ->exists(); if (! $hasHost) { return []; } $largest = $this->largestPlaceableGb($datacenter); $atRisk = []; foreach ($sellable as $key => $plan) { $needs = (int) ($plan['disk_gb'] ?? 0); if ($needs > 0 && $needs > $largest) { $atRisk[$key] = [ 'name' => (string) ($plan['name'] ?? $key), 'needs' => $needs, 'largest' => $largest, ]; } } return $atRisk; } /** * The runs parked for want of a machine, as a query. * * Recognised by where they are standing, not by a flag: a run WAITING at * the reservation step is a run that could not be placed. A second column * saying the same thing is a second thing to keep in step with the first. * * `current_step` is an INDEX into the pipeline, not the step's key — it is * cast to integer on the model. Comparing it to 'reserve_resources' looks * right and is not: the database casts the string to 0 and quietly matches * the FIRST step of every pipeline instead. Hence the lookup, and hence the * pipeline filter next to it — index 1 of the host pipeline is a different * step entirely, and an onboarding waiting on SSH is not a parked order. * * @return \Illuminate\Database\Eloquent\Builder */ public function parkedRuns(): Builder { $pipeline = (array) config('provisioning.pipelines.customer', []); $index = array_search(ReserveResources::class, $pipeline, true); return ProvisioningRun::query() ->where('pipeline', 'customer') ->where('status', ProvisioningRun::STATUS_WAITING) // -1 matches nothing, which is the honest answer for a pipeline // that does not contain the step at all. ->where('current_step', $index === false ? -1 : (int) $index); } /** Is this particular run waiting for a machine rather than for a step? */ public function isParked(ProvisioningRun $run): bool { return $run->exists && $this->parkedRuns()->whereKey($run->getKey())->exists(); } /** * Paid orders parked for want of a machine. * * @return array */ public function parked(): array { $runs = $this->parkedRuns() ->with('subject') ->orderBy('created_at') ->get(); $parked = []; foreach ($runs as $run) { $order = $run->subject; if (! $order instanceof Order) { continue; } $plan = $this->planOf($order); $parked[] = [ 'run' => $run, 'order' => $order, 'plan' => (string) $order->plan, 'name' => (string) ($plan['name'] ?? $order->plan), 'needs' => (int) ($plan['disk_gb'] ?? 0), 'waiting_since' => $run->created_at, 'datacenter' => (string) $order->datacenter, // Where an operator has said this one goes, if anywhere. The // step is what honours it; this is only so the console can show // the choice it is displaying a dropdown for. 'pinned_host_id' => ($id = $run->context('preferred_host_id')) === null ? null : (int) $id, ]; } return $parked; } /** * What to buy, in one sentence's worth of numbers. * * The BIGGEST parked package decides the minimum machine — an instance * lives on one host, so a server that cannot take the largest one leaves it * parked however much total space it has. The sum is what clears the queue * in one purchase. * * @return array{count: int, largest: int, total: int, by_plan: array} */ public function queueDemand(): array { $parked = $this->parked(); $byPlan = []; foreach ($parked as $entry) { $byPlan[$entry['name']] = ($byPlan[$entry['name']] ?? 0) + 1; } arsort($byPlan); return [ 'count' => count($parked), 'largest' => (int) (collect($parked)->max('needs') ?? 0), 'total' => (int) collect($parked)->sum('needs'), 'by_plan' => $byPlan, ]; } /** * Where a new order should be placed when nobody has said. * * The customer is not asked which datacenter they want — one location is * what is sold, and a dropdown of internal site codes would be a question * nobody outside this building can answer. So the shop picks: the site * whose roomiest host has the most left, which is where the order will * actually roll out fastest. * * Falls back to the first configured datacenter, and then to the same * literal the Stripe webhook has always defaulted to, so a checkout is * never blocked by an estate that has not been built yet. */ public function preferredDatacenter(): string { $best = Host::query() ->where('status', 'active') ->get() ->sortByDesc(fn (Host $host) => $host->availableGb()) ->first(); return $best?->datacenter ?? Datacenter::query()->orderBy('code')->value('code') ?? 'fsn'; } /** * How soon a package of this size can be delivered. * * Two answers, and the second one is a promise about US, not about a * machine: if there is room it rolls out on its own; if there is not, * somebody has to order a server, and two to three working days is what * that honestly takes including a weekend. */ public function deliveryFor(int $diskGb, ?string $datacenter = null): string { $largest = $this->largestPlaceableGb($datacenter); return $diskGb > 0 && $diskGb <= $largest ? 'immediate' : 'scheduled'; } /** * The sizes this customer actually bought. * * From their frozen contract, never from the catalogue — the same rule the * provisioning steps follow. Sizing the queue from today's catalogue would * recommend a machine for a package the waiting customer did not buy. * * @return array */ private function planOf(Order $order): array { $subscription = Subscription::query()->where('order_id', $order->id)->first(); return $subscription === null ? [] : [ 'name' => __('billing.plan.'.$subscription->plan), 'disk_gb' => (int) $subscription->disk_gb, ]; } }