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">$</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
*/