It sold all year and today it cannot be bought
Products with twelve months of order history that no customer can buy right now — listed with what each one earned, so the list starts where the money is.
The symptom
There is no symptom. That is the entire problem.
A product that sold steadily for a year stops being purchasable — out of stock, unpurchasable, no valid price — and nothing anywhere says so. No error, no notice, no email. Orders for it simply stop arriving, and a shop that receives orders every day does not notice the absence of one particular product among them.
You find out months later, usually because a customer asks.
What it costs
This is the check the whole plugin exists for, and the reason is arithmetic: you already know what the product is worth. It earned a specific amount over the past twelve months. Today it earns nothing. The difference is not a projection — it is the product’s own track record.
The reason it hides so well is that WooCommerce has both halves of the answer and never puts them together:
- The stock report lists everything that is out of stock — including three thousand products that never sold a single unit. Nobody reads it twice.
- The sales report lists what sold — but says nothing about whether you can still buy it.
The interesting set is the intersection: things that sold and cannot be bought. No screen in WooCommerce shows it, no plugin in the free directory showed it when this one was written, and it is the one list a shop owner would actually act on.
How to check it yourself
Two steps. First, what earned money in the last twelve months — WooCommerce keeps an analytics table for exactly this, which is far cheaper to query than order line items:
wp db query "SELECT product_id, SUM(product_net_revenue) AS revenue, SUM(product_qty) AS qty
FROM wp_wc_order_product_lookup
WHERE date_created > DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY product_id
ORDER BY revenue DESC"
Then check the current state of the top of that list. Do not do this in SQL — ask WooCommerce, because purchasability depends on stock status, price, product type, variations and status together, and reproducing that logic in a query is how you produce a wrong list:
wp eval '
foreach ( [123, 456, 789] as $id ) { // ids from the query above
$p = wc_get_product( $id );
if ( ! $p ) { echo "$id: gone\n"; continue; }
if ( ! $p->is_purchasable() || ! $p->is_in_stock() ) {
printf( "%d\t%s\t%s\n", $id, $p->get_name(), $p->get_stock_status() );
}
}'
How to fix it
- Work from the top of the money column and stop early. Three products restocked is a result you can measure this week. Two hundred products reviewed is a week with nothing to show.
- Decide, do not restock reflexively. For each one there are three honest answers: bring it back, replace it with the successor product and redirect, or take it out of the catalogue so it stops absorbing traffic — see sold, then hidden for why hidden beats visible-and-dead.
- Check whether it was a decision at all. A product that went out of stock on the day of an import is not the same story as one that sold out.
- Then look at what it points to. A supplier who stopped delivering, a variation that was deleted, a stock sync that has not run since spring — the finding is often a symptom of something with a wider reach.
When it is a false positive
- Deliberately discontinued lines. Kept in the catalogue for order history, correctly. Hide the finding and it stays hidden across scans and updates.
- Seasonal goods in the wrong half of the year.
- One-off or limited items — vintage, handmade, single-piece stock. They sold once and were never meant to come back.
- Products replaced by a successor SKU. The revenue moved to the new product; the old one is correctly dead. Worth a redirect rather than a restock.
How Merchlint finds it
Two stages, and the split matters more than it looks.
A narrow read of the analytics table selects candidates. wc_order_product_lookup is the
only place where revenue per product is already aggregated; walking orders through the normal API
to sum twelve months of sales on a store with twenty thousand orders is not something you do while
somebody waits.
Then WooCommerce confirms each candidate through wc_get_product() before anything is
reported. Purchasability is decided by WooCommerce itself, not by our reading of the tables.
The consequence is worth stating plainly, because it is a deliberate design choice: if the underlying schema changes, this check can miss a finding — it can never invent one. Silence under-reports; it does not lie. On a store where the analytics table is empty or disabled, the check says it could not run, rather than reporting zero problems.
Confidence: F — fact from the database, confirmed a second time through WooCommerce’s own API.
Revenue is never summed across currencies. A store selling in euros and złoty gets a separate figure for each, with the currency shown, because adding them without a rate would be inventing a number — and one invented number costs the credibility of the entire report.
Disagrees with what you see in your store? Write to us — we read every message. contact@merchlint.com