diff --git a/app/Http/Controllers/LandingController.php b/app/Http/Controllers/LandingController.php index 4609c49..1729beb 100644 --- a/app/Http/Controllers/LandingController.php +++ b/app/Http/Controllers/LandingController.php @@ -62,6 +62,12 @@ class LandingController extends Controller // und __invoke() reicht sie der View als `officeOnSale` weiter, die // damit dieselbe Kachel „Office im Browser" aus der Aufzählung „Was // enthalten ist" filtert. + // + // Derselbe Riegel sperrt seit derselben Stelle auch `extra_backups` + // (App\Services\Billing\AddonCatalogue::EXTRA_BACKUPS_ON_SALE) — ohne + // eigenen View-Schalter wie `officeOnSale`, weil der zweite + // Sicherungsort in keiner Aufzählung „Was enthalten ist" steht, sondern + // nur in der Modulliste. modulePrices() unten reicht dafür allein. /** * Catalogue feature keys, in the words a customer uses. @@ -446,6 +452,16 @@ class LandingController extends Controller continue; } + // Der zweite Sicherungsort ist vom Markt genommen (siehe + // AddonCatalogue::EXTRA_BACKUPS_ON_SALE), solange der + // Speicherserver für das zweite Ziel fehlt — ein eigener + // Schalter, weil dieses Modul aus einem anderen Grund als + // Office nicht verkauft wird und zu einem anderen Zeitpunkt + // zurückkommt. + if ($key === 'extra_backups' && ! AddonCatalogue::EXTRA_BACKUPS_ON_SALE) { + continue; + } + $cents = $catalogue->priceCents((string) $key); // Nothing is sold for nothing. An entry without a price reads diff --git a/app/Livewire/Billing.php b/app/Livewire/Billing.php index f775115..e65165f 100644 --- a/app/Livewire/Billing.php +++ b/app/Livewire/Billing.php @@ -98,10 +98,12 @@ class Billing extends Component // saleRefusal() zuerst: ob es das Modul überhaupt zu kaufen gibt, // ist die grundsätzlichere Frage — ein Kunde soll nicht hören, // er habe es bereits, wenn er es in Wahrheit nie hätte kaufen - // können. Heute betrifft das nur `collabora_pro` (Office liefert - // nicht, siehe AddonCatalogue::OFFICE_ON_SALE), aber der Aufruf - // ist derselbe, der auch die Preistafel filtert — ein Schalter - // für beide Seiten. + // können. Heute betrifft das `collabora_pro` (Office liefert + // nicht, siehe AddonCatalogue::OFFICE_ON_SALE) und + // `extra_backups` (der zweite Sicherungsort fehlt, siehe + // AddonCatalogue::EXTRA_BACKUPS_ON_SALE), aber der Aufruf ist + // derselbe, der auch die Preistafel filtert — ein Schalter für + // beide Seiten, je Modul einer. $refusal = app(AddonCatalogue::class)->saleRefusal($key) ?? app(AddonCatalogue::class)->duplicateRefusal($contract, $key) ?? $this->cartRefusal($customer, $key); @@ -531,10 +533,11 @@ class Billing extends Component * is how one double click turns into two charges for one thing. * * Dieselbe Regel gilt für ein Modul, das AddonCatalogue::saleRefusal() - * ganz grundsätzlich ablehnt (heute nur `collabora_pro`, solange Office - * nicht liefert): die Karte verschwindet, statt einen Kauf anzubieten, - * den purchase() ohnehin abweisen würde — aber auch hier nur, solange - * niemand das Modul schon gebucht hat oder es im Warenkorb liegt. + * ganz grundsätzlich ablehnt (heute `collabora_pro`, solange Office nicht + * liefert, und `extra_backups`, solange der zweite Sicherungsort fehlt): + * die Karte verschwindet, statt einen Kauf anzubieten, den purchase() + * ohnehin abweisen würde — aber auch hier nur, solange niemand das Modul + * schon gebucht hat oder es im Warenkorb liegt. * * @param array> $rows * @param Collection $pending diff --git a/app/Services/Billing/AddonCatalogue.php b/app/Services/Billing/AddonCatalogue.php index 7ae74db..8555652 100644 --- a/app/Services/Billing/AddonCatalogue.php +++ b/app/Services/Billing/AddonCatalogue.php @@ -80,6 +80,46 @@ final class AddonCatalogue */ public const OFFICE_ON_SALE = false; + /** + * Ob der zweite Sicherungsort schon verkauft wird. + * + * Auf false, bis der Speicherserver für das zweite, getrennte Ziel steht — + * siehe App\Support\OpenWork::all(). Anders als bei Office fehlt hier + * nicht die ganze Leistung: die nächtliche Sicherung selbst läuft längst, + * über App\Provisioning\Steps\Customer\RegisterBackup auf die lokale + * Ablage des Hosts. Was fehlt, ist ausschließlich der ZWEITE, davon + * getrennte Ort, den das Modul verspricht. + * + * Eigene Konstante statt OFFICE_ON_SALE mitzubenutzen: die beiden Module + * hängen an verschiedenen Voraussetzungen (Office am Dokumentenserver, + * dieses hier am Speicherserver) und werden nicht am selben Tag fertig — + * ein gemeinsamer Schalter würde beim Fertigstellen des einen versehentlich + * auch das andere freigeben. + * + * Derselbe Mechanismus wie bei OFFICE_ON_SALE im Übrigen: saleRefusal() + * unten weist jede Buchung von `extra_backups` ab, solange diese Konstante + * false ist, und LandingController::modulePrices() liest dieselbe + * Konstante, um die Zeile aus der Preistafel zu nehmen. Der Werbetext + * (ADDONS im LandingController und lang/*\/billing.php) bleibt stehen, + * damit er beim Wiederanschalten nicht neu geschrieben werden muss. + * + * Die Begründung, die der Kunde liest, ist bewusst eine ANDERE als bei + * Office (siehe saleRefusal()): wer nach dem zweiten Ort fragt, sorgt sich + * um seine Daten und soll hören, dass sie sehr wohl täglich gesichert + * werden — nur eben noch an einem einzigen Ort. + * + * Bewusst NICHT gespiegelt in priceCents()/knows(): App\Actions\BookAddon + * kennt den Preis unverändert weiter, aus demselben Grund wie bei Office — + * ein Bestandsfall oder ein Geschenk des Betreibers (GrantAddon) sind + * keine Verkäufe an einen Kunden, der die Preistafel liest. + * + * Wieder anschalten, sobald der Speicherserver liefert: hier auf true + * stellen, sobald der Bereitstellungslauf tatsächlich ein zweites + * Sicherungsziel anlegt — kein zweiter Handgriff nötig wie bei Office, + * weil kein Paketmerkmal von diesem Modul abhängt. + */ + public const EXTRA_BACKUPS_ON_SALE = false; + /** Today's monthly net price for one unit, or null if we do not sell it. */ public function priceCents(string $key): ?int { @@ -172,8 +212,20 @@ final class AddonCatalogue * Anders als availabilityRefusal() unten, das fragt, ob EIN Paket ein * Modul haben darf: die Antwort hier ist für jeden Vertrag und jedes * Paket gleich, weil das, was verkauft würde, schlicht nicht existiert. - * Heute betrifft das nur `collabora_pro` — siehe OFFICE_ON_SALE oben für - * die zwei Handgriffe, mit denen der Verkauf wieder aufgenommen wird. + * Heute betrifft das `collabora_pro` und `extra_backups` — siehe + * OFFICE_ON_SALE bzw. EXTRA_BACKUPS_ON_SALE oben für die Handgriffe, mit + * denen der jeweilige Verkauf wieder aufgenommen wird. Zwei eigene + * Konstanten mit Absicht: die Module hängen an verschiedenen + * Voraussetzungen und werden nicht am selben Tag fertig, also braucht + * jedes seinen eigenen Schalter — sonst schaltete das Fertigstellen des + * einen das andere versehentlich mit frei. + * + * Auch der SATZ, den der Kunde liest, ist bewusst nicht derselbe: bei + * Office fehlt das ganze Modul, beim zweiten Sicherungsort nur der + * zweite ORT — die erste, tägliche Sicherung läuft unverändert weiter. + * Ein Kunde, der nach dem zweiten Ort fragt, soll das lesen können, + * statt denselben allgemeinen Satz wie für ein Modul zu bekommen, das es + * gar nicht gibt. * * Gefragt an den zwei Stellen, an denen ein Kunde ein Modul NEU bucht: * App\Http\Controllers\LandingController für die Preistafel und @@ -187,11 +239,15 @@ final class AddonCatalogue */ public function saleRefusal(string $key): ?string { - if ($key !== 'collabora_pro' || self::OFFICE_ON_SALE) { - return null; + if ($key === 'collabora_pro' && ! self::OFFICE_ON_SALE) { + return __('billing.addon_not_on_sale', ['module' => $this->name($key)]); } - return __('billing.addon_not_on_sale', ['module' => $this->name($key)]); + if ($key === 'extra_backups' && ! self::EXTRA_BACKUPS_ON_SALE) { + return __('billing.addon_second_location_not_on_sale', ['module' => $this->name($key)]); + } + + return null; } /** diff --git a/app/Support/OpenWork.php b/app/Support/OpenWork.php index 8ba780b..978ccee 100644 --- a/app/Support/OpenWork.php +++ b/app/Support/OpenWork.php @@ -36,11 +36,11 @@ final class OpenWork { return [ [ - 'title' => 'Zweiter Sicherungsort wird verkauft, aber nicht geliefert', - 'state' => self::SOLD, - 'where' => 'config/provisioning.php → addons.extra_backups', - 'missing' => 'Das Modul kostet 6 € im Monat und verspricht „eine zusätzliche verschlüsselte Kopie Ihrer Daten, getrennt vom ersten Ort gelagert". Im ganzen Repo kommt der Schlüssel nur im LandingController und in der Konfiguration vor — es gibt keinen Provisionierungsschritt, der ein zweites Sicherungsziel anlegt.', - 'risk' => 'Wer es bucht, zahlt monatlich für eine Leistung, die nicht erbracht wird.', + 'title' => 'Zweiter Sicherungsort ist aus dem Verkauf genommen', + 'state' => self::PLANNED, + 'where' => 'App\\Services\\Billing\\AddonCatalogue::EXTRA_BACKUPS_ON_SALE', + 'missing' => 'Das Modul kostete 6 € im Monat und versprach „eine zusätzliche verschlüsselte Kopie Ihrer Daten, getrennt vom ersten Ort gelagert" — es gab dafür aber keinen Provisionierungsschritt, der ein zweites Sicherungsziel anlegt, nur die nächtliche vzdump-Sicherung auf die lokale Ablage des Hosts (App\\Provisioning\\Steps\\Customer\\RegisterBackup). Preistafel und Abrechnungsseite bieten das Modul jetzt nicht mehr an — der Code-Schalter ist die Konstante `EXTRA_BACKUPS_ON_SALE`, eigenständig von `OFFICE_ON_SALE`, weil dieses Modul an einem anderen Server hängt (dem künftigen Speicherserver, nicht dem Dokumentenserver) und darum zu einem anderen Zeitpunkt zurückkommt. Auf der Entwicklungsmaschine enthielt `subscription_addons` zum Zeitpunkt dieser Änderung keine einzige Zeile mit `extra_backups` — ob im Live-Betrieb schon gebucht wurde, ist von hier aus nicht sichtbar; sollte doch, entscheidet der Betreiber, was mit diesen Verträgen geschieht, diese Sperre betrifft nur NEUE Buchungen.', + 'risk' => 'Kein akuter Schaden mehr — weder die Preistafel noch die Abrechnungsseite versprechen etwas, das nicht eingelöst wird. Das zweite Sicherungsziel selbst ist an keiner Stelle im Repository angefangen; das bleibt die Arbeit, die vor dem Wiederanschalten noch aussteht.', ], [ 'title' => 'Office (ONLYOFFICE über einen gemeinsamen Dokumentenserver) ist aus dem Verkauf genommen', diff --git a/config/provisioning.php b/config/provisioning.php index 31dcce0..ca80523 100644 --- a/config/provisioning.php +++ b/config/provisioning.php @@ -392,6 +392,11 @@ return [ 'unavailable_on' => ['enterprise'], ], 'addons' => [ + // Der Preis bleibt hier stehen, auch während des Verkaufs — siehe + // App\Services\Billing\AddonCatalogue::EXTRA_BACKUPS_ON_SALE. Der + // Preis wird für Bestandsverträge und ein Geschenk des Betreibers + // weiter gebraucht (App\Actions\BookAddon); NEUE Buchungen weist + // AddonCatalogue::saleRefusal() ab, solange die Konstante false ist. 'extra_backups' => ['price_cents' => 500, 'sold_as' => 'entitlement'], 'priority_support' => ['price_cents' => 2900, 'sold_as' => 'entitlement'], // Die PRO-Stufe eines Office, das im Paket schon drin ist — „mehr diff --git a/lang/de/billing.php b/lang/de/billing.php index 5d09df1..3adb806 100644 --- a/lang/de/billing.php +++ b/lang/de/billing.php @@ -161,6 +161,13 @@ return [ // Paket — nicht weil der Vertrag es nicht hergibt, sondern weil wir es // (noch) nicht liefern. Siehe App\Services\Billing\AddonCatalogue::saleRefusal(). 'addon_not_on_sale' => ':module wird derzeit nicht verkauft.', + // Eigener Text statt "addon_not_on_sale": bei Office fehlt das ganze + // Modul, beim zweiten Sicherungsort nur der zweite ORT — die erste, + // tägliche Sicherung läuft unverändert weiter. Wer nach diesem Modul + // fragt, sorgt sich um seine Daten, und "wird derzeit nicht verkauft" + // ließe offen, ob überhaupt gesichert wird. Siehe + // App\Services\Billing\AddonCatalogue::EXTRA_BACKUPS_ON_SALE. + 'addon_second_location_not_on_sale' => '„:module“ ist noch nicht verfügbar — es fehlt der zweite, getrennte Ort für Ihre Daten. Die tägliche verschlüsselte Sicherung an Ihrem ersten Ort läuft davon unberührt weiter.', // Ein Block ist Platz auf einer bestimmten Maschine — eine laufende // Instanz zieht nicht um. Gemeint ist der Host, nicht der Vertrag. 'storage_no_room' => 'Auf Ihrem Server ist derzeit kein Platz für weiteren Speicher. Bitte wenden Sie sich an den Support — wir sorgen für Raum.', diff --git a/lang/en/billing.php b/lang/en/billing.php index f87a9eb..2025181 100644 --- a/lang/en/billing.php +++ b/lang/en/billing.php @@ -160,6 +160,13 @@ return [ // package — not because the contract rules it out, but because we do // not deliver it (yet). See App\Services\Billing\AddonCatalogue::saleRefusal(). 'addon_not_on_sale' => ':module is not for sale right now.', + // Eigener Text statt "addon_not_on_sale": bei Office fehlt das ganze + // Modul, beim zweiten Sicherungsort nur der zweite ORT — die erste, + // tägliche Sicherung läuft unverändert weiter. Wer nach diesem Modul + // fragt, sorgt sich um seine Daten, und "is not for sale" ließe offen, + // ob überhaupt gesichert wird. Siehe + // App\Services\Billing\AddonCatalogue::EXTRA_BACKUPS_ON_SALE. + 'addon_second_location_not_on_sale' => '":module" is not available yet — the second, separate location for your data is missing. Your first, daily encrypted backup keeps running as usual.', // A pack is room on a particular machine — a running instance does not // move. This is about the host, not the contract. 'storage_no_room' => 'Your server currently has no room for more storage. Please contact support — we will make room.', diff --git a/tests/Feature/Admin/OpenWorkTest.php b/tests/Feature/Admin/OpenWorkTest.php index 8073502..2551534 100644 --- a/tests/Feature/Admin/OpenWorkTest.php +++ b/tests/Feature/Admin/OpenWorkTest.php @@ -38,30 +38,37 @@ it('nennt zu jedem Punkt wo, was fehlt und was passiert, wenn es so bleibt', fun } }); -it('führt das bezahlte Modul, das nichts tut, als solches', function () { - // Der Anlass für die ganze Seite. Verschwindet es hier stillschweigend, - // ist die Liste wertlos — es kostet Geld, solange es offen ist. - // - // Bis zur Office-Umstellung standen hier zwei Module (Sicherungsort UND - // Office) — Office ist jetzt kein SOLD-Punkt mehr, siehe die eigene - // Prüfung unten, die genau diesen Wechsel festhält. - $sold = collect(OpenWork::all())->where('state', OpenWork::SOLD)->pluck('title')->implode(' '); - - expect($sold)->toContain('Sicherungsort'); +it('führt inzwischen kein Modul mehr als verkauft und nicht geliefert', function () { + // Der Anlass für die ganze Seite war ein SOLD-Punkt — ein Modul, das + // Geld kostet, während es offen ist. Hier standen einmal zwei + // (Sicherungsort UND Office); beide sind inzwischen aus dem Verkauf + // genommen statt weiter verkauft und nicht geliefert zu werden, siehe + // die zwei eigenen Prüfungen unten, die je einen dieser Wechsel + // festhalten. Verschwände SOLD hier künftig wieder unbemerkt aus der + // Liste, wäre die Seite wertlos — dieser Test bleibt deshalb stehen, + // auch wenn er heute nichts mehr findet. + expect(collect(OpenWork::all())->where('state', OpenWork::SOLD))->toBeEmpty(); }); it('führt Office nicht mehr als verkauft, sondern als aus dem Verkauf genommen', function () { - // Der Grund, warum der Test oben nur noch ein Modul findet: Office wurde - // von der Preistafel genommen, statt weiter verkauft und nicht geliefert - // zu werden — der Unterschied zwischen SOLD (kostet gerade Geld) und - // PLANNED (tut nichts, bis jemand anfängt) ist genau das, was die - // Reihenfolge auf der Seite bestimmt (siehe ORDER unten). + // Der Unterschied zwischen SOLD (kostet gerade Geld) und PLANNED (tut + // nichts, bis jemand anfängt) ist genau das, was die Reihenfolge auf der + // Seite bestimmt (siehe ORDER unten). $office = collect(OpenWork::all())->first(fn (array $item) => str_contains($item['title'], 'Office')); expect($office)->not->toBeNull() ->and($office['state'])->toBe(OpenWork::PLANNED); }); +it('führt den zweiten Sicherungsort nicht mehr als verkauft, sondern als aus dem Verkauf genommen', function () { + // Derselbe Wechsel wie bei Office, nur später und aus einem anderen + // Grund: hier fehlt nicht das ganze Modul, nur der zweite Ort. + $backups = collect(OpenWork::all())->first(fn (array $item) => str_contains($item['title'], 'Sicherungsort')); + + expect($backups)->not->toBeNull() + ->and($backups['state'])->toBe(OpenWork::PLANNED); +}); + it('steht jedem offen, der die Konsole öffnen darf', function () { // Kein eigenes Recht: wer hier arbeitet, soll wissen, worauf er sich // verlassen kann und worauf nicht. diff --git a/tests/Feature/Billing/AddonEntitlementTest.php b/tests/Feature/Billing/AddonEntitlementTest.php index acbf827..f78cccb 100644 --- a/tests/Feature/Billing/AddonEntitlementTest.php +++ b/tests/Feature/Billing/AddonEntitlementTest.php @@ -190,13 +190,18 @@ it('stops offering an entitlement the customer already has, and keeps offering p }); it('marks a module waiting in the cart so the card stops offering it', function () { + // priority_support, not extra_backups: since the second backup location + // was closed to every customer (see + // tests/Feature/Billing/ExtraBackupsNotSoldTest.php), purchase() would + // refuse it before it ever reaches the cart, and this test is about what + // happens once a purchase DOES reach it. [, $user] = entitlementShopper(); - Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'addon', 'extra_backups'); + Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'addon', 'priority_support'); $component = Livewire::actingAs($user)->test(Billing::class); - expect($component->viewData('addons')['extra_backups']['in_cart'])->toBeTrue(); + expect($component->viewData('addons')['priority_support']['in_cart'])->toBeTrue(); $component->assertSee(__('billing.addon_in_cart_badge')); }); diff --git a/tests/Feature/Billing/ExtraBackupsNotSoldTest.php b/tests/Feature/Billing/ExtraBackupsNotSoldTest.php new file mode 100644 index 0000000..412894c --- /dev/null +++ b/tests/Feature/Billing/ExtraBackupsNotSoldTest.php @@ -0,0 +1,88 @@ +create(); + $user = User::factory()->create(['email' => $customer->email]); + $order = Order::factory()->withSubscription()->for($customer)->create(['plan' => $plan]); + $instance = Instance::factory()->for($customer)->create([ + 'order_id' => $order->id, + 'plan' => $plan, + 'status' => 'active', + ]); + + $order->subscription->update(['instance_id' => $instance->id]); + + // purchase() refuses outright without a Stripe key for the active mode + // (Task 5) — the refusal this test is about must not be masked by that + // earlier one. + withStripeSecret(); + + return [$customer, $user]; +} + +it('weist eine Buchung von extra_backups am Formular vorbei ab, solange der zweite Ort fehlt', function () { + // Absichtlich NICHT über einen fehlenden Knopf geprüft: das würde nur + // zeigen, dass die Anzeige geschlossen ist. Dieser Aufruf geht am + // Formular vorbei — genau der Weg, den ein zweites Browser-Fenster, eine + // alte Sitzung oder ein direkter Aufruf des Komponentennamens auch nähme. + [$customer, $user] = backupsShopper('team'); + + Livewire::actingAs($user)->test(Billing::class) + ->call('purchase', 'addon', 'extra_backups') + ->assertDispatched('notify', message: __('billing.addon_second_location_not_on_sale', [ + 'module' => __('billing.addon.extra_backups.name'), + ])); + + // Kein Auftrag, also nichts, das später fälschlich zur Buchung würde. + expect(Order::query()->where('customer_id', $customer->id)->where('type', 'addon')->exists())->toBeFalse(); +}); + +it('zeigt die Sicherungsort-Karte im Portal nicht mehr an, solange sie ohnehin abgewiesen würde', function () { + [, $user] = backupsShopper('team'); + + Livewire::actingAs($user)->test(Billing::class) + ->assertDontSee(__('billing.addon.extra_backups.name')); +}); + +it('weist eine andere Meldung ab als Office, weil hier nur der zweite Ort fehlt', function () { + // Der eigentliche Punkt der Erweiterung: nicht derselbe Satz mit + // ausgetauschtem Modulnamen, sondern ein anderer Grund. Wer nach dem + // zweiten Ort fragt, soll erfahren, dass seine Daten sehr wohl täglich + // gesichert werden — nur eben noch an einem Ort. + [, $user] = backupsShopper('team'); + + $officeMessage = __('billing.addon_not_on_sale', [ + 'module' => __('billing.addon.collabora_pro.name'), + ]); + + Livewire::actingAs($user)->test(Billing::class) + ->call('purchase', 'addon', 'extra_backups') + ->assertDispatched('notify', message: __('billing.addon_second_location_not_on_sale', [ + 'module' => __('billing.addon.extra_backups.name'), + ])) + ->assertNotDispatched('notify', message: $officeMessage); +}); diff --git a/tests/Feature/Billing/OfficeNotSoldTest.php b/tests/Feature/Billing/OfficeNotSoldTest.php index 46582b1..766b4d6 100644 --- a/tests/Feature/Billing/OfficeNotSoldTest.php +++ b/tests/Feature/Billing/OfficeNotSoldTest.php @@ -72,12 +72,15 @@ it('zeigt die Office-Karte im Portal nicht mehr an, solange sie ohnehin abgewies }); it('lässt die anderen Module unberührt, sowohl in der Anzeige als auch bei der Buchung', function () { - // Der Riegel gilt nur `collabora_pro` — für alles andere muss - // saleRefusal() weiterhin null bleiben, sonst hätte diese Änderung den - // ganzen Shop zugesperrt statt eines einzelnen Moduls. + // Der Riegel gilt nur `collabora_pro` (und, seit + // tests/Feature/Billing/ExtraBackupsNotSoldTest.php, `extra_backups`) — + // für alles andere muss saleRefusal() weiterhin null bleiben, sonst hätte + // diese Änderung den ganzen Shop zugesperrt statt zweier einzelner + // Module. `extra_backups` steht deshalb absichtlich NICHT mehr in dieser + // Liste. $catalogue = app(AddonCatalogue::class); - foreach (['extra_backups', 'priority_support', 'custom_domain', AddonCatalogue::STORAGE] as $key) { + foreach (['priority_support', 'custom_domain', AddonCatalogue::STORAGE] as $key) { expect($catalogue->saleRefusal($key))->toBeNull(); } diff --git a/tests/Feature/LandingPriceSheetTest.php b/tests/Feature/LandingPriceSheetTest.php index 05694b6..26eaa29 100644 --- a/tests/Feature/LandingPriceSheetTest.php +++ b/tests/Feature/LandingPriceSheetTest.php @@ -2,6 +2,8 @@ use App\Models\PlanFamily; use App\Models\PlanVersion; +use App\Support\CompanyProfile; +use App\Support\Settings; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -37,7 +39,7 @@ use Illuminate\Support\Str; */ function priceSheetGross(int $netCents): string { - $cents = (int) round($netCents * (1 + App\Support\CompanyProfile::taxRate() / 100)); + $cents = (int) round($netCents * (1 + CompanyProfile::taxRate() / 100)); $amount = $cents % 100 === 0 ? number_format($cents / 100, 0, ',', '.') @@ -272,7 +274,7 @@ it('shows the platform address in the shape a customer will type it, under the c // Through the setting, not the config: the setting is what // ProvisioningSettings::dnsZone() reads first, and since instances moved to // their own zone there is a row in it on every installation. - App\Support\Settings::set('provisioning.dns_zone', 'wolke.test'); + Settings::set('provisioning.dns_zone', 'wolke.test'); $this->get('/') ->assertOk() @@ -292,12 +294,14 @@ it('names the modules it sells, with the prices the catalogue charges for them', $page->assertSee('Optional dazubuchbar'); - // `collabora_pro` fehlt hier absichtlich: es ist konfiguriert (BookAddon - // kennt den Preis weiterhin — für Bestandsfälle und ein Geschenk des - // Betreibers, siehe App\Actions\BookAddon), aber vom Markt genommen — - // siehe die eigene Prüfung unten, AddonCatalogue::OFFICE_ON_SALE und - // tests/Feature/Billing/OfficeNotSoldTest.php für die Abrechnungsseite. - foreach (['extra_backups', 'priority_support', 'custom_domain'] as $key) { + // `collabora_pro` und `extra_backups` fehlen hier absichtlich: beide sind + // konfiguriert (BookAddon kennt ihren Preis weiterhin — für Bestandsfälle + // und ein Geschenk des Betreibers, siehe App\Actions\BookAddon), aber vom + // Markt genommen — siehe die eigenen Prüfungen unten, + // AddonCatalogue::OFFICE_ON_SALE/EXTRA_BACKUPS_ON_SALE und + // tests/Feature/Billing/OfficeNotSoldTest.php bzw. + // ExtraBackupsNotSoldTest.php für die Abrechnungsseite. + foreach (['priority_support', 'custom_domain'] as $key) { $page->assertSee($money((int) config("provisioning.addons.{$key}.price_cents")), false); } @@ -333,6 +337,29 @@ it('bewirbt Office nicht, solange der Dokumentenserver nicht steht', function () ->assertDontSee(priceSheetGross((int) config('provisioning.addons.collabora_pro.price_cents')), false); }); +/** + * Der zweite Sicherungsort liefert nicht — siehe App\Support\OpenWork::all() + * — und darf deshalb nicht mehr in der Liste „Optional dazubuchbar" stehen. + * Anders als bei Office betrifft das nur diese eine Zeile: das Modul kommt + * in keiner Aufzählung „Was enthalten ist" und keiner Vergleichstabelle vor, + * die tägliche Sicherung selbst (`daily_backups`) bleibt unverändert stehen. + * + * Absichtlich rot gedreht während der Entwicklung — + * `EXTRA_BACKUPS_ON_SALE` auf `true` gestellt zeigte die Zeile wieder, + * dieser Test schlug wie erwartet fehl. Siehe den Bericht für das Ergebnis. + */ +it('bewirbt den zweiten Sicherungsort nicht, solange es keinen zweiten Ort gibt', function () { + $page = $this->get('/')->assertOk(); + + // Die tägliche Sicherung selbst bleibt beworben — nur der ZWEITE Ort + // fehlt, nicht die Sicherung. + $page->assertSee('Tägliche verschlüsselte Sicherung'); + + // Das Zusatzmodul und sein Preis in der Liste „Optional dazubuchbar". + $page->assertDontSee('Zweiter Sicherungsort') + ->assertDontSee(priceSheetGross((int) config('provisioning.addons.extra_backups.price_cents')), false); +}); + it('keeps the sheet up when nothing is on sale on top of a package', function () { // An installation that sells no modules is a configuration, not a failure. // The block disappears; the cells that could have been an offer go back to