Enterprise wird angefragt, nicht ausgepreist
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 <noreply@anthropic.com>feat/neue-pakete
parent
e643769be2
commit
1692fd3977
|
|
@ -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<int, array<string, mixed>> $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<int, array<string, mixed>> empty when the catalogue cannot be read
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// 175 GB ist die Kontingentgrenze des größten noch verkauften Pakets
|
||||
// (Business). Darüber ist Enterprise keine Katalogzeile mehr, sondern
|
||||
// eine eigene Maschine — siehe LandingController::enterprise().
|
||||
'enterprise_title' => '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',
|
||||
];
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// 175 GB is the quota ceiling of the largest package still on sale
|
||||
// (Business). Above that, Enterprise is no longer a catalogue row but a
|
||||
// dedicated machine — see LandingController::enterprise().
|
||||
'enterprise_title' => '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',
|
||||
];
|
||||
|
|
@ -761,6 +761,24 @@
|
|||
</details>
|
||||
@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)
|
||||
<div class="rv mt-4 overflow-hidden rounded-xl border border-line bg-surface p-7 shadow-xs sm:flex sm:items-center sm:justify-between sm:gap-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold tracking-[-0.02em] text-ink">{{ $enterprise['title'] }}</h3>
|
||||
<p class="mt-2 max-w-[52ch] text-sm leading-relaxed text-muted">{{ $enterprise['body'] }}</p>
|
||||
</div>
|
||||
<x-ui.button href="{{ $enterprise['href'] }}" variant="ink" size="lg" class="mt-5 w-full shrink-0 sm:mt-0 sm:w-auto">
|
||||
{{ $enterprise['cta'] }}
|
||||
</x-ui.button>
|
||||
</div>
|
||||
@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. --}}
|
||||
|
|
|
|||
|
|
@ -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<');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue