feature-order-item-status-analysis.md 24 KB

Feature Analysis — Per-Order-Item Print Status

Date: 2026-05-12 (revised after dropping the "independent statuses" requirement) Target plugin version after implementation: 1.5.0 (minor bump — new feature, additive, non-breaking). Status: Analysis / planning document. No code has been written yet.


1. Requirements as received

  1. Add new status for order item called order-item-status:
    • pending-print — order item is waiting for printing process — initial
    • in-print — order item is currently processing for print
    • done-print — order item was printed
    • skip-print — order item is skipped by some custom / system reason
  2. Add to order detail to order item possibility (combo box) to change order-item-status manually.
  3. When Order status is going to change to done, all order items must be != pending-print and != in-print.

(Previous requirement #3 — "Order status and order-item-status are independent" — was withdrawn by the stakeholder; this analysis assumes order ↔ item coupling is allowed and beneficial. See §3 for the proposed coupling rules.)


2. Logical revisions

2.1 "When order status is going to change to done" — clarify what "done" means

WooCommerce has no wc-done status. The plugin already adds wc-to-print and wc-in-print. The natural interpretation is the WC built-in wc-completed.

Proposed revision: "When the order status is going to change to wc-completed".

2.2 What about non-product line items?

Orders contain product items but also fee items, shipping items, coupon items, tax items, and refund items. None of these are printed.

Proposed revision: the completion guard inspects only product line items (WC_Order_Item_Product). Other item types are not assigned a print status and never block completion.

2.3 "Initial" value of pending-print — when is it set?

The spec implies new items default to pending-print. Two strategies:

  • Lazy (recommended): the per-item meta is not written when the order is created. The get_status() helper returns 'pending-print' when the meta is missing. Migration of existing orders is unnecessary.
  • Eager: write pending-print to every new item via woocommerce_new_order_item. Existing orders need a one-time backfill migration.

Proposed revision: lazy default. Missing meta = pending-print. This avoids touching every existing order at activation time.


3. Coupling rules between order status and item statuses

With the "independence" constraint dropped, the design space opens up. The cleanest model is one-way coupling, order → items: changing an order status can propagate to items; changing an item status never propagates back to the order. Within that, we need to decide which order-status transitions propagate and how.

The proposed rules:

Order status transition Effect on product items
wc-to-print (Prepare to Printing) No automatic change. Items typically already pending-print (lazy default).
wc-in-print (In Printing) Items in pending-print advance to in-print. done-print / skip-print items are untouched.
wc-completed Gated, not propagating — the transition is blocked if any product item is still pending-print or in-print. Operator (or protocol import) must advance items first.
wc-cancelled / wc-refunded / wc-failed No automatic change. Items stay where they are (historical record preserved).
wc-processing / wc-pending / wc-on-hold No automatic change.
Item status changed manually Never propagates to order. The operator drives the order status separately.

Two distinct kinds of coupling are at play:

  • Propagation (→ in-print): the order transition drives item state. The listener that handles woocommerce_order_status_changed runs the propagation.
  • Guard (→ completed): the order transition requires items to already be in the right state. If not, the transition is reverted. The same listener implements the guard.

Both behaviours live in a single hook on woocommerce_order_status_changed. The protocol imports become natural consumers of these rules:

  • InPrint Protocol calls $order->update_status('in-print', ...). The propagation rule advances pending items to in-print automatically. No special-case code in the protocol importer.
  • Delivered Protocol calls $order->update_status('completed', ...). The guard would block this if items are still pending-print or in-print. The protocol importer must therefore call advance_remaining_to_done($order) before the status flip.

4. Open questions for stakeholder decision

Before implementation begins, the following decisions should be confirmed:

  1. Propagation on → in-print (§3): confirm "advance pending-print items to in-print" is the desired behaviour for manual order-status changes from the admin dropdown, bulk actions, and the InPrint Protocol import. Alternative: leave items untouched, require explicit per-item operations.
  2. Audit trail: should item-status changes write an entry to the order's activity feed? Recommendation: yes, with a brief note like "Item #5 (Product Name): Pending Print → Done Print."
  3. skip-print reason: is there a free-text "reason" field, or just the status? Recommendation: status only in v1.5.0; reason field as a future enhancement.
  4. Bulk per-item operations across orders: is there a need? Recommendation: not in v1.5.0 — single-order manual control plus protocol/propagation auto-advance covers the workflow.
  5. CSV-export filter: should "Prepare to Printing Export" filter to only pending-print items, or include everything? Recommendation: include everything, add prod_print_status column. Filtering can happen downstream.
  6. Backwards-compatible behaviour for existing completed orders: the completion guard applies only to new transitions to wc-completed. Already-completed orders are unaffected even if their items are pending-print (lazy default). Confirm this is acceptable.

5. Proposed final specification

After applying §2's revisions and §3's coupling rules:

  1. Each order item (specifically each WC_Order_Item_Product) has a print status with one of four values, persisted to per-item meta key _print_status. Missing meta defaults to pending-print.
  2. The order detail screen (classic order edit) adds a "Print Status" column to the line-items table with a <select> per product row. Saving the order persists changes.
  3. Order → item propagation:
    • Transition into wc-in-print advances every pending-print item to in-print. done-print and skip-print items are untouched.
    • All other order-status transitions leave item statuses unchanged.
  4. Completion guard: transition into wc-completed is blocked if any product item is pending-print or in-print. Blocked transition reverts to the previous status with an admin notice listing the blocking items.
  5. Protocol imports:
    • InPrint Protocol sets order to wc-in-print; items auto-propagate per rule (3a). No explicit per-item code in the importer.
    • Delivered Protocol explicitly advances every pending-print / in-print item to done-print before setting the order to wc-completed. The guard is satisfied; the operator gets one summary notice.
  6. Item-status manual changes never propagate to the order. Operator drives the order status independently.
  7. The CSV export from "Prepare to Printing Export" gains a prod_print_status column reflecting each item's current status.
  8. Item-status changes (manual or propagated) generate an order note: "Item #ID (Product name): Old Status → New Status."

6. Data model

6.1 Storage

  • Meta key: _print_status (underscore prefix per WC convention for internal meta).
  • Storage layer: WC_Order_Item::update_meta_data() / get_meta().
  • Underlying table: {$wpdb->prefix}woocommerce_order_itemmeta. This table exists under both HPOS and legacy CPT — WC uses the same item-meta storage in both modes.
  • No database migration required (lazy default).

6.2 Constants

class Order_Item_Status_Manager {
    const STATUS_PENDING = 'pending-print';
    const STATUS_IN      = 'in-print';
    const STATUS_DONE    = 'done-print';
    const STATUS_SKIP    = 'skip-print';

    const STATUSES_THAT_SATISFY_COMPLETION = array(
        self::STATUS_DONE,
        self::STATUS_SKIP,
    );

    public static function labels() {
        return array(
            self::STATUS_PENDING => __('Pending Print', 'studiou-wc-ord-print-statuses'),
            self::STATUS_IN      => __('In Print',      'studiou-wc-ord-print-statuses'),
            self::STATUS_DONE    => __('Done Print',    'studiou-wc-ord-print-statuses'),
            self::STATUS_SKIP    => __('Skip Print',    'studiou-wc-ord-print-statuses'),
        );
    }
}

6.3 API surface

A new Order_Item_Status_Manager class exposes:

Method Purpose
get_status(WC_Order_Item_Product $item): string Returns the item's print status; defaults to STATUS_PENDING when the meta is missing.
set_status(WC_Order_Item_Product $item, string $status, bool $audit = true): bool Validates the status, writes meta, optionally appends an order note.
valid_statuses(): array Returns the four constants.
is_order_ready_for_completion(WC_Order $order): array{ready:bool, blocking:WC_Order_Item_Product[]} Used by the completion guard and admin UI.
advance_pending_to_in(WC_Order $order): int Used by the propagation listener on → in-print. Returns count advanced.
advance_remaining_to_done(WC_Order $order): int Used by Delivered Protocol import before completing. Returns count advanced.

7. Files & hooks

7.1 New file: includes/class-order-item-status-manager.php

Hooks registered in constructor:

Hook Purpose
woocommerce_admin_order_item_headers Inject the "Print Status" column header.
woocommerce_admin_order_item_values Render the <select> per line item.
woocommerce_process_shop_order_meta Read $_POST['print_status'][$item_id], persist via set_status(). Runs after WC's own item processing.
woocommerce_order_status_changed (priority 5) Combined propagation + completion guard (see §8).

7.2 Modified: includes/class-db-manager.php

  • import_inprint_protocol(): no functional change — propagation happens automatically via the listener when $order->update_status('in-print', ...) fires. Optionally surface the advanced count in the admin notice: "3 line items advanced to In Print."
  • import_delivered_protocol(): before calling $order->update_status('completed', ...), call Order_Item_Status_Manager::advance_remaining_to_done($order) so the guard is satisfied. Surface the advanced count in the import-summary notice.
  • get_orders_for_printing(): extend the SELECT to surface item-level print status. Join {$wpdb->prefix}woocommerce_order_items (the item row) and {$wpdb->prefix}woocommerce_order_itemmeta (the _print_status meta) on order_item_id. Add prod_print_status to the CSV output. Note: the existing query groups by (order, product, variation) — multiple line items in one group would need a deterministic pick (e.g., MIN() again, or join via order_products.order_item_id if the lookup table exposes it).

7.3 Modified: studiou-wc-ord-print-statuses.php

Add the new manager to load_dependencies() and initialize_managers(), identical to the existing pattern. Bump STUDIOU_WC_OPS_VERSION to 1.5.0.

7.4 Modified: includes/class-bulk-actions-manager.php

No code change. Bulk actions "Set Status to Prepare to Printing" and "Set Status to In Printing" already call $order->update_status(...) — the propagation listener handles items automatically when the transition is to in-print. The "Prepare to Printing" path doesn't propagate (per §3).

7.5 Modified: README.md, CLAUDE.md, docs/translations.txt, languages/studiou-wc-ord-print-statuses-cs_CZ.po

  • README: new "Order item statuses" feature section + 1.5.0 changelog entry + coupling rules table from §3.
  • CLAUDE.md: extend the manager-pattern list with the new manager + Recent Changes entry.
  • Translations: add the four status labels, "Print Status" column header, guard error message, audit-note format, and the protocol auto-advance summary strings.

8. Combined propagation + completion guard

A single listener on woocommerce_order_status_changed handles both behaviours. Pseudocode:

public function on_order_status_changed($order_id, $from, $to, $order) {
    // Re-entrancy guard: a guard-revert below fires this hook again. Skip
    // the recursive invocation.
    if (self::$inside_self_revert) {
        return;
    }

    if ($to === 'in-print') {
        $advanced = $this->advance_pending_to_in($order);
        if ($advanced > 0) {
            UtilsLog::log("Order #{$order_id}: advanced {$advanced} item(s) to in-print on order → in-print");
        }
        return;
    }

    if ($to === 'completed') {
        $check = $this->is_order_ready_for_completion($order);
        if ($check['ready']) {
            return;
        }
        self::$inside_self_revert = true;
        try {
            $order->update_status(
                $from,
                __('Cannot complete: product line items still pending print. Resolve item statuses first.', 'studiou-wc-ord-print-statuses')
            );
        } finally {
            self::$inside_self_revert = false;
        }
        UtilsLog::message(
            sprintf(
                __('Order #%1$d completion blocked — %2$d product items not yet "Done Print" or "Skip Print".', 'studiou-wc-ord-print-statuses'),
                $order_id,
                count($check['blocking'])
            ),
            'error'
        );
    }
}

Caveats:

  1. woocommerce_order_status_changed fires after the change is committed. Reverting via update_status is post-hoc — the order shows the reverted state on the next page load. Slightly disorienting but workable, and the admin notice explains it.
  2. The static $inside_self_revert flag prevents infinite recursion when the revert itself fires the hook.
  3. For cleaner UX on the admin form path, a complementary interception in woocommerce_admin_process_order_object can validate before save. The post-hoc revert still covers programmatic / API / protocol paths. Belt-and-braces.

9. UI mockup (text)

In the order edit screen's Items meta box, the table grows by one column:

| Item                  | Cost   | Qty | Total  | Print Status         |
|-----------------------|--------|-----|--------|----------------------|
| Product A (variation) | 100.00 |  2  | 200.00 | [Pending Print  ▼]   |
| Product B (variation) |  50.00 |  1  |  50.00 | [Done Print     ▼]   |
| Shipping              |      — |  —  |  10.00 | —                    |
| Coupon: SUMMER10      | -20.00 |  —  | -20.00 | —                    |

Non-product items show and aren't editable.

Optional v1.5.0 sweetener: a one-line summary above the table — "Print readiness: 3 of 4 items ready (1 pending)."


10. Edge cases & open behaviour decisions

Scenario Proposed behaviour
Order has 0 product items Completion allowed (vacuously satisfies the guard).
Order has only refunds / shipping / fees Completion allowed.
Existing wc-completed order with items in pending-print No effect — the guard only fires on the transition into completed, not on stored state.
Manual order status flip Pending → Completed (skipping in-print) Guard fires. Items are still pending-print → blocked. Operator must advance items or use protocol imports.
Order in wc-in-print reverted to wc-processing then back to wc-in-print First revert: no item change (per §3). Second forward: pending items advance again (no-op since they're already in-print). Done items stay done.
Item is removed from order after being set to done-print The meta is deleted along with the item. No orphan record.
Item status changed after order is completed Allowed. Order doesn't revert.
Bulk action "Set Status to In Printing" on 100 orders Each order's pending items propagate to in-print via the listener.
Multiple wc-in-printwc-to-print ping-pong Each → in-print advances any remaining pending-print items. Items previously moved stay in in-print.

11. Test plan

11.1 Unit-style scenarios

  1. Default value: new order item has no _print_status meta; get_status() returns 'pending-print'.
  2. Set status: set_status($item, 'in-print') writes the meta and (with $audit=true) appends an order note.
  3. Invalid status: set_status($item, 'foo') rejects and returns false without writing.
  4. Completion guard ready: all items in done-print or skip-printis_order_ready_for_completion() returns {ready:true}.
  5. Completion guard blocked: at least one item in pending-print or in-print — returns {ready:false, blocking:[…]}.
  6. Non-product items ignored: an order with one product item (done-print) and one shipping item — guard returns ready.
  7. advance_pending_to_in: order with items [pending, in, done, skip] → result has items [in, in, done, skip]. Count = 1.
  8. advance_remaining_to_done: order with items [pending, in, done, skip] → result [done, done, done, skip]. Count = 2.

11.2 Integration

  1. Admin UI manual change: open an order with three items, change one to done-print, click Update, reload → meta persisted, audit note added.
  2. Propagation: change order status to wc-in-print via the admin dropdown → all pending-print items in the order are now in-print. Order note from update_status plus per-item audit notes.
  3. Completion guard via admin: with one item still pending-print, change order status to wc-completed, click Update → order reverts to previous status (visible after reload), admin notice shows the block reason.
  4. Completion guard via programmatic call: wp wc shop_order update <id> --status=completed on an order with pending-print items → status reverts.
  5. InPrint Protocol import: prepare a CSV with one order_no, run the import → order is in in-print, all the order's pending-print items advanced to in-print via the listener, done-print and skip-print items untouched. Import notice mentions advanced count.
  6. Delivered Protocol import: prepare a CSV with one order_no, run the import → items advance to done-print first, then order flips to completed. Guard is satisfied. Import notice mentions advanced count.
  7. CSV export: "Prepare to Printing Export" bulk action — output contains a prod_print_status column with per-item values.

11.3 Regression

  1. The "Set Status to Prepare to Printing" bulk action still works without touching items (no propagation on → to-print).
  2. The "Set Status to In Printing" bulk action now also propagates items (new behaviour — expected).
  3. Existing completed orders are unaffected by the new guard.
  4. The Print Information panel on the order edit screen still renders.
  5. The custom orders-list columns still render.
  6. The order search by external reference still works.

12. Rollout

12.1 Versioning

  • New plugin version: 1.5.0 (minor — feature addition, no breaking changes).
  • STUDIOU_WC_OPS_VERSION constant updated accordingly.

12.2 Activation behaviour

  • No database migration. Lazy defaults handle existing items.
  • No new tables. The woocommerce_order_itemmeta table is already created by WC.

12.3 Deactivation behaviour

  • Same policy as today: meta keys are not cleaned up. Reactivation preserves all data.

12.4 Internationalisation

~12 new translatable strings:

  • 4 status labels: Pending Print, In Print, Done Print, Skip Print.
  • 1 column header: Print Status.
  • 1 placeholder for non-product items: (literal, not translated).
  • 1 completion-guard order note: Cannot complete: product line items still pending print. Resolve item statuses first.
  • 1 admin notice: Order #%1$d completion blocked — %2$d product items not yet "Done Print" or "Skip Print".
  • 1 audit-note format: Item #%1$d (%2$s): %3$s → %4$s.
  • 2 protocol-auto-advance order notes: %d items advanced to In Print by order-status change., %d items advanced to Done Print by Delivered Protocol import.
  • 1 readiness summary (optional UI sweetener): Print readiness: %1$d of %2$d items ready (%3$d pending).

12.5 Documentation

  • README: new "Order item statuses" feature section + coupling rules table + 1.5.0 changelog entry + Known Limitations updated (block-based screen still unsupported).
  • CLAUDE.md: new manager added to the manager-pattern list + Recent Changes entry.
  • New review doc (post-release): docs/revise-1.5.0.md once the feature lands.
  • docs/feature-order-item-status-analysis.md (this document) → archive or supersede after implementation.

12.6 Phasing (suggested)

  • Phase 1 (v1.5.0): core feature — data model, admin UI, propagation listener, completion guard, Delivered Protocol explicit advance, CSV export column. Classic order edit screen only.
  • Phase 2 (v1.5.x or later): AJAX live status changes (no full page reload), bulk-change items across orders, optional skip-print reason field, block-based order edit screen support.

13. Effort estimate

Rough sizing for Phase 1:

Area Effort
Order_Item_Status_Manager class (data model + admin UI + combined propagation/guard listener) ~6–10 hours
Studiou_DB_Manager integration (Delivered Protocol advance, CSV column with new join) ~2–3 hours
Bootstrap + manager wiring + version bump ~30 minutes
README / CLAUDE.md / translations / .po ~2 hours
Test pass (manual scenarios in §11) ~3–4 hours
Total ~14–20 hours

Phase 2 items add ~6–10 hours each depending on scope.


14. Risk register

Risk Likelihood Impact Mitigation
Listener recursion (propagation → save → propagation, or guard-revert → revert → revert) Medium High Static $inside_self_revert flag; propagation only fires on → in-print not on every save.
Existing orders break on first edit because meta is missing Low Low Lazy default handles this — get_status returns pending-print for missing meta.
Operator surprise: clicking "Set Status to In Printing" silently advances items Low Low Documented in README; per-item audit notes show what happened.
Block-based order edit screen doesn't render the new column Medium Low Documented as a known limitation since 1.4.1; carried forward.
_print_status meta key collides with another plugin Very low Medium If collision found, fall back to a namespaced key (_studiou_print_status).
Delivered Protocol "advance to done" advances items the operator intended to leave in skip-print None skip-print already satisfies the completion guard; the advance helper only touches pending-print / in-print.

15. Recommendation

Implement Phase 1 as described above, subject to stakeholder confirmation of the §4 open questions — particularly:

  • Open question 1 (propagation on → in-print). The proposed design assumes "yes, advance pending items". This is the workflow-consistent choice, but it does change the behaviour of the existing bulk action "Set Status to In Printing" (previously order-only, now also touches items). If the stakeholder prefers the bulk action to remain strictly order-only, the propagation rule can be limited to specific paths (e.g., only the InPrint Protocol import) — but that re-introduces some of the awkwardness the old §2.5 grappled with.

The shape of the design with requirement #3 dropped is materially cleaner than the original analysis: a single listener implements both behaviours, the protocol-import code paths are minimally changed, and the rules can be summarised in one table (§3).


End of analysis.