Merchlint
Available now
Next, in this order
  • DeutschDE
  • EspañolES
  • FrançaisFR
  • ItalianoIT
  • PortuguêsPT
  • NederlandsNL
  • SvenskaSV
  • TürkçeTR

A language shows up here once its pages are actually written. Nothing on this site is machine-translated.

Install free
← All checks

A sale that will not stop

The end date passed weeks ago and the product is still selling at the promotional price. Every order since then has cost you the difference.

Group
Money
Confidence
F — fact from the database
Version
Free
Updated
Code
A4

The symptom

You set a sale to run for two weeks in March. It is September. The product page still shows the crossed-out regular price and charges the promotional one.

Nobody complained, because from the customer’s side nothing is wrong. It is a good price.

What it costs

This is the rare finding where the cost is exact, not estimated: units sold since the end date × the difference between regular and sale price. No modelling, no assumptions about conversion.

The mechanism is worth understanding, because it explains why this is so common. WooCommerce does not compare dates when it renders a product. It stores three values — _regular_price, _sale_price and _price — and a scheduled job called wc_scheduled_sales rewrites _price back to the regular one when a sale expires.

That job runs on the queue. If the queue has stopped, the sale never ends, and it never ends quietly — no notice, no email, no entry in any log. See scheduled actions not running: on most stores these two findings appear together, and fixing the queue is the actual repair.

How to check it yourself

The important part of this query is the last line: it only reports products where the discount is still in effect, not merely products with an old date lying in a meta field.

wp db query "SELECT p.ID, p.post_title,
    FROM_UNIXTIME(dt.meta_value) AS ended,
    rp.meta_value AS regular, sp.meta_value AS sale
  FROM wp_posts p
  JOIN wp_postmeta dt ON dt.post_id = p.ID AND dt.meta_key = '_sale_price_dates_to'
  JOIN wp_postmeta rp ON rp.post_id = p.ID AND rp.meta_key = '_regular_price'
  JOIN wp_postmeta sp ON sp.post_id = p.ID AND sp.meta_key = '_sale_price'
  JOIN wp_postmeta pr ON pr.post_id = p.ID AND pr.meta_key = '_price'
  WHERE p.post_status = 'publish'
    AND dt.meta_value <> '' AND dt.meta_value < UNIX_TIMESTAMP()
    AND pr.meta_value = sp.meta_value"

Replace wp_ with your table prefix. Variations keep their own copies of these three fields, so on a variable catalogue add post_type IN ('product','product_variation') — a parent that looks fine can still have one variation stuck on last spring’s price.

How to fix it

  1. Let WooCommerce do it, once, by hand:

    wp eval 'wc_scheduled_sales();'

    This is the same function the queue was supposed to run. It clears expired sales properly, including the _price field and the product’s cached lookup row.

  2. Fix the queue, or it happens again next quarter. This is the real repair.

  3. Then decide what to do about the orders already placed. Nothing forces a decision here, but it is worth knowing the number before it becomes a surprise in a margin report.

Editing _price directly in the database is the one thing not to do — WooCommerce keeps derived copies in its lookup tables, and a hand-edited row makes the catalogue and the reports disagree.

When it is a false positive

  • You extended the promotion on purpose and never updated the end date. Common, harmless, and worth tidying so the next audit is quiet.
  • The sale price equals the regular price. Some importers write both fields identically; there is no discount to lose. Merchlint excludes these.
  • A price plugin overrides everything — dynamic pricing, role-based pricing, wholesale rules. Those set the final price at runtime, so the stored fields say little. If you run one, treat this finding as a prompt to check, not as a verdict.

How Merchlint finds it

Merchlint reads _sale_price_dates_to, _regular_price, _sale_price and _price for every published product and variation, and reports the item only when the end date is in the past and _price still equals _sale_price — that is, when the discount is genuinely still being charged. The finding shows the end date, both prices and the difference.

Confidence: F — fact from the database. Two stored values are compared against a stored date; nothing is inferred.

Where order data is available, the finding also carries the number of units sold since the end date and the resulting difference — one of the few figures in the whole report that is arithmetic rather than estimate.

Run this check on your store

Disagrees with what you see in your store? Write to us — we read every message. contact@merchlint.com