Skip to content

Label print button - #60

Merged
adriaanzon merged 18 commits into
mainfrom
label-print-button
Aug 7, 2026
Merged

adriaanzon merged 18 commits into
mainfrom
label-print-button

Conversation

@adriaanzon

@adriaanzon adriaanzon commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Add a per-order label print button on the order overview

Summary by CodeRabbit

  • New Features

    • Added streamlined single-order and bulk shipment creation and label-printing workflows.
    • Added shipment status tracking with progress, success, and error notifications.
    • Added order-list actions for creating shipments and printing labels.
    • Added support for printing existing labels without creating new shipments.
  • Improvements

    • Improved local asset refreshing during development.
    • Updated Dutch translations for shipment and label workflows.
    • Raised the minimum supported WordPress version to 6.2.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The plugin adds AJAX shipment creation and status handling, shared order-list label printing, processing-method-specific behavior, debug asset versioning, updated translations, and expanded automated test coverage.

Changes

Shipment and label workflow

Layer / File(s) Summary
Asset versioning and runtime wiring
docker/provision.sh, lib/Plugin.php, lib/Modules/Admin/Settings.php, lib/Modules/Checkout.php, lib/Utils/BlocksIntegration.php, lib/Modules/Orders/Single.php, sendy.php, readme.txt, tests/AssetVersionTest.php
Assets use modification-time versions when SCRIPT_DEBUG is enabled. The plugin falls back to its version, registers new order modules, and now requires WordPress 6.2.
Shipment endpoints and order actions
lib/Modules/Orders/OrdersModule.php, lib/Modules/Orders/CreateShipments.php, lib/Modules/Orders/Single.php, lib/Modules/Orders/PrintLabels.php, lib/Modules/Orders/RowActions.php, lib/Modules/Orders/BulkActions.php
The plugin adds shipment creation and status endpoints, shared AJAX validation, processing-method routing, package persistence, order-row actions, and bulk shipment handling.
Shared order-list print flow
resources/js/orders-list-print-flow.js, resources/js/admin-order-bulk.js, resources/css/order-actions.css
The order list uses a shared modal, shipment-status polling, label printing, cancellation handling, delegated row actions, and busy-state styling.
Validation, test infrastructure, and translations
tests/Sendy_Ajax_TestCase.php, tests/CreateShipmentsTest.php, tests/ShipmentsStatusTest.php, tests/SingleCreateShipmentTest.php, tests/BulkActionsTest.php, tests/PrintLabelsTest.php, tests/RowActionsTest.php, tests/PrintFlowAssetsTest.php, tests/bootstrap.php, tests/doubles.php, languages/sendy.pot, languages/sendy-nl_NL.po, languages/sendy-nl_NL.mo
Tests cover shipment creation, status resolution, authorization, API failures, metadata persistence, processing methods, assets, and order actions. Translation catalogs include the new shipment and label messages.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OrderList
  participant PrintFlow
  participant CreateShipments
  participant SendyAPI
  OrderList->>PrintFlow: Start label-print flow
  PrintFlow->>CreateShipments: Request shipment statuses
  CreateShipments->>SendyAPI: Create or retrieve shipment
  SendyAPI-->>CreateShipments: Return shipment status and packages
  CreateShipments-->>PrintFlow: Return statuses
  PrintFlow->>OrderList: Print labels or reload
Loading

Possibly related PRs

Suggested reviewers: wgriffioen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a label print button to the orders overview.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (9)
tests/ShipmentsStatusTest.php (1)

198-210: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a test for the unauthenticated case.

fake_transport() always writes the three token options, so every test authenticates. The endpoint has no coverage for the state in which sendy_access_token is empty. In that state ApiClientFactory::buildConnectionUsingTokens() throws \RuntimeException, which shipment_status() does not catch. A test that deletes the token options and asserts a JSON response would lock the fix for the issue raised in lib/Modules/Orders/CreateShipments.php.

