=== M GTIN Finder for WooCommerce ===
Contributors: majevski
Tags: gtin, ean, google merchant center, product feed, woocommerce
Requires at least: 6.4
Tested up to: 7.0
Requires PHP: 8.1
WC requires at least: 9.2
WC tested up to: 10.9
Stable tag: 1.1.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Finds, validates and fills missing GTINs in WooCommerce, then publishes a Google Merchant Center feed. Never guesses a barcode.

== Description ==

Most WooCommerce stores already hold their barcodes somewhere: a `_ean` field left behind by an old plugin, a "Barcode" product attribute, a SKU that happens to be a real UPC. M GTIN Finder searches those places, validates every value it finds against the GS1 rules, and shows you what it found and where it came from — before anything is written to a product.

It writes to WooCommerce's own GTIN field (`_global_unique_id`, added in WooCommerce 9.2), not to a private meta key of its own. That means any other plugin that reads the core field — including the official Google for WooCommerce plugin — picks the values up with no integration work.

= What it does =

* **Scans** your catalogue in background batches for products with no GTIN, using the indexed `wc_product_meta_lookup.global_unique_id` column so a large catalogue does not crawl.
* **Discovers** the barcode-shaped custom fields that already exist on your store, ranked by how many of their values actually validate, so you choose which ones to trust instead of guessing.
* **Validates** every candidate: GS1 mod-10 check digit, length, GTIN-8/12/13/14 classification, ISBN-10 to ISBN-13 conversion, optional UPC-E expansion, and recovery of leading zeros that a spreadsheet stripped.
* **Reviews**: a sortable, filterable table of every product missing a GTIN, with the suggestion, its source and the evidence for it. Approve, edit-then-approve, reject, or mark the product as legitimately having no GTIN.
* **Imports** a supplier CSV. The delimiter and encoding are detected, every row is validated in a dry run first, and you get a downloadable report of the rows that need attention.
* **Logs** every write with its previous value, so any run can be reverted in one click from the Log tab.
* **Finds duplicates** — the same GTIN stored on more than one product, which Google treats as a data error.
* **Preflights** your catalogue against Google Merchant Center's requirements, per product and site-wide, before you send anything.
* **Publishes** a Merchant Center product file at a secret, token-authenticated URL, when and only when you switch it on — in all three formats Google accepts: RSS 2.0 XML, tab-delimited `.txt` and tab-delimited `.tsv`. Each has its own live URL; you register one of them as a scheduled fetch.
* **Tests the URL for you** before you hand it to Google, because a feed URL your server answers itself instead of passing to WordPress looks fine in a browser and fails in Merchant Center days later.
* **Fills in the rest of Google's specification.** The file covers Google's full attribute table, taken from WooCommerce wherever the data really exists, from a per-product override on the product edit screen where it does not, and from a store-wide default for the facts that are about your shop rather than about one product.
* **WP-CLI**: `wp gtin scan`, `apply`, `import`, `report`, `duplicates`, `mc preflight`, `mc feed`, `mc sync`. Everything that writes or transmits is a dry run unless you pass `--live`.

= How much a suggestion is trusted =

Every suggestion is labelled with where it came from, and the label decides what may be done with it automatically.

* **Tier 1 — data already in your database.** Custom fields you enabled, product attributes, and SKUs that validate as real GTINs. Only tier 1 is ever eligible for bulk approval or optional auto-apply.
* **Tier 2 — data you supplied.** Rows from a supplier CSV you imported.
* **Tier 3 — external, unverified.** This release ships no live lookup service. The tier exists as a documented extension point, and the provider that ships with it finds nothing, on purpose.

Tier 2 and tier 3 values are never written to a product without a person approving them, whatever the settings say.

= What it refuses to do =

