From 4df7e1373a24bb1a10b4c5b71f4255db5545cd28 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Fri, 25 Sep 2026 13:02:03 +0200 Subject: [PATCH] fix(stripe): name Checkout lines after the product, options only when there are some MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A product with a single price showed "Shinsou Standee – Default" on Checkout; the price name is now only added when the product sells in more than one price. - A line added by price id was named after the price ("A4 – A4"); it is named after the price's product now ("Flat Art – A4"). - The test for this lived inside the fake Stripe Session class at the end of the file and never ran; it is a real test again. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Models/Cart.php | 23 ++++- .../Checkout/CartCheckoutSessionTest.php | 96 ++++++++++++------- 2 files changed, 82 insertions(+), 37 deletions(-) diff --git a/src/Models/Cart.php b/src/Models/Cart.php index 75d996e..2f10811 100644 --- a/src/Models/Cart.php +++ b/src/Models/Cart.php @@ -2353,6 +2353,16 @@ class Cart extends Model }); } + /** Whether the purchasable is sold in more than one price, i.e. the buyer picked an option. */ + protected function sellsInOptions(mixed $purchasable): bool + { + if (! is_object($purchasable) || ! method_exists($purchasable, 'prices')) { + return false; + } + + return $purchasable->prices()->count() > 1; + } + /** * Resolve the Stripe `recurring` descriptor for a price, or null when the * price is one-time. Tolerates both the package's enum-cast model and a @@ -2499,18 +2509,23 @@ class Cart extends Model foreach ($this->items as $item) { $product = $item->purchasable; - $priceModel = $item->price()->first(); + $priceModel = $item->price()->first() + ?? ($product instanceof ProductPrice ? $product : null); + + // A line added by price id holds the price itself: name it after its product. + $named = $product instanceof ProductPrice ? ($product->purchasable ?? $product) : $product; // Get product name (use short_description if available, otherwise name) - $productName = $product->name ?? 'Product ['.$product->id.']'; + $productName = $named->name ?? 'Product ['.$named->id.']'; // Optionally name the chosen option ("Flat Art – A4"), so a product sold in // several prices shows the buyer which one they pay for (shop.stripe.line_item_price_name). + // A product with a single price has no option to name ("… – Default"). $priceName = trim((string) ($priceModel?->name ?? '')); - if ($priceName !== '' && config('shop.stripe.line_item_price_name', false)) { + if ($priceName !== '' && config('shop.stripe.line_item_price_name', false) && $this->sellsInOptions($named)) { $productName .= ' – '.$priceName; } - $description = $product->short_description ?? null; + $description = $named->short_description ?? null; // Build description with booking dates if available if ($item->from && $item->until) { diff --git a/tests/Feature/Checkout/CartCheckoutSessionTest.php b/tests/Feature/Checkout/CartCheckoutSessionTest.php index 162c337..54c4cc4 100644 --- a/tests/Feature/Checkout/CartCheckoutSessionTest.php +++ b/tests/Feature/Checkout/CartCheckoutSessionTest.php @@ -579,6 +579,69 @@ class CartCheckoutSessionTest extends TestCase ]); } + /** The first line item name of every session checkoutSession() creates. */ + private array $lineNames = []; + + private function recordLineNames(): void + { + $this->lineNames = []; + \Stripe\Checkout\Session::$createCallback = function ($params) { + $this->lineNames[] = $params['line_items'][0]['price_data']['product_data']['name']; + $mock = new \stdClass; + $mock->id = 'mock_session_id'; + + return $mock; + }; + } + + #[Test] + public function line_item_names_the_chosen_price_only_when_enabled() + { + config(['shop.stripe.enabled' => true, 'services.stripe.secret' => 'sk_test_fake']); + + $product = Product::factory()->create(['name' => 'Flat Art', 'manage_stock' => false]); + ProductPrice::factory()->create([ + 'purchasable_id' => $product->id, 'purchasable_type' => Product::class, + 'unit_amount' => 5000, 'currency' => 'EUR', 'is_default' => true, 'name' => 'A5', + ]); + $a4 = ProductPrice::factory()->create([ + 'purchasable_id' => $product->id, 'purchasable_type' => Product::class, + 'unit_amount' => 7000, 'currency' => 'EUR', 'is_default' => false, 'name' => 'A4', + ]); + + $this->cart->addToCart($a4, 1); + $this->recordLineNames(); + + $this->cart->checkoutSession(['success_url' => 'https://example.com/s', 'cancel_url' => 'https://example.com/c']); + config(['shop.stripe.line_item_price_name' => true]); + $this->cart->checkoutSession(['success_url' => 'https://example.com/s', 'cancel_url' => 'https://example.com/c']); + + $this->assertSame(['Flat Art', 'Flat Art – A4'], $this->lineNames); + } + + #[Test] + public function line_item_skips_the_price_name_of_a_product_with_one_price() + { + config([ + 'shop.stripe.enabled' => true, + 'services.stripe.secret' => 'sk_test_fake', + 'shop.stripe.line_item_price_name' => true, + ]); + + $product = Product::factory()->create(['name' => 'Shinsou Standee', 'manage_stock' => false]); + ProductPrice::factory()->create([ + 'purchasable_id' => $product->id, 'purchasable_type' => Product::class, + 'unit_amount' => 800, 'currency' => 'EUR', 'is_default' => true, 'name' => 'Default', + ]); + + $this->cart->addToCart($product, 1); + $this->recordLineNames(); + + $this->cart->checkoutSession(['success_url' => 'https://example.com/s', 'cancel_url' => 'https://example.com/c']); + + $this->assertSame(['Shinsou Standee'], $this->lineNames); + } + /** * Mock Stripe Checkout Session creation to avoid actual API calls */ @@ -615,37 +678,4 @@ class Session { self::$createCallback = null; } - - #[Test] - public function line_item_names_the_chosen_price_only_when_enabled() - { - config(['shop.stripe.enabled' => true, 'services.stripe.secret' => 'sk_test_fake']); - - $product = Product::factory()->create(['name' => 'Flat Art', 'manage_stock' => false]); - ProductPrice::factory()->create([ - 'purchasable_id' => $product->id, 'purchasable_type' => Product::class, - 'unit_amount' => 5000, 'currency' => 'EUR', 'is_default' => true, 'name' => 'A5', - ]); - $a4 = ProductPrice::factory()->create([ - 'purchasable_id' => $product->id, 'purchasable_type' => Product::class, - 'unit_amount' => 7000, 'currency' => 'EUR', 'is_default' => false, 'name' => 'A4', - ]); - - $this->cart->addToCart($a4, 1); - - $names = []; - \Stripe\Checkout\Session::$createCallback = function ($params) use (&$names) { - $names[] = $params['line_items'][0]['price_data']['product_data']['name']; - $mock = new \stdClass; - $mock->id = 'mock_session_id'; - - return $mock; - }; - - $this->cart->checkoutSession(['success_url' => 'https://example.com/s', 'cancel_url' => 'https://example.com/c']); - config(['shop.stripe.line_item_price_name' => true]); - $this->cart->checkoutSession(['success_url' => 'https://example.com/s', 'cancel_url' => 'https://example.com/c']); - - $this->assertSame(['Flat Art', 'Flat Art – A4'], $names); - } }