Do you want me to write that test?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ShipmentsStatusTest.php` around lines 198 - 210, Add an
unauthenticated-case test alongside
test_an_api_error_is_reported_as_failed_with_a_flash_notice by deleting the
token options without calling fake_transport(), dispatching the shipment-status
request, and asserting it returns the expected JSON response rather than
propagating the RuntimeException from
ApiClientFactory::buildConnectionUsingTokens().
tests/doubles.php (2)

28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the WC_Order stub with class_exists().

The stub is declared unconditionally. If any future bootstrap loads WooCommerce, PHP fatals with a duplicate class declaration, and the failure points at this file rather than at the new test.

♻️ Proposed fix
-class WC_Order
-{
-}
+if (! class_exists('WC_Order')) {
+    class WC_Order
+    {
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doubles.php` around lines 28 - 30, Guard the WC_Order stub declaration
with class_exists('WC_Order') so it is only defined when WooCommerce has not
already loaded the class. Preserve the existing empty stub behavior when the
class is unavailable.

202-217: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

send() fails with an undefined offset when the queue is empty.

fake_transport() accepts zero responses. ShipmentsStatusTest::test_orders_without_a_shipment_or_that_do_not_resolve_are_reported_as_none() does not call fake_transport() at all, so today this path is not reached. If a later test constructs the transport without responses and the code under test makes one call, $this->responses[0] emits an undefined-offset warning and returns null, which violates the Response return type and throws a TypeError.

The condition also makes the last response repeat for every further call. Make both behaviours explicit.

💚 Proposed fix
     public function send(Request $request): Response
     {
         $this->lastRequest = $request;
         $this->requests[] = $request;
 
-        if (count($this->responses) > 1) {
-            return array_shift($this->responses);
-        }
-
-        return $this->responses[0];
+        if ($this->responses === []) {
+            throw new LogicException('Sendy_Fake_Transport received an unexpected request; no response was queued.');
+        }
+
+        // The last queued response is reused, so polling loops do not need one
+        // response per call.
+        return count($this->responses) > 1 ? array_shift($this->responses) : $this->responses[0];
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doubles.php` around lines 202 - 217, Update the response handling in
send() so an empty responses queue is handled explicitly without accessing index
0 or violating the Response return type, and ensure the final configured
response is not implicitly reused on subsequent calls. Preserve the existing
request tracking while defining the intended behavior for both empty and
exhausted response queues.
lib/Modules/Orders/CreateShipments.php (1)

46-70: 🩺 Stability & Availability | 🔵 Trivial

The loop makes one synchronous API call per order with no bound.

Each iteration calls the Sendy API in the request thread. The orders list allows a large selection, so a single request can exceed max_execution_time or the gateway timeout. The client then sees a failed request although some shipments already exist, and a retry re-enters the loop for the remaining orders only because of the _sendy_shipment_id guard.

Consider capping the number of orders per request and letting the frontend flow send batches. That also gives the user progress feedback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Modules/Orders/CreateShipments.php` around lines 46 - 70, Limit the
order-processing loop in the shipment creation flow to a defined maximum number
of orders per request, using the existing _sendy_shipment_id guard for
already-created shipments. Update the frontend submission flow to send the
selected orders in batches and report progress between requests, while
preserving the existing error handling and created-order tracking in
create_shipment processing.
lib/Modules/Orders/OrdersModule.php (1)

63-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fully qualify the @throws type.

The file does not import GuzzleException, so both @throws GuzzleException tags resolve to Sendy\WooCommerce\Modules\Orders\GuzzleException, which does not exist. This is the cause of the two PHPStan throws.notThrowable errors at Line 65 and Line 121. Use the fully qualified name, as Single::handle_create_shipment_from_form() already does.

♻️ Proposed fix
-     * `@throws` GuzzleException
+     * `@throws` \GuzzleHttp\Exception\GuzzleException
      */
     protected function create_shipment(\WC_Order $order, string $shopId, string $preferenceId, int $amount): void

Apply the same change to the tag above create_shipment_from_order() at Line 119.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Modules/Orders/OrdersModule.php` around lines 63 - 70, Update the `@throws`
annotations above create_shipment() and create_shipment_from_order() to use the
fully qualified Guzzle exception class name, matching
Single::handle_create_shipment_from_form(), so PHPStan resolves both tags to a
throwable type.

Source: Linters/SAST tools

lib/Modules/Orders/BulkActions.php (1)

59-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the sendy_amount fallback with the AJAX endpoint.

lib/Modules/Orders/CreateShipments.php defaults amount to '1'. Here the fallback is '', which casts to 0. For the Sendy processing method the amount field is not rendered, so the value is missing and the cast produces 0. The smart-rules path ignores the amount today, so no user-visible defect exists. Use '1' to keep both entry points consistent.

♻️ Proposed change
-        $amount = sanitize_key($_REQUEST['sendy_amount'] ?? '');
+        $amount = sanitize_key($_REQUEST['sendy_amount'] ?? '1');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Modules/Orders/BulkActions.php` at line 59, Update the sendy_amount
fallback in the bulk-actions request handling to use '1' instead of an empty
string, matching the amount default used by CreateShipments.php and preserving
the expected value when the field is omitted.
resources/js/admin-order-bulk.js (1)

86-112: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add a rejection handler to the modal promise chain.

window.sendyOpenCreateShipmentsModal() builds its promise around tb_show(). If tb_show() throws, the promise rejects and the failure surfaces only as an unhandled rejection. Add a catch so the failure is logged, which matches the logging that resources/js/orders-list-print-flow.js performs in window.sendyOrdersListPrintFlow.

♻️ Proposed change
 					bulkActionsForm.submit();
-				} );
+				} )
+				.catch( ( error ) =>
+					console.error(
+						'Sendy: opening the create shipments modal failed',
+						error
+					)
+				);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/admin-order-bulk.js` around lines 86 - 112, Update the promise
chain from window.sendyOpenCreateShipmentsModal in the bulk shipment flow to add
a rejection handler after the existing then callback. Log the caught error using
the same established logging pattern as window.sendyOrdersListPrintFlow, while
preserving the current field handling and form submission behavior.
resources/js/orders-list-print-flow.js (1)

55-65: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the nonce element lookup.

document.getElementById( 'sendy-print-labels-nonce' ) returns null when print_labels_nonce_field() did not render, for example when a third-party plugin alters the orders screen id. The property read then raises a TypeError that only reaches the console. Read the value defensively and fail with a clear message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/orders-list-print-flow.js` around lines 55 - 65, Update
fetchStatuses to guard the sendy-print-labels-nonce element lookup before
reading its value; when the element is absent, fail with a clear error message
instead of dereferencing null, while preserving the existing AJAX behavior when
the nonce is available.
tests/SingleCreateShipmentTest.php (1)