* It never invents a GTIN. "No GTIN found" is a real answer and the plugin says so plainly.
* It refuses restricted-circulation and coupon ranges (prefixes 02, 04, 2, 05, 98, 99) even though they pass the check digit, because Google rejects them and they do not identify a manufacturer's product.
* It never sets `identifier_exists` to `no` just because a GTIN is missing. That value is only emitted when you have explicitly flagged the product as having no identifier.
* It never writes a GTIN that is already held by a different product.
* It never serves a comma-delimited file at the feed URL, and never lets you configure one as the format, because Merchant Center cannot parse one.
* It never serves a half-written or empty product file. A product file is a full snapshot, so an empty one would delete your entire Merchant Center inventory.
* It never touches WooCommerce's `_global_unique_id` on uninstall. Those values belong to WooCommerce.

= Google Merchant Center =

The product file maps WooCommerce products onto Merchant Center's exact attribute names and formats: prices as `15.00 EUR`, availability limited to `in_stock`, `out_of_stock`, `preorder` and `backorder`, condition limited to `new`, `refurbished` and `used`, titles capped at 150 characters and descriptions at 5000, variations emitted as their own items grouped by `item_group_id`, and `gtin` emitted only for values Merchant Center will accept.

It covers Google's whole attribute table, not a convenient subset. Every value comes from one of exactly three places, tried in order: WooCommerce itself wherever the data genuinely exists there; a per-product override in the **Google Merchant Center** box on the product edit screen; or a store-wide default in Settings for the handful of attributes that describe your shop rather than one product — where you ship from, how long you take to pack an order, which return policy label applies. If none of the three has a value, the attribute is left out. Nothing is inferred from something that merely correlates with it, because Google disapproves items for wrong attributes far more readily than for missing optional ones.

= Which file format to give Google =

Merchant Center accepts **`.txt`, `.xml` and `.tsv`, and nothing else.** The plugin publishes all three, each at its own URL, and they contain the same catalogue. Pick one.

It does **not** publish CSV, and neither should you. Google's own instruction is *"Don't use CSV files due to commas being a reserved character used to separate attribute values."* In a Merchant Center file the comma separates the values of a repeated attribute, so it cannot also separate columns. A CSV download is offered for opening the catalogue in a spreadsheet, clearly labelled as not being a product file, and the live feed URL refuses `.csv` outright — a comma-delimited file renamed to `.txt` passes Google's extension check and is then mis-parsed, which gives you an accepted upload full of wrong data instead of a clean rejection.

= Preflight =

The preflight screen tells you, before you publish, which products would be held back and why — not purchasable, no price, no image, no description, out of stock, virtual or downloadable, gift card, invalid GTIN, excluded by you — and which carry warnings, such as no GTIN and no MPN, missing apparel attributes, or an image below Google's minimum size. A site-wide checklist covers HTTPS, returns policy, contact details, terms, privacy policy and an active payment method.

= External services and what leaves your site =

**This plugin contacts no third-party service.** There is no live external lookup provider in this release, no telemetry, no usage tracking, no update check against anything other than WordPress.org, and nothing that phones home. Scanning, validation, attribute mapping, preflight and CSV import all happen on your own server, against your own database. A CSV you upload is stored in a protected folder under `wp-content/uploads` and is read locally.

There is exactly one way data can leave your site, and it is off by default:

**The Merchant Center product file.** If an administrator switches it on in Settings, your site publishes the file at a secret, token-bearing URL (for example `https://example.com/mgtin-feed/<token>.xml`, `.txt` or `.tsv`). Google Merchant Center — operated by **Google LLC** — then fetches that URL on the schedule you configure inside your own Merchant Center account. Your site never pushes anything to Google; Google pulls the file.

For each product included in the file, it contains what you have supplied for that product out of Google's product data specification. In a default installation with nothing filled in by hand, that is: product id, title, description, product link, image URL and additional image URLs, availability, price, sale price and sale price effective date, brand, GTIN, MPN, `identifier_exists`, condition, item group id and item group title, Google product category, product type, product and shipping dimensions and weight, shipping label, ships-from country, the variant attributes colour, size, material, pattern, age group and gender, and any WooCommerce product attributes that have no dedicated Google attribute, as product details. If you fill in the per-product overrides or the store-wide defaults, those values are included as well — you can see exactly what is being sent by downloading the file from the Merchant Center tab. Nothing else is included: no customer data, no order data, no site credentials.

