CluPilotCloud/tests/Feature/LandingPriceSheetTest.php

129 lines
5.2 KiB
PHP

<?php
use App\Models\PlanFamily;
use App\Models\PlanVersion;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* The public price sheet reads the catalogue.
*
* It used to be written into the page by hand, and the two had already drifted:
* the site advertised 249 € for a plan the checkout charged 399 € for. These
* tests exist so that cannot come back — and so a catalogue that is broken
* takes the price sheet down, never the whole front page.
*/
it('quotes the price the catalogue would actually charge', function () {
$team = PlanFamily::query()->where('key', 'team')->firstOrFail();
$version = PlanVersion::query()->where('plan_family_id', $team->id)->firstOrFail();
$monthly = $version->prices()->where('term', 'monthly')->firstOrFail();
$page = $this->get('/')->assertOk();
// Whole euros, German grouping, non-breaking space — the exact string the
// sheet renders, so a formatting change cannot slip past this test.
$page->assertSee(number_format($monthly->amount_cents / 100, 0, ',', '.')."\u{00A0}€", false);
$page->assertSee('bis '.$version->seats);
// The included traffic is sold and metered but was simply never printed.
$page->assertSee('Datenvolumen');
$page->assertSee($version->traffic_gb >= 1000 && $version->traffic_gb % 1000 === 0
? ($version->traffic_gb / 1000).' TB'
: $version->traffic_gb.' GB');
});
it('follows the catalogue when the owner changes a price', function () {
// The point of the whole exercise: one place to change a price.
DB::table('plan_prices')
->where('plan_version_id', PlanVersion::query()
->where('plan_family_id', PlanFamily::query()->where('key', 'team')->value('id'))
->value('id'))
->where('term', 'monthly')
->update(['amount_cents' => 21500]);
$this->get('/')
->assertOk()
->assertSee("215\u{00A0}€", false)
->assertDontSee("179\u{00A0}€", false);
});
it('keeps the front page up when the catalogue cannot be read', function () {
// A second version of the same plan on sale at the same moment. The
// catalogue refuses to guess which one is being sold — correctly, because
// guessing would mean quoting a price at random. The marketing page must
// survive that: an outage in the shop is not an outage of the company.
Log::spy();
$team = PlanFamily::query()->where('key', 'team')->firstOrFail();
$original = PlanVersion::query()->where('plan_family_id', $team->id)->firstOrFail();
$clone = collect($original->getAttributes())->except('id')->all();
$clone['uuid'] = (string) Str::uuid();
$clone['version'] = $original->version + 1;
DB::table('plan_versions')->insert($clone);
$this->get('/')
->assertOk()
->assertSee('Preise derzeit auf Anfrage')
// No number survives from the broken catalogue.
->assertDontSee("179\u{00A0}€", false);
Log::shouldHaveReceived('error')
->withArgs(fn (string $message) => str_contains($message, 'plan catalogue'))
->once();
});
it('names the currency the catalogue is priced in, not the one the page was written in', function () {
// The currency is configurable. A sheet that prints "€" while the checkout
// charges francs is the same two-sources-of-truth mistake as a hard-coded
// amount — only harder to spot, because the number looks right.
config()->set('provisioning.currency', 'CHF');
DB::table('plan_prices')->update(['currency' => 'CHF']);
$this->get('/')
->assertOk()
->assertSee("179\u{00A0}CHF", false)
->assertDontSee("179\u{00A0}€", false);
});
it('marks the plan the console recommends, and only that one', function () {
DB::table('plan_families')->update(['is_recommended' => false]);
DB::table('plan_families')->where('key', 'business')->update(['is_recommended' => true]);
$content = $this->get('/')->assertOk()->getContent();
expect(strpos($content, '>Business<'))->toBeInt()
->and(strpos($content, 'Empfohlen'))->toBeGreaterThan(strpos($content, '>Business<'))
// Singular: exactly one plan carries the mark on the page.
->and(substr_count($content, 'Empfohlen'))->toBe(1);
});
it('shows the audience line the console saved, not a string compiled into the controller', function () {
// The point of this migration: the console is the one place this
// sentence is written, same as the price.
DB::table('plan_families')
->where('key', 'team')
->update(['audience' => 'Für Redaktionen und Kanzleien']);
$this->get('/')
->assertOk()
->assertSee('Für Redaktionen und Kanzleien')
->assertDontSee('Für wachsende Teams');
});
it('drops a catalogue feature it has no wording for, rather than printing its key', function () {
$version = PlanVersion::query()
->where('plan_family_id', PlanFamily::query()->where('key', 'team')->value('id'))
->firstOrFail();
DB::table('plan_versions')->where('id', $version->id)->update([
'features' => json_encode([...$version->features, 'quantum_entanglement']),
]);
$this->get('/')
->assertOk()
->assertDontSee('quantum_entanglement');
});