Commit e584c70f906 for woocommerce
commit e584c70f9066ec8dca4f715aca724e0d55d0f417
Author: Tung Du <dinhtungdu@gmail.com>
Date: Mon Nov 2 21:25:35 2026 +0700
Don't grant downloads for order lines refunded before the order is completed (#70535)
* Add regression test for downloads of lines refunded before completion
Fails on trunk: completing an order, or regenerating its permissions,
grants downloads for lines that a refund already covered.
Amp-Thread-ID: https://ampcode.com/threads/T-01a19b55-1079-7498-bf68-59f7bd4fbe9a
Co-authored-by: Amp <amp@ampcode.com>
* Don't grant download permissions for lines refunded before completion
wc_downloadable_product_permissions() granted every downloadable line, so a
line refunded while the order was on hold, or one revoked by a refund and then
regenerated, got a working download. Skip every line a refund names, which is
the same set wc_create_refund() revokes.
Amp-Thread-ID: https://ampcode.com/threads/T-01a19b55-1079-7498-bf68-59f7bd4fbe9a
Co-authored-by: Amp <amp@ampcode.com>
* Reword the refunded downloads test in plain English
Amp-Thread-ID: https://ampcode.com/threads/T-01a19b55-1079-7498-bf68-59f7bd4fbe9a
Co-authored-by: Amp <amp@ampcode.com>
* Remove fixed PHPStan baseline entry for wc_downloadable_file_permission
Amp-Thread-ID: https://ampcode.com/threads/T-01a19b55-1079-7498-bf68-59f7bd4fbe9a
Co-authored-by: Amp <amp@ampcode.com>
---------
Co-authored-by: Amp <amp@ampcode.com>
diff --git a/plugins/woocommerce/changelog/fix-refunded-download-permissions b/plugins/woocommerce/changelog/fix-refunded-download-permissions
new file mode 100644
index 00000000000..8a2e7a12d49
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-refunded-download-permissions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Don't grant downloads for order lines that were refunded before the order was completed.
diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php
index 628c8191198..50fc95bef22 100644
--- a/plugins/woocommerce/includes/wc-order-functions.php
+++ b/plugins/woocommerce/includes/wc-order-functions.php
@@ -469,7 +469,7 @@ function wc_downloadable_file_permission( $download_id, $product, $order, $qty =
function wc_downloadable_product_permissions( $order_id, $force = false ) {
$order = wc_get_order( $order_id );
- if ( ! $order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) {
+ if ( ! $order instanceof WC_Order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) {
return;
}
@@ -477,9 +477,20 @@ function wc_downloadable_product_permissions( $order_id, $force = false ) {
return;
}
+ $refunded_item_ids = array();
+ foreach ( $order->get_refunds() as $refund ) {
+ foreach ( $refund->get_items() as $refund_item ) {
+ $refunded_item_ids[] = absint( $refund_item->get_meta( '_refunded_item_id' ) );
+ }
+ }
+
$line_items = $order->get_items();
if ( count( $line_items ) > 0 ) {
foreach ( $line_items as $item ) {
+ if ( in_array( $item->get_id(), $refunded_item_ids, true ) ) {
+ continue;
+ }
+
$product = $item->get_product();
if ( $product && $product->exists() && $product->is_downloadable() ) {
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 5d2afeb5600..0aa5d25acf6 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -34005,11 +34005,6 @@ parameters:
count: 2
path: includes/wc-order-functions.php
- -
- message: '#^Parameter \#3 \$order of function wc_downloadable_file_permission expects WC_Order, WC_Order\|WC_Order_Refund given\.$#'
- identifier: argument.type
- count: 1
- path: includes/wc-order-functions.php
-
message: '#^Call to an undefined method WC_Data_Store\:\:add_metadata\(\)\.$#'
diff --git a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
index 1ed39c627ab..d8d0961b026 100644
--- a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
@@ -1138,4 +1138,66 @@ class WC_Order_Functions_Test extends \WC_Unit_Test_Case {
$this->assertSame( array(), array_values( $coupon->get_used_by() ) );
$this->assertEquals( 0, $coupon->get_usage_count() );
}
+
+ /**
+ * @testdox Completing an order, or regenerating its downloads, skips lines that were refunded.
+ */
+ public function test_wc_downloadable_product_permissions_skips_refunded_lines() {
+ wc_get_container()->get( Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register::class )->add_approved_directory( 'https://always.trusted' );
+ $products = array();
+ foreach ( array( 'qty-refunded', 'amount-refunded', 'kept' ) as $name ) {
+ $products[ $name ] = WC_Helper_Product::create_downloadable_product(
+ array(
+ array(
+ 'name' => $name,
+ 'file' => "https://always.trusted/$name.pdf",
+ ),
+ )
+ );
+ }
+
+ $order = new WC_Order();
+ foreach ( $products as $product ) {
+ $order->add_product( $product );
+ }
+ $order->calculate_totals();
+ $order->set_status( OrderStatus::ON_HOLD );
+ $order->save();
+
+ $items = array_values( $order->get_items() );
+ wc_create_refund(
+ array(
+ 'amount' => $items[0]->get_total() + 1,
+ 'order_id' => $order->get_id(),
+ 'line_items' => array(
+ $items[0]->get_id() => array(
+ 'qty' => 1,
+ 'refund_total' => $items[0]->get_total(),
+ ),
+ $items[1]->get_id() => array(
+ 'qty' => 0,
+ 'refund_total' => 1,
+ ),
+ ),
+ )
+ );
+
+ wc_get_order( $order->get_id() )->update_status( OrderStatus::COMPLETED );
+ $this->assertSame( array( $products['kept']->get_id() ), $this->get_download_permission_product_ids( $order ), 'Completing the order should give a download only for the line that was not refunded.' );
+
+ WC_Data_Store::load( 'customer-download' )->delete_by_order_id( $order->get_id() );
+ wc_downloadable_product_permissions( $order->get_id(), true );
+ $this->assertSame( array( $products['kept']->get_id() ), $this->get_download_permission_product_ids( $order ), 'Regenerating the downloads should give a download only for the line that was not refunded.' );
+ }
+
+ /**
+ * Get the product IDs of an order's download permissions.
+ *
+ * @param WC_Order $order The order.
+ * @return int[]
+ */
+ private function get_download_permission_product_ids( WC_Order $order ): array {
+ $downloads = WC_Data_Store::load( 'customer-download' )->get_downloads( array( 'order_id' => $order->get_id() ) );
+ return array_map( fn( $download ) => (int) $download->get_product_id(), $downloads );
+ }
}