The file URL is the only credential protecting it, so treat it as a password. It is served with `X-Robots-Tag: noindex, nofollow, no-archive`, a wrong token returns a bare 404 that does not reveal whether a file exists, and the token can be rotated at any time.

**One more request, and it does not leave your network.** The **Test URL** button on the Merchant Center tab asks your own site to fetch its own feed URL, so it can tell you whether the URL actually reaches WordPress before you hand it to Google. The request goes from your server to your server. If your host blocks a site from fetching itself — many do — the result is reported as inconclusive rather than as a failure, because it says nothing about whether Google can fetch the URL.

* Google Privacy Policy: https://policies.google.com/privacy
* Google Merchant Center Terms of Service: https://support.google.com/merchants/answer/160173

Because publishing the feed transmits your product data to Google, merchants who enable it should reflect that in their own site privacy policy. The plugin does not write your privacy policy for you.

= For developers =

Filters: `mgtin_providers`, `mgtin_registered_providers`, `mgtin_meta_key_patterns`, `mgtin_discovered_meta_keys`, `mgtin_gtin_not_applicable`, `mgtin_validation_options`, `mgtin_attribute_terms`, `mgtin_discovered_attributes`, `mgtin_mc_attribute_<name>`, `mgtin_mc_product_details`, `mgtin_mapped_product`, `mgtin_feed_formats`, `mgtin_feed_items`, `mgtin_feed_tsv_columns`, `mgtin_feed_cache_ttl`, `mgtin_preflight_max_products`, `mgtin_eligibility_blocks`, `mgtin_eligibility_warnings`, `mgtin_service_keywords`, `mgtin_giftcard_keywords`, `mgtin_healthcare_keywords`, `mgtin_alcohol_keywords`, `mgtin_restricted_keywords`, `mgtin_lookup_cache_ttl`, `mgtin_lookup_request_budget`, `mgtin_batch_size`, `mgtin_csv_max_upload_bytes`, `mgtin_csv_allowed_path`, `mgtin_csv_row_report_limit`, `mgtin_csv_stage_limit`.

Actions: `mgtin_before_apply`, `mgtin_after_apply`, `mgtin_after_revert`, `mgtin_scan_complete`, `mgtin_feed_generated`, `mgtin_feed_failed`.

Every Merchant Center attribute passes through `mgtin_mc_attribute_<name>`, where `<name>` is the exact Google attribute name — `mgtin_mc_attribute_shipping_label`, `mgtin_mc_attribute_gtin`. Whatever you return is re-checked against Google's own value list and length limits before it reaches the file. Per-product overrides live in post meta as `_mgtin_mc_<attribute>`, again using Google's own attribute names, so anything that can write post meta can supply them.

Output formats are pluggable: implement `MGtinFinder\Merchant\Writer\WriterInterface` and register it on `mgtin_feed_formats`.

Helper functions: `mgtin_get_gtin()`, `mgtin_gtin_is_not_applicable()`, `mgtin_locate_template()`. All templates live in `templates/` and can be overridden from a theme.

== Installation ==

1. Install and activate WooCommerce 9.2 or newer. The plugin will refuse to run on anything older, because 9.2 is the release that added WooCommerce's own GTIN, UPC, EAN or ISBN field.
2. Upload the plugin folder to `/wp-content/plugins/`, or install it from the Plugins screen, then activate it.
3. Go to **WooCommerce → M GTIN Finder → Settings** and look at *Barcode fields found on this store*. Tick the custom fields that genuinely hold barcodes. Nothing is read from a field you have not ticked.
4. Go to the **Dashboard** tab and run a scan. It writes nothing to your products; it only records suggestions.
5. Go to the **Review** tab, check the suggestions and approve the ones that are right. Mark hand-made or own-brand products as having no GTIN.
6. Optional: open the **Merchant Center** tab and run the preflight, then fix what it reports.
7. Optional: enable the product file in Settings, and fill in any store-wide attribute defaults you can answer for the whole shop. Back on the **Merchant Center** tab, choose a format, press **Test URL**, copy the URL, and register it in Merchant Center as a scheduled fetch.

