Commit f4c48c85c4a for woocommerce

commit f4c48c85c4a917af50f7e423f2704fd82df82918
Author: agent-sandbox-woocommerce[bot] <282076412+agent-sandbox-woocommerce[bot]@users.noreply.github.com>
Date:   Thu Jun 4 08:37:28 2026 -0400

    Fix TypeError: string - string in get_formatted_order_total() on PHP 8.3 with refunded orders (#65507)

    * Fix TypeError in get_formatted_order_total() with refunds on PHP 8.3

    Cast \$order_total and \$total_refunded to float before subtraction in
    get_formatted_order_total(). Both values can be strings (get_total()
    returns a wc_format_decimal()-formatted string; the HPOS data store's
    get_total_refunded() can also return a string), and PHP 8.3 throws a
    TypeError on string - string arithmetic.

    Triggered-By: kraftbj

    * Remove stale PHPStan baseline entry for class-wc-order.php

    The binary operation error pattern "float - non-falsy-string" in
    class-wc-order.php was resolved by the previous commit which added
    explicit (float) casts to both operands in get_formatted_order_total().
    The baseline entry now triggers a PHPStan "unmatched ignored error"
    warning, so remove it.

    Triggered-By: prettyboymp

    * Fix remaining string-arithmetic bug class in class-wc-order.php

    Extend the (float) cast fix to all four un-cast subtraction sites in the
    file, not just the line-246 path already fixed:

    - Line 231 (get_formatted_order_total, tax-inclusive itemized path):
      `(float) $tax->amount - (float) get_total_tax_refunded_by_rate_id()`
    - Line 235 (get_formatted_order_total, tax-inclusive non-itemized path):
      `(float) get_total_tax() - (float) get_total_tax_refunded()`
    - Line 2493 (get_remaining_refund_amount):
      `(float) get_total() - (float) get_total_refunded()`
    - Line 2502 (get_remaining_refund_items):
      `(int) get_item_count() - (int) get_item_count_refunded()`

    The tax-inclusive paths (231/235) are reached before the previously-fixed
    line 246, so a non-numeric operand would still trigger a fatal TypeError
    on PHP 8.x for tax-inclusive price displays on refunded orders.

    Also remove the two remaining binaryOp.invalid PHPStan baseline entries
    for class-wc-order.php that are now resolved by the casts on lines 231
    and 2502.

    Update the regression test to force a non-numeric return from
    get_total() via the woocommerce_order_get_total filter, so the test
    actually exercises the TypeError-prone code path rather than the
    numeric-string − float path which never threw.

    Triggered-By: prettyboymp

    * Remove unused closure parameter in refund formatted-total test

    The `woocommerce_order_get_total` filter callback declared an unused
    `$value` parameter, tripping phpcs Generic.CodeAnalysis.UnusedFunctionParameter
    and failing the branch-level Lint job. The callback doesn't use the argument,
    so drop it.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: matticbot <matticbot@users.noreply.github.com>
    Co-authored-by: Michael Pretty <prettyboymp@users.noreply.github.com>
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/wooplug-6663-fix-typeerror-formatted-order-total b/plugins/woocommerce/changelog/wooplug-6663-fix-typeerror-formatted-order-total
new file mode 100644
index 00000000000..afd6780b2ca
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-6663-fix-typeerror-formatted-order-total
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix TypeError: string - string in get_formatted_order_total() on PHP 8.3 with refunded orders
diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php
index 9acbf62e782..d8afdd2c389 100644
--- a/plugins/woocommerce/includes/class-wc-order.php
+++ b/plugins/woocommerce/includes/class-wc-order.php
@@ -228,11 +228,11 @@ class WC_Order extends WC_Abstract_Order {

 			if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) {
 				foreach ( $tax_totals as $code => $tax ) {
-					$tax_amount         = ( $total_refunded && $display_refunded ) ? wc_price( WC_Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), array( 'currency' => $this->get_currency() ) ) : $tax->formatted_amount;
+					$tax_amount         = ( $total_refunded && $display_refunded ) ? wc_price( WC_Tax::round( (float) $tax->amount - (float) $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), array( 'currency' => $this->get_currency() ) ) : $tax->formatted_amount;
 					$tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label );
 				}
 			} elseif ( ! empty( $tax_totals ) ) {
-				$tax_amount         = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax();
+				$tax_amount         = ( $total_refunded && $display_refunded ) ? (float) $this->get_total_tax() - (float) $this->get_total_tax_refunded() : $this->get_total_tax();
 				$tax_string_array[] = sprintf( '%s %s', wc_price( $tax_amount, array( 'currency' => $this->get_currency() ) ), WC()->countries->tax_or_vat() );
 			}

