From 1692fd39777d6e661e5db5e82a3626905960af18 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 1 Aug 2026 15:48:41 +0200 Subject: [PATCH] Enterprise wird angefragt, nicht ausgepreist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ab 500 GB ist es eine eigene Maschine — im Shop faende placeableIn() auf 388 GB vergebbarem Platz keinen Host, und der Kunde erfuehre das nach der Zahlung. Der Anfrage-Block erscheint als vierter Bestandteil neben der Vergleichstabelle, sobald PlanCatalogue::sellable() Enterprise nicht mehr liefert; die Familie selbst bleibt bestehen, nur sales_enabled entscheidet (gesetzt wird das erst in Task 9). Co-Authored-By: Claude Opus 5 --- app/Http/Controllers/LandingController.php | 48 ++++++++++++++++++++++ lang/de/landing.php | 10 +++++ lang/en/landing.php | 10 +++++ resources/views/landing.blade.php | 18 ++++++++ tests/Feature/LandingPriceSheetTest.php | 24 +++++++++++ 5 files changed, 110 insertions(+) create mode 100644 lang/de/landing.php create mode 100644 lang/en/landing.php diff --git a/app/Http/Controllers/LandingController.php b/app/Http/Controllers/LandingController.php index 6bcd6d2..eb4d961 100644 --- a/app/Http/Controllers/LandingController.php +++ b/app/Http/Controllers/LandingController.php @@ -41,6 +41,11 @@ use Throwable; * an own domain replaced it, when provisioning issues both. Each block now * makes one kind of statement: BASELINE what everyone gets, the comparison * table what differs, ADDONS what is for sale on top. + * + * A fourth block, ENTERPRISE, sits beside the comparison table rather than + * inside it: once a family drops out of PlanCatalogue::sellable() it has no + * price left to compare, and a dash would say "not included" about something + * that is not for sale at any figure. See enterprise() for when it shows. */ class LandingController extends Controller { @@ -223,6 +228,7 @@ class LandingController extends Controller 'baseline' => $this->baseline($plans), 'comparison' => $this->comparison($plans, $modules), 'addons' => $this->addons($modules), + 'enterprise' => $this->enterprise($plans), ]); } @@ -469,6 +475,48 @@ class LandingController extends Controller return $rows; } + /** + * The request-a-quote block that replaces Enterprise's column once the + * family is no longer a priced catalogue entry. + * + * Null while Enterprise is still in `$plans` — today's state, and the one + * every install starts in — so the sheet renders exactly as before this + * method existed. Read off `$plans` rather than queried again from + * PlanFamily: `$plans` already IS PlanCatalogue::sellable()'s answer for + * this request, and a second "is Enterprise on sale" question here could + * drift from it the moment either changed — the same split-brain this + * class exists to avoid. + * + * The copy is fixed rather than pulled from the catalogue on purpose: + * there is no price to quote. From 500 GB a customer gets a dedicated + * machine, and the shop would misprice it anyway — placeableIn() finds no + * host on the 388 GB a shared machine actually has to give away, so a + * checkout would take the payment and only then discover it cannot + * deliver. + * + * @param array> $plans + * @return array{title: string, body: string, cta: string, href: string}|null + */ + private function enterprise(array $plans): ?array + { + if (in_array('enterprise', array_column($plans, 'key'), true)) { + return null; + } + + return [ + 'title' => __('landing.enterprise_title'), + 'body' => __('landing.enterprise_body'), + 'cta' => __('landing.enterprise_cta'), + // No `contact` route exists (checked against routes/web.php), and + // adding a page or a route for one button would be new surface + // for a task that only has to react to a switch. mailto: is + // already how this exact page offers "talk to us" everywhere else + // on it — the FAQ aside and the closing section both point at the + // same address. + 'href' => 'mailto:office@clupilot.com', + ]; + } + /** * @return array> empty when the catalogue cannot be read */ diff --git a/lang/de/landing.php b/lang/de/landing.php new file mode 100644 index 0000000..014e904 --- /dev/null +++ b/lang/de/landing.php @@ -0,0 +1,10 @@ + 'Mehr als 175 GB? Dann eine eigene Maschine.', + 'enterprise_body' => 'Ab 500 GB richten wir einen Server ein, auf dem nur Ihre Cloud läuft — Größe, Leistung und Sicherung nach Ihrem Bedarf. Wir rechnen Ihnen das durch.', + 'enterprise_cta' => 'Angebot anfragen', +]; diff --git a/lang/en/landing.php b/lang/en/landing.php new file mode 100644 index 0000000..0adf56c --- /dev/null +++ b/lang/en/landing.php @@ -0,0 +1,10 @@ + 'More than 175 GB? Then a dedicated machine.', + 'enterprise_body' => 'From 500 GB we set up a server that runs only your cloud — sized, powered and backed up to your needs. We work out the numbers with you.', + 'enterprise_cta' => 'Request a quote', +]; diff --git a/resources/views/landing.blade.php b/resources/views/landing.blade.php index 329dbb1..69d24f5 100644 --- a/resources/views/landing.blade.php +++ b/resources/views/landing.blade.php @@ -761,6 +761,24 @@ @endif + {{-- Enterprise's replacement for a column, once it has none: this + renders exactly when LandingController::enterprise() found the + family missing from $plans, i.e. no longer sellable. Its own + block rather than a row in the table above it, because it has no + price left to sit beside — a dash there would claim "not + included", when the truth is "not for sale at any figure". --}} + @if ($enterprise) +
+
+

{{ $enterprise['title'] }}

+

{{ $enterprise['body'] }}

+
+ + {{ $enterprise['cta'] }} + +
+ @endif + {{-- What costs extra, with the price it costs. The modules were on sale the whole time and the page named none of them; the owner's complaint was simply that nothing on it said so. --}} diff --git a/tests/Feature/LandingPriceSheetTest.php b/tests/Feature/LandingPriceSheetTest.php index 511ef0a..7e2ae02 100644 --- a/tests/Feature/LandingPriceSheetTest.php +++ b/tests/Feature/LandingPriceSheetTest.php @@ -312,3 +312,27 @@ it('drops a catalogue feature it has no wording for, rather than printing its ke ->assertOk() ->assertDontSee('quantum_entanglement'); }); + +it('leaves Enterprise in the comparison table while it is still on sale', function () { + // Today's state (sales_enabled defaults to true): nothing about this task + // changes the page until the owner actually flips the switch. + $content = $this->get('/')->assertOk()->getContent(); + + expect(priceSheetTable($content))->toContain('>Enterprise<') + ->and($content)->not->toContain(__('landing.enterprise_title')); +}); + +it('shows Enterprise as a request for a quote once it is taken off sale', function () { + // The state Task 9's cutover migration will eventually set. Toggled by + // hand here on purpose — setting sales_enabled is that task's job, only + // reacting to it is this one's. + App\Models\PlanFamily::query()->where('key', 'enterprise')->update(['sales_enabled' => false]); + + $content = $this->get('/')->assertOk()->getContent(); + + expect($content)->toContain(__('landing.enterprise_title')) + ->and($content)->toContain(__('landing.enterprise_cta')) + // No price is left to compare it against once it drops out of + // PlanCatalogue::sellable() — not a dash in a row, no column at all. + ->and(priceSheetTable($content))->not->toContain('>Enterprise<'); +});