You need the `manage_woocommerce` capability for every screen and action.

== Frequently Asked Questions ==

= Why does it never guess a GTIN? =

Because a wrong GTIN is worse than a missing one. A missing GTIN costs you some visibility in Google Shopping. A GTIN that belongs to somebody else's product is a misrepresentation, and Merchant Center suspends accounts over mismatched identifiers — a suspension you then have to argue your way out of by hand.

Nothing in this plugin does a fuzzy match on a product title, a "best guess" from a brand and a model number, or a lookup against a barcode database that returns "probably this". If none of your data yields a value that validates, the answer is "no GTIN found", and that is a correct answer, not a failure. For products that genuinely have no barcode — hand-made goods, own-brand items, services — mark them as having no GTIN. The feed will then tell Google so explicitly, which is far safer than inventing a number.

= I have barcodes starting with 2 (or 02, or 04) and the plugin rejects them. Why? =

Those are restricted-circulation numbers. Shops assign them internally for weighed goods, in-store packing and own-store labelling, and the number means something only inside that store's own till system. They pass the mod-10 check digit perfectly, because the check digit only proves the digits were transcribed correctly — it says nothing about whether the number identifies a real, globally unique trade item.

Google rejects GTINs in the restricted ranges (prefixes 02, 04 and 2) and in the coupon ranges (05, 98 and 99). The plugin rejects them at the point of validation so they never reach a product, rather than letting you publish them and find out from a Merchant Center disapproval.

ISBN, ISSN and ISMN prefixes (977, 978, 979) are not restricted — they are valid GTINs, and the plugin accepts them, converting a valid ISBN-10 to its ISBN-13 form when you allow it.

= Which format should I give Merchant Center — XML, .txt or .tsv? =

Any of the three. They carry the same catalogue and Google accepts all three. Register exactly one of them as your data source; a second source submitting the same product IDs produces duplicate-item errors.

XML is the safer default if your data uses the richer attributes. A repeated attribute — several `additional_image_link` values, several `shipping` entries — repeats the element in XML, whereas a tab-delimited file has to flatten it into one comma-joined cell with colons inside the group values. Both are correct and both are accepted; the XML is simply easier to read when something goes wrong. `.txt` and `.tsv` are byte-for-byte identical here; `.tsv` is the extension Google recommends for tab-delimited data.

= Why is there no CSV feed? =

Because Google does not accept one. Its wording is: *"Don't use CSV files due to commas being a reserved character used to separate attribute values. If you use Text files, use the TSV (tab-separated values) format."* In a Merchant Center product file the comma already means "next value of this attribute" and the colon means "next sub-attribute", so a comma cannot also mean "next column" — the file is ambiguous by construction and information is genuinely lost. A product with two values in `product_type` and a product with one value containing a comma come out as exactly the same bytes.

There is still a CSV download, because merchants want to open the catalogue in a spreadsheet. It is labelled as not being a product file, it is never the default, and the live feed URL refuses `.csv` outright. Do not work around that by renaming a CSV to `.txt`: the extension check passes and Google then mis-parses the contents, so instead of a clean rejection you get an accepted upload full of wrong data, and every product in it disapproved for reasons that make no sense.

= What is the Test URL button for? =

For catching, in five seconds, a failure that otherwise shows up in Merchant Center as a failed fetch several days later.

The pretty feed URL ends in a file extension, and some server configurations answer file extensions themselves instead of handing the request to WordPress — several nginx setups do, and so does PHP's built-in development server. The URL then looks completely normal, and only Google ever discovers that it does not work. **Test URL** asks your own site to fetch the URL and tells you what came back. If it answers with an HTTP error, the plugin offers the query-string fallback URL, which has no extension and therefore always reaches WordPress.