80-114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep wp_send_json_success() outside the recovery try block.

WPAjaxDieContinueException extends Exception, so the current catch invokes wp_send_json_error() after the success response in AJAX tests. Limit the try block to validation and shipment work, then call wp_send_json_success() after the catch. The test helper can then use dispatch_ajax() without repairing output buffers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SingleCreateShipmentTest.php` around lines 80 - 114, Update the AJAX
handler invoked by dispatch so its try block only covers validation and shipment
work; keep the catch handling failures, then move wp_send_json_success() after
the catch and outside recovery. Preserve the successful response payload,
allowing dispatch to use dispatch_ajax() without output-buffer repair.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/Modules/Orders/BulkActions.php`:
- Around line 61-63: Update the bulk loop around create_shipment() to resolve
each order first, skip the iteration when wc_get_order($id) returns false, and
catch SendyException per order so failures do not abort processing of subsequent
orders. Match the unresolved-order and per-order error handling used by
CreateShipments while preserving the existing shipment arguments and amount
conversion.

In `@lib/Modules/Orders/CreateShipments.php`:
- Around line 39-41: Clamp the requested shipment amount to at least one package
in both entry points: update lib/Modules/Orders/CreateShipments.php lines 39-41
to derive the amount with absint and max(1, ...), then pass that clamped value
to create_shipment() and remember_previously_used(); apply the same max(1,
absint(...)) handling in lib/Modules/Orders/Single.php lines 128-133, replacing
the current casted sanitize_key value.
- Around line 190-201: Update both shipment handlers, handle_shipments_status()
and handle_create_shipments(), to handle RuntimeException from
ApiClientFactory::buildConnectionUsingTokens() in addition to SendyException.
Catch \Exception (or validate the connection before proceeding) and return the
handlers’ expected JSON error response instructing the user to reconnect,
instead of allowing an HTML 500 response.
- Around line 205-210: Update the generated-status branch in CreateShipments so
it returns ready only when shipment['packages'] exists and is non-empty;
otherwise preserve the appropriate non-ready outcome and avoid persisting an
empty _sendy_packages value. Keep the existing metadata save and ready flow
unchanged for valid package data.

In `@resources/js/orders-list-print-flow.js`:
- Around line 32-44: Update the ajaxPost helper to parse non-2xx JSON responses,
extract and propagate the server-provided message when available, and retain a
sensible fallback for malformed responses. In window.sendyOrdersListPrintFlow,
surface the propagated error message to the user before the existing console
reporting, or reload through the existing notice flow so failures such as
expired nonces are explained.

