Commit 4f5b37676c5 for woocommerce
commit 4f5b37676c59af7ce07eaff83d90e4a33ff59a7e
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date: Thu Oct 29 16:11:51 2026 +0200
Fix CSV import ending sales a day early for date-only end dates (#66634)
* feat: add end-of-day parser for CSV 'date on sale to' field
Date-only values are bumped to 23:59:59 to match the product edit
screen; explicit time components and Unix timestamps are preserved.
Part of the fix for #35321.
* fix: store CSV 'date sale price ends' as end-of-day to match admin
Closes #35321.
* chore: add changelog for #35321 CSV sale end date fix
* refactor: use wp_tempnam() for temp CSV fixtures in importer tests
* fix: update legacy importer test for end-of-day sale end date parsing
* fix: keep CSV sale end date on its calendar day in any timezone
* fix: detect CSV sale end time with date_parse, not a colon regex
* test: drop redundant CSV sale end date parser tests
* chore: tighten changelog entry for CSV sale end date fix
* chore: trim verbose comments in CSV sale end date code and tests
* fix: keep timezone-qualified date-only sale end dates unchanged in CSV import
Date-only values that carry a timezone (e.g. 2099-01-26Z) are points in time, so pass them through via date_parse()'s is_localtime flag instead of bumping them to end of day. Adds round-trip tests and shortens the changelog entry.
* fix: treat weekday-prefixed date-only sale end dates as end of day in CSV import
* fix: satisfy PHPStan and PHP 7.4 in sale end date import test
Cast the preg_replace() probe to string so PHPStan accepts the date_parse()
argument, and drop the hour-only ISO assertion, which strtotime() rejects
on PHP 7.4.
* refactor: trim comments in the CSV sale end date parser and its tests
* chore: bump @since to 11.4.0
* fix: restore @since 11.3.0 on existing method
* fix: put @since 11.4.0 on the new method, keep 11.3.0 on the existing one
diff --git a/plugins/woocommerce/changelog/fix-35321-csv-import-sale-end-date b/plugins/woocommerce/changelog/fix-35321-csv-import-sale-end-date
new file mode 100644
index 00000000000..98342b72377
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-35321-csv-import-sale-end-date
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Imported sale end dates without a time now run to the end of that day, matching the product editor.
diff --git a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
index b7177108d19..075eec53a92 100644
--- a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
+++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
@@ -727,6 +727,42 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
return null;
}
+ /**
+ * Parse the "date on sale to" field from a CSV.
+ *
+ * A date-only value becomes the end of that day (23:59:59), matching the product edit
+ * screen. Values with a time, a timezone, a Unix timestamp or a relative expression are
+ * left as written.
+ *
+ * @since 11.4.0
+ *
+ * @param string $value Field value.
+ *
+ * @return string|null
+ */
+ public function parse_date_on_sale_to_field( $value ) {
+ $parsed = $this->parse_datetime_field( $value );
+
+ if ( null === $parsed ) {
+ return null;
+ }
+
+ // date_parse() reports hour=0 for "Monday, January 26, 2099", so strip the weekday before probing.
+ $probe = (string) preg_replace( '/^(?:mon|tue(?:s)?|wed(?:nes)?|thu(?:rs)?|fri|sat(?:ur)?|sun)(?:day)?\.?,?\s+/i', '', $parsed );
+ $date = date_parse( $probe );
+
+ // Date-only input reports hour === false; a time, a timezone (is_localtime) or a relative expression does not.
+ if (
+ false === $date['year'] || false === $date['month'] || false === $date['day']
+ || false !== $date['hour'] || isset( $date['relative'] ) || ! empty( $date['is_localtime'] )
+ ) {
+ return $parsed;
+ }
+
+ // Built from Y/M/D, not a timestamp, so no offset can move the day.
+ return sprintf( '%04d-%02d-%02d 23:59:59', $date['year'], $date['month'], $date['day'] );
+ }
+
/**
* Parse backorders from a CSV.
*
@@ -896,7 +932,7 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
'published' => array( $this, 'parse_published_field' ),
'featured' => array( $this, 'parse_bool_field' ),
'date_on_sale_from' => array( $this, 'parse_datetime_field' ),
- 'date_on_sale_to' => array( $this, 'parse_datetime_field' ),
+ 'date_on_sale_to' => array( $this, 'parse_date_on_sale_to_field' ),
'name' => array( $this, 'parse_skip_field' ),
'short_description' => array( $this, 'parse_description_field' ),
'description' => array( $this, 'parse_description_field' ),
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
index 185a419c3d7..396911040bf 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
@@ -663,7 +663,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
'short_description' => 'Lorem ipsum dolor sit amet, at exerci civibus appetere sit, iuvaret hendrerit mea no. Eam integre feugait liberavisse an.',
'description' => 'Lorem ipsum dolor sit amet, at exerci civibus appetere sit, iuvaret hendrerit mea no. Eam integre feugait liberavisse an.',
'date_on_sale_from' => '2023-07-08 05:10:15',
- 'date_on_sale_to' => '2023/07/13',
+ 'date_on_sale_to' => '2023-07-13 23:59:59',
'tax_status' => ProductTaxStatus::TAXABLE,
'tax_class' => 'standard',
'stock_status' => ProductStockStatus::IN_STOCK,
diff --git a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
index 18fa7d90186..de6b9ff619e 100644
--- a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
+++ b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
@@ -1872,6 +1872,342 @@ class WC_Product_CSV_Importer_Test extends \WC_Unit_Test_Case {
$this->assertSame( '', $importer->parse_float_field( '' ), 'Empty values should be returned unchanged.' );
}
+ /**
+ * @testdox Date-only "date on sale to" values are stored as end-of-day to match admin behaviour (#35321).
+ */
+ public function test_parse_date_on_sale_to_field_date_only_is_end_of_day() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( '2099-01-26 23:59:59', $importer->parse_date_on_sale_to_field( '2099-01-26' ) );
+ $this->assertSame( '2022-10-25 23:59:59', $importer->parse_date_on_sale_to_field( '2022-10-25' ) );
+ }
+
+ /**
+ * @testdox "date on sale to" values that already include a time component are preserved.
+ */
+ public function test_parse_date_on_sale_to_field_preserves_time_component() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( '2099-01-26 10:30:00', $importer->parse_date_on_sale_to_field( '2099-01-26 10:30:00' ) );
+ $this->assertSame( '2099-01-26 10:30', $importer->parse_date_on_sale_to_field( '2099-01-26 10:30' ) );
+ $this->assertSame( '2099-01-26T10:30:00Z', $importer->parse_date_on_sale_to_field( '2099-01-26T10:30:00Z' ) );
+ }
+
+ /**
+ * @testdox "date on sale to" date-only values that carry a timezone are preserved.
+ */
+ public function test_parse_date_on_sale_to_field_preserves_timezone_qualified_date() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( '2099-01-26Z', $importer->parse_date_on_sale_to_field( '2099-01-26Z' ) );
+ $this->assertSame( '2099-01-26 +02:00', $importer->parse_date_on_sale_to_field( '2099-01-26 +02:00' ) );
+ }
+
+ /**
+ * @testdox "date on sale to" values with a colonless time component are preserved.
+ */
+ public function test_parse_date_on_sale_to_field_preserves_colonless_time_component() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( '20990126T103000', $importer->parse_date_on_sale_to_field( '20990126T103000' ) );
+ $this->assertSame( 'January 26, 2099 10am', $importer->parse_date_on_sale_to_field( 'January 26, 2099 10am' ) );
+ $this->assertSame( '2099-01-26 10.30', $importer->parse_date_on_sale_to_field( '2099-01-26 10.30' ) );
+ // Epoch syntax parses as a relative expression.
+ $this->assertSame( '@4073068800', $importer->parse_date_on_sale_to_field( '@4073068800' ) );
+ }
+
+ /**
+ * @testdox Relative "date on sale to" expressions are never bumped to end-of-day.
+ */
+ public function test_parse_date_on_sale_to_field_preserves_relative_expressions() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( 'now', $importer->parse_date_on_sale_to_field( 'now' ) );
+ $this->assertSame( '+1 week', $importer->parse_date_on_sale_to_field( '+1 week' ) );
+ $this->assertSame( 'tomorrow', $importer->parse_date_on_sale_to_field( 'tomorrow' ) );
+ }
+
+ /**
+ * @testdox Date-only "date on sale to" values in non-Y-m-d formats are normalised to Y-m-d end-of-day.
+ */
+ public function test_parse_date_on_sale_to_field_alternate_date_formats() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( '2099-01-26 23:59:59', $importer->parse_date_on_sale_to_field( '26-01-2099' ) );
+ $this->assertSame( '2099-01-26 23:59:59', $importer->parse_date_on_sale_to_field( '2099/01/26' ) );
+ $this->assertSame( '2099-01-26 23:59:59', $importer->parse_date_on_sale_to_field( 'January 26, 2099' ) );
+ }
+
+ /**
+ * @testdox Date-only "date on sale to" values with a leading weekday name (Excel "Long Date") are bumped to end-of-day.
+ */
+ public function test_parse_date_on_sale_to_field_weekday_prefixed_date_only_is_end_of_day() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( '2099-01-26 23:59:59', $importer->parse_date_on_sale_to_field( 'Monday, January 26, 2099' ) );
+ $this->assertSame( '2099-01-26 23:59:59', $importer->parse_date_on_sale_to_field( 'Mon 26 Jan 2099' ) );
+ $this->assertSame( '2099-01-26 23:59:59', $importer->parse_date_on_sale_to_field( 'Monday 26 January 2099' ) );
+ }
+
+ /**
+ * @testdox Weekday-prefixed "date on sale to" values with a time, and bare or relative weekdays, are preserved.
+ */
+ public function test_parse_date_on_sale_to_field_weekday_prefixed_values_with_time_or_relative_are_preserved() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertSame( 'Monday, January 26, 2099 00:00:00', $importer->parse_date_on_sale_to_field( 'Monday, January 26, 2099 00:00:00' ) );
+ $this->assertSame( 'Monday, January 26, 2099 10:00', $importer->parse_date_on_sale_to_field( 'Monday, January 26, 2099 10:00' ) );
+ $this->assertSame( 'next monday', $importer->parse_date_on_sale_to_field( 'next monday' ) );
+ $this->assertSame( 'monday', $importer->parse_date_on_sale_to_field( 'monday' ) );
+ }
+
+ /**
+ * @testdox "date on sale to" Unix timestamps are normalised to ISO8601 without being shifted to end-of-day.
+ */
+ public function test_parse_date_on_sale_to_field_unix_timestamp_preserved() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ // The Unix timestamp 4073068800 corresponds to 2099-01-26 00:00:00 UTC.
+ $this->assertSame( '2099-01-26T00:00:00Z', $importer->parse_date_on_sale_to_field( '4073068800' ) );
+ }
+
+ /**
+ * @testdox Empty / invalid "date on sale to" values are returned as null.
+ */
+ public function test_parse_date_on_sale_to_field_empty_and_invalid() {
+ $csv_file = __DIR__ . '/sample.csv';
+ $importer = new WC_Product_CSV_Importer( $csv_file );
+
+ $this->assertNull( $importer->parse_date_on_sale_to_field( '' ) );
+ $this->assertNull( $importer->parse_date_on_sale_to_field( 'not-a-date' ) );
+ }
+
+ /**
+ * @testdox Importing a CSV with a date-only "date sale price ends" stores end-of-day, matching admin (#35321).
+ */
+ public function test_import_date_on_sale_to_date_only_is_end_of_day() {
+ // Non-UTC zone so any day drift shows up.
+ update_option( 'timezone_string', 'Pacific/Auckland' );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_sku( 'issue-35321-sku' );
+ $product->set_regular_price( '10.00' );
+ $product->save();
+
+ $timed_product = WC_Helper_Product::create_simple_product();
+ $timed_product->set_sku( 'issue-35321-timed-sku' );
+ $timed_product->set_regular_price( '10.00' );
+ $timed_product->save();
+
+ $excel_product = WC_Helper_Product::create_simple_product();
+ $excel_product->set_sku( 'issue-35321-excel-sku' );
+ $excel_product->set_regular_price( '10.00' );
+ $excel_product->save();
+
+ // wp_tempnam() always appends .tmp; the importer requires a .csv/.txt extension.
+ $tmp_path = wp_tempnam( 'wc-csv' );
+ $csv_path = $tmp_path . '.csv';
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Tests rename a tmp file we control.
+ rename( $tmp_path, $csv_path );
+ $lines = array(
+ '"ID","date sale price starts","date sale price ends","Sale price"',
+ sprintf( '%d,"2022-10-25","2099-01-26","2.00"', $product->get_id() ),
+ sprintf( '%d,"2022-10-25 09:00:00","2099-01-26 10:30:00","2.00"', $timed_product->get_id() ),
+ // Excel "Long Date" cell format.
+ sprintf( '%d,"Tuesday, October 25, 2022","Monday, January 26, 2099","2.00"', $excel_product->get_id() ),
+ );
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Tests write to a tmp file we control.
+ file_put_contents( $csv_path, implode( "\n", $lines ) . "\n" );
+
+ $importer = new WC_Product_CSV_Importer(
+ $csv_path,
+ array(
+ 'update_existing' => true,
+ 'parse' => true,
+ 'mapping' => array(
+ 'ID' => 'id',
+ 'date sale price starts' => 'date_on_sale_from',
+ 'date sale price ends' => 'date_on_sale_to',
+ 'Sale price' => 'sale_price',
+ ),
+ )
+ );
+ $importer->import();
+
+ $updated = wc_get_product( $product->get_id() );
+ $date_to = $updated->get_date_on_sale_to();
+ $date_from = $updated->get_date_on_sale_from();
+
+ $this->assertNotNull( $date_to, 'date_on_sale_to should be set' );
+ $this->assertNotNull( $date_from, 'date_on_sale_from should be set' );
+ $this->assertSame( '23:59:59', $date_to->date( 'H:i:s' ), 'date_on_sale_to should be end-of-day (site-local) to match admin' );
+ $this->assertSame( '2099-01-26', $date_to->date( 'Y-m-d' ), 'date_on_sale_to calendar day must not drift in a non-UTC site timezone' );
+ $this->assertSame( '00:00:00', $date_from->date( 'H:i:s' ), 'date_on_sale_from should remain start-of-day' );
+ $this->assertSame( '2022-10-25', $date_from->date( 'Y-m-d' ) );
+
+ $updated_timed = wc_get_product( $timed_product->get_id() );
+ $timed_date_to = $updated_timed->get_date_on_sale_to();
+ $timed_date_from = $updated_timed->get_date_on_sale_from();
+
+ $this->assertNotNull( $timed_date_to, 'timed date_on_sale_to should be set' );
+ $this->assertNotNull( $timed_date_from, 'timed date_on_sale_from should be set' );
+ $this->assertSame( '2099-01-26 10:30:00', $timed_date_to->date( 'Y-m-d H:i:s' ), 'explicit end time must be preserved site-local, not bumped to end-of-day' );
+ $this->assertSame( '2022-10-25 09:00:00', $timed_date_from->date( 'Y-m-d H:i:s' ), 'explicit start time must be preserved site-local' );
+
+ $updated_excel = wc_get_product( $excel_product->get_id() );
+ $this->assertSame( '2099-01-26 23:59:59', $updated_excel->get_date_on_sale_to()->date( 'Y-m-d H:i:s' ), 'weekday-prefixed date-only end date must be bumped to end-of-day' );
+ $this->assertSame( '2022-10-25 00:00:00', $updated_excel->get_date_on_sale_from()->date( 'Y-m-d H:i:s' ) );
+
+ WC_Helper_Product::delete_product( $product->get_id() );
+ WC_Helper_Product::delete_product( $timed_product->get_id() );
+ WC_Helper_Product::delete_product( $excel_product->get_id() );
+ wp_delete_file( $csv_path );
+ }
+
+ /**
+ * @testdox Re-importing the same CSV leaves the sale-end timestamp and the pending end-sale action unchanged.
+ */
+ public function test_import_date_on_sale_to_reimport_is_idempotent() {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_sku( 'issue-35321-idempotent-sku' );
+ $product->set_regular_price( '10.00' );
+ $product->save();
+
+ // wp_tempnam() always appends .tmp; the importer requires a .csv/.txt extension.
+ $tmp_path = wp_tempnam( 'wc-csv' );
+ $csv_path = $tmp_path . '.csv';
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Tests rename a tmp file we control.
+ rename( $tmp_path, $csv_path );
+ $lines = array(
+ '"ID","date sale price starts","date sale price ends","Sale price"',
+ sprintf( '%d,"2022-10-25","2099-01-26","2.00"', $product->get_id() ),
+ );
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Tests write to a tmp file we control.
+ file_put_contents( $csv_path, implode( "\n", $lines ) . "\n" );
+
+ $make_importer = function () use ( $csv_path ) {
+ return new WC_Product_CSV_Importer(
+ $csv_path,
+ array(
+ 'update_existing' => true,
+ 'parse' => true,
+ 'mapping' => array(
+ 'ID' => 'id',
+ 'date sale price starts' => 'date_on_sale_from',
+ 'date sale price ends' => 'date_on_sale_to',
+ 'Sale price' => 'sale_price',
+ ),
+ )
+ );
+ };
+
+ // Args and group per wc_schedule_product_sale_events() in wc-product-functions.php.
+ $pending_end_actions = function () use ( $product ) {
+ return as_get_scheduled_actions(
+ array(
+ 'hook' => 'wc_product_end_scheduled_sale',
+ 'args' => array( 'product_id' => $product->get_id() ),
+ 'group' => 'woocommerce-sales',
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ 'per_page' => 10,
+ ),
+ 'ids'
+ );
+ };
+
+ $make_importer()->import();
+ $first = wc_get_product( $product->get_id() )->get_date_on_sale_to();
+
+ $this->assertNotNull( $first, 'date_on_sale_to should be set after the first import' );
+ $this->assertSame( '23:59:59', $first->date( 'H:i:s' ) );
+ $first_timestamp = $first->getTimestamp();
+ $this->assertCount( 1, $pending_end_actions(), 'exactly one pending end-sale action after the first import' );
+
+ $make_importer()->import();
+ $second = wc_get_product( $product->get_id() )->get_date_on_sale_to();
+
+ $this->assertSame( $first_timestamp, $second->getTimestamp(), 're-import must not change the stored sale-end timestamp' );
+ $this->assertSame( '23:59:59', $second->date( 'H:i:s' ), 'the end-of-day bump must not compound on re-import' );
+ $this->assertCount( 1, $pending_end_actions(), 'still exactly one pending end-sale action after re-import' );
+
+ WC_Helper_Product::delete_product( $product->get_id() );
+ wp_delete_file( $csv_path );
+ }
+
+ /**
+ * @testdox Re-importing a date-only value stored as midnight before the fix moves it to 23:59:59 and reschedules the end-sale action (#35321).
+ */
+ public function test_import_date_on_sale_to_first_reimport_after_upgrade_is_not_a_noop() {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_sku( 'issue-35321-upgrade-sku' );
+ $product->set_regular_price( '10.00' );
+ // Midnight is what the importer stored for a date-only value before the fix.
+ $product->set_date_on_sale_to( '2099-01-26 00:00:00' );
+ $product->save();
+
+ $pending_end_actions = function () use ( $product ) {
+ return as_get_scheduled_actions(
+ array(
+ 'hook' => 'wc_product_end_scheduled_sale',
+ 'args' => array( 'product_id' => $product->get_id() ),
+ 'group' => 'woocommerce-sales',
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ 'per_page' => 10,
+ ),
+ 'ids'
+ );
+ };
+
+ $before = wc_get_product( $product->get_id() )->get_date_on_sale_to();
+ $this->assertSame( '00:00:00', $before->date( 'H:i:s' ), 'the pre-upgrade value should start at midnight' );
+
+ // wp_tempnam() always appends .tmp; the importer requires a .csv/.txt extension.
+ $tmp_path = wp_tempnam( 'wc-csv' );
+ $csv_path = $tmp_path . '.csv';
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Tests rename a tmp file we control.
+ rename( $tmp_path, $csv_path );
+ $lines = array(
+ '"ID","date sale price ends","Sale price"',
+ sprintf( '%d,"2099-01-26","2.00"', $product->get_id() ),
+ );
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Tests write to a tmp file we control.
+ file_put_contents( $csv_path, implode( "\n", $lines ) . "\n" );
+
+ $importer = new WC_Product_CSV_Importer(
+ $csv_path,
+ array(
+ 'update_existing' => true,
+ 'parse' => true,
+ 'mapping' => array(
+ 'ID' => 'id',
+ 'date sale price ends' => 'date_on_sale_to',
+ 'Sale price' => 'sale_price',
+ ),
+ )
+ );
+ $importer->import();
+
+ $after = wc_get_product( $product->get_id() )->get_date_on_sale_to();
+
+ $this->assertNotNull( $after, 'date_on_sale_to should still be set after the re-import' );
+ $this->assertSame( '23:59:59', $after->date( 'H:i:s' ), 'the same date-only value re-imported after upgrade should move to end-of-day' );
+ $this->assertSame( '2099-01-26', $after->date( 'Y-m-d' ), 'the calendar day should not change' );
+ $this->assertNotSame( $before->getTimestamp(), $after->getTimestamp(), 'the stored timestamp must change: this re-import is not a no-op' );
+ $this->assertCount( 1, $pending_end_actions(), 'exactly one pending end-sale action after the re-import, rescheduled for the new end-of-day time' );
+
+ WC_Helper_Product::delete_product( $product->get_id() );
+ wp_delete_file( $csv_path );
+ }
+
/**
* @testdox Constructing the importer with a CSV file that cannot be opened should fail with a clear error.
*/