If the test cannot complete at all, the result says *inconclusive* and keeps recommending the normal URL. That is not the plugin hedging: many hosts block a site from making HTTP requests back to itself, and that says nothing about whether Google — which fetches from outside — can reach it.

= Google says my products expired. Why? =

Products expire 30 days after the last successful fetch, so a scheduled fetch has to run at least monthly. Daily is what most shops want, because prices and stock go stale long before 30 days are up.

The other way to lose products is to serve an incomplete file. A Merchant Center product file is a full snapshot, not a list of changes: a product missing from the file is deleted from your account. The plugin writes each file to a temporary name and renames it into place for exactly this reason, never serves a half-written or zero-byte document, and leaves the previous complete file in place if a rebuild fails.

= Do I have to fill in all those Merchant Center attributes? =

No. Leaving a field empty is the correct answer whenever you have no true value for it — the attribute is then left out of the file entirely, which is what Google asks for. Everything Google requires and WooCommerce actually knows is filled in for you automatically.

The per-product box and the store-wide defaults exist for the facts WooCommerce has no field for and cannot infer: that a dress is sold in EU sizes, that you ship from Poland, that a bottle of wine must stay out of one destination. Anything you type there is a statement you are making to Google about your products, so type it only when it is true.

= Google says my products are missing a legally required attribute [unit_pricing_measure]. What do I do? =

First, the reassuring part: that message is a **compliance warning, not a disapproval**. The products are still being shown. Google's own wording is that they "may be missing" the attribute, and it works this out automatically from your titles and descriptions — it is a guess about your catalogue, not a ruling about your legal obligations. Nothing is offline and nothing needs fixing in a panic.

What the attribute means is the **net quantity of the product itself, without its packaging** — 750ml of shampoo, 500g of coffee. Combined with `unit_pricing_base_measure` and your price, it is what produces a "€4.98 per litre" line next to your product in Shopping.

**The plugin will not guess it from your shipping weight, and this is deliberate.** WooCommerce's weight field is what you quote a courier: it includes the box, the padding and the packing material. On our own test store, a setting that filled the attribute in from weight produced a measure for 201 products in one click — a USB cable came out at 0.2 lb, which at €24.99 advertises **€124.95 per pound of USB cable**. A missing unit price is a warning. A wrong one is a false price shown to a shopper, which is a consumer-protection problem. So there is no "derive from weight" switch, and there is no bulk-fill-everything button.

**Where to enter it.** On the product edit screen, in the *Google Merchant Center* box, in its own **Unit pricing** section: **Unit pricing measure** (e.g. `750ml`) and **Unit pricing base measure** (e.g. `100ml`, and it must use the same unit as the measure). The screen shows you the resulting unit price in your own currency as you type, so a wrong figure is visible before it ever reaches Google. There is also a **Unit pricing** tab under WooCommerce → M GTIN Finder listing the products that look like they need one, so you can work through them product by product without opening each one.

If a title already contains the quantity, the plugin will offer it as a **suggestion** — one click to accept. It is only ever a suggestion: it is stored separately, it never goes into the feed on its own, and anything ambiguous (`6 x 330ml`, `250ml / 500ml`, a bare number like `020`) produces no suggestion at all rather than a plausible-looking wrong one.

If you run **WooCommerce Germanized**, the net quantity you already entered there for the German unit-price display is read directly. Nothing to type twice.

**Products that genuinely are not sold by measure** — a T-shirt, a mug, a phone case — can be marked *Not sold by measure*. That records your decision so the plugin stops asking and the preflight stops flagging it.

**One honest caveat:** marking it does not remove the warning from Merchant Center. There is no feed attribute that opts out of it — Google does not publish one. The documented escape hatch is Merchant Center's own per-product **"Request review"** / **"I disagree with the issue"** flow, which is a conversation with Google, not something a feed can say. What the plugin can do is stop you sending a wrong number, and make it quick to send the right one.

