=== Merchlint – Duplicate SKU and GTIN Finder for WooCommerce ===
Contributors: baltano
Tags: duplicate sku, gtin, ean, upc, invalid sku
Requires at least: 6.5
Tested up to: 7.1
Requires PHP: 7.4
Requires Plugins: woocommerce
Stable tag: 0.1.3
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Finds products sharing an SKU, and products sharing or missing a GTIN. Read-only: it never writes to your catalogue.

== Description ==

Two products with the same SKU throw off stock levels, integrations and every export file your store
produces. Two products with the same GTIN tell a shopping service they are the same item, so one of
them quietly disappears from search. A product with no identifier at all usually drops out of the
feed altogether.

None of this breaks your store, which is why it goes unnoticed for years. Nothing in the WooCommerce
admin points at any of it.

This plugin checks three things and nothing else:

* **Two products share the same SKU.** Translations of one product are not counted as a duplicate —
  a multilingual store gives every translation the same code on purpose, and reporting that pair
  would be a false alarm. We read the translation groups of Polylang and WPML.
* **Product with no GTIN, UPC, EAN or ISBN.** Stays quiet in shops that do not use identifiers at
  all: if not one product has the field filled, you get a single sentence saying so instead of a
  list of your entire catalogue. Handmade goods, services and your own production carry no
  manufacturer's barcode and need none.
* **Two products share the same identifier.**

= What it does not do =

It **never writes to your catalogue**. There is no "Fix it" button, and there will not be one: a
bulk change to product data is not something a report should do on your behalf. Every finding shows
the value that was read, so you can check the claim before you act on it.

It reads no order data, which makes the scan far cheaper than a full audit. The scan still runs
in a single request, so on a very large catalogue it can run into your host's time limit — the
screen says so when that becomes likely, and the WP-CLI command has no such limit.

= Evidence, not adjectives =

Every finding shows the field that was read and the value found, with a link straight to the product
in your admin. A rule that could not run says so by name — on WooCommerce older than 9.2 the
identifier field does not exist at all, and the report says that rather than reporting every product
as faulty.

= A small part of Merchlint =

