{"id":360163,"date":"2026-09-01T16:59:19","date_gmt":"2026-09-01T16:59:19","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/merchlint\/"},"modified":"2026-10-12T21:29:50","modified_gmt":"2026-10-12T21:29:50","slug":"merchlint","status":"publish","type":"plugin","link":"https:\/\/kaa.wordpress.org\/plugins\/merchlint\/","author":23551451,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.3.7","stable_tag":"0.3.7","tested":"7.1.3","requires":"6.5","requires_php":"7.4","requires_plugins":null,"header_name":"Merchlint \u2013 Catalog Audit for WooCommerce","header_author":"Baltano","header_description":"Store catalogue audit. Shows how much you are losing, and changes not a single byte.","assets_banners_color":"bad2e1","last_updated":"2026-10-12 21:29:50","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/merchlint.com","header_author_uri":"https:\/\/baltano.com","rating":0,"author_block_rating":0,"active_installs":0,"downloads":640,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"0.2.0":{"tag":"0.2.0","author":"baltano","date":"2026-09-01 16:58:44","revision":3676537},"0.2.1":{"tag":"0.2.1","author":"baltano","date":"2026-09-07 09:47:53","revision":3684589},"0.3.0":{"tag":"0.3.0","author":"baltano","date":"2026-09-17 23:36:40","revision":3701111},"0.3.1":{"tag":"0.3.1","author":"baltano","date":"2026-09-18 20:13:26","revision":3702735},"0.3.2":{"tag":"0.3.2","author":"baltano","date":"2026-09-19 11:22:44","revision":3703234},"0.3.3":{"tag":"0.3.3","author":"baltano","date":"2026-09-25 13:44:11","revision":3713130},"0.3.4":{"tag":"0.3.4","author":"baltano","date":"2026-10-02 00:10:01","revision":3724011},"0.3.5":{"tag":"0.3.5","author":"baltano","date":"2026-10-08 19:04:19","revision":3735376},"0.3.6":{"tag":"0.3.6","author":"baltano","date":"2026-10-11 21:25:53","revision":3740157},"0.3.7":{"tag":"0.3.7","author":"baltano","date":"2026-10-12 21:29:50","revision":3742531}},"upgrade_notice":{"0.3.7":"<p>A new check finds published products that WooCommerce keeps out of the shop because of their\nparent - since WooCommerce 11.1 also when the parent doesn&#039;t exist. Also brings back the evidence\nin a check&#039;s full list on phones.<\/p>","0.3.6":"<p>Each check that found something gets a card with its first five items on the audit screen, and\nthe evidence uses WooCommerce&#039;s own words. Hidden items get an optional reason and a list where\nyou can restore them. Also fixes the way back after hiding an item in a shop installed in a folder.<\/p>","0.3.5":"<p>A summary at the top of the audit screen, a new check for products marked sold out while their\nvariations are in stock, and fewer false findings in several checks. Some counts change after\nupdating because the checks changed, not your shop - the Changes screen says which.<\/p>","0.3.4":"<p>Small fixes: the evidence gives its reason in your language, amounts in the Polish admin read\n&quot;7 740,00&quot;, and the notes about identifiers kept in another plugin&#039;s field give advice that can be\nfollowed. Nothing in your shop changes.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3676536,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3676536,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3676536,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3676536,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3676536,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3742531,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"meta\":{\"title\":\"Merchlint demo store\",\"description\":\"A small WooCommerce store with typical catalogue problems and a year of orders, audited by Merchlint.\",\"author\":\"baltano\",\"categories\":[\"WooCommerce\"]},\"landingPage\":\"\\\/wp-admin\\\/admin.php?page=merchlint\",\"preferredVersions\":{\"php\":\"8.3\",\"wp\":\"latest\"},\"features\":{\"networking\":true},\"steps\":[{\"step\":\"login\",\"username\":\"admin\"},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"woocommerce\"},\"options\":{\"activate\":true}},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"merchlint\"},\"options\":{\"activate\":true}},{\"step\":\"writeFile\",\"path\":\"\\\/wordpress\\\/wp-content\\\/mu-plugins\\\/merchlint-preview-sqlite.php\",\"data\":\"<?php\\n\\\/**\\n * Plugin Name: Merchlint preview - SQLite compatibility\\n * Description: Playground runs WordPress on SQLite through a MySQL emulation layer that rejects GROUP_CONCAT(... SEPARATOR ',') and RIGHT(). Queries that use them fail instead of returning data; this drops the separator clause (',' is MySQL's default) and writes RIGHT(x, n) as SUBSTRING(x, -n), so Merchlint's queries run as they do on a real MySQL store.\\n *\\n * TYLKO PODGLAD W PLAYGROUND - blueprint zapisuje ten plik w mu-plugins, wtyczka go nie niesie.\\n *\\n * SKAD. Playground trzyma baze w SQLite, a zapytania MySQL tlumaczy mu warstwa\\n * `sqlite-database-integration` (sterownik WP_MySQL_On_SQLite). Dwoch konstrukcji z zapytan\\n * Merchlinta nie przyjmuje - oba zapytania padaly po cichu (wpdb oddaje pusta liste, bledu\\n * PHP nie ma), a na prawdziwym sklepie (MySQL, MariaDB) dzialaja. Zmierzone 10.10.2026,\\n * Playground CLI 3.1.57 i playground.wordpress.net:\\n *\\n * 1. GROUP_CONCAT( ... SEPARATOR ',' ) - \\\"near SEPARATOR: syntax error\\\". Uzywa go \\\"Where to\\n *    start\\\" (WpScanStore::topProductsByImpact): najwazniejsza sekcja ekranu wychodzila pusta.\\n *    ',' to domyslny separator MySQL, wiec zapytanie bez tej klauzuli znaczy to samo.\\n *    DISTINCT i ORDER BY wewnatrz GROUP_CONCAT warstwa przyjmuje - zostaja.\\n * 2. RIGHT() - \\\"no such function: RIGHT\\\" (LEFT warstwa tlumaczy, RIGHT nie). Uzywa go\\n *    zapytanie o niewidoczne znaki w SKU i GTIN (KodyWBazie), przy kazdym skanie; wyszlo\\n *    dopiero z dziennika PHP sondy. SUBSTRING( x, -n ) to w MySQL dokladnie RIGHT( x, n ).\\n *    W SQLite warstwa liczy znaki, nie bajty, wiec wielobajtowy znak na koncu kodu (np. spacja\\n *    U+3000) nie zostanie rozpoznany tak jak w MySQL; ASCII i zwykle spacje - tak. Sklep\\n *    podgladu takich kodow nie ma.\\n *\\n * ZAKRES: kazde zapytanie, ale tylko te dwie konstrukcje i tylko w tej postaci - obie\\n * w SQLite koncza sie bledem, wiec zamiana nie zmienia zadnego zapytania, ktore dzialalo.\\n * Zapytanie o niewidoczne znaki czyta wp_postmeta i slowa \\\"merchlint\\\" nie zawiera; pierwsza\\n * wersja, ograniczona do zapytan z tym slowem, go nie poprawila (sonda, 10.10.2026).\\n *\\n * DO USUNIECIA, gdy wydanie w katalogu nie bedzie mialo tych konstrukcji (propozycja poprawki\\n * w Merchlint\\\/Plan\\\/32 par. 5) - wtedy ten filtr nie ma czego zmieniac.\\n *\\n * @package Merchlint\\\\Playground\\n *\\\/\\n\\nadd_filter(\\n\\t'query',\\n\\tstatic function ( $sql ) {\\n\\t\\tif ( ! is_string( $sql ) ) {\\n\\t\\t\\treturn $sql;\\n\\t\\t}\\n\\t\\tif ( false !== stripos( $sql, 'SEPARATOR' ) ) {\\n\\t\\t\\t$sql = (string) preg_replace( \\\"\\\/\\\\s+SEPARATOR\\\\s+','\\\\s*\\\\)\\\/i\\\", ' )', $sql );\\n\\t\\t}\\n\\t\\tif ( false !== stripos( $sql, 'RIGHT(' ) ) {\\n\\t\\t\\t$sql = (string) preg_replace( '\\\/\\\\bRIGHT\\\\(\\\\s*(CAST\\\\(\\\\s*[\\\\w.]+\\\\s+AS\\\\s+BINARY\\\\s*\\\\))\\\\s*,\\\\s*(\\\\d+)\\\\s*\\\\)\\\/i', 'SUBSTRING( $1, -$2 )', $sql );\\n\\t\\t}\\n\\t\\treturn $sql;\\n\\t}\\n);\\n\"},{\"step\":\"writeFile\",\"path\":\"\\\/wordpress\\\/wp-content\\\/mu-plugins\\\/merchlint-preview-queue.php\",\"data\":\"<?php\\n\\\/**\\n * Plugin Name: Merchlint preview - background queue\\n * Description: Playground cannot send a request to itself, so WP-Cron and Action Scheduler never start on their own. While the audit screen asks for scan progress, this runs the queue in that request - the work a real server does in a background request.\\n *\\n * TYLKO PODGLAD W PLAYGROUND - blueprint zapisuje ten plik w mu-plugins, wtyczka go nie niesie.\\n *\\n * SKAD. Skan z ekranu jedzie na Action Schedulerze: przycisk zaklada skan, a kawalki\\n * dowozi kolejka. Na serwerze kolejke rusza zapytanie, ktore WordPress wysyla sam do\\n * siebie (WP-Cron i asynchroniczny uruchamiacz Action Schedulera). W Playground\\n * w przegladarce takiego zapytania nie ma - zmierzone 10.10.2026 na playground.wordpress.net:\\n * po \\\"Skanuj teraz\\\" pasek stal na \\\"0% - 0 z 48\\\" przez ponad 45 s i nic go nie ruszalo.\\n * Odwiedzajacy, ktory kliknie najbardziej widoczny przycisk ekranu, dostalby skan bez konca.\\n *\\n * CO ROBI. Ekran pyta o postep co dwie sekundy (akcja AJAX `merchlint_progress`). Przed\\n * odpowiedzia przerabiamy kolejke w tym samym zadaniu - dokladnie to, co zrobilby uruchamiacz\\n * Action Schedulera w osobnym zadaniu na serwerze. Cala kolejka, nie tylko skan: na serwerze\\n * uruchamiacz tez nie wybiera.\\n *\\n * Nie zalezy od klas Merchlinta - tylko od nazwy akcji AJAX, ktora wola jego ekran. Gdyby\\n * sie zmienila, skan z ekranu znow stanie; przebieg w przegladarce przed wydaniem\\n * (Projekt\\\/playground\\\/README.md) ma to zobaczyc.\\n *\\n * @package Merchlint\\\\Playground\\n *\\\/\\n\\nadd_action(\\n\\t'wp_ajax_merchlint_progress',\\n\\tstatic function () {\\n\\t\\t\\\/\\\/ Te same uprawnienia, ktorych wymaga ekran audytu - zanim cokolwiek ruszymy.\\n\\t\\tif ( current_user_can( 'manage_woocommerce' ) && class_exists( 'ActionScheduler_QueueRunner' ) ) {\\n\\t\\t\\tActionScheduler_QueueRunner::instance()->run( 'Merchlint preview' );\\n\\t\\t}\\n\\t},\\n\\t1\\n);\\n\"},{\"step\":\"runPHP\",\"code\":\"<?php\\n\\\/**\\n * Sklep przykladowy dla podgladu Merchlinta w WordPress Playground.\\n *\\n * GDZIE TO DZIALA. Przycisk \\\"Preview\\\" na stronie wtyczki w wordpress.org otwiera WordPress\\n * Playground: WordPress uruchomiony w przegladarce odwiedzajacego, bez serwera i bez konta.\\n * Co w nim stoi, mowi blueprint - `assets\\\/blueprints\\\/blueprint.json` w SVN. Blueprint instaluje\\n * WooCommerce i Merchlinta z katalogu, a ten plik (wklejony do kroku `runPHP` przez\\n * `playground\\\/zbuduj.php`) zamienia pusty WordPress w maly sklep z usterkami, ktore audyt ma\\n * znalezc. Po nim blueprint uruchamia skan i otwiera ekran audytu.\\n *\\n * ZADNEGO KODU WTYCZKI. Skrypt zna tylko WordPress i WooCommerce (CRUD). Podglad bierze\\n * Merchlinta z katalogu, czyli po kazdym wydaniu chodzi juz na nowej wersji - zasiew przywiazany\\n * do jej klas psulby sie po cichu przy pierwszej zmianie wnetrza. Skan blueprint uruchamia\\n * publicznym poleceniem `wp merchlint scan`.\\n *\\n * USTERKI ZAPISANE Z POMINIECIEM CRUD. WooCommerce nie da przez swoje API zapisac dwoch\\n * produktow z tym samym SKU albo GTIN, a wariantowy produkt oznaczony jako wyprzedany sam\\n * przestawi na stan wariantow. W sklepach takie dane i tak powstaja - robia je importy,\\n * synchronizacje z hurtownia i wtyczki piszace meta wprost - wiec zapisujemy je tak samo:\\n * update_post_meta() po zapisie przez CRUD.\\n *\\n * KWOTY. Audyt liczy przychod z tabeli analitycznej WooCommerce (wc_order_product_lookup).\\n * WooCommerce wypelnia ja kolejka w tle, ktorej w Playground nic nie uruchamia - dlatego po\\n * kazdym zamowieniu wolamy import wprost.\\n *\\n * BEZ LOSOWANIA. Kazdy odwiedzajacy dostaje ten sam sklep. Daty zamowien liczymy od chwili\\n * uruchomienia, zeby okno \\\"ostatnie 12 miesiecy\\\" zawsze je obejmowalo.\\n *\\n * NIC TU NIE MOZE WYWROCIC PODGLADU. Wyjatek w kroku blueprintu konczy sie ekranem bledu\\n * u odwiedzajacego, wiec kazdy etap ma wlasny try\\\/catch: lepszy sklep z jedna usterka mniej\\n * niz brak sklepu. To, czy wszystko weszlo, sprawdza sonda (playground\\\/sonda.ps1), nie ten plik.\\n *\\n * Uruchomienie poza Playground (proba na lokalnym WordPressie): wp eval-file dane-demo.php\\n *\\n * @package Merchlint\\\\Playground\\n *\\\/\\n\\nif ( ! defined( 'ABSPATH' ) ) {\\n\\trequire_once '\\\/wordpress\\\/wp-load.php';\\n}\\n\\n\\\/\\\/ Zamowienia zmieniaja status, a WooCommerce wysyla wtedy maile do klientow. W Playground\\n\\\/\\\/ poczty nie ma, a na prawdziwym WordPressie (proba lokalna) poszlyby do example.com.\\nadd_filter( 'pre_wp_mail', '__return_false' );\\n\\n\\\/\\\/ PIERWSZE WEJSCIE DO PANELU NOWEGO SKLEPU. WooCommerce konczy instalacje dopiero przy\\n\\\/\\\/ pierwszym admin_init (WC_Install::newly_installed): wlacza wtedy HPOS, o ile sklep nie ma\\n\\\/\\\/ jeszcze zamowien, i ustawia reszte domyslnych opcji nowego sklepu. Ten skrypt chodzi PRZED\\n\\\/\\\/ pierwszym admin_init, wiec bez tego zamowienia trafilyby na stale do starego magazynu\\n\\\/\\\/ (wp_posts) - a nowy sklep tak nie wyglada. (Pomiar sesji Merchgaina, 10.10.2026.)\\nif ( 'yes' === get_option( 'woocommerce_newly_installed' ) && is_callable( array( 'WC_Install', 'newly_installed' ) ) ) {\\n\\tWC_Install::newly_installed();\\n}\\n\\n\\\/\\\/ JEDNA TRANSAKCJA NA CALY ZASIEW. W Playground baza to SQLite pod warstwa tlumaczaca MySQL,\\n\\\/\\\/ a kazde zapytanie poza transakcja zatwierdza sie osobno. Kazdy etap ma wlasny try\\\/catch,\\n\\\/\\\/ wiec do COMMIT na koncu dochodzi zawsze, takze po bledzie pojedynczego produktu.\\n$GLOBALS['wpdb']->query( 'START TRANSACTION' );\\n\\n$merchlint_demo_bledy = array();\\n$merchlint_demo_czasy = array();\\n\\n\\\/**\\n * Wykonuje etap zasiewu tak, zeby jego awaria nie zatrzymala nastepnych.\\n *\\n * Mierzy tez czas - sumowany po pierwszym slowie nazwy (\\\"produkt\\\", \\\"zamowienie\\\"), bo\\n * odwiedzajacy czeka na caly zasiew, a w przegladarce kazde zapytanie idzie przez warstwe\\n * tlumaczaca MySQL na SQLite.\\n *\\n * @param string   $nazwa Nazwa etapu do dziennika.\\n * @param callable $etap  Etap.\\n * @return mixed Wynik etapu albo null po bledzie.\\n *\\\/\\nfunction merchlint_demo_etap( $nazwa, $etap ) {\\n\\tglobal $merchlint_demo_bledy, $merchlint_demo_czasy;\\n\\t$start = microtime( true );\\n\\ttry {\\n\\t\\treturn $etap();\\n\\t} catch ( Throwable $e ) {\\n\\t\\t$merchlint_demo_bledy[] = $nazwa . ': ' . $e->getMessage();\\n\\t\\terror_log( 'merchlint-demo: ' . $nazwa . ': ' . $e->getMessage() );\\n\\t\\treturn null;\\n\\t} finally {\\n\\t\\tmerchlint_demo_czas( strtok( $nazwa, ' ' ), $start );\\n\\t}\\n}\\n\\n\\\/**\\n * Dolicza czas od $start do grupy w dzienniku zasiewu.\\n *\\n * @param string $grupa Nazwa grupy.\\n * @param float  $start microtime( true ) z poczatku odcinka.\\n *\\\/\\nfunction merchlint_demo_czas( $grupa, $start ) {\\n\\tglobal $merchlint_demo_czasy;\\n\\t$merchlint_demo_czasy[ $grupa ] = ( $merchlint_demo_czasy[ $grupa ] ?? 0 ) + microtime( true ) - $start;\\n}\\n\\n\\\/**\\n * EAN-13 z prefiksem 20 - zakres \\\"do uzytku wewnetrznego\\\", ktorego GS1 nie przydziela\\n * zadnemu producentowi. Numer z podgladu nie trafi wiec na prawdziwy towar.\\n *\\n * @param int $n Numer kolejny.\\n *\\\/\\nfunction merchlint_demo_ean( $n ) {\\n\\t$kod   = '20' . str_pad( (string) $n, 10, '0', STR_PAD_LEFT );\\n\\t$suma  = 0;\\n\\t$dlug  = strlen( $kod );\\n\\tfor ( $i = 0; $i < $dlug; $i++ ) {\\n\\t\\t$suma += (int) $kod[ $i ] * ( 0 === $i % 2 ? 1 : 3 );\\n\\t}\\n\\treturn $kod . ( ( 10 - $suma % 10 ) % 10 );\\n}\\n\\n\\\/**\\n * Obrazek produktu narysowany na miejscu: Playground nie ma sieci, z ktorej moglby pobrac zdjecia.\\n *\\n * Plaski kafelek z prostym ksztaltem - wystarczy, zeby sklep wygladal na sklep, a reguly\\n * zdjec (A2, C2, D1, D6) mialy na czym pracowac. Podrozmiarow nie generujemy: kazdy to\\n * kilka przebiegow GD w przegladarce, a WordPress bez nich pokazuje oryginal.\\n *\\n * @param string $plik  Nazwa pliku bez rozszerzenia.\\n * @param array  $o     kolor [r,g,b], ksztalt, alt, ciezki (PNG bez kompresji, ponad 1 MB),\\n *                      bez_pliku (wpis w mediach bez pliku na dysku).\\n * @return int Identyfikator zalacznika.\\n *\\\/\\nfunction merchlint_demo_obrazek( $plik, array $o ) {\\n\\t$katalog = wp_upload_dir();\\n\\tif ( ! empty( $katalog['error'] ) ) {\\n\\t\\tthrow new RuntimeException( 'uploads: ' . $katalog['error'] );\\n\\t}\\n\\twp_mkdir_p( $katalog['path'] );\\n\\n\\t$bok     = ! empty( $o['ciezki'] ) ? 600 : 400;\\n\\t$sciezka = trailingslashit( $katalog['path'] ) . $plik . '.png';\\n\\n\\tif ( empty( $o['bez_pliku'] ) ) {\\n\\t\\t$im    = imagecreatetruecolor( $bok, $bok );\\n\\t\\t$tlo   = imagecolorallocate( $im, $o['kolor'][0], $o['kolor'][1], $o['kolor'][2] );\\n\\t\\t$bialy = imagecolorallocate( $im, 255, 255, 255 );\\n\\t\\t$cien  = imagecolorallocate( $im, (int) ( $o['kolor'][0] * 0.7 ), (int) ( $o['kolor'][1] * 0.7 ), (int) ( $o['kolor'][2] * 0.7 ) );\\n\\t\\timagefilledrectangle( $im, 0, 0, $bok, $bok, $tlo );\\n\\n\\t\\t$s = $bok \\\/ 400;\\n\\t\\tswitch ( $o['ksztalt'] ) {\\n\\t\\t\\tcase 'sluchawki':\\n\\t\\t\\t\\timagesetthickness( $im, (int) ( 18 * $s ) );\\n\\t\\t\\t\\timagearc( $im, (int) ( 200 * $s ), (int) ( 210 * $s ), (int) ( 220 * $s ), (int) ( 220 * $s ), 180, 360, $bialy );\\n\\t\\t\\t\\timagefilledrectangle( $im, (int) ( 78 * $s ), (int) ( 200 * $s ), (int) ( 128 * $s ), (int) ( 290 * $s ), $bialy );\\n\\t\\t\\t\\timagefilledrectangle( $im, (int) ( 272 * $s ), (int) ( 200 * $s ), (int) ( 322 * $s ), (int) ( 290 * $s ), $bialy );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'mikrofon':\\n\\t\\t\\t\\timagefilledellipse( $im, (int) ( 200 * $s ), (int) ( 150 * $s ), (int) ( 110 * $s ), (int) ( 150 * $s ), $bialy );\\n\\t\\t\\t\\timagefilledrectangle( $im, (int) ( 185 * $s ), (int) ( 220 * $s ), (int) ( 215 * $s ), (int) ( 320 * $s ), $bialy );\\n\\t\\t\\t\\timagefilledrectangle( $im, (int) ( 140 * $s ), (int) ( 315 * $s ), (int) ( 260 * $s ), (int) ( 330 * $s ), $bialy );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'pudelko':\\n\\t\\t\\t\\timagefilledrectangle( $im, (int) ( 90 * $s ), (int) ( 150 * $s ), (int) ( 310 * $s ), (int) ( 260 * $s ), $bialy );\\n\\t\\t\\t\\timagefilledellipse( $im, (int) ( 150 * $s ), (int) ( 205 * $s ), (int) ( 50 * $s ), (int) ( 50 * $s ), $cien );\\n\\t\\t\\t\\timagefilledellipse( $im, (int) ( 250 * $s ), (int) ( 205 * $s ), (int) ( 50 * $s ), (int) ( 50 * $s ), $cien );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'glosnik':\\n\\t\\t\\t\\timagefilledrectangle( $im, (int) ( 130 * $s ), (int) ( 80 * $s ), (int) ( 270 * $s ), (int) ( 320 * $s ), $bialy );\\n\\t\\t\\t\\timagefilledellipse( $im, (int) ( 200 * $s ), (int) ( 140 * $s ), (int) ( 60 * $s ), (int) ( 60 * $s ), $cien );\\n\\t\\t\\t\\timagefilledellipse( $im, (int) ( 200 * $s ), (int) ( 240 * $s ), (int) ( 100 * $s ), (int) ( 100 * $s ), $cien );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'swiatlo':\\n\\t\\t\\t\\timagesetthickness( $im, (int) ( 26 * $s ) );\\n\\t\\t\\t\\timagearc( $im, (int) ( 200 * $s ), (int) ( 200 * $s ), (int) ( 220 * $s ), (int) ( 220 * $s ), 0, 360, $bialy );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tdefault:\\n\\t\\t\\t\\timagesetthickness( $im, (int) ( 14 * $s ) );\\n\\t\\t\\t\\timageline( $im, (int) ( 110 * $s ), (int) ( 300 * $s ), (int) ( 290 * $s ), (int) ( 110 * $s ), $bialy );\\n\\t\\t\\t\\timagefilledellipse( $im, (int) ( 110 * $s ), (int) ( 300 * $s ), (int) ( 50 * $s ), (int) ( 50 * $s ), $bialy );\\n\\t\\t\\t\\timagefilledellipse( $im, (int) ( 290 * $s ), (int) ( 110 * $s ), (int) ( 50 * $s ), (int) ( 50 * $s ), $bialy );\\n\\t\\t}\\n\\n\\t\\t\\\/\\\/ Stopien kompresji 0 daje plik wielkosci surowych pikseli niezaleznie od tresci -\\n\\t\\t\\\/\\\/ tak powstaje zdjecie ponad progiem D1 (500 kB), a reszta zostaje lekka.\\n\\t\\timagepng( $im, $sciezka, ! empty( $o['ciezki'] ) ? 0 : 9 );\\n\\t\\timagedestroy( $im );\\n\\t}\\n\\n\\t$id = wp_insert_attachment(\\n\\t\\tarray(\\n\\t\\t\\t'post_mime_type' => 'image\\\/png',\\n\\t\\t\\t'post_title'     => $o['tytul'],\\n\\t\\t\\t'post_status'    => 'inherit',\\n\\t\\t\\t'guid'           => trailingslashit( $katalog['url'] ) . $plik . '.png',\\n\\t\\t),\\n\\t\\t$sciezka\\n\\t);\\n\\tif ( is_wp_error( $id ) || ! $id ) {\\n\\t\\tthrow new RuntimeException( 'wp_insert_attachment: ' . $plik );\\n\\t}\\n\\n\\tupdate_post_meta(\\n\\t\\t$id,\\n\\t\\t'_wp_attachment_metadata',\\n\\t\\tarray(\\n\\t\\t\\t'width'    => $bok,\\n\\t\\t\\t'height'   => $bok,\\n\\t\\t\\t'file'     => _wp_relative_upload_path( $sciezka ),\\n\\t\\t\\t'filesize' => file_exists( $sciezka ) ? filesize( $sciezka ) : 0,\\n\\t\\t\\t'sizes'    => array(),\\n\\t\\t)\\n\\t);\\n\\tif ( '' !== $o['alt'] ) {\\n\\t\\tupdate_post_meta( $id, '_wp_attachment_image_alt', $o['alt'] );\\n\\t}\\n\\n\\treturn (int) $id;\\n}\\n\\n\\\/\\\/ ---------------------------------------------------------------------------\\n\\\/\\\/ Sklep\\n\\\/\\\/ ---------------------------------------------------------------------------\\n\\nmerchlint_demo_etap(\\n\\t'sklep',\\n\\tstatic function () {\\n\\t\\tupdate_option( 'blogname', 'Studio Supply Co.' );\\n\\t\\tupdate_option( 'blogdescription', 'Audio and video gear for home studios' );\\n\\t\\tupdate_option( 'woocommerce_currency', 'USD' );\\n\\t\\tupdate_option( 'woocommerce_default_country', 'US:CA' );\\n\\n\\t\\t\\\/\\\/ Kreator WooCommerce przy pierwszym wejsciu do panelu przekierowuje na swoj ekran -\\n\\t\\t\\\/\\\/ odwiedzajacy wyladowalby tam zamiast na audycie. Tryb \\\"coming soon\\\" nowych sklepow\\n\\t\\t\\\/\\\/ zostawia w pasku panelu plakietke i zaslania sklep przed tym, kto kliknie \\\"View\\\".\\n\\t\\tdelete_transient( '_wc_activation_redirect' );\\n\\t\\tupdate_option( 'woocommerce_onboarding_profile', array( 'skipped' => true ) );\\n\\t\\tupdate_option( 'woocommerce_task_list_hidden', 'yes' );\\n\\t\\tupdate_option( 'woocommerce_coming_soon', 'no' );\\n\\t\\tupdate_option( 'woocommerce_show_marketplace_suggestions', 'no' );\\n\\t\\tupdate_option( 'woocommerce_allow_tracking', 'no' );\\n\\t}\\n);\\n\\n$merchlint_demo_kategorie = merchlint_demo_etap(\\n\\t'kategorie',\\n\\tstatic function () {\\n\\t\\t$ids = array();\\n\\t\\tforeach ( array(\\n\\t\\t\\t'headphones' => 'Headphones',\\n\\t\\t\\t'microphones' => 'Microphones',\\n\\t\\t\\t'interfaces' => 'Audio interfaces',\\n\\t\\t\\t'monitors'   => 'Studio monitors',\\n\\t\\t\\t'lighting'   => 'Lighting',\\n\\t\\t\\t'stands'     => 'Stands and mounts',\\n\\t\\t\\t'cables'     => 'Cables and accessories',\\n\\t\\t\\t'acoustics'  => 'Acoustic treatment',\\n\\t\\t) as $slug => $nazwa ) {\\n\\t\\t\\t$term = term_exists( $slug, 'product_cat' );\\n\\t\\t\\tif ( ! $term ) {\\n\\t\\t\\t\\t$term = wp_insert_term( $nazwa, 'product_cat', array( 'slug' => $slug ) );\\n\\t\\t\\t}\\n\\t\\t\\tif ( is_wp_error( $term ) ) {\\n\\t\\t\\t\\tthrow new RuntimeException( $term->get_error_message() );\\n\\t\\t\\t}\\n\\t\\t\\t$ids[ $slug ] = (int) $term['term_id'];\\n\\t\\t}\\n\\t\\treturn $ids;\\n\\t}\\n);\\nif ( ! is_array( $merchlint_demo_kategorie ) ) {\\n\\t$merchlint_demo_kategorie = array();\\n}\\n\\n\\\/\\\/ ---------------------------------------------------------------------------\\n\\\/\\\/ Produkty\\n\\\/\\\/\\n\\\/\\\/ Kazdy wiersz to jeden produkt, a uwaga nad nim mowi, ktora regula ma go znalezc.\\n\\\/\\\/ rytm\\\/faza: produkt jest w co `rytm`-tym zamowieniu, zaczynajac od `faza`; `ile` - sztuk w jednym\\n\\\/\\\/ zamowieniu; `do_dni` - ostatnia sprzedaz najpozniej tyle dni temu (towar zszedl ze stanu albo\\n\\\/\\\/ z witryny).\\n\\\/\\\/\\n\\\/\\\/ MALO POZYCJI, A KWOTY JAK W SKLEPIE. Kazda pozycja zamowienia to kilkanascie zapytan, a kazde\\n\\\/\\\/ zapytanie w przegladarce idzie przez warstwe tlumaczaca MySQL na SQLite - pierwsza wersja\\n\\\/\\\/ (52 zamowienia, 259 pozycji) zajmowala 84 s z 137 s krokow w Playground CLI. Kwoty w audycie\\n\\\/\\\/ licza sie tylko produktom z A1-A3, wiec to one sprzedaja sie czesto (SM-50 po dwie sztuki),\\n\\\/\\\/ a reszta - kilka razy w roku, dla wygladu listy zamowien.\\n\\\/\\\/ ---------------------------------------------------------------------------\\n\\n$merchlint_demo_reszta = ' Every item ships from our warehouse within one business day, is tested before it leaves us and comes with a two-year warranty and 30-day returns.';\\n\\n$merchlint_demo_produkty = array(\\n\\t\\\/\\\/ A1: bestseller, ktorego nie da sie kupic.\\n\\t'sm50'      => array( 'nazwa' => 'SM-50 Studio Monitor Headphones', 'sku' => 'SSC-SM50', 'cena' => '149.00', 'kat' => 'headphones', 'stan' => 'outofstock', 'ksztalt' => 'sluchawki', 'kolor' => array( 72, 110, 160 ), 'ile' => 2, 'rytm' => 2, 'faza' => 0, 'do_dni' => 20,\\n\\t\\t'opis' => 'Closed-back studio headphones with a flat, honest response for tracking and mixing. Memory-foam pads stay comfortable through long sessions, and the detachable coiled cable reaches from desk to booth.' ),\\n\\t\\\/\\\/ A1.\\n\\t'cm1'       => array( 'nazwa' => 'CM-1 Large-Diaphragm Condenser Microphone', 'sku' => 'SSC-CM1', 'cena' => '199.00', 'kat' => 'microphones', 'stan' => 'outofstock', 'ksztalt' => 'mikrofon', 'kolor' => array( 150, 96, 70 ), 'rytm' => 2, 'faza' => 1, 'do_dni' => 26,\\n\\t\\t'opis' => 'A one-inch cardioid condenser for vocals, acoustic guitar and voice-over. A low self-noise of 8 dBA keeps quiet passages clean, and the switchable pad handles loud sources without distortion.' ),\\n\\t'dm7'       => array( 'nazwa' => 'DM-7 Dynamic Podcast Microphone', 'sku' => 'SSC-DM7', 'cena' => '119.00', 'kat' => 'microphones', 'ksztalt' => 'mikrofon', 'kolor' => array( 60, 60, 66 ), 'rytm' => 6, 'faza' => 0,\\n\\t\\t'opis' => 'An end-address dynamic microphone built for spoken word. It rejects room noise and keyboard clatter, so untreated rooms still sound focused, and the XLR output works with any interface or mixer.' ),\\n\\t\\\/\\\/ B4: dwa produkty z tym samym SKU (drugi z importu zestawow).\\n\\t'uai22'     => array( 'nazwa' => 'UAI-22 USB Audio Interface 2x2', 'sku' => 'SSC-UAI22', 'cena' => '129.00', 'kat' => 'interfaces', 'ksztalt' => 'pudelko', 'kolor' => array( 170, 60, 60 ), 'rytm' => 5, 'faza' => 2,\\n\\t\\t'opis' => 'Two combo inputs with clean preamps, two balanced outputs and a headphone jack with its own level control. Bus-powered over USB-C, so it records anywhere your laptop goes, with zero-latency direct monitoring.' ),\\n\\t'uai22set'  => array( 'nazwa' => 'UAI-22 Recording Bundle (interface, cable, headphones)', 'sku' => 'SSC-UAI22-BND', 'sku_jak' => 'uai22', 'cena' => '139.00', 'kat' => 'interfaces', 'ksztalt' => 'pudelko', 'kolor' => array( 190, 84, 70 ), 'rytm' => 9, 'faza' => 4,\\n\\t\\t'opis' => 'Everything a first home studio needs in one box: the UAI-22 interface, a three-metre XLR cable and a pair of closed-back headphones. The fastest way from an empty desk to a first recording.' ),\\n\\t\\\/\\\/ A2: sprzedaje sie, a nie ma zdjecia.\\n\\t'mon5'      => array( 'nazwa' => 'MX5 Active Studio Monitors (pair)', 'sku' => 'SSC-MX5', 'cena' => '299.00', 'kat' => 'monitors', 'ksztalt' => 'glosnik', 'kolor' => array( 50, 50, 56 ), 'bez_zdjecia' => true, 'rytm' => 3, 'faza' => 0,\\n\\t\\t'opis' => 'Five-inch bi-amplified near-field monitors with a front bass port, so they work close to a wall. Room-correction switches tame the low end on a desk, and balanced and unbalanced inputs connect to anything.' ),\\n\\t\\\/\\\/ A3: sprzedawal sie, a teraz jest szkicem.\\n\\t'led60'     => array( 'nazwa' => 'LP-60 LED Panel Light 60 W', 'sku' => 'SSC-LP60', 'cena' => '89.00', 'kat' => 'lighting', 'status' => 'draft', 'ksztalt' => 'swiatlo', 'kolor' => array( 214, 160, 52 ), 'rytm' => 3, 'faza' => 1, 'do_dni' => 30,\\n\\t\\t'opis' => 'A bi-colour LED panel from 3200 K to 5600 K with a high colour rendering index, so skin tones look natural on camera. Silent, fanless cooling makes it safe to use next to a microphone.' ),\\n\\t'sm30'      => array( 'nazwa' => 'SM-30 Studio Headphones', 'sku' => 'SSC-SM30', 'cena' => '89.00', 'kat' => 'headphones', 'ksztalt' => 'sluchawki', 'kolor' => array( 90, 130, 110 ), 'rytm' => 6, 'faza' => 3,\\n\\t\\t'opis' => 'Lightweight closed-back headphones for monitoring and everyday listening. The folding design and the carry pouch make them the pair that travels, and replaceable pads keep them going for years.' ),\\n\\t\\\/\\\/ C2: zdjecie bez tekstu alternatywnego.\\n\\t'boom'      => array( 'nazwa' => 'BA-2 Boom Arm Microphone Stand', 'sku' => 'SSC-BA2', 'cena' => '59.00', 'kat' => 'stands', 'ksztalt' => 'kabel', 'kolor' => array( 110, 110, 120 ), 'alt' => '', 'rytm' => 5, 'faza' => 0,\\n\\t\\t'opis' => 'A spring-balanced boom arm that clamps to desks up to six centimetres thick. Internal cable routing keeps the desk tidy, and it holds microphones up to 1.2 kg in any position without drifting.' ),\\n\\t\\\/\\\/ D1: zdjecie ponad 1 MB.\\n\\t'ring'      => array( 'nazwa' => 'RL-18 Ring Light 18 inch', 'sku' => 'SSC-RL18', 'cena' => '69.00', 'kat' => 'lighting', 'ksztalt' => 'swiatlo', 'kolor' => array( 200, 120, 150 ), 'ciezki' => true, 'rytm' => 7, 'faza' => 2,\\n\\t\\t'opis' => 'An eighteen-inch dimmable ring light with a phone holder and a two-metre stand. Even, shadow-free light for video calls, product photos and streaming, with colour temperature adjustable in ten steps.' ),\\n\\t\\\/\\\/ A4: promocja skonczyla sie 18 dni temu, a cena promocyjna dalej obowiazuje.\\n\\t'pop'       => array( 'nazwa' => 'PF-2 Pop Filter, Double Mesh', 'sku' => 'SSC-PF2', 'cena' => '24.99', 'promocja' => '19.99', 'promocja_do_dni' => 18, 'kat' => 'cables', 'ksztalt' => 'swiatlo', 'kolor' => array( 70, 70, 70 ), 'rytm' => 4, 'faza' => 1,\\n\\t\\t'opis' => 'A double-layer pop filter that stops plosives before they reach the capsule. The gooseneck holds its position, and the clamp fits stands and boom arms up to thirty millimetres across.' ),\\n\\t'xlr3'      => array( 'nazwa' => 'XLR Cable 3 m (10 ft)', 'sku' => 'SSC-XLR3', 'cena' => '14.99', 'kat' => 'cables', 'ksztalt' => 'kabel', 'kolor' => array( 40, 90, 140 ), 'ile' => 3, 'rytm' => 2, 'faza' => 1,\\n\\t\\t'opis' => 'A balanced microphone cable with oxygen-free copper conductors and a braided shield that keeps hum and phone interference out. Gold-plated contacts and strain-relieved connectors survive years of studio use.' ),\\n\\t\\\/\\\/ G3b: ten sam GTIN co kabel 3 m (dwa rozmiary z jednego kodu hurtowni).\\n\\t'xlr6'      => array( 'nazwa' => 'XLR Cable 6 m (20 ft)', 'sku' => 'SSC-XLR6', 'gtin_jak' => 'xlr3', 'cena' => '19.99', 'kat' => 'cables', 'ksztalt' => 'kabel', 'kolor' => array( 40, 110, 160 ), 'rytm' => 6, 'faza' => 5,\\n\\t\\t'opis' => 'The six-metre version of our balanced microphone cable, long enough to reach from the control desk to a vocal booth. The same braided shield, oxygen-free copper and gold-plated contacts as the short one.' ),\\n\\t\\\/\\\/ G3a: bez GTIN.\\n\\t'ha2'       => array( 'nazwa' => 'HA-2 Headphone Amplifier', 'sku' => 'SSC-HA2', 'gtin' => '', 'cena' => '89.00', 'kat' => 'headphones', 'ksztalt' => 'pudelko', 'kolor' => array( 52, 120, 120 ), 'rytm' => 9, 'faza' => 7,\\n\\t\\t'opis' => 'A four-channel headphone amplifier, so a band can track together with everyone hearing their own mix level. Each output has its own volume control, and the metal case stands up to life on a studio floor.' ),\\n\\t\\\/\\\/ B1: poza kazda kategoria.\\n\\t'strap'     => array( 'nazwa' => 'Leather Camera Strap', 'sku' => 'SSC-STRAP', 'cena' => '34.00', 'kat' => null, 'ksztalt' => 'kabel', 'kolor' => array( 140, 100, 70 ), 'rytm' => 8, 'faza' => 3,\\n\\t\\t'opis' => 'A full-grain leather strap that gets better with age. Quick-release clips move it between cameras in seconds, and the padded shoulder section takes the weight of a full-frame body with a zoom lens.' ),\\n\\t\\\/\\\/ C1 i G3a: opis za krotki, brak GTIN.\\n\\t'shock'     => array( 'nazwa' => 'SM-1 Shock Mount for CM-1', 'sku' => 'SSC-SHOCK1', 'gtin' => '', 'cena' => '29.00', 'kat' => 'stands', 'ksztalt' => 'mikrofon', 'kolor' => array( 120, 120, 120 ), 'rytm' => 7, 'faza' => 5,\\n\\t\\t'opis' => 'Shock mount for the CM-1.', 'krotki_opis' => true ),\\n\\t\\\/\\\/ C1.\\n\\t'deskstand' => array( 'nazwa' => 'DS-1 Desktop Microphone Stand', 'sku' => 'SSC-DS1', 'cena' => '24.00', 'kat' => 'stands', 'ksztalt' => 'kabel', 'kolor' => array( 100, 100, 110 ), 'rytm' => 10, 'faza' => 6,\\n\\t\\t'opis' => 'Desk stand, weighted base.', 'krotki_opis' => true ),\\n\\t\\\/\\\/ D6: wpis w mediach bez pliku na dysku.\\n\\t'pads'      => array( 'nazwa' => 'IP-1 Monitor Isolation Pads (pair)', 'sku' => 'SSC-IP1', 'cena' => '39.00', 'kat' => 'acoustics', 'ksztalt' => 'pudelko', 'kolor' => array( 80, 80, 80 ), 'bez_pliku' => true, 'rytm' => 8, 'faza' => 6,\\n\\t\\t'opis' => 'High-density foam pads that decouple studio monitors from the desk, so the bass stops booming through the furniture. The adjustable wedge angles the speakers up toward your ears for a cleaner stereo image.' ),\\n\\t'clean'     => array( 'nazwa' => 'Lens and Screen Cleaning Kit', 'sku' => 'SSC-CLEAN', 'cena' => '12.00', 'kat' => 'cables', 'ksztalt' => 'kabel', 'kolor' => array( 60, 140, 200 ), 'rytm' => 6, 'faza' => 2,\\n\\t\\t'opis' => 'A blower, a soft brush, two microfibre cloths and an alcohol-free fluid that is safe for coated lenses and screens. Everything packs into a zip pouch small enough to live in a camera bag.' ),\\n\\t\\\/\\\/ A5: opublikowany, na stanie, bez ceny.\\n\\t'softbox'   => array( 'nazwa' => 'SB-2 Softbox Lighting Kit (2-pack)', 'sku' => 'SSC-SB2', 'cena' => '', 'kat' => 'lighting', 'ksztalt' => 'swiatlo', 'kolor' => array( 180, 170, 150 ),\\n\\t\\t'opis' => 'Two 50 x 70 cm softboxes with daylight bulbs, stands and a carry bag. Soft, even light for interviews and product video, set up in five minutes and folded back into the bag in two.' ),\\n\\t\\\/\\\/ B11: zestaw z importu, ktoremu arkusz wpisal rodzica - zeszloroczny zestaw, ktory zostal w szkicu\\n\\t\\\/\\\/ (rodzic wpisany na koncu zasiewu, etap \\\"rodzic z importu\\\"). Szkic ukrywa produkt w kazdej wersji\\n\\t\\\/\\\/ WooCommerce (Merchlint\\\/Plan\\\/33 par. 3). Oba bez rytmu: szkic nic nie sprzedal, wiec A3 milczy.\\n\\t'kit25'     => array( 'nazwa' => 'Podcast Starter Kit (2025 edition)', 'sku' => 'SSC-PSK25', 'cena' => '229.00', 'kat' => 'microphones', 'status' => 'draft', 'ksztalt' => 'mikrofon', 'kolor' => array( 120, 80, 140 ),\\n\\t\\t'opis' => 'The 2025 podcast bundle: the DM-7 microphone, a desk stand, an XLR cable and a pop filter. Replaced by the 2026 edition, which adds headphones and a longer cable at a slightly higher price.' ),\\n\\t'kit26'     => array( 'nazwa' => 'Podcast Starter Kit (2026 edition)', 'sku' => 'SSC-PSK26', 'cena' => '249.00', 'kat' => 'microphones', 'ksztalt' => 'mikrofon', 'kolor' => array( 140, 90, 160 ), 'rodzic_jak' => 'kit25',\\n\\t\\t'opis' => 'Everything a two-person podcast needs: the DM-7 dynamic microphone, a desk stand, a six-metre XLR cable, a pop filter and SM-30 headphones. Plug into any interface and record the first episode the same evening.' ),\\n);\\n\\n\\\/\\\/ Produkty wariantowe. Warianty maja wlasne SKU i GTIN - tak, jak tego chce Google.\\n$merchlint_demo_wariantowe = array(\\n\\t\\\/\\\/ B5 i A1: wszystkie warianty wyprzedane.\\n\\t'foam' => array( 'nazwa' => 'AF-12 Acoustic Foam Panels (12-pack)', 'sku' => 'SSC-AF12', 'cena' => '49.00', 'kat' => 'acoustics', 'ksztalt' => 'pudelko', 'kolor' => array( 60, 70, 90 ), 'rytm' => 4, 'faza' => 2, 'do_dni' => 35,\\n\\t\\t'atrybut' => 'Colour', 'warianty' => array( 'Black' => 'outofstock', 'Charcoal' => 'outofstock', 'Blue' => 'outofstock' ),\\n\\t\\t'opis' => 'Twelve 30 x 30 cm wedge panels that soak up flutter echo and slap-back from bare walls. Fire-rated foam, cut clean on all sides, so the panels butt together without gaps along a wall or ceiling.' ),\\n\\t\\\/\\\/ B10: produkt oznaczony jako wyprzedany, a warianty na stanie.\\n\\t'earpads' => array( 'nazwa' => 'Replacement Ear Pads for SM-50', 'sku' => 'SSC-EP50', 'cena' => '24.00', 'kat' => 'headphones', 'ksztalt' => 'sluchawki', 'kolor' => array( 150, 150, 160 ), 'rytm' => 5, 'faza' => 3, 'rodzic_wyprzedany' => true,\\n\\t\\t'atrybut' => 'Material', 'warianty' => array( 'Velour' => 'instock', 'Leatherette' => 'instock' ),\\n\\t\\t'opis' => 'A fresh pair of memory-foam ear pads for the SM-50, in breathable velour or easy-clean leatherette. They twist on in under a minute, and new pads restore the seal and the bass of the original headphones.' ),\\n);\\n\\n$merchlint_demo_obiekty = array();\\n\\n$merchlint_demo_numer = 0;\\nforeach ( $merchlint_demo_produkty as $merchlint_demo_klucz => $merchlint_demo_p ) {\\n\\t++$merchlint_demo_numer;\\n\\t$merchlint_demo_obiekty[ $merchlint_demo_klucz ] = merchlint_demo_etap(\\n\\t\\t'produkt ' . $merchlint_demo_klucz,\\n\\t\\tstatic function () use ( $merchlint_demo_klucz, $merchlint_demo_p, $merchlint_demo_numer, $merchlint_demo_kategorie, $merchlint_demo_reszta ) {\\n\\t\\t\\t$p = $merchlint_demo_p;\\n\\n\\t\\t\\t$produkt = new WC_Product_Simple();\\n\\t\\t\\t$produkt->set_name( $p['nazwa'] );\\n\\t\\t\\t$produkt->set_sku( $p['sku'] );\\n\\t\\t\\t$produkt->set_regular_price( $p['cena'] );\\n\\t\\t\\t$produkt->set_status( $p['status'] ?? 'publish' );\\n\\t\\t\\t$produkt->set_stock_status( $p['stan'] ?? 'instock' );\\n\\t\\t\\t$produkt->set_description( $p['opis'] . ( empty( $p['krotki_opis'] ) ? $merchlint_demo_reszta : '' ) );\\n\\t\\t\\t$produkt->set_short_description( '' );\\n\\t\\t\\tif ( null !== $p['kat'] && isset( $merchlint_demo_kategorie[ $p['kat'] ] ) ) {\\n\\t\\t\\t\\t$produkt->set_category_ids( array( $merchlint_demo_kategorie[ $p['kat'] ] ) );\\n\\t\\t\\t}\\n\\t\\t\\tif ( ! array_key_exists( 'gtin', $p ) && ! isset( $p['gtin_jak'] ) && method_exists( $produkt, 'set_global_unique_id' ) ) {\\n\\t\\t\\t\\t$produkt->set_global_unique_id( merchlint_demo_ean( 1000 + $merchlint_demo_numer ) );\\n\\t\\t\\t}\\n\\t\\t\\tif ( isset( $p['promocja'] ) ) {\\n\\t\\t\\t\\t$produkt->set_sale_price( $p['promocja'] );\\n\\t\\t\\t\\t$produkt->set_date_on_sale_from( time() - 60 * DAY_IN_SECONDS );\\n\\t\\t\\t\\t$produkt->set_date_on_sale_to( time() - $p['promocja_do_dni'] * DAY_IN_SECONDS );\\n\\t\\t\\t}\\n\\t\\t\\tif ( empty( $p['bez_zdjecia'] ) ) {\\n\\t\\t\\t\\t$produkt->set_image_id(\\n\\t\\t\\t\\t\\tmerchlint_demo_obrazek(\\n\\t\\t\\t\\t\\t\\t'ssc-' . $merchlint_demo_klucz,\\n\\t\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t\\t'tytul'     => $p['nazwa'],\\n\\t\\t\\t\\t\\t\\t\\t'kolor'     => $p['kolor'],\\n\\t\\t\\t\\t\\t\\t\\t'ksztalt'   => $p['ksztalt'],\\n\\t\\t\\t\\t\\t\\t\\t'alt'       => array_key_exists( 'alt', $p ) ? $p['alt'] : $p['nazwa'],\\n\\t\\t\\t\\t\\t\\t\\t'ciezki'    => ! empty( $p['ciezki'] ),\\n\\t\\t\\t\\t\\t\\t\\t'bez_pliku' => ! empty( $p['bez_pliku'] ),\\n\\t\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t);\\n\\t\\t\\t}\\n\\t\\t\\t$produkt->save();\\n\\n\\t\\t\\tif ( isset( $p['promocja'] ) ) {\\n\\t\\t\\t\\t\\\/\\\/ Promocja, ktorej koniec minal, a cena promocyjna obowiazuje dalej: tak zostaje,\\n\\t\\t\\t\\t\\\/\\\/ gdy zadanie WooCommerce konczace promocje sie nie wykonalo. Przed zamowieniami,\\n\\t\\t\\t\\t\\\/\\\/ zeby i one szly po tej cenie - stad ponowny odczyt produktu.\\n\\t\\t\\t\\tupdate_post_meta( $produkt->get_id(), '_price', $p['promocja'] );\\n\\t\\t\\t\\treturn wc_get_product( $produkt->get_id() );\\n\\t\\t\\t}\\n\\n\\t\\t\\treturn $produkt;\\n\\t\\t}\\n\\t);\\n}\\n\\nforeach ( $merchlint_demo_wariantowe as $merchlint_demo_klucz => $merchlint_demo_p ) {\\n\\t++$merchlint_demo_numer;\\n\\t$merchlint_demo_obiekty[ $merchlint_demo_klucz ] = merchlint_demo_etap(\\n\\t\\t'produkt ' . $merchlint_demo_klucz,\\n\\t\\tstatic function () use ( $merchlint_demo_klucz, $merchlint_demo_p, $merchlint_demo_numer, $merchlint_demo_kategorie, $merchlint_demo_reszta ) {\\n\\t\\t\\t$p = $merchlint_demo_p;\\n\\n\\t\\t\\t$atrybut = new WC_Product_Attribute();\\n\\t\\t\\t$atrybut->set_name( $p['atrybut'] );\\n\\t\\t\\t$atrybut->set_options( array_keys( $p['warianty'] ) );\\n\\t\\t\\t$atrybut->set_visible( true );\\n\\t\\t\\t$atrybut->set_variation( true );\\n\\n\\t\\t\\t$produkt = new WC_Product_Variable();\\n\\t\\t\\t$produkt->set_name( $p['nazwa'] );\\n\\t\\t\\t$produkt->set_sku( $p['sku'] );\\n\\t\\t\\t$produkt->set_status( 'publish' );\\n\\t\\t\\t$produkt->set_description( $p['opis'] . $merchlint_demo_reszta );\\n\\t\\t\\t$produkt->set_category_ids( array( $merchlint_demo_kategorie[ $p['kat'] ] ?? 0 ) );\\n\\t\\t\\t$produkt->set_attributes( array( $atrybut ) );\\n\\t\\t\\t$produkt->set_image_id(\\n\\t\\t\\t\\tmerchlint_demo_obrazek(\\n\\t\\t\\t\\t\\t'ssc-' . $merchlint_demo_klucz,\\n\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t'tytul'   => $p['nazwa'],\\n\\t\\t\\t\\t\\t\\t'kolor'   => $p['kolor'],\\n\\t\\t\\t\\t\\t\\t'ksztalt' => $p['ksztalt'],\\n\\t\\t\\t\\t\\t\\t'alt'     => $p['nazwa'],\\n\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t)\\n\\t\\t\\t);\\n\\t\\t\\t$produkt->save();\\n\\n\\t\\t\\t$i = 0;\\n\\t\\t\\tforeach ( $p['warianty'] as $wartosc => $stan ) {\\n\\t\\t\\t\\t++$i;\\n\\t\\t\\t\\t$wariant = new WC_Product_Variation();\\n\\t\\t\\t\\t$wariant->set_parent_id( $produkt->get_id() );\\n\\t\\t\\t\\t$wariant->set_attributes( array( sanitize_title( $p['atrybut'] ) => $wartosc ) );\\n\\t\\t\\t\\t$wariant->set_sku( $p['sku'] . '-' . strtoupper( substr( sanitize_title( $wartosc ), 0, 4 ) ) );\\n\\t\\t\\t\\t$wariant->set_regular_price( $p['cena'] );\\n\\t\\t\\t\\t$wariant->set_stock_status( $stan );\\n\\t\\t\\t\\tif ( method_exists( $wariant, 'set_global_unique_id' ) ) {\\n\\t\\t\\t\\t\\t$wariant->set_global_unique_id( merchlint_demo_ean( 2000 + 10 * $merchlint_demo_numer + $i ) );\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\t$wariant->save();\\n\\t\\t\\t}\\n\\n\\t\\t\\tWC_Product_Variable::sync( $produkt->get_id() );\\n\\n\\t\\t\\treturn wc_get_product( $produkt->get_id() );\\n\\t\\t}\\n\\t);\\n}\\n\\n\\\/\\\/ Kody powielone przez import (B4, G3b). Zapis wprost, bo CRUD odrzuca powtorzony kod.\\nmerchlint_demo_etap(\\n\\t'kody powielone',\\n\\tstatic function () use ( $merchlint_demo_produkty, $merchlint_demo_obiekty ) {\\n\\t\\tforeach ( $merchlint_demo_produkty as $klucz => $p ) {\\n\\t\\t\\tif ( ! isset( $merchlint_demo_obiekty[ $klucz ] ) ) {\\n\\t\\t\\t\\tcontinue;\\n\\t\\t\\t}\\n\\t\\t\\t$id = $merchlint_demo_obiekty[ $klucz ]->get_id();\\n\\t\\t\\tif ( isset( $p['sku_jak'] ) && isset( $merchlint_demo_obiekty[ $p['sku_jak'] ] ) ) {\\n\\t\\t\\t\\tupdate_post_meta( $id, '_sku', $merchlint_demo_obiekty[ $p['sku_jak'] ]->get_sku() );\\n\\t\\t\\t}\\n\\t\\t\\tif ( isset( $p['gtin_jak'] ) && isset( $merchlint_demo_obiekty[ $p['gtin_jak'] ] ) ) {\\n\\t\\t\\t\\tupdate_post_meta( $id, '_global_unique_id', get_post_meta( $merchlint_demo_obiekty[ $p['gtin_jak'] ]->get_id(), '_global_unique_id', true ) );\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n);\\n\\n\\\/\\\/ ---------------------------------------------------------------------------\\n\\\/\\\/ Zamowienia: jedno co dwa tygodnie przez ostatni rok, najstarsze 354 dni temu.\\n\\\/\\\/ ---------------------------------------------------------------------------\\n\\n\\\/\\\/ Maile WooCommerce przy zmianie statusu: pre_wp_mail zatrzymuje dopiero wysylke, a szablon\\n\\\/\\\/ maila i tak by sie skladal - przy kazdym zamowieniu, w przegladarce odwiedzajacego.\\nforeach ( array( 'new_order', 'customer_processing_order', 'customer_completed_order', 'customer_on_hold_order' ) as $merchlint_demo_mail ) {\\n\\tadd_filter( 'woocommerce_email_enabled_' . $merchlint_demo_mail, '__return_false' );\\n}\\n\\n$merchlint_demo_klienci = array( 'Alex', 'Jordan', 'Sam', 'Taylor', 'Morgan', 'Casey', 'Riley', 'Jamie' );\\n$merchlint_demo_teraz   = time();\\n$merchlint_demo_zamowien = 0;\\n\\nfor ( $merchlint_demo_i = 0; $merchlint_demo_i < 26; $merchlint_demo_i++ ) {\\n\\t$merchlint_demo_dni = 4 + 14 * $merchlint_demo_i;\\n\\t$merchlint_demo_ok  = merchlint_demo_etap(\\n\\t\\t'zamowienie ' . $merchlint_demo_i,\\n\\t\\tstatic function () use ( $merchlint_demo_i, $merchlint_demo_dni, $merchlint_demo_produkty, $merchlint_demo_wariantowe, $merchlint_demo_obiekty, $merchlint_demo_klienci, $merchlint_demo_teraz ) {\\n\\t\\t\\t$pozycje = array();\\n\\t\\t\\tforeach ( array_merge( $merchlint_demo_produkty, $merchlint_demo_wariantowe ) as $klucz => $p ) {\\n\\t\\t\\t\\tif ( empty( $p['rytm'] ) || ! isset( $merchlint_demo_obiekty[ $klucz ] ) ) {\\n\\t\\t\\t\\t\\tcontinue;\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\tif ( $merchlint_demo_i % $p['rytm'] !== $p['faza'] ) {\\n\\t\\t\\t\\t\\tcontinue;\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\tif ( isset( $p['do_dni'] ) && $merchlint_demo_dni < $p['do_dni'] ) {\\n\\t\\t\\t\\t\\tcontinue;\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\t$produkt = $merchlint_demo_obiekty[ $klucz ];\\n\\t\\t\\t\\tif ( $produkt->is_type( 'variable' ) ) {\\n\\t\\t\\t\\t\\t$warianty = $produkt->get_children();\\n\\t\\t\\t\\t\\tif ( array() === $warianty ) {\\n\\t\\t\\t\\t\\t\\tcontinue;\\n\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t$produkt = wc_get_product( $warianty[ $merchlint_demo_i % count( $warianty ) ] );\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\t$pozycje[] = array( $produkt, $p['ile'] ?? 1 );\\n\\t\\t\\t}\\n\\t\\t\\tif ( array() === $pozycje ) {\\n\\t\\t\\t\\treturn false;\\n\\t\\t\\t}\\n\\n\\t\\t\\t$kiedy = $merchlint_demo_teraz - $merchlint_demo_dni * DAY_IN_SECONDS - ( $merchlint_demo_i % 9 ) * HOUR_IN_SECONDS;\\n\\t\\t\\t$imie  = $merchlint_demo_klienci[ $merchlint_demo_i % count( $merchlint_demo_klienci ) ];\\n\\n\\t\\t\\t$t = microtime( true );\\n\\t\\t\\t$zamowienie = wc_create_order( array( 'created_via' => 'merchlint-demo' ) );\\n\\t\\t\\tif ( is_wp_error( $zamowienie ) ) {\\n\\t\\t\\t\\tthrow new RuntimeException( $zamowienie->get_error_message() );\\n\\t\\t\\t}\\n\\t\\t\\tforeach ( $pozycje as $pozycja ) {\\n\\t\\t\\t\\t\\\/\\\/ Cena z dnia zamowienia: PF-2 sprzedawal sie wtedy w promocji, a produkt bez\\n\\t\\t\\t\\t\\\/\\\/ ceny (A5) nie sprzedaje sie wcale - nie ma rytmu.\\n\\t\\t\\t\\t$zamowienie->add_product( $pozycja[0], $pozycja[1] );\\n\\t\\t\\t}\\n\\t\\t\\tmerchlint_demo_czas( '.pozycje', $t );\\n\\t\\t\\t$zamowienie->set_address(\\n\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t'first_name' => $imie,\\n\\t\\t\\t\\t\\t'last_name'  => 'Example',\\n\\t\\t\\t\\t\\t'email'      => strtolower( $imie ) . '@example.com',\\n\\t\\t\\t\\t\\t'city'       => 'San Diego',\\n\\t\\t\\t\\t\\t'state'      => 'CA',\\n\\t\\t\\t\\t\\t'postcode'   => '92101',\\n\\t\\t\\t\\t\\t'country'    => 'US',\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'billing'\\n\\t\\t\\t);\\n\\t\\t\\t$zamowienie->set_payment_method( 'cod' );\\n\\t\\t\\t$zamowienie->set_payment_method_title( 'Cash on delivery' );\\n\\t\\t\\t$zamowienie->calculate_totals( false );\\n\\t\\t\\t$zamowienie->set_date_created( $kiedy );\\n\\t\\t\\t$zamowienie->set_date_paid( $kiedy );\\n\\t\\t\\t$zamowienie->set_date_completed( $kiedy + 2 * DAY_IN_SECONDS );\\n\\t\\t\\t$zamowienie->set_status( 'completed' );\\n\\t\\t\\t$t = microtime( true );\\n\\t\\t\\t$zamowienie->save();\\n\\t\\t\\tmerchlint_demo_czas( '.zapis', $t );\\n\\n\\t\\t\\t\\\/\\\/ Tabela, z ktorej audyt liczy kwoty. Bez tego zostalaby pusta do czasu, az kolejka\\n\\t\\t\\t\\\/\\\/ WooCommerce ruszy - a w Playground nie ruszy sama.\\n\\t\\t\\t$import = '\\\\Automattic\\\\WooCommerce\\\\Internal\\\\Admin\\\\Schedulers\\\\OrdersScheduler';\\n\\t\\t\\tif ( ! is_callable( array( $import, 'import' ) ) ) {\\n\\t\\t\\t\\tthrow new RuntimeException( 'brak OrdersScheduler::import - WooCommerce zmienil API analityki' );\\n\\t\\t\\t}\\n\\t\\t\\t$t = microtime( true );\\n\\t\\t\\tcall_user_func( array( $import, 'import' ), $zamowienie->get_id() );\\n\\t\\t\\tmerchlint_demo_czas( '.import', $t );\\n\\n\\t\\t\\treturn true;\\n\\t\\t}\\n\\t);\\n\\tif ( true === $merchlint_demo_ok ) {\\n\\t\\t++$merchlint_demo_zamowien;\\n\\t}\\n}\\n\\n\\\/\\\/ Stan rodzica zapisany z pominieciem wariantow (B10) - tak zostawia go import, ktory\\n\\\/\\\/ aktualizuje produkty, a nie warianty. NA SAMYM KONCU i po wymuszonej synchronizacji:\\n\\\/\\\/ zapis wariantu odklada synchronizacje rodzica do zamkniecia zadania, wiec wczesniej\\n\\\/\\\/ zapisany stan WooCommerce nadpisalby przy wyjsciu (zmierzone: B10 \\\"clean\\\").\\nmerchlint_demo_etap(\\n\\t'stan z importu',\\n\\tstatic function () use ( $merchlint_demo_wariantowe, $merchlint_demo_obiekty ) {\\n\\t\\tif ( is_callable( array( 'WC_Post_Data', 'do_deferred_product_sync' ) ) ) {\\n\\t\\t\\tWC_Post_Data::do_deferred_product_sync();\\n\\t\\t}\\n\\t\\tforeach ( $merchlint_demo_wariantowe as $klucz => $p ) {\\n\\t\\t\\tif ( ! empty( $p['rodzic_wyprzedany'] ) && isset( $merchlint_demo_obiekty[ $klucz ] ) ) {\\n\\t\\t\\t\\tupdate_post_meta( $merchlint_demo_obiekty[ $klucz ]->get_id(), '_stock_status', 'outofstock' );\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n);\\n\\n\\\/\\\/ Rodzic wpisany przez import (B11) - REST API i import CSV przyjmuja dowolny numer rodzica i go\\n\\\/\\\/ nie sprawdzaja, a zapis produktu przez WooCommerce zostawia go bez zmian (Merchlint\\\/Plan\\\/33 par. 1).\\nmerchlint_demo_etap(\\n\\t'rodzic z importu',\\n\\tstatic function () use ( $merchlint_demo_produkty, $merchlint_demo_obiekty ) {\\n\\t\\tforeach ( $merchlint_demo_produkty as $klucz => $p ) {\\n\\t\\t\\tif ( isset( $p['rodzic_jak'], $merchlint_demo_obiekty[ $klucz ], $merchlint_demo_obiekty[ $p['rodzic_jak'] ] ) ) {\\n\\t\\t\\t\\t$produkt = $merchlint_demo_obiekty[ $klucz ];\\n\\t\\t\\t\\t$produkt->set_parent_id( $merchlint_demo_obiekty[ $p['rodzic_jak'] ]->get_id() );\\n\\t\\t\\t\\t$produkt->save();\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n);\\n\\nupdate_option(\\n\\t'merchlint_demo_zasiew',\\n\\tarray(\\n\\t\\t'produktow'  => count( array_filter( $merchlint_demo_obiekty ) ),\\n\\t\\t'zamowien'   => $merchlint_demo_zamowien,\\n\\t\\t'bledy'      => $merchlint_demo_bledy,\\n\\t\\t'czasy'      => array_map(\\n\\t\\t\\tstatic function ( $s ) {\\n\\t\\t\\t\\treturn round( $s, 1 );\\n\\t\\t\\t},\\n\\t\\t\\t$merchlint_demo_czasy\\n\\t\\t),\\n\\t\\t'kiedy'      => gmdate( 'c' ),\\n\\t\\t'hpos'       => get_option( 'woocommerce_custom_orders_table_enabled' ),\\n\\t),\\n\\tfalse\\n);\\n\\n$GLOBALS['wpdb']->query( 'COMMIT' );\\n\"},{\"step\":\"wp-cli\",\"command\":\"wp cron event run --due-now\"},{\"step\":\"wp-cli\",\"command\":\"wp merchlint scan\"}]}"}},"all_blocks":[],"tagged_versions":["0.2.0","0.2.1","0.3.0","0.3.1","0.3.2","0.3.3","0.3.4","0.3.5","0.3.6","0.3.7"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3740156,"resolution":"1","location":"assets","locale":"","width":1280,"height":440},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3740156,"resolution":"2","location":"assets","locale":"","width":1280,"height":899},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3740156,"resolution":"3","location":"assets","locale":"","width":1280,"height":631},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3733222,"resolution":"4","location":"assets","locale":"","width":1280,"height":2193},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3740156,"resolution":"5","location":"assets","locale":"","width":1280,"height":440}},"screenshots":{"1":"The result of the last audit on the first screen you see after logging in: the revenue sitting in\nproducts that can no longer be bought, and how many checks are in each group \u2014 Important, Worth\nfixing, Clean, Not checked.","2":"The top of the audit screen: the revenue from products that can no longer be bought, the checks\ncounted by group, and where to start \u2014 the three products with the most money at stake, one row\nper product with everything that is wrong with it.","3":"Every check that found something has its own card on the audit screen: what to do about it,\nthen the first items, each with the evidence in WooCommerce's own words \u2014 \"Out of stock\", not\n\"outofstock\" \u2014 and the money at stake.","4":"Everything we checked: a bar for each area of the store, then every rule with its count, split\ninto what came from your own data and what came from your theme or plugins \u2014 including the rules\nthat found nothing, which say \"clean\" rather than staying silent.","5":"What changed since the previous audit \u2014 here, findings that disappeared because the store itself\nwas fixed. When a number moves because a rule of ours got smarter, the screen says so separately."}},"plugin_section":[262246],"plugin_tags":[279555,282711,282712,282713,282714],"plugin_category":[45,55],"plugin_contributors":[278694],"plugin_business_model":[],"class_list":["post-360163","plugin","type-plugin","status-publish","hentry","plugin_section-dashboard-widgets","plugin_tags-autoloaded-options","plugin_tags-catalogue-audit","plugin_tags-duplicate-gtin","plugin_tags-missing-gtin","plugin_tags-sku-audit","plugin_category-ecommerce","plugin_category-seo-and-marketing","plugin_contributors-baltano","plugin_committers-baltano"],"banners":{"banner":"https:\/\/ps.w.org\/merchlint\/assets\/banner-772x250.png?rev=3676536","banner_2x":"https:\/\/ps.w.org\/merchlint\/assets\/banner-1544x500.png?rev=3676536","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/merchlint\/assets\/icon.svg?rev=3676536","icon":"https:\/\/ps.w.org\/merchlint\/assets\/icon.svg?rev=3676536","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/merchlint\/assets\/screenshot-1.png?rev=3740156","caption":"The result of the last audit on the first screen you see after logging in: the revenue sitting in\nproducts that can no longer be bought, and how many checks are in each group \u2014 Important, Worth\nfixing, Clean, Not checked."},{"src":"https:\/\/ps.w.org\/merchlint\/assets\/screenshot-2.png?rev=3740156","caption":"The top of the audit screen: the revenue from products that can no longer be bought, the checks\ncounted by group, and where to start \u2014 the three products with the most money at stake, one row\nper product with everything that is wrong with it."},{"src":"https:\/\/ps.w.org\/merchlint\/assets\/screenshot-3.png?rev=3740156","caption":"Every check that found something has its own card on the audit screen: what to do about it,\nthen the first items, each with the evidence in WooCommerce's own words \u2014 \"Out of stock\", not\n\"outofstock\" \u2014 and the money at stake."},{"src":"https:\/\/ps.w.org\/merchlint\/assets\/screenshot-4.png?rev=3733222","caption":"Everything we checked: a bar for each area of the store, then every rule with its count, split\ninto what came from your own data and what came from your theme or plugins \u2014 including the rules\nthat found nothing, which say \"clean\" rather than staying silent."},{"src":"https:\/\/ps.w.org\/merchlint\/assets\/screenshot-5.png?rev=3740156","caption":"What changed since the previous audit \u2014 here, findings that disappeared because the store itself\nwas fixed. When a number moves because a rule of ours got smarter, the screen says so separately."}],"raw_content":"<!--section=description-->\n<p>Merchlint reads your WooCommerce catalogue and tells you where it is losing you money. Every\nfinding comes with the value it was based on, so you can check it yourself instead of taking our\nword for it.<\/p>\n\n<p>It changes nothing. No product, no price, no stock level, no order. The audit reads and reports;\nevery fix is yours to make, in your own admin.<\/p>\n\n<p>https:\/\/www.youtube.com\/watch?v=0JRXpF6y1U0<\/p>\n\n<h4>What it looks for<\/h4>\n\n<p>Nineteen checks, in six groups:<\/p>\n\n<ul>\n<li><strong>Money<\/strong> \u2014 products that sold in the past year and can no longer be bought; goods selling\nwithout a photo or hidden from the catalogue; sale prices whose scheduled end date has passed\nand are still running; products with no price at all.<\/li>\n<li><strong>Catalogue and navigation<\/strong> \u2014 products outside every category; duplicate SKUs; variable\nproducts with every variation sold out, or marked sold out while their variations are still in stock;\npublished products WooCommerce keeps out of the shop because their parent doesn't exist or isn't published.<\/li>\n<li><strong>Content<\/strong> \u2014 descriptions too short to answer a customer's question; images with no alt text.<\/li>\n<li><strong>Performance and media<\/strong> \u2014 oversized image files; bloated autoloaded options; media entries\nwhose file is missing from disk.<\/li>\n<li><strong>Environment<\/strong> \u2014 PHP, WordPress or WooCommerce on a version that no longer gets security fixes;\na scheduled-task queue that stopped running \u2014 WP-Cron and Action Scheduler are reported\nseparately \u2014 which is what stalls every piece of work meant to happen later.<\/li>\n<li><strong>Product identifiers<\/strong> \u2014 products with no GTIN, UPC, EAN or ISBN, and two products carrying\nthe same one. An empty or duplicated identifier is the usual reason an item quietly drops out of\na shopping feed, and nothing in WooCommerce says a word about it.<\/li>\n<\/ul>\n\n<h4>What makes it different<\/h4>\n\n<ul>\n<li><strong>Every finding shows its evidence.<\/strong> Not \"12 636 problems\" but the field we read, the value we\nfound and the threshold we compared it against.<\/li>\n<li><strong>It tells you where to start.<\/strong> The three items with the largest amount at stake, first \u2014 before\nthe full list.<\/li>\n<li><strong>A zero means \"checked, clean\".<\/strong> A rule that could not run says so instead of quietly\nreporting nothing. \"No data\" and \"no problems\" are different answers, and you get the right one.<\/li>\n<li><strong>It never adds up currencies.<\/strong> A store selling in euros and dollars gets a separate figure for\neach. Adding them without an exchange rate would be inventing a number.<\/li>\n<li><strong>Two scans of an unchanged store give the same result<\/strong>, item for item. When something does\nchange, a separate screen tells you what \u2014 and whether the difference came from your store or\nfrom a rule of ours that got smarter.<\/li>\n<li><strong>\"Hide from report\" is permanent.<\/strong> Seasonal stock you already know about stops coming back\nafter every scan, and stays hidden across plugin updates.<\/li>\n<\/ul>\n\n<h4>Every check has a page that explains it<\/h4>\n\n<p>Nineteen checks, nineteen pages \u2014 linked from the audit screen, next to the rule they belong to.\nEach page says what the problem costs, which fields the rule reads and which thresholds it\ncompares them against, how to fix it, and when the finding is <strong>not<\/strong> a problem in your store.<\/p>\n\n<p>That last part is why the pages exist. A rule you cannot argue with is a rule you end up ignoring\nwholesale, so every page names the cases where the right answer is \"hide this and move on\":\nseasonal goods, spare parts, products sold only by phone.<\/p>\n\n<ul>\n<li><a href=\"https:\/\/merchlint.com\/checks\/\">All nineteen checks, explained<\/a><\/li>\n<li><a href=\"https:\/\/merchlint.com\/changes-nothing\/\">Why there is no \"Fix it\" button<\/a><\/li>\n<li><a href=\"https:\/\/merchlint.com\/sample-report\/\">What a finished report looks like<\/a><\/li>\n<\/ul>\n\n<h4>Your data stays yours<\/h4>\n\n<p>The free version works entirely on your server. No account, no sign-up, no phoning home: the\nplugin makes no outbound connections at all, and an automated check enforces that on every build.<\/p>\n\n<p>The links to merchlint.com are ordinary links. They open in your browser when you click them, and\nthe plugin sends nothing along the way \u2014 not your store's address, not its version, not an\nidentifier of any kind. The only thing in the address is the language your admin is set to, so\nthat a Polish store gets the Polish page. Without a click, nothing leaves your store at all.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install and activate the plugin. WooCommerce must be active first \u2014 WordPress enforces this for\nyou.<\/li>\n<li>Open <strong>Store audit<\/strong> in the admin menu.<\/li>\n<li>Press <strong>Scan now<\/strong>. A store with 50 000 products takes about a minute and a half on the hardware\nwe measure on; a slower host takes longer. You can stop the scan at any point.<\/li>\n<\/ol>\n\n<p>The audit reads your catalogue and writes only to its own tables. Removing the plugin removes them.<\/p>\n\n<p><strong>Short PHP time limit?<\/strong> Nothing to do \u2014 the audit reads your host's own limit and sizes each chunk\nto fit inside it, then books the next one itself. A shorter chunk means more of them, not less work:\nthe audit still finishes, and it still resumes from where it stopped.<\/p>\n\n<p>If your host reports a limit it does not actually enforce, you can set the chunk yourself in\n    wp-config.php:<\/p>\n\n<pre><code>define( 'MERCHLINT_BUDGET_SECONDS', 8 );\n<\/code><\/pre>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20it%20change%20anything%20in%20my%20store%3F\"><h3>Does it change anything in my store?<\/h3><\/dt>\n<dd><p>No. It reads your catalogue and reports what it finds. Every change is made by you, in your own\nadmin. This is enforced in the code, not just promised: an automated check refuses to build the\nplugin if it calls any function that would modify products, categories, media or orders.<\/p><\/dd>\n<dt id=\"will%20it%20slow%20my%20store%20down%3F\"><h3>Will it slow my store down?<\/h3><\/dt>\n<dd><p>The scan runs in the background, in short slices, and it only touches the admin side. Your\nstorefront is not involved. If the scan is interrupted \u2014 a server restart, a deployment \u2014 it picks\nup where it left off and gives the same result as an uninterrupted run.<\/p><\/dd>\n<dt id=\"can%20i%20audit%20a%20store%20with%20tens%20of%20thousands%20of%20products%3F\"><h3>Can I audit a store with tens of thousands of products?<\/h3><\/dt>\n<dd><p>Yes. A scan of 50 000 products completes in about 90 seconds and the results are paginated, so\na rule with twelve thousand findings still opens instantly.<\/p><\/dd>\n<dt id=\"why%20does%20it%20check%20wp-cron%20and%20action%20scheduler%3F\"><h3>Why does it check WP-Cron and Action Scheduler?<\/h3><\/dt>\n<dd><p>Because a stalled queue fails quietly. WP-Cron and Action Scheduler are what WooCommerce and its\nextensions book later work on \u2014 order emails, the end of a scheduled sale, stock released from\nabandoned carts, subscription renewals \u2014 and neither your storefront nor your order list will tell\nyou they stopped. The store keeps selling. It just stops doing everything that was meant to happen\nlater.<\/p>\n\n<p>The two queues are reported as separate findings, because they stop for different reasons and are\nfixed in different places. What the rule measures is the age of the oldest overdue action, not how\nmany are waiting: a store with no visitors always has a queue full of them and is perfectly\nhealthy, because the first visitor clears it. Anything overdue by more than a day is not quiet, it\nis broken \u2014 and that threshold is yours to move.<\/p><\/dd>\n<dt id=\"what%20does%20%22store%20health%22%20cover%20here%3F\"><h3>What does \"store health\" cover here?<\/h3><\/dt>\n<dd><p>Everything the nineteen checks touch, not only product fields: the catalogue and its categories, the\nmedia library and the files behind it, the size of your autoloaded options, and the versions of PHP,\nWordPress and WooCommerce you are running. A store can have faultless product data and still be\nlosing money to a stalled queue or to an unsupported PHP version, so the report covers both.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20multilingual%20stores%3F\"><h3>Does it work with multilingual stores?<\/h3><\/dt>\n<dd><p>Yes. Translations of one product are counted as one product: their SKU is shared deliberately, and\ntheir stock is one physical stock, so we never report the same goods twice or double the amount at\nstake. Polylang is tested with the plugin itself; WPML is recognised through its translation table,\nwhich we test against the same data it produces.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20hpos%20%28high-performance%20order%20storage%29%3F\"><h3>Does it work with HPOS (High-Performance Order Storage)?<\/h3><\/dt>\n<dd><p>Yes, with HPOS on and off. Both modes give identical results, amounts included.<\/p><\/dd>\n<dt id=\"some%20of%20the%20findings%20do%20not%20apply%20to%20my%20store.\"><h3>Some of the findings do not apply to my store.<\/h3><\/dt>\n<dd><p>Then hide them. Every rule's page explains when the finding is NOT a problem \u2014 seasonal goods,\nspare parts, products sold only by phone \u2014 and hiding takes two clicks, with an optional reason.\nThe decision survives future scans, and the hidden items stay visible as a count and in a list\nyou can restore them from, so nothing disappears silently.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>0.3.7<\/h4>\n\n<ul>\n<li><p><strong>New check: \"Published, but the shop doesn't show it\".<\/strong> Since WooCommerce 11.1, a product\nwhose parent points to an entry that doesn't exist or isn't published disappears from the shop\npage, categories and search, and a simple product can't be added to the cart - while the admin\nshows it as published and visible. A simple product doesn't use a parent at all, but CSV imports\nand tools that create products through the REST API sometimes write one. WooCommerce itself\ndecides, in the version you run, and the check follows it: from 11.3 a parent that isn't a\nproduct no longer hides anything. The evidence names the parent and what it does to customers,\nand the check's page shows how to clear it.<\/p><\/li>\n<li><p><strong>On a phone, the full list of a check shows the evidence again.<\/strong> Since 0.3.6 the \"What we\nread\" column was hidden on narrow screens with no way to open it; each row now has the button\nthat shows it.<\/p><\/li>\n<\/ul>\n\n<h4>0.3.6<\/h4>\n\n<ul>\n<li><p><strong>Every check that found something now has its own card on the audit screen.<\/strong> The cards stand\nunder \"What we found, check by check\", below the list of checks. Each shows the first five\nitems - with a thumbnail, the evidence and the amount - and what to do about them. \"Show all\"\nopens the full list, and the card says how many items are hidden. The check names in \"Where\nto start\" and in the list of checks lead to their cards.<\/p><\/li>\n<li><p><strong>The evidence reads as \"field: value\", in the words WooCommerce uses.<\/strong> Stock status,\ncatalogue visibility, product type and publication status show their WooCommerce and WordPress\nlabels - \"Out of stock\" rather than \"outofstock\" - on the screen and in the CSV export. An empty\nmain image field shows as \"(empty)\" rather than \"0\", and \"Product outside every category\" names\nthe category - \"Uncategorized (#15)\" rather than \"15\". Revenue and prices in the evidence follow\nthe number format of your admin language: \"7,740.00 USD\" and \"195.30\" on an English screen,\nwhere they used to read \"7 740,00 USD\" and \"195.3\". WP-CLI output has not changed.<\/p><\/li>\n<li><p><strong>\"Hide from report\" takes an optional reason, and everything you have hidden has its own\nlist.<\/strong> After hiding an item from a card you are back on that card. The new \"Hidden items\"\nbutton, next to \"Download everything (CSV)\", opens a list of every hidden item with the date\nand the reason. The list says when a check has changed since an item was hidden, and lets you\nrestore any of them - also items the latest audit no longer finds.<\/p><\/li>\n<li><p><strong>The summary gives the size of the catalogue.<\/strong> \"Products in the catalogue\" now stands next to\nthe number of findings, counted when the audit ran.<\/p><\/li>\n<li><p><strong>On the \"Why did this change?\" screen, each example links to its item.<\/strong> The checks there are\nlisted in code order, B4 before B10, and the screen says how many items were hidden since the\nearlier audit, with a link to the list.<\/p><\/li>\n<li><p><strong>Fixed the way back after hiding or restoring an item in a shop installed in a folder.<\/strong> With\nan address such as example.com\/shop, the page you landed on afterwards did not exist. The item\nitself was hidden or restored.<\/p><\/li>\n<li><p><strong>\"When this is NOT a problem\" no longer suggests hiding where hiding is the wrong answer.<\/strong>\nUnder \"Duplicate SKU\", \"Variation the customer can never pick\" and \"Sold out, but its variations\nare in stock\" the advice to use \"Hide from report\" is gone: these checks have practically no\nexceptions, and the answer is to fix the product. The button itself stays on every card. The\nplugin description now calls it by its current name, \"Hide from report\", rather than \"Hide\".<\/p><\/li>\n<\/ul>\n\n<h4>0.3.5<\/h4>\n\n<ul>\n<li><p><strong>The audit screen now starts with a summary.<\/strong> Above the full list: the revenue sitting in\nproducts that sold but can no longer be bought, and four tiles - Important, Worth fixing,\nClean, Not checked - each with the number of checks in it. \"Everything we checked\" opens with\none bar per area of the catalogue, and \"Where to start\" shows each product's photo and a bar for\nits amount. Check names now come before their codes, \"When this is NOT a problem\" no longer\nhides behind a click, and the plugin's box on the product page sits right under \"Publish\".\nThe dashboard widget shows the same four counts.<\/p><\/li>\n<li><p><strong>New check: \"Sold out, but its variations are in stock\".<\/strong> The product is marked out of\nstock, yet at least one of its variations can still be added to the cart. Shoppers see \"out of\nstock\" in the shop and in search results, while the product page sells the variation - one of\nthe two is wrong, and the data alone cannot tell which, so the advice starts there. \"They were\nselling, now nobody can buy them\" no longer lists these products, because they can be bought.<\/p><\/li>\n<li><p><strong>Checks based on sales skip products that earned nothing in the period.<\/strong> A product sold only\nat a price of 0 - a free gift added to orders - or whose every sale was refunded is no longer\nlisted by \"They were selling, now nobody can buy them\", \"Selling without a photo\" or \"Selling,\nbut hidden\". The amount at stake for it was zero anyway.<\/p><\/li>\n<li><p><strong>\"Product with no price or a zero price\" no longer lists products that are out of stock, or\ngift cards.<\/strong> Most such items turned out to be withdrawn products, free gifts and vouchers, not\nmistakes. The evidence now says whether the price is missing or zero, and the advice starts\nwith checking whether it is intentional - and hiding the item if it is.<\/p><\/li>\n<li><p><strong>\"No GTIN, UPC, EAN or ISBN\" no longer lists gift cards.<\/strong> Gift card products from gift card\nplugins have no barcode by nature.<\/p><\/li>\n<li><p><strong>SKUs and GTINs are compared the same way on every database.<\/strong> A code that came with an\ninvisible character or a non-breaking space - common when it is pasted from a spreadsheet - is\nnow the same code as the one without it. Until now this depended on the database behind the\nshop: on MySQL 8 such a duplicate was missed, or only one product of the pair was listed. A GTIN\nfield holding nothing but such characters now counts as empty.<\/p><\/li>\n<li><p><strong>\"Image with no alt text\" also counts a file name, a random code or digits only.<\/strong> Text such\nas \"IMG_2034.jpg\" in the alternative text field describes nothing to a person who cannot see the\nimage, so it is treated like an empty field. The advice also says what WooCommerce 9.3.3 and\nlater do with an empty field: the product page shows the product name there, depending on the\ntheme.<\/p><\/li>\n<li><p><strong>\"Where to start\" counts each product's amount once.<\/strong> A product both hidden from the\ncatalogue and out of stock had its revenue counted twice and could rank above products that\nearned more.<\/p><\/li>\n<li><p><strong>Evidence on an English screen is in English.<\/strong> A few values written by the checks themselves,\nsuch as \"yes\" and \"no\", \"(empty)\", \"(not on disk)\" and the note about supported WooCommerce\nversions, appeared in Polish whatever language your admin uses. The results screen and the CSV\nexport now translate them. WP-CLI keeps them as they were, because scripts read them.<\/p><\/li>\n<\/ul>\n\n<h4>0.3.4<\/h4>\n\n<ul>\n<li><p><strong>The evidence now gives its reason in words, in the language of your admin.<\/strong> Where a check says\nwhy an item is listed - a product hidden from the catalogue, a task queue that stopped - the\nreason appeared as an internal code in Polish, such as \"wylaczony z katalogu\", whatever language\nyour admin uses. The results screen and the CSV export now say \"excluded from the catalogue\",\ntranslated like the rest of the screen. WP-CLI keeps the code, because scripts read it.<\/p><\/li>\n<li><p><strong>Amounts and counts in the Polish admin no longer read \"7,740,00\" or \"1,234\".<\/strong> The Polish\ntranslation of WordPress changes the decimal separator to a comma but leaves the thousands\nseparator as it was, so both were the same character. When that happens, thousands are now split\nwith a non-breaking space - \"7 740,00\" - as in the evidence. Other languages see no change.<\/p><\/li>\n<li><p><strong>The note about numbers kept in EAN for WooCommerce now names that plugin's own copy tool.<\/strong> EAN\nfor WooCommerce can copy its numbers into WooCommerce's identifier field itself: WooCommerce \u2192\nSettings \u2192 EAN \u2192 Tools \u2192 Copy to product meta. The note now says so and warns that the copy skips\nWooCommerce's own checks, so run the audit again afterwards - the duplicate check shows any\nnumber that came across twice. Fields of other plugins keep the general advice.<\/p><\/li>\n<li><p><strong>On WooCommerce 9.0 and 9.1 the note about another plugin's field no longer sends you to a field\nthat is not there.<\/strong> Those versions have no field for a product identifier, yet the note advised\nmoving the numbers into it. It now says the field arrived in WooCommerce 9.2 and gives the same\nadvice as the identifier checks themselves: update WooCommerce, then run the audit again.<\/p><\/li>\n<li><p><strong>\"wp merchlint status --format=json\" now also gives the counts the table shows.<\/strong> \"counts\"\nincludes the findings you have hidden, so a script reading it got other numbers than a person\nlooking at the screen. \"visible_counts\" and \"hidden_counts\" now stand next to it, with every\ncheck of the scan, zeros included. \"counts\" itself has not changed.<\/p><\/li>\n<\/ul>\n\n<h4>0.3.3<\/h4>\n\n<ul>\n<li><p><strong>The plugin no longer does anything at all when WooCommerce is not active.<\/strong> Until now it\ncreated its own tables on every page load, registered the audit screen and added its WP-CLI\ncommand even on a site with no WooCommerce - and then showed an empty screen without a word\nof explanation. It now checks first, says so in one sentence on the dashboard, and touches\nnothing. Saved results are left exactly as they were: activate WooCommerce and everything\ncomes back.<\/p><\/li>\n<li><p><strong>New display name: Merchlint - Catalog Audit for WooCommerce.<\/strong> Only the displayed name\nchanged: same plugin, same directory address, same settings, same saved audits. Nothing in\nyour shop needs doing, and the screen in your admin menu is where it was.<\/p><\/li>\n<li><p><strong>The identifier checks now say when your numbers are kept in another plugin's field.<\/strong> Both\nchecks read WooCommerce's own GTIN, UPC, EAN or ISBN field. When products hold their number only\nin a field of EAN for WooCommerce, Germanized for WooCommerce, Trusted Shops Easy Integration,\nGoogle for WooCommerce, Customer Reviews for WooCommerce or Product Feed Manager for WooCommerce,\nthe audit now says how many, names the field and\nsays plainly which check cannot see those numbers \u2014 on the results screen, on the check's own\npage, in WP-CLI and in the CSV export. Germanized passes its numbers through WooCommerce's own\nfield when that field is empty, so with Germanized active only the duplicate check is affected.\nNothing is counted differently: no finding appears or disappears, and earlier scans stay\ncomparable.<\/p><\/li>\n<li><p><strong>A check's own page now shows what that check could not see.<\/strong> Notes about gaps were only on\nthe main results screen, above the list of checks \u2014 not next to the findings they explain.<\/p><\/li>\n<li><p><strong>\"What this audit could NOT check\" no longer says \"these rules found nothing\" about rules that\nran.<\/strong> A rule that missed only part of what it looks at is now listed separately, with a sentence\nof its own.<\/p><\/li>\n<li><p><strong>Invisible characters in product codes now show in the evidence.<\/strong> Two codes that differ only\nby a non-breaking space or a zero-width character look the same on screen, so a pair of\nduplicates could show one value twice, with no way to tell which product carries the extra\ncharacter. The evidence now writes each such character as \u27e8U+00A0\u27e9 and similar, on the screen and\nin the CSV export. Clean codes look exactly as before.<\/p><\/li>\n<li><p><strong>Three WP-CLI commands now stop on bad input instead of guessing.<\/strong> With a mistyped check code,\n\"findings\" listed and \"export\" exported every finding instead of none; an invalid separator in\n\"export\" ended in a critical error; \"cancel\" reported success for a scan that does not exist.\nEach of them now says what was wrong.<\/p><\/li>\n<li><p><strong>Two WP-CLI outputs now keep gaps apart from findings.<\/strong> The message after \"export\" counted the\nrows about gaps \u2014 what the audit could not check \u2014 as findings, so a check that never ran was\nreported as one saved finding. It now counts findings only and names the gap rows separately.\n\"status --format=json\" now lists the gaps the same way \"rule --format=json\" does; until now a\ncheck that did not run simply disappeared from its counts.<\/p><\/li>\n<li><p><strong>The advice for missing sales data now points to a button that exists.<\/strong> When WooCommerce's\norder summary table was empty, the audit told you to run \"Regenerate the order lookup tables\" in\nStatus \u2192 Tools, and WooCommerce has no such tool. It now sends you to Analytics \u2192 Settings \u2192\n\"Import historical data\", and the WP-CLI warning gives the same advice instead of naming a script\nthat is not part of the plugin.<\/p><\/li>\n<li><p><strong>One request for a review, under the results.<\/strong> It appears once per user after a finished\naudit and goes away for good when you open it or choose \"Don't ask again\". No reward is offered\nand no rating is suggested.<\/p><\/li>\n<\/ul>\n\n<h4>0.3.2<\/h4>\n\n<ul>\n<li><p><strong>On WooCommerce older than 9.2 the two identifier checks now say they did not run, instead of\nreporting numbers.<\/strong> That WooCommerce has no field for a product identifier at all. If the\nfield had been written straight into the database - after moving back from a newer version, or\nby an import - the plugin used to treat the store as one that keeps identifiers while reading\nevery one of them as empty: the duplicate check then reported nothing even when duplicates\nwere there. Both checks now stay quiet and say why, naming the WooCommerce version that first had the field.<\/p><\/li>\n<li><p><strong>The evidence column no longer shows a Polish word on an English screen.<\/strong> Where a check reads\na field and finds it empty, the value now reads \"(empty)\". It had been coming straight from the audit\nengine, which has no access to translations, so the Polish \"(puste)\" appeared for anyone reading the\nscreen in English - in the one column whose whole job is to let you verify us.<\/p><\/li>\n<li><p><strong>The duplicate-identifier check now says when it had nothing to compare.<\/strong> On a catalogue where\nnot one product carries an identifier, it used to report \"nothing found - this check is clean\" - a clean\nbill of health for a field nobody fills, and two different sentences about the same field one under the\nother, because the missing-identifier check said so correctly. Both checks now give the same answer,\nwith the same reason.<\/p><\/li>\n<li><p><strong>A finding on a variation now links straight to the tab where you fix it.<\/strong> The link in\nthe list opened the parent product at the top of the page; on a product with twenty\nvariations that is a long way from the one you were sent to. The export file had carried\nthe right link all along - only the screen did not.<\/p><\/li>\n<\/ul>\n\n<h4>0.3.1<\/h4>\n\n<ul>\n<li><strong>Both duplicate checks now see codes stored on variations, not only on products.<\/strong> Until now the\ncomparison ran over products alone, so a code repeated between one product and another product's\nvariation was invisible \u2014 even though an export file treats every variation as a separate item.\nOn a store with variable products the numbers can go up after this update.<\/li>\n<li><strong>The identifier check also reports a product that shares its number with its own variation. The\nSKU check does not<\/strong> \u2014 and that difference is deliberate. WooCommerce shows the parent's SKU on a\nvariation that has none, so for SKU that pair is how the platform behaves, not a fault. For the\nidentifier there is no such inheritance: WooCommerce requires it to be unique across products and\nvariations, yet never notices when an import writes the pair straight to the database, so this is\nthe only place it surfaces. It matters when an export file carries the parent alongside its\nvariations \u2014 then two items go out with one number.<\/li>\n<li>One pair is still left out and the knowledge pages now say so: two variations of one product\nsharing a code while the product itself carries none. A finding belongs to a product, and there\nis nothing to attach it to there.<\/li>\n<li><strong>The three checks whose behaviour changed carry a new version number, and that is deliberate.<\/strong>\nThe \"Changes since the previous audit\" screen reads those numbers to tell you when a movement\ncomes from us rather than from your store, and anything you hid under the old version is marked\nfor a second look \u2014 your decision was an answer to a question that is now worded differently.\nNothing is un-hidden behind your back; it is only flagged.<\/li>\n<li><strong>Variations sitting in the trash no longer count \u2014 anywhere.<\/strong> A variable product whose living\nvariations all carried an identifier could still be reported as missing one, because a trashed\nvariation had none; the same ghost could make a code look duplicated. Nothing in the shop shows\nthose variations, and WooCommerce's own uniqueness check ignores the trash as well \u2014 now so do we.\nThis one was in the previous release too, not only in the change above.<\/li>\n<li><strong>Two additions to the identifier check's knowledge page.<\/strong> What to do when the check is clean\nand the export file still shows no identifier \u2014 an export plugin may read the field through a\ncached list of fields, which a product save does not refresh. And why a number copied onto\nvariations while a file is being built hides the gap rather than closing it: a shopping service\nexpects each variation to carry its own, and this check reports what WooCommerce holds.<\/li>\n<\/ul>\n\n<h4>0.3.0<\/h4>\n\n<ul>\n<li><strong>Two new checks, both free and with no product cap: a product with no GTIN, UPC, EAN or ISBN,\nand two products sharing the same one.<\/strong> Shopping services use that identifier to match your\noffer to the same product sold elsewhere, so an empty or duplicated one is the usual reason an\nitem quietly drops out of a feed. WooCommerce has had the field since 9.2, on the Inventory tab,\nand nothing in the admin ever points at it. The check reads WooCommerce's own field; identifiers\nkept in a plugin's field or in a product attribute are not read.<\/li>\n<li><strong>The missing-identifier check stays quiet in shops that do not use identifiers at all.<\/strong> If not\none product has the field filled, you get a single sentence saying so instead of a list of your\nentire catalogue \u2014 handmade goods, services and your own production carry no manufacturer's\nbarcode and need none.<\/li>\n<li>On a variable product the identifier belongs on each variation, so such a product gives <strong>one<\/strong>\nfinding with the evidence \"6 variations without a GTIN\" \u2014 not six separate rows.<\/li>\n<li>Translations of one product are not reported as a duplicate, exactly as with SKU.<\/li>\n<li><strong>After an update, \"Changes since the previous audit\" now names the checks that are new.<\/strong>\nTwo checks arriving at once means findings arriving at once, and the screen showed the\nmovement without saying where it came from \u2014 so it read as if something had broken in your\nstore overnight. A check that is new and found nothing stays out of the way.<\/li>\n<li><strong>A check that could not run no longer reads as a clean one \u2014 on every screen, not just some.<\/strong>\nThe audit screen has always had a section saying what could not be checked and why. Two places\ndid not: the dashboard tile counted such a check among the rules it had gone through, and\n\"Changes since the previous audit\" showed its findings dropping to zero exactly as it shows a\nreal fix. On a store that keeps no product identifiers at all, that meant being congratulated\nfor something nobody had measured. The tile now says how many checks could not run, and the\ncomparison says plainly that a drop there is not a fix.<\/li>\n<li><strong>An empty catalogue is no longer reported as a clean one.<\/strong> A store with no products got a row\nof zeros and a tile saying nothing needed fixing \u2014 which is what a fresh WooCommerce install\nlooks like before the first product is added. The audit now says plainly that there was nothing\nto check.<\/li>\n<li>Ready since 0.2.2 and shipping here: <strong>the audit engine no longer assumes every store can answer\nevery question.<\/strong> A rule now says what it needs, and a store that cannot provide it gets a plain\nnote saying the rule was not applicable \u2014 instead of a silent zero that reads like a clean\nresult. On WooCommerce every rule still applies, so every check you already had gives exactly\nthe same result as it did under 0.2.1.<\/li>\n<li>The list of rules moved into the audit engine itself.<\/li>\n<\/ul>\n\n<h4>0.2.1<\/h4>\n\n<ul>\n<li><strong>New display name: Merchlint \u2013 Store Audit for WooCommerce.<\/strong> Same plugin, same address, same\nsettings \u2014 the name now says what it does, for people who find plugins by searching for the job\nrather than for a brand.<\/li>\n<li><strong>Every rule now links to its own page<\/strong>, which explains what the finding costs, exactly how the\nrule decides, how to fix it, and when it is not a problem worth fixing. The link opens in your\nbrowser and carries nothing but the language of your admin; the plugin itself still makes no\noutbound connections at all.<\/li>\n<li>The plugin's name on your plugins list is now a link to its home page. Until now it led nowhere.<\/li>\n<li>No change to what the audit checks or reports. A scan of an unchanged store gives exactly the\nsame result as it did under 0.2.0.<\/li>\n<\/ul>\n\n<h4>0.2.0<\/h4>\n\n<ul>\n<li><strong>First public release.<\/strong> Versions 0.1.0 and 0.1.1 below were pre-release: they were reviewed but\nnever published, so this is the first version you can install from the directory. Everything the\naudit does is listed under 0.1.0.<\/li>\n<li>Two extension points so add-ons can build on the audit: an action when a scan finishes and\na filter over the scan settings, plus the scan history behind both.<\/li>\n<li>Rules the current scan did not run are now marked \"not in this audit\" instead of \"clean\".<\/li>\n<li>A scan now keeps its settings even if an add-on hands it a value that cannot be stored \u2014 before,\none such value silently dropped every threshold the scan was running with, and the second half of\nthe scan used the defaults.<\/li>\n<li>Comparing two audits no longer writes a PHP notice to the log when a setting holds a list rather\nthan a number.<\/li>\n<li>No change to what the audit checks on its own.<\/li>\n<\/ul>\n\n<h4>0.1.1<\/h4>\n\n<ul>\n<li>Translations now come from translate.wordpress.org instead of files bundled in the plugin.<\/li>\n<li>No change to what the audit checks or reports.<\/li>\n<\/ul>\n\n<h4>0.1.0<\/h4>\n\n<ul>\n<li>First release. Fifteen checks, resumable scanning, CSV export, per-rule knowledge pages,\npermanent \"hide\", English and Polish.<\/li>\n<\/ul>","raw_excerpt":"Read-only WooCommerce product audit: goods that sold but are out of stock, duplicate SKUs, missing prices \u2014 each with the money at stake.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/360163","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=360163"}],"author":[{"embeddable":true,"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/baltano"}],"wp:attachment":[{"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=360163"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=360163"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=360163"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=360163"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=360163"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/kaa.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=360163"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}