None of the above is legal advice. Which of your products need a unit price, and what the reference unit is, depends on where you sell; check your own obligations.

= Does this push products to Google over the API? =

No. This release publishes a feed that Google Merchant Center fetches on a schedule you set in your Merchant Center account. There is no push transport, and `wp gtin mc sync --live` will tell you so rather than pretend.

The reason is deliberate. The old Content API for Shopping v2.1 is deprecated and sunsets on 18 August 2026, so building on it now would ship something with a known expiry date. Its replacement, Merchant API v1, is generally available, and a push integration against it is planned for a future release — the plugin already records a per-product payload hash so a future push can skip unchanged products. Until that ships, the honest position is a scheduled-fetch feed, which is a fully supported, first-class way to get a catalogue into Merchant Center.

= Does it work with the official Google for WooCommerce plugin? =

Yes, and you do not have to configure anything to make it so. This plugin writes to WooCommerce's core `_global_unique_id` field via `set_global_unique_id()`. Google for WooCommerce reads that field automatically and it takes precedence over that plugin's own GTIN meta and attribute mapping. So the GTIN work you do here shows up in their sync with no integration code.

The gap is `identifier_exists`. Google for WooCommerce does not support that attribute, so it has no way to tell Google that a particular product legitimately has no barcode — and the default when the attribute is omitted is `yes`, meaning Google keeps looking for an identifier that does not exist. That is exactly why the feed in this plugin exists. If every one of your products has a GTIN, you may not need this feed at all: do the GTIN work here and let Google for WooCommerce do the syncing.

= Will it change my products without asking? =

Not unless you ask it to. Scanning only records suggestions. Applying is a separate, explicit step. Bulk approval is limited to tier 1. Automatic approval of tier 1 is a setting that is off by default, with a warning next to it explaining why. Every write records the previous value and every run can be reverted in one click from the Log tab. On the command line, everything that writes is a dry run until you add `--live`.

= What happens on uninstall? =

Nothing is deleted by default. If you tick the option to remove data, the plugin drops its own two tables, its options, its own post meta and the cached feed files. GTINs written to WooCommerce's `_global_unique_id` are never removed under any setting — they are WooCommerce's data, not this plugin's, and they may have taken you weeks to collect.

= My spreadsheet turned a barcode into 5.06011E+12. Is that recoverable? =

Sometimes. The importer recognises scientific notation and expands it when the value can be recovered exactly. When precision has already been lost, the row is reported as unrecoverable rather than silently rounded into a plausible-looking wrong number. Leading zeros stripped by a spreadsheet are recovered safely, because zero-padding never changes the check digit or which trade item is meant.

= Why is UPC-E expansion off by default? =

An 8-digit code beginning with 0 can be a genuine GTIN-8 or a compressed UPC-E, and the check digit cannot tell the two apart. Expanding blindly would corrupt real GTIN-8 values. Switch it on only if you know your data is UPC-E.

= Can I add my own barcode lookup service? =

Yes. Extend `MGtinFinder\Providers\HttpLookupProvider`, implement `lookup()`, and register it on the `mgtin_providers` filter. Caching of hits and misses, exponential backoff and a per-run request budget come from the base class, and whatever your provider returns is put through the same validator as everything else before it can become a suggestion. It will be labelled tier 3 and will never be applied without approval.

= Does it work on variable products? =

Yes. Variations are scanned and fixed individually, and in the feed each variation is its own item sharing the parent's `item_group_id`. The variable parent itself is never sent as an item, because it is not something a customer can buy.

= My product attributes are not in English. Will they still be recognised? =

Yes, in two layers. Common attribute names in two dozen languages are recognised out of the box — "Spalva" becomes `color`, "Prekės ženklas" becomes `brand` — and everything else is listed under Settings, "Your product attributes", where you tell the plugin what each of your attributes means. Deliberately ambiguous names, such as a "Gamintojas"/"Hersteller" field that can hold either the brand or the legal manufacturer's name, are never guessed: they stay in `product_detail` until you map them. A "Manufacturer" mapping is available that fills `brand` only on products where nothing brand-labelled exists.

