Commit 45bb71760a4 for woocommerce

commit 45bb71760a4790cafa0ac6d28e2b134a3ca50be8
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Mon Jun 29 11:33:45 2026 +0200

    Fix variation bulk sale schedule dates (#66013)

    * Fix: validate end-date >= start-date in variation bulk sale-schedule action

    The "Set scheduled sale dates" bulk action on a variable product's
    Variations tab used two raw window.prompt() calls with no cross-field
    validation. A user could enter an end date earlier than the start
    date, which silently saved an empty sale window (variation never on
    sale).

    Add a client-side check after the second prompt that parses both
    values with Date.parse() and aborts the bulk action with an alert
    when end < start, matching the constraint the single-variation
    datepicker already enforces via minDate/maxDate.

    Fixes #64634

    * Fix variation bulk sale schedule dates

    * Update changelog for variation bulk sale dates

    * clean up test

    * improve logic

    * use const

    ---------

    Co-authored-by: Samrat Biswas <samrat.biswas@automattic.com>

diff --git a/plugins/woocommerce/changelog/64634-fix-variation-bulk-sale-schedule-end-before-start b/plugins/woocommerce/changelog/64634-fix-variation-bulk-sale-schedule-end-before-start
new file mode 100644
index 00000000000..1382cab6848
--- /dev/null
+++ b/plugins/woocommerce/changelog/64634-fix-variation-bulk-sale-schedule-end-before-start
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Variation bulk action "Set scheduled sale dates": block submission when the sale end date is earlier than the sale start date, and clear sale dates submitted as blank instead of storing 1970-01-01.
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js
index 9a04122d4d0..1d37ce9e4c5 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js
@@ -1336,7 +1336,6 @@ jQuery( function ( $ ) {
 					data.date_to = window.prompt(
 						woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end
 					);
-
 					if ( null === data.date_from ) {
 						data.date_from = false;
 					}
@@ -1347,6 +1346,20 @@ jQuery( function ( $ ) {

 					if ( false === data.date_to && false === data.date_from ) {
 						cancel = true;
+					} else if ( data.date_from && data.date_to ) {
+						const from_ts = Date.parse( data.date_from );
+						const to_ts = Date.parse( data.date_to );
+
+						if (
+							! isNaN( from_ts ) &&
+							! isNaN( to_ts ) &&
+							to_ts < from_ts
+						) {
+							window.alert(
+								woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end_before_start
+							);
+							cancel = true;
+						}
 					}
 					break;
 				default:
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index 88ca40adecf..d627054ad50 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -582,6 +582,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
 					'i18n_remove_variation'               => esc_js( __( 'Are you sure you want to remove this variation?', 'woocommerce' ) ),
 					'i18n_scheduled_sale_start'           => esc_js( __( 'Sale start date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ),
 					'i18n_scheduled_sale_end'             => esc_js( __( 'Sale end date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ),
+					'i18n_scheduled_sale_end_before_start' => esc_js( __( 'The sale end date cannot be earlier than the sale start date.', 'woocommerce' ) ),
 					'i18n_edited_variations'              => esc_js( __( 'Save changes before changing page?', 'woocommerce' ) ),
 					'i18n_variation_count_single'         => esc_js( __( '1 variation', 'woocommerce' ) ),
 					'i18n_variation_count_plural'         => esc_js( __( '%qty% variations', 'woocommerce' ) ),
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index 4dea2ad4f33..599c9d2a7ce 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -3010,16 +3010,19 @@ class WC_AJAX {
 			return;
 		}

+		$date_from = isset( $data['date_from'] ) ? wc_clean( $data['date_from'] ) : false;
+		$date_to   = isset( $data['date_to'] ) ? wc_clean( $data['date_to'] ) : false;
+
 		foreach ( $variations as $variation_id ) {
 			$variation = wc_get_product( $variation_id );

-			if ( 'false' !== $data['date_from'] ) {
-				$date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( wc_clean( $data['date_from'] ) ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+			if ( false !== $date_from && 'false' !== $date_from ) {
+				$date_on_sale_from = '' === $date_from ? null : date( 'Y-m-d 00:00:00', strtotime( $date_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
 				$variation->set_date_on_sale_from( $date_on_sale_from );
 			}

-			if ( 'false' !== $data['date_to'] ) {
-				$date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( wc_clean( $data['date_to'] ) ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+			if ( false !== $date_to && 'false' !== $date_to ) {
+				$date_on_sale_to = '' === $date_to ? null : date( 'Y-m-d 23:59:59', strtotime( $date_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
 				$variation->set_date_on_sale_to( $date_on_sale_to );
 			}

diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index d365b6e6ab1..414bdbe8193 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -606,6 +606,38 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
 		$product->delete( true );
 	}

+	/**
+	 * @testdox Should clear variation sale dates when bulk schedule dates are blank.
+	 * @group ajax
+	 */
+	public function test_bulk_sale_schedule_clears_blank_dates(): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_date_on_sale_from( '2026-06-01 00:00:00' );
+		$variation->set_date_on_sale_to( '2026-06-30 23:59:59' );
+		$variation->save();
+
+		$method = new ReflectionMethod( WC_AJAX::class, 'variation_bulk_action_variable_sale_schedule' );
+		$method->setAccessible( true );
+
+		$method->invokeArgs(
+			null,
+			array(
+				array( $variation->get_id() ),
+				array(
+					'date_from' => '',
+					'date_to'   => '',
+				),
+			)
+		);
+
+		$variation = wc_get_product( $variation->get_id() );
+
+		$this->assertNull( $variation->get_date_on_sale_from( 'edit' ), 'The sale start date should be cleared when the bulk action start date is blank.' );
+		$this->assertNull( $variation->get_date_on_sale_to( 'edit' ), 'The sale end date should be cleared when the bulk action end date is blank.' );
+
+		$variation->delete( true );
+	}
+
 	/**
 	 * Does the 'hard work' of triggering an ajax endpoint and capturing the response.
 	 *