From 0bc42039f7f436bcc2adae5d092c0edb1304dd38 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Tue, 29 Sep 2026 13:13:05 +0100 Subject: [PATCH] fix: read the merchant name from lunar.checkout.merchant The package merges its config under `lunar.checkout`, but the checkout header and the root view's tab title read `checkout.merchant`, so CHECKOUT_MERCHANT and a published config value were never used. The processing page ignored the setting altogether and showed `app.name`. Both pages now resolve the name from one helper, and the tab title reads the same key. Also pins that the checkout renders, and refuses to pay, before any payment method is registered. Co-Authored-By: Claude Opus 5.5 (1M context) --- resources/views/app.blade.php | 2 +- spec/0008-checkout-ui-and-theming.md | 2 +- src/Http/Controllers/CheckoutController.php | 13 +++++++++++-- tests/Feature/CheckoutRouteTest.php | 4 ++-- tests/Feature/CheckoutThemeBrandingTest.php | 2 +- tests/Feature/PaymentMethodAvailabilityTest.php | 17 +++++++++++++++++ tests/Feature/PaymentReleaseTest.php | 11 +++++++++++ 7 files changed, 44 insertions(+), 7 deletions(-) diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 7284121..78b2430 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -13,7 +13,7 @@ - Checkout · {{ config('checkout.merchant') ?: config('app.name') }} + Checkout · {{ config('lunar.checkout.merchant') ?: config('app.name') }} {{-- Tab icon (CheckoutTheme::favicon). The checkout is its own document, so without this the tab shows the browser's blank globe. --}} diff --git a/spec/0008-checkout-ui-and-theming.md b/spec/0008-checkout-ui-and-theming.md index e32a7ea..2fdd014 100644 --- a/spec/0008-checkout-ui-and-theming.md +++ b/spec/0008-checkout-ui-and-theming.md @@ -201,7 +201,7 @@ from the current channel. Same seam, runtime-resolved. **Favicon and tab title (added in run 1 of the master test plan).** `CheckoutTheme::favicon` is a URL/path validated by the same chokepoint as `logo` and `stylesheet`, projected as `favicon` and rendered by the root view as ``. The tab title is -"Checkout · {merchant}" from `checkout.merchant`, falling back to the app name. Without +"Checkout · {merchant}" from `lunar.checkout.merchant`, falling back to the app name. Without either, the checkout's own document showed the browser's blank globe and a bare "Checkout". ### E. Three customisation tiers (light → heavy) diff --git a/src/Http/Controllers/CheckoutController.php b/src/Http/Controllers/CheckoutController.php index f6e0f5a..0bdf6ee 100644 --- a/src/Http/Controllers/CheckoutController.php +++ b/src/Http/Controllers/CheckoutController.php @@ -386,7 +386,7 @@ private function projectCheckout(CheckoutDriver $driver, CheckoutSessionModel $s return [ 'uuid' => $session->uuid, - 'merchant' => config('checkout.merchant') ?: config('app.name'), + 'merchant' => $this->merchant(), 'currency' => $snapshot->currencyCode, 'items' => array_map(fn (array $line): array => [ 'id' => $line['identifier'], @@ -451,6 +451,15 @@ private function projectCheckout(CheckoutDriver $driver, CheckoutSessionModel $s ]; } + /** + * The store name in the checkout header: the configured merchant, falling + * back to the application name. The root view's tab title reads the same. + */ + private function merchant(): string + { + return (string) (config('lunar.checkout.merchant') ?: config('app.name')); + } + /** * The session's write/navigation endpoints the checkout Vue app (and the * host-page express mount's JSON start response) drive it through. @@ -1243,7 +1252,7 @@ public function processing(CheckoutSessionModel $session, ReconcilesCheckoutSess return Inertia::render('Processing', [ 'pollUrl' => route('lunar.checkout.processing', $session->uuid), - 'merchant' => config('app.name', 'Store'), + 'merchant' => $this->merchant(), 'theme' => $theme->tokens(), 'branding' => $theme->branding(), 'stylesheet' => $theme->stylesheet(), diff --git a/tests/Feature/CheckoutRouteTest.php b/tests/Feature/CheckoutRouteTest.php index d67982b..dd486c7 100644 --- a/tests/Feature/CheckoutRouteTest.php +++ b/tests/Feature/CheckoutRouteTest.php @@ -98,7 +98,7 @@ function routeTestCart(): Cart }); it('projects the configured merchant name into the header prop', function () { - config(['checkout.merchant' => 'Edwardes Bros']); + config(['lunar.checkout.merchant' => 'Edwardes Bros']); // Needs lines: show() bounces a session whose cart is empty. $cart = CheckoutCart::orderable(); @@ -114,7 +114,7 @@ function routeTestCart(): Cart }); it('falls back to the app name when no merchant is configured', function () { - config(['checkout.merchant' => null, 'app.name' => 'Lunar Store']); + config(['lunar.checkout.merchant' => null, 'app.name' => 'Lunar Store']); // Needs lines: show() bounces a session whose cart is empty. $cart = CheckoutCart::orderable(); diff --git a/tests/Feature/CheckoutThemeBrandingTest.php b/tests/Feature/CheckoutThemeBrandingTest.php index 53cd285..e2a8e20 100644 --- a/tests/Feature/CheckoutThemeBrandingTest.php +++ b/tests/Feature/CheckoutThemeBrandingTest.php @@ -13,7 +13,7 @@ */ it('renders the theme favicon and the merchant name in the root view head', function () { app()->bind(CheckoutTheme::class, fn (): CheckoutTheme => CheckoutTheme::tender()->with(favicon: '/favicon.svg')); - config()->set('checkout.merchant', 'Edwardes Bros'); + config()->set('lunar.checkout.merchant', 'Edwardes Bros'); $cart = CheckoutCart::orderable(); $session = CheckoutCart::session($cart); diff --git a/tests/Feature/PaymentMethodAvailabilityTest.php b/tests/Feature/PaymentMethodAvailabilityTest.php index 27b204c..e9a1ba5 100644 --- a/tests/Feature/PaymentMethodAvailabilityTest.php +++ b/tests/Feature/PaymentMethodAvailabilityTest.php @@ -193,6 +193,23 @@ public function unavailableReason(Cart $cart): ?string expect($registry->unavailableReasons($large))->toBe([]); }); +it('renders the checkout before any payment method is registered', function () { + $cart = CheckoutCart::orderable(); + $session = CheckoutCart::session($cart); + + $this->get(route('lunar.checkout.show', $session->uuid), ['X-Inertia' => 'true']) + ->assertOk() + ->assertJsonCount(0, 'props.checkout.paymentMethods') + ->assertJsonPath('props.checkout.paymentUnavailable', []); + + $this->postJson(route('lunar.checkout.pay', $session->uuid), [ + 'fingerprint' => CheckoutCart::fingerprint($session), + 'payment_method' => 'offline', + ])->assertStatus(422); + + expect(Order::query()->count())->toBe(0); +}); + it('projects why the payment region is empty', function () { app(PaymentMethodRegistry::class)->add(MinimumSpendMethod::class); diff --git a/tests/Feature/PaymentReleaseTest.php b/tests/Feature/PaymentReleaseTest.php index 9ceaf27..640b969 100644 --- a/tests/Feature/PaymentReleaseTest.php +++ b/tests/Feature/PaymentReleaseTest.php @@ -221,6 +221,17 @@ function pinnedSession(array $overrides = []) expect($session->refresh()->status)->toBeInstanceOf(PaymentProcessing::class); }); +it('processing shows the same merchant name as the checkout', function () { + ReleaseTestGateway::$status = PaymentIntentStatus::Pending; + config(['lunar.checkout.merchant' => 'Edwardes Bros', 'app.name' => 'Lunar Store']); + + $session = pinnedSession(); + + $this->get(route('lunar.checkout.processing', $session->uuid), ['X-Inertia' => 'true']) + ->assertOk() + ->assertJsonPath('props.merchant', 'Edwardes Bros'); +}); + it('processing redirects an already completed session straight to success', function () { $session = pinnedSession(); $session->forceFill(['status' => Completed::$name, 'success_url' => 'https://store.test/thanks'])->save();