# Implementation Plan — Auto "Done Print" on bulk "Change status to Completed" **File:** `docs/order-done-plan-00.md` **Target version:** 1.5.1 **Status:** Draft / not yet implemented --- ## 1. Goal On the **HPOS Orders list** (`wp-admin/admin.php?page=wc-orders`), when the operator runs the **built-in WooCommerce bulk action "Change status to Completed"** (`mark_completed`, Czech "Změnit stav na Dokončeno"), the plugin must — **before** the order status is flipped — advance every still-unresolved product line-item print status to **Done Print** (`done-print`). "Unresolved / unset" means any product item currently in `pending-print` or `in-print`. Items already in `done-print` or `skip-print` are left alone. This is exactly the semantics of the existing `Order_Item_Status_Manager::advance_remaining_to_done()` — we reuse it, we do not reimplement it. ### Hard scope constraint This auto-advance is **proprietary to this one entry point only**: - the **`mark_completed` bulk action**, and - the **`woocommerce_page_wc-orders` (HPOS) screen**. It must **not** fire for: - single-order completion from the order-edit status dropdown, - programmatic / REST / cron status changes, - the InPrint or Delivered protocol imports (the Delivered import already does its own advance), - any other bulk action (Prepare to Printing, In Printing, export, trash, etc.), - the legacy CPT orders screen (`edit-shop_order`) — out of scope; operator uses HPOS. Everywhere else, the existing **completion guard** behaviour is preserved: completing an order while items are still pending/in-print is blocked and reverted with an admin notice. --- ## 2. Why this is needed (current behaviour) Today the only coupling on `→ wc-completed` is the **completion guard** in `Order_Item_Status_Manager::on_order_status_changed()` (hooked to `woocommerce_order_status_changed`, priority 5). It does the opposite of what we want here: if any product item is still `pending-print`/`in-print` it **reverts** the order back to its previous status and shows an error notice. So a bulk "mark completed" over orders whose items are still pending currently **fails** for those orders — WooCommerce sets them to `completed`, our guard immediately reverts them, and the operator sees N "completion blocked" notices. The operator's intent in this specific bulk action is the reverse: "I'm done — mark the items done and complete the orders." The fix is to advance the items **before** the status change reaches the guard, so the guard sees a ready order and lets the completion stand. --- ## 3. How WooCommerce dispatches the bulk action WooCommerce routes **all** orders-list bulk actions — including the core `mark_*` ones — through the WordPress filter: ``` handle_bulk_actions-woocommerce_page_wc-orders ( $redirect_to, $action, $ids ) ``` The plugin already proves this: `Bulk_Actions_Manager` registers its custom actions on this exact filter at priority 10 and they work. WooCommerce core's own handler for `mark_*` (which loops the selected IDs and calls `$order->update_status( 'completed', ... )`) is just another callback on the same filter, also at priority 10. **Key lever:** a callback registered on the same filter at a **lower priority number runs first**. So a callback at **priority 9** runs *before* WooCommerce changes any status — the exact window we need to advance items. (Priority is what orders callbacks, not registration time. Our managers register at `plugins_loaded` 10; WC registers its list-table handler later in the request during screen setup. Using priority 9 guarantees we still run first regardless of registration order.) --- ## 4. Chosen approach (Approach A — pre-flip in the bulk filter) Add one new callback on `handle_bulk_actions-woocommerce_page_wc-orders` at **priority 9** that: 1. returns `$redirect_to` untouched unless `$action === 'mark_completed'`; 2. bails (returns `$redirect_to` untouched) if the user lacks `edit_shop_orders` — let WC's own handler reject the request; we never mutate without the cap; 3. sanitises the ID list (`array_filter(array_map('intval', (array) $ids))`); 4. for each ID: `wc_get_order($id)`, skip falsy, then call `advance_remaining_to_done($order)` (reused as-is); 5. adds a per-order summary order note when count > 0 ("N line items advanced to Done Print before bulk completion."); 6. accumulates totals and emits one `UtilsLog::message(..., 'success')` summary across the batch (e.g. "Auto-advanced 12 items to Done Print across 4 orders before completion."); 7. **returns `$redirect_to` unchanged** — it does *not* short-circuit. WooCommerce's priority-10 handler then runs the actual status change, and because every item is now `done-print`/`skip-print`, the completion guard at priority 5 sees a ready order and lets it through silently. No change is needed to the completion guard itself. The guard stays as the safety net for every *other* path. ### Where the code lives — recommendation Put the new hook + handler in **`Order_Item_Status_Manager`** (not `Bulk_Actions_Manager`): - It already owns `advance_remaining_to_done()`, `set_status()`, `get_status()`, the completion guard, and the recursion-guard flag — all the moving parts of this feature are in one class, so the author reasons about the guard/advance interaction in one place. - It already hooks WooCommerce actions in its constructor, so adding one `add_filter(...)` line there is idiomatic. - It avoids wiring a new cross-manager dependency. Constructor addition: ```php add_filter( 'handle_bulk_actions-woocommerce_page_wc-orders', array($this, 'bulk_complete_advance_items'), 9, // before WooCommerce's own mark_completed handler (priority 10) 3 ); ``` New method (sketch — final code to match house style): ```php /** * Proprietary behaviour for the HPOS Orders list "Change status to Completed" * bulk action ONLY: advance every still-pending/in-print product item to * done-print BEFORE WooCommerce flips the order, so the completion guard is * satisfied instead of reverting. Runs at priority 9 (before WC's priority-10 * mark_completed handler). Returns the redirect unchanged so WC still performs * the status change. */ public function bulk_complete_advance_items($redirect_to, $action, $ids) { if ($action !== 'mark_completed') { return $redirect_to; } if (!current_user_can('edit_shop_orders')) { return $redirect_to; // WC's handler will reject; we don't mutate. } $ids = array_filter(array_map('intval', (array) $ids)); if (empty($ids)) { return $redirect_to; } $total_items = 0; $total_orders = 0; foreach ($ids as $id) { $order = wc_get_order($id); if (!$order) { continue; } $advanced = $this->advance_remaining_to_done($order); if ($advanced > 0) { $order->add_order_note(sprintf( _n( '%d line item advanced to "Done Print" before bulk completion.', '%d line items advanced to "Done Print" before bulk completion.', $advanced, 'studiou-wc-ord-print-statuses' ), $advanced )); $total_items += $advanced; $total_orders++; } } if ($total_items > 0) { UtilsLog::message( sprintf( /* translators: 1: item count, 2: order count */ __('Auto-advanced %1$d line item(s) to "Done Print" across %2$d order(s) before completion.', 'studiou-wc-ord-print-statuses'), $total_items, $total_orders ), 'success' ); UtilsLog::log(sprintf('bulk mark_completed: advanced %d items across %d orders', $total_items, $total_orders)); } return $redirect_to; } ``` ### Alternative placement (if preferred) If the team would rather keep *all* bulk-action hooks in `Bulk_Actions_Manager`, the identical logic can live there instead, with the `Order_Item_Status_Manager` instance injected via the constructor. The main plugin would then instantiate `Order_Item_Status_Manager` first and pass it into `new Bulk_Actions_Manager($itemStatus)`. Functionally equivalent; more wiring. **Recommendation stays with `Order_Item_Status_Manager`.** --- ## 5. Interaction with the existing completion guard Confirm during implementation: - The guard runs at `woocommerce_order_status_changed` **priority 5**; it fires *after* WC's bulk handler calls `update_status('completed')`, i.e. *after* our priority-9 pre-advance. - Sequence for one order in the bulk action: 1. our priority-9 filter advances items → `done-print` and saves them; 2. WC's priority-10 filter calls `$order->update_status('completed')`; 3. that fires `woocommerce_order_status_changed` → guard (priority 5) → `is_order_ready_for_completion()` now returns `ready` → guard returns without reverting. - The `$inside_self_revert` recursion flag is **not** involved (no revert happens), so no conflict. - Orders already `completed` before the action: `update_status('completed')` is a no-op (no `status_changed`), so the guard never fires — harmless. Our advance still runs and is also effectively a no-op if items are already resolved. --- ## 6. Edge cases & decisions - **Terminal-status orders selected (cancelled / refunded / failed):** WooCommerce core's `mark_completed` will move them to `completed` regardless (it has no terminal guard for bulk mark-complete). Our pre-advance will advance their items too. This matches the operator's explicit selection + explicit "complete" choice. **Decision: do not special-case terminal statuses here** — note it as expected behaviour. (Contrast: the protocol *imports* deliberately skip `Studiou_DB_Manager::TERMINAL_STATUSES`, because there the order set is data-driven, not an explicit operator click.) If the team wants to protect these, add a `TERMINAL_STATUSES` skip in the loop — flagged as Open Question Q2. - **Empty selection:** handled (returns early). - **Capability:** mirror existing pattern (`edit_shop_orders`); bail without mutating if absent rather than `wp_die()` — WC's handler already enforces caps and we don't want to change the failure UX of the core action. - **Non-product items** (shipping/fee/coupon): `advance_remaining_to_done()` already skips anything that isn't a `WC_Order_Item_Product`. No change. - **Performance:** `advance_remaining_to_done()` saves each changed item individually (`$item->save()`), consistent with the existing import path. For very large bulk sets this is N item-saves; acceptable and matches current code. Not optimising now. - **Idempotency:** re-running the bulk action over the same orders is safe (already-done items are skipped, count 0, no note). --- ## 7. Fallback approach (only if Approach A proves unreliable) If a future WooCommerce version stops routing `mark_completed` through `handle_bulk_actions-woocommerce_page_wc-orders`, or our priority-9 callback cannot be made to run first, fall back to **context-aware guard relaxation**: - In `on_order_status_changed()`, when `$to === 'completed'`, detect the bulk-complete request context — `is_admin()` and the request carries the wc-orders bulk action, e.g. `($_REQUEST['page'] ?? '') === 'wc-orders'` and the resolved bulk action (`$_REQUEST['action']` / `action2`) is `mark_completed`. - In that context only, instead of reverting, call `advance_remaining_to_done($order)` and allow completion. - Everywhere else, keep the current block-and-revert. This is more coupled (inspects superglobals inside a domain listener) and is documented here only as a contingency. **Verify Approach A on staging first (see §9); prefer it.** --- ## 8. Files to change - `includes/class-order-item-status-manager.php` - add the `add_filter('handle_bulk_actions-woocommerce_page_wc-orders', ..., 9, 3)` in the constructor; - add `bulk_complete_advance_items()` method. - `studiou-wc-ord-print-statuses.php` - bump `Version:` header and `STUDIOU_WC_OPS_VERSION` → `1.5.1`. - `README.md` - extend §6 "Per-order-item print status" coupling notes with the new bulk-complete-on-wc-orders behaviour (be explicit that it is screen- and action-scoped and does NOT change single-order or programmatic completion); - add a `## 1.5.1` changelog entry. - `CLAUDE.md` - add a "Version 1.5.1" entry under Recent Changes; - extend the `Order_Item_Status_Manager` architecture bullet with the new bulk-complete pre-advance hook (priority 9 on `handle_bulk_actions-woocommerce_page_wc-orders`, scoped to `mark_completed`). - (No JS/CSS change → no asset cache-bust concern. Version bump still required by repo release convention.) Add a one-line pointer to this plan in CLAUDE.md's **Documentation** list: `- [`docs/order-done-plan-00.md`](docs/order-done-plan-00.md) — Plan for auto "Done Print" on the HPOS bulk "Change status to Completed" action (1.5.1).` No new translation strings beyond the two new notice/note formats — add their English sources and refresh `cs_CZ` (`docs/translations.txt`, `.po`/`.mo`) per the existing process. --- ## 9. Test plan Manual, on staging with HPOS enabled and `WP_DEBUG` on: 1. **Happy path:** create an order with several product items in mixed statuses (`pending-print`, `in-print`, one `skip-print`). On `wp-admin/admin.php?page=wc-orders`, select it, run **Change status to Completed**. Expect: order becomes `completed`; the pending/in items are now `done-print`; the `skip-print` item is untouched; one order note "N line items advanced to Done Print before bulk completion."; one success admin notice; **no** "completion blocked" notice. 2. **Multi-order batch:** select several orders; verify per-order notes and a single batch summary notice with correct totals. 3. **Already-ready order:** items all `done-print`/`skip-print` → completes with no advance note (count 0). 4. **Scope — single order edit screen:** open one order with pending items, set status to Completed via the order dropdown + Update. Expect the **old** behaviour: blocked + reverted + error notice (auto-advance must NOT apply here). 5. **Scope — other bulk actions:** run "Set Status to In Printing" / "Prepare to Printing Export" — unchanged; no done-print advance. 6. **Scope — programmatic:** trigger a completion via WP-CLI/REST (or a quick snippet) — guard still blocks pending orders; no auto-advance. 7. **Capability:** as a role without `edit_shop_orders`, confirm no item mutation occurs. 8. **Hook-order verification (the load-bearing assumption):** with `WP_DEBUG_LOG`, log the item statuses at the top of `bulk_complete_advance_items()` and inside the completion guard; confirm the advance log line precedes the guard line for the same order in one request. This proves priority 9 runs before WC's status flip on the installed WC version. --- ## 10. Open questions - **Q1 (placement):** keep the handler in `Order_Item_Status_Manager` (recommended) or move to `Bulk_Actions_Manager` with dependency injection? Default: `Order_Item_Status_Manager`. - **Q2 (terminal orders):** should cancelled/refunded/failed orders selected in the bulk action be skipped (not advanced, not completed) like the protocol imports do, or follow WooCommerce core and complete them? Default: follow core (no skip), since the operator explicitly chose them. - **Q3 (legacy CPT screen):** confirm the legacy `edit-shop_order` screen is genuinely out of scope (operator uses HPOS only). If it must be covered too, mirror the hook on `handle_bulk_actions-edit-shop_order`.