order-done-plan-00.md 15 KB

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:

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):

/**
 * 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_VERSION1.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.