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.
 	 */