Commit d4c01ac0323 for woocommerce
commit d4c01ac032378c292fb477c00958e45677d0fddb
Author: Marin Atanasov <8436925+tyxla@users.noreply.github.com>
Date: Thu Jul 9 14:27:41 2026 +0300
Fix fatal when calculating totals for an order with a blank fee value (#66231)
* Fix fatal when calculating totals for an order with a blank fee value
Treat a non-numeric fee total as 0 in WC_Order_Item_Fee::calculate_taxes()
so an order with a fee stored as an empty string no longer triggers
"TypeError: Unsupported operand types: float * string".
Co-Authored-By: 79mplus Admin <36501099+79mplus-admin@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add changelog entry for blank fee calculate_totals fix
Co-Authored-By: 79mplus Admin <36501099+79mplus-admin@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Remove resolved string*float error from PHPStan baseline
The blank-fee fix eliminates the previously baselined
"Binary operation * between string and float" error in
class-wc-order-item-fee.php.
Co-Authored-By: 79mplus Admin <36501099+79mplus-admin@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add test for negative-fee tax apportionment path
* Remove "see" annotations from comments
Co-authored-by: Raluca Stan <ralucastn@gmail.com>
---------
Co-authored-by: 79mplus Admin <36501099+79mplus-admin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Raluca Stan <ralucastn@gmail.com>
diff --git a/plugins/woocommerce/changelog/fix-44859-blank-fee-calculate-taxes-fatal b/plugins/woocommerce/changelog/fix-44859-blank-fee-calculate-taxes-fatal
new file mode 100644
index 00000000000..a102bcf9001
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-44859-blank-fee-calculate-taxes-fatal
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent a fatal error when calculating totals for an order that has a fee stored with a blank value; the blank fee total is now treated as 0 during tax calculation.
diff --git a/plugins/woocommerce/includes/class-wc-order-item-fee.php b/plugins/woocommerce/includes/class-wc-order-item-fee.php
index ecbbfc927b1..3249ae98309 100644
--- a/plugins/woocommerce/includes/class-wc-order-item-fee.php
+++ b/plugins/woocommerce/includes/class-wc-order-item-fee.php
@@ -91,8 +91,13 @@ class WC_Order_Item_Fee extends WC_Order_Item {
if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) {
return false;
}
+
+ // Fee totals may be stored as a blank string (e.g. a fee saved with its value cleared). Coerce to a
+ // float so a non-numeric total is treated as 0, avoiding a TypeError during tax calculation.
+ $total = (float) $this->get_total();
+
// Use regular calculation unless the fee is negative.
- if ( 0 <= $this->get_total() ) {
+ if ( 0 <= $total ) {
unset( $calculate_tax_for['prices_include_tax'] );
return parent::calculate_taxes( $calculate_tax_for );
}
@@ -109,7 +114,7 @@ class WC_Order_Item_Fee extends WC_Order_Item {
continue;
}
$proportion = $tax_class_cost / $total_costs;
- $cart_discount_proportion = $this->get_total() * $proportion;
+ $cart_discount_proportion = $total * $proportion;
$calculate_tax_for['tax_class'] = $tax_class;
$tax_rates = WC_Tax::find_rates( $calculate_tax_for );
$discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, $tax_rates, ! empty( $calculate_tax_for['prices_include_tax'] ) ) );
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 8228d25c3aa..8ab124ee289 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -12402,12 +12402,6 @@ parameters:
count: 1
path: includes/class-wc-order-item-coupon.php
- -
- message: '#^Binary operation "\*" between string and float results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: includes/class-wc-order-item-fee.php
-
-
message: '#^Call to an undefined method WC_Order_Item\:\:get_total\(\)\.$#'
identifier: method.notFound
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-order-item-fee-test.php b/plugins/woocommerce/tests/php/includes/class-wc-order-item-fee-test.php
index 0f9ae4d4cd0..a3b5266f95a 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-order-item-fee-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-order-item-fee-test.php
@@ -274,6 +274,97 @@ class WC_Order_Item_Fee_Test extends WC_Unit_Test_Case {
$order->delete( true );
}
+ /**
+ * @testdox calculate_taxes treats a blank fee total as 0 instead of throwing a TypeError.
+ *
+ * Regression test for the fatal reported when an order has a fee stored with an empty-string
+ * total and calculate_totals() is called with taxes enabled:
+ * "TypeError: Unsupported operand types: float * string".
+ *
+ * Repro steps from the issue: enable taxes with a standard rate, create an order with a fee,
+ * blank out the fee value (empty string, not 0), then run calculate_totals().
+ *
+ */
+ public function test_calculate_taxes_with_blank_fee_total_is_treated_as_zero() {
+ // Enable taxes and add a standard rate.
+ update_option( 'woocommerce_calc_taxes', 'yes' );
+ $tax_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => '',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '20.0000',
+ 'tax_rate_name' => 'VAT',
+ 'tax_rate_priority' => '1',
+ 'tax_rate_compound' => '0',
+ 'tax_rate_shipping' => '1',
+ 'tax_rate_order' => '1',
+ 'tax_rate_class' => '',
+ )
+ );
+
+ $order = WC_Helper_Order::create_order();
+
+ // Add a fee with a blank (empty string) total, mirroring the errant DB state.
+ $fee = new WC_Order_Item_Fee();
+ $fee->set_name( 'Blank fee' );
+ $fee->set_total( '' );
+ $fee->set_tax_status( \Automattic\WooCommerce\Enums\ProductTaxStatus::TAXABLE );
+ $order->add_item( $fee );
+ $order->save();
+
+ // Before the fix this threw "Unsupported operand types: float * string".
+ $order->calculate_totals();
+
+ // A blank fee contributes nothing to tax.
+ $this->assertEquals( 0.0, (float) $fee->get_total_tax() );
+
+ WC_Tax::_delete_tax_rate( $tax_rate_id );
+ update_option( 'woocommerce_calc_taxes', 'no' );
+ $order->delete( true );
+ }
+
+ /**
+ * @testdox calculate_taxes apportions taxes for a negative fee across taxable order costs.
+ *
+ * Exercises the negative-fee branch of calculate_taxes(), where the (coerced) fee total is
+ * distributed across the order's taxable costs by tax class. The helper order contains a $40
+ * taxable line item and $10 taxable shipping (both standard rate), so a -$10 fee is apportioned
+ * in full and taxed at 20%, yielding -$2 of fee tax.
+ */
+ public function test_calculate_taxes_apportions_negative_fee_across_taxable_costs() {
+ update_option( 'woocommerce_calc_taxes', 'yes' );
+ $tax_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => '',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '20.0000',
+ 'tax_rate_name' => 'VAT',
+ 'tax_rate_priority' => '1',
+ 'tax_rate_compound' => '0',
+ 'tax_rate_shipping' => '1',
+ 'tax_rate_order' => '1',
+ 'tax_rate_class' => '',
+ )
+ );
+
+ $order = WC_Helper_Order::create_order();
+
+ $fee = new WC_Order_Item_Fee();
+ $fee->set_name( 'Negative fee' );
+ $fee->set_total( '-10' );
+ $fee->set_tax_status( \Automattic\WooCommerce\Enums\ProductTaxStatus::TAXABLE );
+ $order->add_item( $fee );
+ $order->save();
+
+ $order->calculate_totals();
+
+ $this->assertEquals( -2.0, (float) $fee->get_total_tax() );
+
+ WC_Tax::_delete_tax_rate( $tax_rate_id );
+ update_option( 'woocommerce_calc_taxes', 'no' );
+ $order->delete( true );
+ }
+
/**
* @testdox set_taxes handles empty/null values gracefully.
*/