= Why are my product star ratings not showing on Google? =

Star ratings do not travel in the product file at all — Google's product data specification has no rating attribute. They come from the separate Product Ratings program: you sign up for it in Merchant Center and register a review feed. This plugin generates that feed from your shop's real, approved WooCommerce reviews, in Google's schema, at its own secret URL shown on the Merchant Center tab. Google publishes thresholds for the program — around 50 reviews across your account to take part, and roughly 3 reviews on a product before its stars appear — and expects a complete feed at least monthly, which the scheduled fetch satisfies.

= Can the plugin send a 5-star rating with one review for every product? =

No, and no plugin should. The review feed carries the reviews your customers actually wrote, each with the rating they actually gave — including the critical ones, because Google's Product Ratings policy requires reviews to be submitted completely rather than selectively, and fabricated or cherry-picked reviews are prohibited by that policy and by EU consumer law. A product with no reviews sends no rating, which is the truthful answer. This is the same principle the GTIN side of the plugin runs on: data that was never real does not get transmitted.

== Screenshots ==

1. The dashboard: how many products are missing a GTIN, where the suggestions came from, and the background scan.
2. The review queue: every product with no GTIN, its suggestion, the tier badge and the evidence behind it, with approve, edit, reject and "no GTIN" actions on each row.
3. Settings: the barcode-shaped fields discovered on this store, ranked by how many of their values actually validate.
4. Settings: validation options and the warning that guards automatic approval.
5. The CSV importer's dry run, showing matched, valid, invalid, unmatched and duplicate rows before anything is written.
6. Merchant Center preflight: which products would be sent, which would be held back, and why.
7. The product file panel: one live URL per accepted format with Copy and Test URL buttons, the downloads including the clearly-labelled CSV, the token rotation control and the disclosure of exactly what leaves the site.
8. The change log, with every write, its previous value and the button that reverts a whole run.

== Changelog ==

= 1.1.2 =
* Fixed: an update that could not be downloaded now says why. WordPress gives a package download five minutes to finish, which is longer than any host allows a single admin request to run, so when the connection to majevski.com stalled the request was cut off before WordPress could report anything and the Plugins screen showed only the browser's own "Connection lost or the server is busy". Downloads of this plugin's releases are now capped at sixty seconds — far more than a release archive needs, and short enough that the real transport error is reported on the screen instead of the request dying silently.
* New: a Site Health check under Tools → Site Health → Status, "M GTIN Finder for WooCommerce can reach its update server". It makes the same two calls the installer makes — the release manifest, then the package itself — and prints the exact error when either fails, so a blocked outbound connection is named as such instead of appearing as a failed update.

= 1.1.1 =
* Fixed: new releases now appear on the Plugins screen within an hour instead of up to a day. WordPress only re-checks for updates every 12 hours by default and the release manifest was cached for another 12 — visiting the Plugins screen now forces a fresh check (at most hourly), and the manifest cache lasts one hour.
* New: a "Check for updates" link in this plugin's row on the Plugins screen — click it and, if a newer release exists, the native "update now" link is active immediately.

= 1.1.0 =
* New: automatic update checks against majevski.com. When a newer release is published, WordPress shows its standard update prompt with one-click install; a "View details" popup shows the changelog. The update channel is pinned to https://majevski.com over HTTPS, checks are cached and fail open, and equal or older versions are never offered.

