From 8479fc30f84dbb55276ee68a1cf3f0aff85e5479 Mon Sep 17 00:00:00 2001 From: nexxo Date: Thu, 30 Jul 2026 16:05:56 +0200 Subject: [PATCH] Fix the checkout link, and stop the switch moving under the cursor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Every package button led to "Dieses Paket steht nicht (mehr) zum Verkauf".** The link was built as route() plus "?term=" + term — but both are QUERY parameters on this route, so route() had already opened the query string and the result was `?plan=team?term=monthly`. The package arrived as the whole string "team?term=monthly", matched nothing in the catalogue, and the page said so politely while giving no hint why. The address now comes from route() in one piece with a placeholder swapped into it, and a test asserts every checkout link on the page contains exactly one question mark. **The saving moved the buttons it belonged to.** It hung under the switch and was hidden on the yearly view, so choosing a term shifted the very control that chooses it. It sits above now and never disappears: one line, two wordings — an offer while it is an offer, a statement once it is taken. Same on the public sheet, where its vanishing re-centred the row. Co-Authored-By: Claude Opus 5 --- lang/de/order.php | 1 + lang/en/order.php | 1 + resources/views/landing.blade.php | 14 ++++++---- resources/views/livewire/order.blade.php | 23 +++++++++++----- tests/Feature/Billing/YearlyTermTest.php | 34 +++++++++++++++++++++--- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/lang/de/order.php b/lang/de/order.php index 82bf350..57ab377 100644 --- a/lang/de/order.php +++ b/lang/de/order.php @@ -14,6 +14,7 @@ return [ 'terms_first' => 'Bitte oben die AGB annehmen.', 'term_monthly' => 'Monatlich', 'term_yearly' => 'Jährlich', + 'free_months_taken' => '{1} 1 Monat gratis enthalten|[2,*] :count Monate gratis enthalten', 'free_months_hint' => '{1} 1 Monat gratis bei Jahreszahlung|[2,*] :count Monate gratis bei Jahreszahlung', 'yearly_total' => ':total jährlich', 'instead_of' => 'statt :amount', diff --git a/lang/en/order.php b/lang/en/order.php index 160b20d..b055da9 100644 --- a/lang/en/order.php +++ b/lang/en/order.php @@ -14,6 +14,7 @@ return [ 'terms_first' => 'Please accept the terms above.', 'term_monthly' => 'Monthly', 'term_yearly' => 'Yearly', + 'free_months_taken' => '{1} 1 month free included|[2,*] :count months free included', 'free_months_hint' => '{1} 1 month free when paying yearly|[2,*] :count months free when paying yearly', 'yearly_total' => ':total per year', 'instead_of' => 'instead of :amount', diff --git a/resources/views/landing.blade.php b/resources/views/landing.blade.php index 768169e..329dbb1 100644 --- a/resources/views/landing.blade.php +++ b/resources/views/landing.blade.php @@ -520,6 +520,15 @@ charge. The saving is named next to the switch: "jährlich" on its own gives nobody a reason to look. --}}
+ {{-- The saving above the switch, and always present: hidden on the + yearly view it re-centred the row every time somebody changed + their mind. One line, two wordings. --}} + @if ($bestFreeMonths > 0) +

+ {{ $bestFreeMonths === 1 ? '1 Monat gratis' : $bestFreeMonths.' Monate gratis' }} bei Jahreszahlung + {{ $bestFreeMonths === 1 ? '1 Monat gratis' : $bestFreeMonths.' Monate gratis' }} — in jedem Jahrespreis enthalten +

+ @endif
{{-- text-bg on the active option, not a colour that does not exist: `text-on-ink` is not in the palette (the ink pairing @@ -537,11 +546,6 @@ @endforeach
- @if ($bestFreeMonths > 0) - - {{ $bestFreeMonths === 1 ? '1 Monat gratis' : $bestFreeMonths.' Monate gratis' }} bei Jahreszahlung - - @endif
{{-- Stretched, not items-start: the cards carry different numbers of diff --git a/resources/views/livewire/order.blade.php b/resources/views/livewire/order.blade.php index efc6a2a..209f67b 100644 --- a/resources/views/livewire/order.blade.php +++ b/resources/views/livewire/order.blade.php @@ -47,6 +47,16 @@ radius less its own padding. A rounded-md option — Tailwind's untokenised 6px — read as a different component sitting inside this one. --}} + {{-- Above the switch, and ALWAYS present. It hung underneath + and was hidden on the yearly view, so choosing a term moved + the buttons that choose it. One line, two wordings: an offer + while it is an offer, a statement once it is taken. --}} + @if ($bestFreeMonths > 0) + + {{ trans_choice('order.free_months_hint', $bestFreeMonths, ['count' => $bestFreeMonths]) }} + {{ trans_choice('order.free_months_taken', $bestFreeMonths, ['count' => $bestFreeMonths]) }} + + @endif
@foreach (['monthly', 'yearly'] as $option)
- @if ($bestFreeMonths > 0) - - {{ trans_choice('order.free_months_hint', $bestFreeMonths, ['count' => $bestFreeMonths]) }} - - @endif @endif @@ -163,8 +168,14 @@ The term travels in the address, so the checkout shows what was being looked at — and a reload of that page still shows it. --}} + {{-- The whole address from route(), with the term swapped + into it — not route() plus "?term=". Both parameters are + query parameters here, so appending a second "?" built + `?plan=team?term=monthly`, and the package arrived as + the string "team?term=monthly": every click landed on + "this package is not on sale". --}} $plan['recommended'], diff --git a/tests/Feature/Billing/YearlyTermTest.php b/tests/Feature/Billing/YearlyTermTest.php index ceb91ff..ee4eebb 100644 --- a/tests/Feature/Billing/YearlyTermTest.php +++ b/tests/Feature/Billing/YearlyTermTest.php @@ -76,10 +76,36 @@ it('lets a customer pick the term on the order page', function () { // Both figures rendered, one shown: switching costs no round trip. expect($page)->toContain("term = 'yearly'") - ->and($page)->toContain(__('order.term_yearly')) - // And the choice travels to the checkout page in the address, so a - // reload there still shows what was chosen. - ->and($page)->toContain("?term=' + term"); + ->and($page)->toContain(__('order.term_yearly')); +}); + +it('links to the checkout with one query string, not two question marks', function () { + // Both parameters are query parameters, so route() + '?term=' built + // `?plan=team?term=monthly` and the package arrived as the whole string + // "team?term=monthly". Every click landed on "this package is not on sale", + // and nothing in the page or the log said why. + $page = $this->actingAs(termBuyer())->get(route('order'))->assertOk()->getContent(); + + preg_match_all('/href="([^"]*checkout[^"]*)"/', $page, $plain); + preg_match_all('/x-bind:href="\x27([^\x27]*)\x27/', $page, $bound); + + expect($plain[1])->not->toBeEmpty() + ->and($bound[1])->not->toBeEmpty(); + + foreach (array_merge($plain[1], $bound[1]) as $href) { + expect(substr_count($href, '?'))->toBe(1, $href); + } +}); + +it('keeps the saving above the switch, where it cannot move the buttons', function () { + // It hung underneath and was hidden on the yearly view, so choosing a term + // moved the very buttons that choose it. + $page = Illuminate\Support\Facades\File::get(resource_path('views/livewire/order.blade.php')); + + expect(strpos($page, "order.free_months_hint"))->toBeLessThan(strpos($page, "order.term_'.\$option")) + // And it is always there: one line, two wordings, no appearing and + // disappearing. + ->and($page)->toContain('order.free_months_taken'); }); it('carries the chosen term into the checkout page and its form', function () {