Scope: Read-only analytical pass over the entire codebase of studiou-wc-ord-print-statuses at version 1.3.2. No code changes were made. This document catalogs gaps, inconsistencies, and risks discovered by reviewing each manager class against the plugin header, README, CLAUDE.md, and the original feature specification in docs/instructions.txt.
Reviewer date: 2026-05-12 Files reviewed:
studiou-wc-ord-print-statuses.phpincludes/class-db-manager.phpincludes/class-order-status-manager.phpincludes/class-order-fields-manager.phpincludes/class-order-search-manager.phpincludes/class-bulk-actions-manager.phpincludes/class-custom-columns-manager.phpincludes/class-import-manager.phpincludes/utils-log.phpREADME.md, CLAUDE.md, docs/instructions.txt, docs/translations.txtFindings are grouped by severity. The severity reflects user-visible or correctness impact, not implementation effort to fix.
The feature is documented in README §5 and CLAUDE.md §"Order_Search_Manager", and matches spec item 3 in docs/instructions.txt. The current implementation cannot work for the following reasons:
Defect A — Meta key mismatch (would fail even under legacy CPT)
external_ref_ord_no (no leading underscore):
class-order-fields-manager.php:58 — $order->update_meta_data('external_ref_ord_no', …)class-db-manager.php:184 — $order->update_meta_data('external_ref_ord_no', $row['externalorder'])_external_ref_ord_no (with leading underscore):
class-order-search-manager.php:18class-order-search-manager.php:53 ('key' => '_external_ref_ord_no')Even if every other hook below were correct, the meta_query LIKE match would always return zero rows.
Defect B — Wrong hook for HPOS environment
class-order-search-manager.php:8 hooks parse_query and gates with:
if ('edit.php' !== $pagenow || 'shop_order' !== $typenow || !$query->is_main_query())
return $query;
Under HPOS, the orders screen is served from admin.php?page=wc-orders (not edit.php) and $typenow is not 'shop_order'. The guard always returns early; meta_query is never injected. The plugin declares HPOS compatibility, so this code path is dead in the supported configuration.
Defect C — Search field is mounted as a column renderer, not a toolbar field
class-order-search-manager.php:6 hooks add_external_ref_ord_no_to_search to manage_woocommerce_page_wc-orders_columns — that filter expects the orders-list columns array, not a "searchable fields" array. The callback appends _external_ref_ord_no as if it were a column key, creating a phantom column.class-order-search-manager.php:7 hooks add_external_ref_ord_no_search_field to manage_woocommerce_page_wc-orders_custom_column — that hook fires per table cell. The callback renders an <input type="text">, which (if the typenow guard ever passed) would draw a text input inside every row's cell rather than beside the "Search Order" button as the spec requires.Net effect: The search box does not render in the expected place, the search hook does not fire under HPOS, and even on a legacy CPT setup the meta key mismatch would zero out results. This feature is non-functional.
Order_Status_Manager::handle_status_transitions() writes to the wrong table under HPOSclass-order-status-manager.php:59-62:
update_post_meta($order_id, 'to_print_date', current_time('mysql'));
…
update_post_meta($order_id, 'in_print_date', current_time('mysql'));
update_post_meta() writes to wp_postmeta. Under HPOS, WooCommerce reads order meta from wp_wc_orders_meta. The result:
to-print / in-print outside of the bulk-action or import paths (e.g., manual change from the order edit screen, programmatic transitions from third-party code), the timestamp is written only to wp_postmeta and is invisible to $order->get_meta(). The custom column and the "Print Information" panel will show empty values.$order->update_meta_data() + $order->save() (correct under both storage modes), so they happen to work — masking the bug for the common user flow.This is a latent HPOS gap directly contradicting CLAUDE.md §"Important Considerations" point 1.
docs/instructions.txt lines 28 and 35 both state: "Distinct values from import file by column 'Order No'".
Studiou_DB_Manager::import_inprint_protocol() and import_delivered_protocol() iterate every row and call wc_get_order() + update_status() + save() per occurrence. If the same order_no appears twice in a CSV (a realistic provider-side glitch), the order is mutated twice, the success counter increments twice, and the user gets a misleading "X orders updated" total.
Spec (docs/instructions.txt lines 27, 33) specifies headers "Order No", "ExternalOrder", "ExternalOrderDate". The code reads $row['order_no'], $row['externalorder'], $row['externalorderdate'] (lowercased, no spaces). README documents the lowercase form, matching the code.
This is a documentation/spec drift rather than a code bug, but it means CSV files prepared from the original instructions will silently fail header lookup (!isset(...) branch returns the generic "Invalid row format in CSV.").
class-bulk-actions-manager.php:34-50:
$orders_data = Studiou_DB_Manager::get_orders_for_printing($post_ids);
$this->set_status_to_prepare_to_printing($post_ids, $redirect_to); // ← changes status first
…
$this->output_csv($csv_data, 'prepare_to_printing_export.csv'); // ← then output (exits)
If the CSV stream fails mid-output (proxy timeout, client disconnect), orders are already marked to-print. The user has no recovery signal and the file may be incomplete. If output_csv were ever to throw before exit, the bulk-action redirect would still claim success.
Additionally, the $redirect_to returned from the inner set_status_to_prepare_to_printing() call is discarded — the side effect of adding bulk_action=marked_prepare_to_printing to the query is wasted.
Studiou_DB_Manager::get_orders_for_printing() joins term_relationships/term_taxonomy/terms unconstrained. A product mapped to N product_cat terms produces N rows in the CSV for the same order line. If the print provider expects one row per line item, the file will contain duplicate quantities. Whether this is intentional is unclear from the spec; the spec says "custom sql 'SELECT * FROM vsu_orders_car_prod_qty_img'" without defining cardinality.
Bulk_Actions_Manager::output_csv() (lines 84-89) sends:
Content-Type: text/csv
Content-Disposition: attachment; filename="..."
No charset=utf-8, no UTF-8 BOM. Czech product/category names containing diacritics will render as mojibake when opened in Excel on Windows (the most likely consumer for a print-shop export). Field separator is comma; Excel in Czech locale prefers semicolon.
set_order_processing_status() can demote print-state ordersclass-order-status-manager.php:65-74 forces any payment-complete order into processing unless it is already completed. If a payment settles late on an order already moved to to-print or in-print (e.g., a delayed bank transfer), the order is silently rolled back to processing and the to_print_date / in_print_date timestamps remain, leaving the order in an inconsistent state vs. its meta.
The guard should exclude all print statuses, not just completed.
Studiou_DB_Manager::parse_csv():
false) if the file is missing or empty, but the callers check if (!$csv_data) — empty array is falsy in PHP, so the "Error parsing CSV file." path is reached. OK in practice, but reliance on falsy-vs-false is fragile.fgetcsv($handle, 1000, ',') — the 1000-byte line cap will truncate any row longer than 1 KB. Long product names or notes in delivered protocols could be silently chopped.$_FILES[...]['error'] (handles UPLOAD_ERR_INI_SIZE, etc.).accept=".csv" attribute is client-side only.array_combine may emit a warning and produce nonsense rows).Bulk_Actions_Manager::handle_bulk_actions() trusts WordPress's per-screen capability enforcement. WP does check edit_shop_orders to display the bulk action menu, but defense in depth would call current_user_can('edit_shop_orders') (or manage_woocommerce) inside the handler. Compare with Import_Manager, which does call current_user_can('manage_woocommerce') — the inconsistency itself is worth noting.
UtilsLog::message() is dead codeincludes/utils-log.php:15-27 — the entire body is commented out. The method is called in at least three places (class-db-manager.php:128, 153; class-bulk-actions-manager.php:42). The user-facing notice intent (type: info, dismissible: true) is never delivered. Bulk operations therefore have no visible feedback in the admin UI for non-debug users.
UtilsLog::log() guard is overly strictif ( true === WP_DEBUG ) requires WP_DEBUG to be the boolean literal true. If the user defines define('WP_DEBUG', 1) (common in legacy wp-config.php examples), nothing logs. The conventional guard is if ( defined('WP_DEBUG') && WP_DEBUG ).
set_orders_to_prepare_printing logs per iteration; set_orders_to_in_printing does notclass-db-manager.php:120-131 logs entry, success, and not-found for each order. class-db-manager.php:143-157 logs neither entry, success, nor not-found. Symmetry would aid debugging.
Studiou_DB_Manager::set_orders_to_*() calls update_status() regardless of current state. An order in cancelled or refunded would be reactivated into to-print — almost certainly unintended. The bulk action UI itself does not filter eligible orders.
studiou-wc-ord-print-statuses.php:101-107 registers two plugins_loaded callbacks. WordPress runs them in registration order. Order_Status_Manager::__construct() calls _x() / _n_noop() inside the constructor (line 9-15). If Studiou_WC_Ord_Print_Statuses::initPlugin registers before studiou_wc_ord_print_statuses_load_textdomain, those translation calls fire before the text domain is loaded, and untranslated strings are cached. Order is currently: initPlugin registered after load_textdomain, so OK today, but the dependency is implicit and fragile.
intval(), not $wpdb->prepare()class-db-manager.php:93:
WHERE `orders`.`id` IN (" . implode(',', array_map('intval', $order_ids)) . ")
Using intval() on each ID is functionally safe — it coerces to integer and emits zero on garbage input. However, CLAUDE.md §"Important Considerations" point 4 misleadingly claims this "uses $wpdb->prepare() implicitly through intval()" — that is not accurate; $wpdb->prepare() is never called. The documentation should match what the code actually does.
Every page-load triggers UtilsLog::log calls in init(), load_dependencies(), initialize_managers(), register_custom_order_statuses(), register_bulk_actions(), etc. With WP_DEBUG_LOG enabled, the log fills with init noise. Useful logs (status transitions, SQL errors) get drowned. Consider gating init logs behind a higher verbosity flag.
parse_csv line lengthfgetcsv($handle, 1000, ',') — the second parameter is the max line length in bytes. 1000 bytes is short for a row that contains a long order note or product title plus diacritics. Passing 0 removes the cap. Worth raising explicitly.
external_ref_ord_date is stored verbatimclass-db-manager.php:185 stores $row['externalorderdate'] without any date parsing or validation. If the provider sends 31/12/2025 while WP expects ISO format elsewhere, downstream filters and the order-detail panel may render an empty value. The "Print Information" panel does not display this field at all — it is only readable via direct meta access.
external_ref_ord_date is never exposed in the UIThe field is written by import_inprint_protocol() but no manager renders it. Neither Order_Fields_Manager nor Custom_Columns_Manager references it. If the print-shop's order date matters operationally, it is invisible to the operator.
There is no uninstall.php, no register_deactivation_hook, no register_uninstall_hook. Custom post statuses are registered every init, so they vanish when the plugin is deactivated — but order meta keys (to_print_date, external_ref_ord_no, etc.) remain in the DB indefinitely. For long-term hygiene on test sites, consider documenting that removal requires a manual SQL cleanup.
The plugin version lives only in the header comment (Version: 1.3.2). To reference it in code (e.g., for asset cache-busting or upgrade routines) one must call get_file_data(). Defining define('STUDIOU_WC_OPS_VERSION', '1.3.2'); once in the bootstrap would be a minor consistency improvement and matches the pattern used in sibling plugins per CLAUDE.md.
Custom_Columns_Manager::add_custom_shop_order_column_content($column, $post_id) — under HPOS the second argument is actually a WC_Order object, not a post ID. wc_get_order() accepts both, so it works, but the parameter name is misleading and shadows the legacy CPT contract. Renaming to $order_or_id or capturing both branches explicitly would aid readers.
docs/translations.txt) is incompleteStrings present in code but absent from the EN→CS reference:
"Order automatically set to processing by Studiou WC Order Print Statuses plugin." (class-order-status-manager.php:72)"Payment received, order is now processing." (class-order-status-manager.php:70)"Search by External Order Number" (class-order-search-manager.php:30)"You do not have sufficient permissions to access this page." (class-import-manager.php:48, 79)"Error parsing CSV file." (class-import-manager.php:61, 93)"External Order Delivered" field label (class-order-fields-manager.php:38)"In Print Date" field label (class-order-fields-manager.php:25)These strings would compile into the .pot via wp i18n make-pot but were never reviewed by the translator.
Both documents advertise HPOS compatibility without qualification. Given finding §1.1 (search broken under HPOS) and §1.2 (status-transition stamps go to the wrong table), the claim is overstated until those are fixed.
Mentioned in §3.6 — CLAUDE.md should be corrected regardless of whether the code is changed, because it currently misrepresents the security posture.
Some class files open with // visualized (in class-bulk-actions-manager.php, class-import-manager.php, class-order-fields-manager.php, class-custom-columns-manager.php) — apparently a leftover marker from an earlier review pass. It conveys nothing to a fresh reader and should be removed or replaced with a proper docblock.
Order_Status_Manager::add_custom_order_statuses_to_list formattingclass-order-status-manager.php:48-49:
} }
Two } on the same line, asymmetric indentation. Cosmetic only.
Network: declaration in plugin headerMinor — only relevant for multisite installs.
Studiou_DB_Manager and UtilsLog from the manager pattern explanationAlready listed in the table but worth noting that DB Manager is a different category (static service) from the other "manager" classes (hook-registering instances). The README treats them as peers.
docs/instructions.txt)| Spec item | Requirement | Status | Notes |
|---|---|---|---|
| 1 | Custom statuses wc-to-print, wc-in-print after wc-processing |
✅ Implemented | Inserted correctly via wc_order_statuses filter. |
| 2 | Add fields to-print-date, in-print-date, external-ref-ord-no, external-ref-ord-delivered |
⚠️ Partial | Stored with underscore keys (acceptable). external_ref_ord_date exists in code but is not in the spec and not surfaced in UI. |
| 3 | Search orders by external-ref-ord-no near Search Order button |
❌ Broken | See §1.1 — three independent defects. |
| 4 | Three bulk actions | ✅ Implemented | Behaviour matches; concerns about non-atomic export (§2.3) and category multiplication (§2.4). |
| 5 | Three columns appended to orders grid | ✅ Implemented | Inserted after order_total rather than strictly "at the end", but visible and functional. |
| 6 | Import InPrint protocol with deduplication | ⚠️ Partial | Import works; deduplication not implemented (§2.1). CSV headers diverge from spec casing (§2.2). |
| 7 | Import Delivered protocol with deduplication | ⚠️ Partial | Same as item 6. |
Issues most likely to cause a user-reported bug, ordered by expected impact:
UtilsLog::message() is dead (no user feedback after bulk actions).studiou-wc-product-cat-manage or studiou-wc-free-photo-product (mentioned as siblings in recent git history).End of review.