@@ -243,7 +243,7 @@ class WC_Order extends WC_Abstract_Order {
 		}

 		if ( $total_refunded && $display_refunded ) {
-			$formatted_total = '<del aria-hidden="true">' . wp_strip_all_tags( $formatted_total ) . '</del> <ins>' . wc_price( $order_total - $total_refunded, array( 'currency' => $this->get_currency() ) ) . $tax_string . '</ins>';
+			$formatted_total = '<del aria-hidden="true">' . wp_strip_all_tags( $formatted_total ) . '</del> <ins>' . wc_price( (float) $order_total - (float) $total_refunded, array( 'currency' => $this->get_currency() ) ) . $tax_string . '</ins>';
 		} else {
 			$formatted_total .= $tax_string;
 		}
@@ -2490,7 +2490,7 @@ class WC_Order extends WC_Abstract_Order {
 	 * @return string
 	 */
 	public function get_remaining_refund_amount() {
-		return wc_format_decimal( $this->get_total() - $this->get_total_refunded(), wc_get_price_decimals() );
+		return wc_format_decimal( (float) $this->get_total() - (float) $this->get_total_refunded(), wc_get_price_decimals() );
 	}

 	/**
@@ -2499,7 +2499,7 @@ class WC_Order extends WC_Abstract_Order {
 	 * @return int
 	 */
 	public function get_remaining_refund_items() {
-		return absint( $this->get_item_count() - $this->get_item_count_refunded() );
+		return absint( (int) $this->get_item_count() - (int) $this->get_item_count_refunded() );
 	}

 	/**
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index ee1d45c31c5..505cef8ed4a 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -13074,24 +13074,6 @@ parameters:
 			count: 1
 			path: includes/class-wc-order.php

-		-
-			message: '#^Binary operation "\-" between float and non\-falsy\-string results in an error\.$#'
-			identifier: binaryOp.invalid
-			count: 1
-			path: includes/class-wc-order.php
-
-		-
-			message: '#^Binary operation "\-" between float and string results in an error\.$#'
-			identifier: binaryOp.invalid
-			count: 1
-			path: includes/class-wc-order.php
-
-		-
-			message: '#^Binary operation "\-" between int\|string and string results in an error\.$#'
-			identifier: binaryOp.invalid
-			count: 1
-			path: includes/class-wc-order.php
-
 		-
 			message: '#^Call to an undefined method WC_Order_Item\:\:get_item_downloads\(\)\.$#'
 			identifier: method.notFound
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-crud-orders.php b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-crud-orders.php
index 6cfde5c21b0..dbd21a31a3d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-crud-orders.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-crud-orders.php
@@ -1040,6 +1040,40 @@ class WC_Tests_CRUD_Orders extends WC_Unit_Test_Case {
 		$this->assertEquals( '<span class="woocommerce-Price-amount amount"><bdi><span class="woocommerce-Price-currencySymbol" translate="no">&#36;</span>100.00</bdi></span>', $object->get_formatted_order_total() );
 	}

+	/**
+	 * Test: get_formatted_order_total with refunded amount does not throw TypeError on PHP 8.3.
+	 */
+	public function test_get_formatted_order_total_with_refund() {
+		$object = new WC_Order();
+		$object->set_total( 100 );
+		$object->set_currency( 'USD' );
+		$id = $object->save();
+
+		wc_create_refund(
+			array(
+				'order_id'   => $id,
+				'amount'     => '25',
+				'line_items' => array(),
+			)
+		);
+
+		// Reload the order so it picks up the refund.
+		$order = wc_get_order( $id );
+
+		// Force a non-numeric total via filter to confirm the (float) cast prevents a TypeError on
+		// PHP 8.x. Without the cast, `non-numeric-string − float` is a fatal TypeError.
+		$non_numeric_filter = fn() => 'not-a-number';
+		add_filter( 'woocommerce_order_get_total', $non_numeric_filter );
+
+		// Should not throw a TypeError when the filter returns a non-numeric string.
+		$formatted_total = $order->get_formatted_order_total();
+
+		remove_filter( 'woocommerce_order_get_total', $non_numeric_filter );
+
+		$this->assertStringContainsString( '<del', $formatted_total );
+		$this->assertStringContainsString( '<ins', $formatted_total );
+	}
+
 	/**
 	 * Test: set_status
 	 */