= 1.0.1 =
* Product attribute mapping for non-English stores: the plugin discovers every attribute label your shop uses, recognises common names in two dozen languages ("Spalva" becomes `color`, "Prekės ženklas" becomes `brand`), and lets you map the rest under Settings, "Your product attributes". Deliberately ambiguous names are never guessed.
* New "Manufacturer" mapping that fills `brand` only on products where nothing brand-labelled exists — a Gamintojas/Hersteller field often holds the legal manufacturer's name, which is not always the brand.
* Unit pricing from your own attributes: map an attribute such as "Talpa" or "Kiekis" to "Unit pricing quantity" and its values feed `unit_pricing_measure` after validation. Local unit words are understood — `vnt.`, `gab.`, `tk`, `szt`, `ks`, `Stk.`, `buc`, `kom`, `pcs` become Google's `ct`, and `ltr`/`kgs`/`gr` become `l`/`kg`/`g`. Unreadable values emit nothing and stay in `product_detail`.
* Product ratings feed: a second token-authenticated URL serving your real, approved WooCommerce reviews in Google's Product Ratings schema (2.4), for the separate Product Ratings program. All reviews, with the ratings customers actually gave; there is no way to filter or invent them.
* Custom page addresses for the site checklist: when the returns policy or contact page cannot be found automatically — usual on shops whose pages are not titled in English — paste each page's URL in Settings and the checklist uses your answer. A URL pointing at an unpublished local page still fails, with the reason.
* `adult` and `is_bundle` now accept `true`/`false` alongside `yes`/`no`, matching Google's documented values.
* Fixed: non-Latin attribute labels (Cyrillic, Greek, CJK) were discarded before mapping ran.
* Fixed: the per-attribute filter could bypass unit pricing validation.
* Version 1.0.1 requires no database changes; upgrading from 1.0.0 is drop-in.

= 1.0.0 =
* Initial release.
* Scanner over the indexed `wc_product_meta_lookup.global_unique_id` column, running in Action Scheduler batches with keyset pagination.
* Meta key discovery that ranks the barcode-shaped custom fields on your store by how many of their values validate.
* GS1 validation: mod-10 check digit, GTIN-8/12/13/14, restricted-circulation and coupon range rejection, ISBN-10 to ISBN-13 conversion, optional UPC-E expansion, leading-zero recovery and Excel scientific-notation repair.
* Providers for custom fields, product attributes and SKUs (tier 1), supplier CSV (tier 2), and a documented, inert external lookup extension point (tier 3).
* Review queue with per-row approve, edit-then-approve, reject and "no GTIN" actions, plus a bulk approval limited to tier 1.
* CSV import with delimiter and encoding detection, a mandatory dry run and a downloadable error report.
* Duplicate GTIN detection.
* Full audit log with one-click revert of any run.
* Merchant Center eligibility preflight, per product and site-wide.
* Merchant Center product file over Google's full attribute table, in all three accepted formats — RSS 2.0 XML, tab-delimited `.txt` and tab-delimited `.tsv` — each served at its own secret token-authenticated URL with `X-Robots-Tag: noindex`, written atomically and rebuilt in the background when the catalogue changes. A CSV download for spreadsheets, labelled as not being a product file and refused at the feed URL.
* Per-product Merchant Center attribute overrides on the product edit screen, and store-wide defaults in Settings, for the attributes WooCommerce has no field for.
* A **Test URL** button that fetches the feed URL from the site itself, so a server that answers file extensions without involving WordPress is caught before Google is given the URL.
* WP-CLI commands `wp gtin scan|apply|import|report|duplicates|mc preflight|mc feed|mc sync`, all dry-run by default.
* HPOS and cart/checkout blocks compatibility declared.

== Upgrade Notice ==

= 1.1.0 =
Adds self-hosted update checks: from this version on, new releases appear in the normal WordPress update flow. Install this version manually once — older versions do not know about the update channel yet.

= 1.0.1 =
Drop-in upgrade, no database changes. Non-English shops: open Settings, "Your product attributes" after updating — mapping your attribute names there is what turns on brand, colour and unit pricing detection for your language.

= 1.0.0 =
First release. Requires WooCommerce 9.2 or newer, because that is the version that added WooCommerce's own GTIN field. The Merchant Center feed is off until you switch it on; nothing leaves your site before then.