In `@tests/AssetVersionTest.php`:
- Around line 23-27: Update the test method containing $expected and
Plugin::asset_version($path) to exercise debug and non-debug configurations
independently rather than deriving the expected value from the same condition.
Explicitly assert the file modification timestamp when SCRIPT_DEBUG is enabled
and Plugin::VERSION when it is disabled, restoring the original configuration
between cases.

---

Nitpick comments:
In `@lib/Modules/Orders/BulkActions.php`:
- Line 59: Update the sendy_amount fallback in the bulk-actions request handling
to use '1' instead of an empty string, matching the amount default used by
CreateShipments.php and preserving the expected value when the field is omitted.

In `@lib/Modules/Orders/CreateShipments.php`:
- Around line 46-70: Limit the order-processing loop in the shipment creation
flow to a defined maximum number of orders per request, using the existing
_sendy_shipment_id guard for already-created shipments. Update the frontend
submission flow to send the selected orders in batches and report progress
between requests, while preserving the existing error handling and created-order
tracking in create_shipment processing.

In `@lib/Modules/Orders/OrdersModule.php`:
- Around line 63-70: Update the `@throws` annotations above create_shipment() and
create_shipment_from_order() to use the fully qualified Guzzle exception class
name, matching Single::handle_create_shipment_from_form(), so PHPStan resolves
both tags to a throwable type.

In `@resources/js/admin-order-bulk.js`:
- Around line 86-112: Update the promise chain from
window.sendyOpenCreateShipmentsModal in the bulk shipment flow to add a
rejection handler after the existing then callback. Log the caught error using
the same established logging pattern as window.sendyOrdersListPrintFlow, while
preserving the current field handling and form submission behavior.

In `@resources/js/orders-list-print-flow.js`:
- Around line 55-65: Update fetchStatuses to guard the sendy-print-labels-nonce
element lookup before reading its value; when the element is absent, fail with a
clear error message instead of dereferencing null, while preserving the existing
AJAX behavior when the nonce is available.

In `@tests/doubles.php`:
- Around line 28-30: Guard the WC_Order stub declaration with
class_exists('WC_Order') so it is only defined when WooCommerce has not already
loaded the class. Preserve the existing empty stub behavior when the class is
unavailable.
- Around line 202-217: Update the response handling in send() so an empty
responses queue is handled explicitly without accessing index 0 or violating the
Response return type, and ensure the final configured response is not implicitly
reused on subsequent calls. Preserve the existing request tracking while
defining the intended behavior for both empty and exhausted response queues.

In `@tests/ShipmentsStatusTest.php`:
- Around line 198-210: Add an unauthenticated-case test alongside
test_an_api_error_is_reported_as_failed_with_a_flash_notice by deleting the
token options without calling fake_transport(), dispatching the shipment-status
request, and asserting it returns the expected JSON response rather than
propagating the RuntimeException from
ApiClientFactory::buildConnectionUsingTokens().

In `@tests/SingleCreateShipmentTest.php`:
- Around line 80-114: Update the AJAX handler invoked by dispatch so its try
block only covers validation and shipment work; keep the catch handling
failures, then move wp_send_json_success() after the catch and outside recovery.
Preserve the successful response payload, allowing dispatch to use
dispatch_ajax() without output-buffer repair.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2aa284e9-ae6c-4813-8708-2ec2f082928e

📥 Commits

Reviewing files that changed from the base of the PR and between 92b3374 and bc37e2e.

📒 Files selected for processing (28)
  • docker/provision.sh
  • languages/sendy-nl_NL.mo
  • languages/sendy-nl_NL.po
  • languages/sendy.pot
  • lib/Modules/Admin/Settings.php
  • lib/Modules/Checkout.php
  • lib/Modules/Orders/BulkActions.php
  • lib/Modules/Orders/CreateShipments.php
  • lib/Modules/Orders/OrdersModule.php
  • lib/Modules/Orders/PrintLabels.php
  • lib/Modules/Orders/RowActions.php
  • lib/Modules/Orders/Single.php
  • lib/Plugin.php
  • lib/Utils/BlocksIntegration.php
  • resources/css/order-actions.css
  • resources/js/admin-order-bulk.js
  • resources/js/orders-list-print-flow.js
  • sendy.php
  • tests/AssetVersionTest.php
  • tests/CreateShipmentsTest.php
  • tests/PrintFlowAssetsTest.php
  • tests/PrintLabelsTest.php
  • tests/RowActionsTest.php
  • tests/Sendy_Ajax_TestCase.php
  • tests/ShipmentsStatusTest.php
  • tests/SingleCreateShipmentTest.php
  • tests/bootstrap.php
  • tests/doubles.php