These three checks come from [Merchlint](https://wordpress.org/plugins/merchlint/), a free audit of
the whole WooCommerce catalogue from the same author. Besides SKUs and identifiers it finds products
that sold in the past year and can no longer be bought, products with no price or no photo, sale
prices that should have ended and images with no alt text, each with the money at stake. This
plugin carries its own copy of the scanning engine with its own class names, so both can run side
by side.

== Installation ==

1. Install and activate.
2. Open **Tools → Duplicate SKU and GTIN Finder**.
3. Press **Scan now**.

For large catalogues there is a WP-CLI command: `wp merchlint-duplicate-sku-finder scan`.

== Frequently Asked Questions ==

= How do I find duplicate SKUs in WooCommerce? =

Install this plugin, open **Tools → Duplicate SKU and GTIN Finder** and press **Scan now**. Every
product that shares its SKU with another one is listed with the SKU that was read and an Edit link,
and **Download CSV** gives you the same list as a file. It compares published products and their
variations; translations of one product are not counted as duplicates.

WooCommerce itself refuses a repeated SKU when you save a product, but it has no screen that lists
the pairs already in your catalogue. Those got in another way - for example through a tool that
writes to the database directly.

= What does "Invalid or duplicated SKU" mean? =

WooCommerce shows this message when you save a product with an SKU that another product or
variation already has. A draft or a private product holds its SKU too; only a product in the Trash
does not. In the classic product editor your other changes are saved, and the SKU keeps its
previous value.

To find the product that holds it, type the SKU into the search box under
**Products → All Products**. That search covers drafts, private products and the SKUs of variations,
and for a variation it shows the product the variation belongs to. This plugin lists published
products and their variations only, so a draft holding the SKU will not be on its list.

The identifier field has its own version of the message, "Invalid or duplicated GTIN, UPC, EAN or
ISBN." (WooCommerce 9.2 and later), and it works the same way.

Since WooCommerce 11.2 the product CSV importer, with **Update existing products** ticked, also
finds products by their GTIN. A row whose GTIN two products share fails with this message, and
neither product changes; the importer does not say which product holds the other copy. The list
from this plugin shows which published products share it.

= How do I find WooCommerce products without a GTIN? =

Since WooCommerce 9.2 every product has a field for a GTIN, UPC, EAN or ISBN, but nothing in the
admin lists the products where it is empty. This plugin does: each one with an Edit link, and a
variable product once, with the number of its variations that have no code.

= How do I check if a GTIN or EAN is valid? =

This plugin does not check the number itself. It finds products with no GTIN and GTINs used by more
than one product. To check the check digit, the length and the format of a whole list of codes, use
the free [GTIN validator](https://merchlint.com/gtin-validator/) on our website: paste a column of
codes or pick a product export file (CSV, TXT or XLSX). It runs in your browser, and the file is not
sent anywhere.

A well-formed number is not yet proof that it belongs to the product. Only the GS1 registry shows
whether the manufacturer registered it.

= Does it change anything in my shop? =

No. It reads the catalogue and writes nothing back. It keeps a few things of its own: the last
result, so that reloading the page does not require another scan, and a note in your user profile
once you answer the review request or hide the note about Merchlint under the results, so that
neither comes back. Deleting the plugin removes all of them.

= My shop is multilingual. Will translations be reported as duplicates? =

No. Translations of one product share an SKU on purpose, and the check treats one translation group
as one product. Polylang and WPML are both read.

= I do not use GTINs at all. Will I get a list of every product? =

No. If not one product in the catalogue has the field filled, the check stays quiet and tells you
why, in one sentence.

= Can an AI assistant read the results? =

Only if you connect one yourself. On WordPress 6.9 or later the plugin registers two read-only
abilities through the WordPress Abilities API: the summary of the last scan and the list of its
findings. An AI assistant you connect to your store, for example through an MCP plugin, can call
them as a logged-in user with the `manage_woocommerce` permission, and gets the same results as
the Tools screen. The plugin itself sends nothing anywhere and changes nothing in your shop.

== Screenshots ==

1. The summary at the top of the results: one tile per check with the number of products it found,
   in the colour and word of what that number means - Important, Worth fixing, Clean or Not checked.
2. Two checks up close: every finding shows the value that was read, with a link straight to the
   product in your admin.
3. The whole results screen: the summary, the three checks and the CSV export.

== Changelog ==

= 0.1.3 =
* A line under the title now says where these three checks come from: Merchlint, a free audit of
  the whole catalogue from the same author. Below the results, a short note lists what else it
  checks, with a button that opens the standard WordPress plugin details window - nothing is
  installed until you press Install Now there. "Don’t show again" hides the note for your user only;
  deleting the plugin removes that setting too. When Merchlint is already active, the note does not
  appear and the line links to its screen. The links carry no tracking parameters.

= 0.1.2 =
* A summary at the top of the results: one tile per check with the number of products it found,
  coloured by what that number means - Important, Worth fixing, Clean or Not checked - and linked to
  the check below it. The colours and words are the same as in the Merchlint store audit.
* After a scan, a one-time request for a review on WordPress.org, below the results and never above
  them. Writing the review or pressing "Don’t ask again" closes it for good, for your user only.
  Deleting the plugin removes this note too.
* SKUs and GTINs are compared the same way on every database. A code that came with an invisible
  character or a non-breaking space - common when it is pasted from a spreadsheet - is now the same
  code as the one without it. Until now this depended on the database behind the shop: on MySQL 8
  such a duplicate was missed, or only one product of the pair was listed. A GTIN field holding
  nothing but such characters now counts as empty.
* Invisible characters in product codes now show in the evidence. Two codes that differ only by a
  non-breaking space or a zero-width character look the same on screen, so a pair of duplicates
  could show one value twice. The evidence now writes each such character as ⟨U+00A0⟩ and
  similar, on the screen and in the CSV export. Clean codes look exactly as before.
* "Product with no GTIN, UPC, EAN or ISBN" no longer lists gift cards. Gift card products from gift
  card plugins have no barcode by nature.
* Accessibility: each Edit link now also gives the product ID to a screen reader. A duplicate SKU
  usually comes from copying a product, and the copy keeps the name - so two links read out as the
  same "Edit" led to two different products.
* Two read-only abilities for AI assistants (WordPress Abilities API, WordPress 6.9 or later): the
  summary of the last scan and the list of its findings. An assistant you connect to your store
  yourself, for example through an MCP plugin, reads them with the same permission as the Tools
  screen; the plugin sends nothing anywhere.

= 0.1.1 =
* Renamed on the request of the plugin directory review: the display name now leads with the
  Merchlint brand, and the slug and text domain are "merchlint-duplicate-sku-finder".
* One name for everything, so nothing is left half-renamed: the WP-CLI command is now
  `wp merchlint-duplicate-sku-finder scan`, the screen sits at
  Tools -> Duplicate SKU and GTIN Finder (tools.php?page=merchlint-duplicate-sku-finder) and the
  export is saved as merchlint-duplicate-sku-finder.csv. The checks, the findings and the columns
  of the CSV file are unchanged.
* The name also says what the plugin actually looks at: duplicate SKUs and duplicate or missing
  product identifiers, not SKUs alone.
* Accessibility: the table headers now carry `scope`, and each Edit link says which product it
  edits, so a screen reader no longer reads eight links called only "Edit".
* Declared compatibility with the Cart and Checkout blocks, next to the existing High-Performance
  Order Storage declaration. The plugin adds nothing to the front end; without the declaration
  WooCommerce listed it among extensions of uncertain compatibility.

= 0.1.0 =
* First release. Duplicate SKU, missing GTIN and duplicate GTIN, with evidence on every finding,
  CSV export and a WP-CLI command.
