fix(stripe): name Checkout lines after the product, options only when there are some
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
16fdf33f20
commit
4df7e1373a
|
|
@ -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
|
* 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
|
* 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) {
|
foreach ($this->items as $item) {
|
||||||
$product = $item->purchasable;
|
$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)
|
// 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
|
// 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).
|
// 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 ?? ''));
|
$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;
|
$productName .= ' – '.$priceName;
|
||||||
}
|
}
|
||||||
$description = $product->short_description ?? null;
|
$description = $named->short_description ?? null;
|
||||||
|
|
||||||
// Build description with booking dates if available
|
// Build description with booking dates if available
|
||||||
if ($item->from && $item->until) {
|
if ($item->from && $item->until) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Mock Stripe Checkout Session creation to avoid actual API calls
|
||||||
*/
|
*/
|
||||||
|
|
@ -615,37 +678,4 @@ class Session
|
||||||
{
|
{
|
||||||
self::$createCallback = null;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue