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

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.

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

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Run this check on your store

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