fix(hetzner): show out-of-stock server types as disabled in the server form

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-27 17:44:34 +01:00
co-authored by Claude Opus 5.5
parent 101444b8ca
commit 28f44d3aa0
4 changed files with 79 additions and 6 deletions
+18 -2
View File
@@ -323,7 +323,7 @@ class ByHetzner extends Component
$hetznerService = new HetznerService($token);
$this->locations = $hetznerService->getLocations();
$this->serverTypes = $hetznerService->getServerTypes();
$this->serverTypes = $hetznerService->getServerTypes(includeOutOfStock: true);
// Get images and sort by name
$images = $hetznerService->getImages();
@@ -385,7 +385,7 @@ class ByHetzner extends Component
public function getAvailableServerTypesProperty()
{
if (! $this->selected_location) {
return $this->serverTypes;
return [];
}
$filtered = collect($this->serverTypes)
@@ -400,15 +400,24 @@ class ByHetzner extends Component
})
->map(function ($serverType) {
$serverType['cpu_vendor_info'] = $this->getCpuVendorInfo($serverType);
$serverType['out_of_stock'] = $this->isServerTypeOutOfStock($serverType);
return $serverType;
})
->sortBy('out_of_stock')
->values()
->toArray();
return $filtered;
}
private function isServerTypeOutOfStock(array $serverType): bool
{
$typeLocation = collect($serverType['locations'] ?? [])->firstWhere('name', $this->selected_location);
return ($typeLocation['available'] ?? true) === false;
}
public function getAvailableImagesProperty()
{
@@ -670,6 +679,13 @@ class ByHetzner extends Component
{
$this->validate();
$selectedServerType = collect($this->serverTypes)->firstWhere('name', $this->selected_server_type);
if ($selectedServerType && $this->isServerTypeOutOfStock($selectedServerType)) {
$this->addError('selected_server_type', 'This server type is out of stock in the selected location. Select a different server type or location.');
return null;
}
if (! $this->enable_ipv4 && ! $this->enable_ipv6) {
$this->addError('enable_ipv4', 'Enable at least one public IP protocol.');
$this->addError('enable_ipv6', 'Enable at least one public IP protocol.');
+5 -3
View File
@@ -121,16 +121,18 @@ class HetznerService
/**
* Server types that can be ordered now. Each type keeps only the locations
* where it is in stock and not deprecated, and types with no such location are removed.
* With $includeOutOfStock, locations where the type is out of stock are kept.
*/
public function getServerTypes(?string $location = null): array
public function getServerTypes(?string $location = null, bool $includeOutOfStock = false): array
{
$types = $this->requestPaginated('get', '/server_types', 'server_types');
return collect($types)
->reject(fn (array $type) => ($type['deprecated'] ?? false) === true)
->map(function (array $type) {
->map(function (array $type) use ($includeOutOfStock) {
$type['locations'] = collect($type['locations'] ?? [])
->filter(fn (array $typeLocation) => ($typeLocation['available'] ?? true) === true && empty($typeLocation['deprecation']))
->filter(fn (array $typeLocation) => empty($typeLocation['deprecation'])
&& ($includeOutOfStock || ($typeLocation['available'] ?? true) === true))
->values()
->all();
@@ -42,9 +42,11 @@
. ' · ' . ($type['cores'] ?? '?') . ' vCPU'
. ' · ' . ($type['memory'] ?? '?') . ' GB RAM'
. ' · ' . ($type['disk'] ?? '?') . ' GB';
$label = $price ? $label . ' · €' . number_format($price, 2) . '/mo' : $label;
return [
'value' => $type['name'],
'label' => $price ? $label . ' · €' . number_format($price, 2) . '/mo' : $label,
'label' => $type['out_of_stock'] ? $label . ' · Out of stock' : $label,
'disabled' => $type['out_of_stock'],
];
})->values()->all();
$imageOptions = collect($this->availableImages)->map(fn ($image) => [
@@ -363,4 +363,57 @@ describe('Hetzner data loading', function () {
Http::assertNothingSent();
});
test('lists server types that are out of stock in the selected location as out of stock', function () {
Http::fake([
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
'server_types' => [
['id' => 1, 'name' => 'cx22', 'locations' => [
['id' => 1, 'name' => 'nbg1', 'available' => false, 'deprecation' => null],
['id' => 2, 'name' => 'fsn1', 'available' => true, 'deprecation' => null],
]],
['id' => 2, 'name' => 'cx32', 'locations' => [
['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null],
]],
['id' => 3, 'name' => 'cx11', 'locations' => [
['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => ['announced' => '2025-09-24T00:00:00+00:00', 'unavailable_after' => '2025-12-31T00:00:00+00:00']],
]],
],
'meta' => ['pagination' => ['next_page' => null]],
], 200),
'https://api.hetzner.cloud/v1/*' => Http::response(['locations' => [], 'images' => [], 'ssh_keys' => [], 'firewalls' => [], 'networks' => [], 'meta' => ['pagination' => ['next_page' => null]]], 200),
]);
$component = Livewire::test(ByHetzner::class, ['selectedTokenUuid' => $this->hetznerToken->uuid])
->call('loadHetznerData')
->set('selected_location', 'nbg1');
expect(collect($component->instance()->availableServerTypes)->mapWithKeys(fn ($type) => [$type['name'] => $type['out_of_stock']])->all())
->toBe(['cx32' => false, 'cx22' => true]);
});
test('does not create a server with a server type that is out of stock in the selected location', function () {
Http::fake([
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
'server_types' => [
['id' => 1, 'name' => 'cx22', 'locations' => [
['id' => 1, 'name' => 'nbg1', 'available' => false, 'deprecation' => null],
]],
],
'meta' => ['pagination' => ['next_page' => null]],
], 200),
'https://api.hetzner.cloud/v1/*' => Http::response(['locations' => [], 'images' => [], 'ssh_keys' => [], 'firewalls' => [], 'networks' => [], 'meta' => ['pagination' => ['next_page' => null]]], 200),
]);
Livewire::test(ByHetzner::class, ['selectedTokenUuid' => $this->hetznerToken->uuid])
->call('loadHetznerData')
->set('server_name', 'test-server')
->set('selected_location', 'nbg1')
->set('selected_server_type', 'cx22')
->set('selected_image', 15512617)
->set('private_key_id', $this->privateKey->id)
->call('submit')
->assertHasErrors(['selected_server_type']);
Http::assertNotSent(fn ($request) => $request->method() === 'POST');
});
});