Comment on lines 61 to 63
foreach ($objectIds as $id) {
if (get_option('sendy_processing_method') === ProcessingMethod::WooCommerce) {
$this->create_shipment_from_order(
wc_get_order($id),
sanitize_key($_REQUEST['sendy_preference_id'] ?? ''),
sanitize_key($_REQUEST['sendy_shop_id'] ?? ''),
sanitize_key($_REQUEST['sendy_amount'] ?? ''),
);

update_option('sendy_previously_used_preference_id', sanitize_key($_REQUEST['sendy_preference_id'] ?? ''));
update_option('sendy_previously_used_amount', sanitize_key($_REQUEST['sendy_amount'] ?? ''));
} else {
$this->create_shipment_with_smart_rules(
wc_get_order($id),
false,
sanitize_key($_REQUEST['sendy_shop_id'] ?? ''),
);
}
$this->create_shipment(wc_get_order($id), $shopId, $preferenceId, (int) $amount);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Guard the resolved order and contain per-order failures in the bulk loop.

wc_get_order($id) returns false when an id does not resolve. create_shipment() declares \WC_Order $order (see lib/Modules/Orders/OrdersModule.php:65), so a false value raises a TypeError and the whole bulk request fails. The AJAX sibling handles this: lib/Modules/Orders/CreateShipments.php skips unresolved orders and catches SendyException per order. Apply the same handling here, otherwise one bad id or one API error aborts the remaining orders without a notice.

🛡️ Proposed fix
         foreach ($objectIds as $id) {
-            $this->create_shipment(wc_get_order($id), $shopId, $preferenceId, (int) $amount);
+            $order = wc_get_order($id);
+
+            if (! $order) {
+                continue;
+            }
+
+            try {
+                $this->create_shipment($order, $shopId, $preferenceId, (int) $amount);
+            } catch (SendyException $exception) {
+                // translators: %1$s contains the ID of the order, %2$s the error message
+                sendy_flash_admin_notice('error', sprintf(
+                    __('Error while creating shipment for order #%1$s: %2$s', 'sendy'),
+                    $order->get_id(),
+                    $exception->getMessage(),
+                ));
+            }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach ($objectIds as $id) {
if (get_option('sendy_processing_method') === ProcessingMethod::WooCommerce) {
$this->create_shipment_from_order(
wc_get_order($id),
sanitize_key($_REQUEST['sendy_preference_id'] ?? ''),
sanitize_key($_REQUEST['sendy_shop_id'] ?? ''),
sanitize_key($_REQUEST['sendy_amount'] ?? ''),
);
update_option('sendy_previously_used_preference_id', sanitize_key($_REQUEST['sendy_preference_id'] ?? ''));
update_option('sendy_previously_used_amount', sanitize_key($_REQUEST['sendy_amount'] ?? ''));
} else {
$this->create_shipment_with_smart_rules(
wc_get_order($id),
false,
sanitize_key($_REQUEST['sendy_shop_id'] ?? ''),
);
}
$this->create_shipment(wc_get_order($id), $shopId, $preferenceId, (int) $amount);
}
foreach ($objectIds as $id) {
$order = wc_get_order($id);
if (! $order) {
continue;
}
try {
$this->create_shipment($order, $shopId, $preferenceId, (int) $amount);
} catch (SendyException $exception) {
// translators: %1$s contains the ID of the order, %2$s the error message
sendy_flash_admin_notice('error', sprintf(
__('Error while creating shipment for order #%1$s: %2$s', 'sendy'),
$order->get_id(),
$exception->getMessage(),
));
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Modules/Orders/BulkActions.php` around lines 61 - 63, Update the bulk
loop around create_shipment() to resolve each order first, skip the iteration
when wc_get_order($id) returns false, and catch SendyException per order so
failures do not abort processing of subsequent orders. Match the
unresolved-order and per-order error handling used by CreateShipments while
preserving the existing shipment arguments and amount conversion.

Comment on lines +39 to +41
$shopId = sanitize_key($_POST['shop_id'] ?? '');
$preferenceId = sanitize_key($_POST['preference_id'] ?? '');
$amount = sanitize_key($_POST['amount'] ?? '1');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

amount reaches the API unclamped in both entry points. Both call sites cast a sanitize_key() result to int without checking the range, so '', '0' and 'abc' all produce a request for zero packages.

  • lib/Modules/Orders/CreateShipments.php#L39-L41: replace sanitize_key($_POST['amount'] ?? '1') with max(1, absint($_POST['amount'] ?? 1)) and pass the clamped value to create_shipment() and remember_previously_used().
  • lib/Modules/Orders/Single.php#L128-L133: replace (int) sanitize_key($_REQUEST['amount'] ?? '1') with max(1, absint($_REQUEST['amount'] ?? 1)).
📍 Affects 2 files
  • lib/Modules/Orders/CreateShipments.php#L39-L41 (this comment)
  • lib/Modules/Orders/Single.php#L128-L133
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Modules/Orders/CreateShipments.php` around lines 39 - 41, Clamp the
requested shipment amount to at least one package in both entry points: update
lib/Modules/Orders/CreateShipments.php lines 39-41 to derive the amount with
absint and max(1, ...), then pass that clamped value to create_shipment() and
remember_previously_used(); apply the same max(1, absint(...)) handling in
lib/Modules/Orders/Single.php lines 128-133, replacing the current casted
sanitize_key value.

Comment thread lib/Modules/Orders/CreateShipments.php
Comment on lines +205 to +210
if ($status === 'generated') {
$order->update_meta_data('_sendy_packages', $shipment['packages'] ?? []);
$order->save();

return 'ready';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

generated without packages reports ready but stores nothing printable.

If the API reports the status as generated and omits packages, this branch persists an empty array and returns ready. That contradicts the ready definition at Line 186, which requires a non-empty packages array. The flow then tries to print an order for which no package data exists.

🐛 Proposed fix
-        if ($status === 'generated') {
-            $order->update_meta_data('_sendy_packages', $shipment['packages'] ?? []);
-            $order->save();
-
-            return 'ready';
-        }
+        if ($status === 'generated' && ! empty($shipment['packages'])) {
+            $order->update_meta_data('_sendy_packages', $shipment['packages']);
+            $order->save();
+
+            return 'ready';
+        }
+
+        if ($status === 'generated') {
+            return 'pending';
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($status === 'generated') {
$order->update_meta_data('_sendy_packages', $shipment['packages'] ?? []);
$order->save();
return 'ready';
}
if ($status === 'generated' && ! empty($shipment['packages'])) {
$order->update_meta_data('_sendy_packages', $shipment['packages']);
$order->save();
return 'ready';
}
if ($status === 'generated') {
return 'pending';
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Modules/Orders/CreateShipments.php` around lines 205 - 210, Update the
generated-status branch in CreateShipments so it returns ready only when
shipment['packages'] exists and is non-empty; otherwise preserve the appropriate
non-ready outcome and avoid persisting an empty _sendy_packages value. Keep the
existing metadata save and ready flow unchanged for valid package data.

Comment thread resources/js/orders-list-print-flow.js
Comment thread tests/AssetVersionTest.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@resources/js/orders-list-print-flow.js`:
- Line 38: Update the request flow around ajaxPost to create an AbortController,
pass its signal to fetch, and abort the request after a bounded timeout. Clear
the timeout in a finally block so both successful and failed requests release
the timer and the print button cannot remain disabled indefinitely.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2be89a4c-9af3-4a5f-97e6-28006cdcaa96

📥 Commits

Reviewing files that changed from the base of the PR and between bc37e2e and f2a11f8.

📒 Files selected for processing (17)
  • languages/sendy-nl_NL.mo
  • languages/sendy-nl_NL.po
  • languages/sendy.pot
  • lib/Modules/Orders/BulkActions.php
  • lib/Modules/Orders/CreateShipments.php
  • lib/Modules/Orders/OrdersModule.php
  • lib/Modules/Orders/RowActions.php
  • lib/Modules/Orders/Single.php
  • lib/Plugin.php
  • readme.txt
  • resources/js/orders-list-print-flow.js
  • sendy.php
  • tests/BulkActionsTest.php
  • tests/CreateShipmentsTest.php
  • tests/ShipmentsStatusTest.php
  • tests/SingleCreateShipmentTest.php
  • tests/doubles.php
🚧 Files skipped from review as they are similar to previous changes (12)
  • sendy.php
  • languages/sendy-nl_NL.po
  • lib/Plugin.php
  • languages/sendy.pot
  • lib/Modules/Orders/BulkActions.php
  • lib/Modules/Orders/Single.php
  • lib/Modules/Orders/RowActions.php
  • lib/Modules/Orders/CreateShipments.php
  • tests/ShipmentsStatusTest.php
  • lib/Modules/Orders/OrdersModule.php
  • tests/doubles.php
  • tests/CreateShipmentsTest.php

body.append( 'order_ids[]', orderId );
}

const response = await fetch( ajaxurl, { method: 'POST', body } );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the polling path and locate existing print-flow test coverage.
rg -n -C 5 'POLL_TIMEOUT|fetch\(|ajaxPost|fetchStatuses|waitUntilReady' \
  resources/js/orders-list-print-flow.js
fd -t f . | rg 'PrintFlow|orders-list-print-flow|jest|vitest'

Repository: sendynl/woocommerce-plugin

Length of output: 4237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- orders-list-print-flow.js ---'
cat -n resources/js/orders-list-print-flow.js | sed -n '1,225p'

printf '%s\n' '--- browser targets and script loading ---'
rg -n -i -C 3 'browserslist|browser.?support|polyfill|orders-list-print-flow|wp_enqueue_script|script_loader_tag' \
  package.json composer.json webpack* vite* resources includes src tests 2>/dev/null || true

printf '%s\n' '--- AJAX error and button cleanup call sites ---'
rg -n -C 5 'class AjaxError|AjaxError|runPrintFlow|finally|disabled' resources/js tests

Repository: sendynl/woocommerce-plugin

Length of output: 23057


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- existing timeout pattern ---'
cat -n resources/js/print-labels.js | sed -n '1,45p'

printf '%s\n' '--- print-flow completion path ---'
cat -n resources/js/orders-list-print-flow.js | sed -n '225,365p'

printf '%s\n' '--- browser and compatibility configuration ---'
git ls-files | rg '(^|/)(package(-lock)?\.json|\.browserslistrc|babel|webpack|vite|\.github/workflows|readme|README)'
rg -n -i -C 3 'AbortController|browserslist|browser support|polyfill|compatib|wp_enqueue_script' \
  . --glob '!vendor/**' --glob '!node_modules/**' --glob '!*.min.js' 2>/dev/null || true

Repository: sendynl/woocommerce-plugin

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package and webpack configuration ---'
cat package.json
cat webpack.config.js

printf '%s\n' '--- order-list script registration ---'
cat -n lib/Modules/Orders/BulkActions.php | sed -n '85,135p'
cat -n lib/Modules/Orders/Single.php | sed -n '80,110p'

printf '%s\n' '--- standalone timeout probe ---'
node <<'JS'
const POLL_TIMEOUT = 30;
let abortCount = 0;
let cleared = false;

const controller = new AbortController();
const timeoutId = setTimeout(() => {
  abortCount += 1;
  controller.abort();
}, POLL_TIMEOUT);

const pendingFetch = new Promise((resolve, reject) => {
  controller.signal.addEventListener('abort', () => {
    reject(new DOMException('The operation was aborted.', 'AbortError'));
  });
});

pendingFetch.finally(() => {
  clearTimeout(timeoutId);
  cleared = true;
}).catch((error) => {
  console.log(JSON.stringify({
    name: error.name,
    abortCount,
    cleared,
  }));
});
JS

Repository: sendynl/woocommerce-plugin

Length of output: 5490


Add a timeout to ajaxPost.

Pass an AbortSignal to fetch() and abort it after a bounded interval. Clear the timer in finally so a stalled request cannot keep the print button disabled indefinitely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/orders-list-print-flow.js` at line 38, Update the request flow
around ajaxPost to create an AbortController, pass its signal to fetch, and
abort the request after a bounded timeout. Clear the timeout in a finally block
so both successful and failed requests release the timer and the print button
cannot remain disabled indefinitely.

@adriaanzon
adriaanzon merged commit 63dd3df into main Aug 7, 2026
10 checks passed
@adriaanzon
adriaanzon deleted the label-print-button branch August 7, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant