Automattic\WooCommerce\Internal
ScheduledSalePriceReconciler::reconcile_price
Reconcile the active price with the sale schedule when the stored price is stale.
Intentionally not covered: a variable parent's aggregate display (the "From" price, on-sale badge, and min/max prices). Those values are served from the wc_var_prices_{id} transient without passing through this filter, and the cache key has no sale-window component, so they can stay stale until the boundary event or a save refreshes the cache. Covering them would mean changing cache keys or invalidation, not a read-time filter — scoped out along with the product lookup table. The gap is display-only: the cart prices the variation itself, which re-derives its price from the sale dates on every read. Other skipped cases are noted on the guards below.
Method of the class: ScheduledSalePriceReconciler{}
No Hooks.
Returns
string. The active price, reconciled with the sale schedule when applicable.
Usage
$ScheduledSalePriceReconciler = new ScheduledSalePriceReconciler(); $ScheduledSalePriceReconciler->reconcile_price( $price, $product );
- $price(string) (required)
- The product's active price.
- $product(WC_Product) (required)
- The product object.
ScheduledSalePriceReconciler::reconcile_price() ScheduledSalePriceReconciler::reconcile price code WC 11.2.1
public function reconcile_price( $price, $product ) {
if ( ! $product instanceof WC_Product || '' === (string) $price ) {
return $price;
}
// Fast path: products with no sale price (the vast majority, plus variable/grouped
// parents whose own sale price is empty) can never drift out of sync.
$sale_price = $product->get_sale_price( 'edit' );
if ( '' === (string) $sale_price ) {
return $price;
}
// Only scheduled sales can drift: an unscheduled sale's price is fixed at save time.
$date_from = $product->get_date_on_sale_from( 'edit' );
$date_to = $product->get_date_on_sale_to( 'edit' );
if ( ! $date_from && ! $date_to ) {
return $price;
}
// Require a real discount. The strict `>` (i.e. bail on `<=`) mirrors is_on_sale() so
// the resolved price never contradicts the on-sale flag.
$regular_price = $product->get_regular_price( 'edit' );
if ( '' === (string) $regular_price || (float) $regular_price <= (float) $sale_price ) {
return $price;
}
// Respect third-party prices: if another callback already changed the price away from
// the stored value, leave it alone. This is deliberate even for ambient transforms
// such as currency conversion: by this priority the price is already transformed, and
// returning a raw sale/regular price would bypass the transform, which cannot be
// re-applied from here. Those stores keep their pre-existing behavior during the gap.
// The 'edit' context returns the raw stored price without re-applying this filter, so
// there is no recursion.
$stored_price = $product->get_price( 'edit' );
if ( (string) $price !== (string) $stored_price ) {
return $price;
}
// Pure date-window check, intentionally inline rather than calling is_on_sale() to
// avoid recursing through plugins that read get_price() from a
// woocommerce_product_is_on_sale callback.
$now = time();
$within_window = ! ( ( $date_from && $date_from->getTimestamp() > $now ) || ( $date_to && $date_to->getTimestamp() < $now ) );
// Reconcile only when the stored price is exactly the sale or regular price. A
// deliberate custom `_price` that happens to equal one of them is indistinguishable
// from a stale value and gets reconciled; any other custom value falls through.
if ( ! $within_window && (float) $stored_price === (float) $sale_price ) {
return $regular_price;
}
if ( $within_window && (float) $stored_price === (float) $regular_price ) {
return $sale_price;
}
return $price;
}