Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/Pricing/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -888,9 +888,10 @@ public static function price_get_addon_total( $price_array ) {
continue;
}

$the_price = floatval( $price['price'] );
$the_price = self::normalize_price_value( $price['price'] );
$qty = isset( $price['quantity'] ) ? self::normalize_price_value( $price['quantity'] ) : 0;

$total_addon += ( $the_price * $price['quantity'] );
$total_addon += ( $the_price * $qty );

/*
if( $price['type'] == 'quantities' || $price['type'] == 'bulkquantity' ) {
Expand All @@ -913,14 +914,42 @@ public static function price_get_cart_fee_total( $price_array ) {
foreach ( $price_array as $price ) {

if ( $price['apply'] == 'cart_fee' ) {
$total_cart_fee += $price['price'];
$total_cart_fee += self::normalize_price_value( $price['price'] );
}
}
}

return $total_cart_fee;
}

/**
* Coerce a raw addon price value to a float.
*
* Admins occasionally type currency symbols, spaces, or trailing labels
* into the price field (e.g. "$10", "10 USD"). PHP 8+ raises a fatal
* TypeError if such a value is added to an int, so any value that flows
* into pricing arithmetic must be normalized first.
*
* Delegates to wc_format_decimal() so locale separators (e.g. "1.000,50"
* on European stores) and currency symbols are stripped consistently with
* the rest of WooCommerce's price handling.
*
* @param mixed $value Raw price value as stored in field meta.
* @return float
*/
private static function normalize_price_value( $value ) {

if ( is_int( $value ) || is_float( $value ) ) {
return (float) $value;
}

if ( ! is_scalar( $value ) ) {
return 0.0;
}

return (float) wc_format_decimal( (string) $value );
}

// Get total quantities
public static function price_get_total_quantities( $ppom_fields_post, $product_id ) {

Expand Down
103 changes: 103 additions & 0 deletions tests/unit/src/Pricing/test-pricing-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,109 @@ public function test_price_get_cart_fee_total_empty() {
$this->assertSame( 0, Engine::price_get_cart_fee_total( array() ) );
}

/**
* Regression for #642 — admin saved a cart-fee addon price as "$10".
*
* Prior to the fix, summing the int accumulator with the string "$10" raised
* a fatal TypeError on PHP 8+, taking down the whole cart/checkout. After
* the fix the leading currency symbol is stripped and the value is parsed
* as 10.
*
* @return void
*/
public function test_price_get_cart_fee_total_normalizes_currency_prefixed_string() {
$total = Engine::price_get_cart_fee_total(
array(
array( 'apply' => 'cart_fee', 'price' => '$10' ),
array( 'apply' => 'cart_fee', 'price' => ' 5.50' ),
array( 'apply' => 'cart_fee', 'price' => 'USD 2' ),
)
);

$this->assertEqualsWithDelta( 17.5, $total, 0.0001 );
}

/**
* Regression for #642 — trailing junk after the number must not crash.
*
* "10abc" was the second example from the bug report. PHP's float cast
* already handles a trailing non-numeric tail, but we assert it here so a
* future refactor cannot silently break the contract.
*
* @return void
*/
public function test_price_get_cart_fee_total_truncates_trailing_non_numeric() {
$total = Engine::price_get_cart_fee_total(
array(
array( 'apply' => 'cart_fee', 'price' => '10abc' ),
)
);

$this->assertEqualsWithDelta( 10.0, $total, 0.0001 );
}

/**
* Non-scalar / unparseable values must degrade to 0 rather than throw.
*
* @return void
*/
public function test_price_get_cart_fee_total_handles_unparseable_values() {
$total = Engine::price_get_cart_fee_total(
array(
array( 'apply' => 'cart_fee', 'price' => 'free' ),
array( 'apply' => 'cart_fee', 'price' => null ),
array( 'apply' => 'cart_fee', 'price' => array( 'unexpected' ) ),
array( 'apply' => 'cart_fee', 'price' => 7 ),
)
);

$this->assertEqualsWithDelta( 7.0, $total, 0.0001 );
}

/**
* Same regression coverage for the addon-apply path so both helpers stay
* symmetric.
*
* @return void
*/
public function test_price_get_addon_total_normalizes_currency_prefixed_string() {
$total = Engine::price_get_addon_total(
array(
array(
'apply' => 'addon',
'price' => '$10',
'quantity' => 2,
),
array(
'apply' => 'addon',
'price' => '5abc',
'quantity' => 1,
),
)
);

$this->assertEqualsWithDelta( 25.0, $total, 0.0001 );
}

/**
* A non-numeric quantity must not crash the addon sum either.
*
* @return void
*/
public function test_price_get_addon_total_handles_non_numeric_quantity() {
$total = Engine::price_get_addon_total(
array(
array(
'apply' => 'addon',
'price' => 4,
'quantity' => '3x',
),
)
);

$this->assertEqualsWithDelta( 12.0, $total, 0.0001 );
}

/**
* @return void
*/
Expand Down
Loading