Update/payments 2 0#1
Draft
vladolaru wants to merge 7 commits into
Draft
Conversation
vladolaru
pushed a commit
that referenced
this pull request
Jul 6, 2026
…commerce#65439) * Add DataUtils helpers for refund preview endpoint Adds three new public methods to Refunds\DataUtils to support an upcoming refund preview endpoint (POST /wc/v4/refunds/preview): - compute_line_item_refund_total(): tax-inclusive refund total for a given line item at a requested quantity. - build_refund_preview(): returns the structured refund breakdown (products/shipping/fees with per-section subtotal/tax/total plus top-level subtotal/tax/total/max_refundable). - validate_preview_line_items(): validates a preview request against the order — checks status (REFUNDABLE_STATUSES), remaining refundable amount, line item existence, and remaining refundable quantity/total. Reuses the same tax extraction path (WC_Tax::calc_inclusive_tax) as the create endpoint to guarantee preview/create equivalence. Relaxes visibility (private -> protected) on three existing helpers (build_tax_rates_array, convert_line_item_taxes_to_internal_format, convert_proportional_taxes_to_schema_format) so the new methods and tests can reuse them. Part of WOOMOB-2684. The endpoint that consumes these helpers will follow in a separate PR. * Add changefile(s) from automation for the following project(s): woocommerce * Tighten validate_preview_line_items input validation - Reject missing/non-int/non-positive quantity with new code invalid_quantity. Previously `quantity = $line_item['quantity'] ?? 0` silently passed for missing/string/float input, then downstream consumers saw 0 or a coerced value. - Require quantity === 1 for shipping/fee items (they aren't quantity-divisible — the remaining-total branch was already not scaled by quantity). - Switch shipping/fee remaining-total math to abs()-based so legitimately negative-total fees (discount-as-fee pattern) aren't rejected as "fully refunded". - Replace the catch-all invalid_line_item code with 4 distinct codes so clients can distinguish failure modes: invalid_line_item (empty array) -> missing_line_items invalid_line_item (missing id) -> missing_line_item_id invalid_line_item (not found) -> line_item_not_found invalid_line_item (unsupported) -> unsupported_item_type Addresses review issues #1, woocommerce#3, and the lower-priority error-code split from the PR woocommerce#65334 review. * Throw on missing item in build_refund_preview Replace silent `continue` with InvalidArgumentException so callers can't get a successful-looking empty preview when a line_item_id is invalid (e.g. typo, race with delete, validation bypassed). Document precondition in the docblock: callers must invoke validate_preview_line_items() first. Addresses review issue #2. * Accumulate raw floats in build_refund_preview section sums Previously the per-section subtotal/tax/total were accumulated by casting already-formatted decimal strings back to floats via `(float) $item['subtotal']`, which loses precision and can produce a 1-cent drift between `breakdown.products.total` and the sum of `breakdown.products.items[].total` on multi-line refunds. Refactor: keep running raw-float totals per section during the per-item loop, format once at section level. Item-level strings are unchanged. Addresses review issue woocommerce#5. * Log malformed tax data and zero-quantity branches - compute_line_item_refund_total: emit a warning before returning 0.0 when a product item has zero original quantity. Indicates corrupted order data that would otherwise silently produce a $0 preview. - build_refund_preview: emit a warning when an item's taxes array is non-empty but all entries are filtered out by the is_numeric && > 0 check. Surfaces malformed tax metadata for ops without changing user-visible behavior. Both warnings use wc_get_logger() with source 'wc-v4-refunds'. Addresses review issues woocommerce#6 and woocommerce#8. * Add preconditions to compute_line_item_refund_total Guard $quantity >= 1 with an InvalidArgumentException at method entry. Document the precondition in the docblock plus a note that shipping and fee items ignore quantity, and that the return value can be negative for negative-total items (discount fees). The validator catches bad input at the request boundary; this guard protects direct callers since the method is public on an Internal\* class that may be reused by the create endpoint. Addresses review issue woocommerce#7. * Expand DataUtils unit tests, drop reflection tests Delete the two reflection-based test_build_tax_rates_array_* tests. build_tax_rates_array is exercised indirectly by test_convert_line_items_extracts_tax_automatically and test_build_refund_preview_with_tax; the project convention is to test through public interfaces (see tests/php/src/CLAUDE.md). Add 19 unit tests for the helpers introduced in this PR: - compute_line_item_refund_total: * zero-original-quantity branch returns 0.0 * shipping item (full total + tax, quantity ignored) * fee item with positive total * fee item with negative total (sign preserved) * InvalidArgumentException for quantity < 1 (data provider) - build_refund_preview: * shipping-only order (products + fees sections empty) * fee-only order (products + shipping sections empty) * mixed sections (products + shipping + fees aggregate correctly) * multi-item fractional-price aggregation (no drift between section total and sum of item totals) * InvalidArgumentException for missing line_item_id - validate_preview_line_items: * empty array -> missing_line_items * order with no remaining refund amount -> order_not_refundable * missing line_item_id key -> missing_line_item_id * cross-order line_item_id -> line_item_not_found * unsupported item type (tax line) -> unsupported_item_type * invalid quantity values (data provider) -> invalid_quantity * shipping with quantity \!= 1 -> invalid_quantity * shipping fully refunded -> order_not_refundable * negative-total fee passes validation * Fix PHPCS issues in DataUtils.php (escape exception output, align assignments) * Apply PHPCBF auto-fixes to DataUtilsTest * Remove unused @var docblock to satisfy lint * Restore @var docblock with short description for PHPStan type narrowing * Populate HTTP status data on validate_preview_line_items WP_Errors Each WP_Error now carries a per-code 'status' key in its error data, so the REST controller can map to the right HTTP status instead of flattening everything to 400: missing_line_items -> 400 Bad Request missing_line_item_id -> 400 Bad Request invalid_quantity -> 400 Bad Request line_item_not_found -> 404 Not Found order_not_refundable -> 422 Unprocessable Entity unsupported_item_type -> 422 Unprocessable Entity quantity_exceeds_refundable -> 422 Unprocessable Entity The controller-side switch to get_route_error_response_from_object landed in the endpoint PR (woocommerce#65335); this commit activates it by populating the data the helper reads. * Make per-line refund_total optional on POST /wc/v4/refunds When a client sends a line item without `refund_total`, the backend now computes the tax-inclusive total from the order line item's unit price × quantity, via the existing DataUtils::compute_line_item_refund_total() helper (introduced in PR woocommerce#65334 for the preview endpoint). This is the third v4 enhancement from the POS Refunds API spec (WOOMOB-2685). It eliminates the need for mobile POS clients to duplicate the backend's tax/rounding logic just to assemble a refund request. Changes: - DataUtils::fill_missing_refund_totals() — new helper that fills refund_total for any line item that omits it. Items that can't be resolved (missing id, item not on order, bad quantity, unsupported type) are left untouched; the existing validator surfaces the right error. - Refunds\Controller::create_item() — calls fill_missing_refund_totals after order resolution and before validation, so all downstream code (validator, converter, calculator) sees fully-populated input. - RefundSchema — removes the `default: 0` on line_items[].refund_total so "missing" is detectable downstream. Updates the field description to document the new optional behavior. Backward compatibility is preserved: clients that send refund_total explicitly continue to work unchanged, and clients that send the top-level `amount` continue to work unchanged. The existing under- refund check (amount must not be less than the line items' total) still runs against the auto-computed value. * Add tests for simplified refund creation Unit tests (DataUtilsTest) for fill_missing_refund_totals: - fills product line item from unit price × quantity - preserves explicit refund_total when present - skips items with line_item_id not on the order - skips bad quantity (data provider: null, 0, -1, "abc", 1.5) - fills shipping line item (full total, quantity ignored) - processes a mixed array (some with, some without refund_total) Integration tests (v4 refunds controller): - test_refunds_create_simplified_form_no_tax — auto-computed amount for a single product with no tax - test_refunds_create_simplified_form_with_tax — tax extraction works on the auto-computed total; per-line refund_tax is populated - test_refunds_create_simplified_matches_explicit — sending the computed refund_total explicitly produces the same amount as omitting it - test_refunds_create_mixed_with_and_without_refund_total — mixed request: one item auto-computed, one explicit - test_refunds_create_simplified_form_rejects_over_quantity — over- quantity is still rejected even when refund_total is auto-computed * Apply phpcbf auto-fixes to new tests * Fix inline comment punctuation in test_fill_missing_refund_totals_mixed * Add changefile(s) from automation for the following project(s): woocommerce * Reject missing/non-positive quantity with a specific error When a client sent {line_item_id: X} (no quantity), validate_line_items silently passed the existing checks (PHP's `int < null` evaluates to false) and the request cascaded into a misleading "Refund total must be greater than zero" error. The new method's docblock claimed "the downstream validator surfaces the right error" — until now, that claim was false for missing quantity. Add an explicit precondition in validate_line_items: quantity must be set, an integer, and >= 1. The error code stays the same (invalid_line_item) but the message names the actual problem. Also guards the existing $line_item['refund_total'] comparison with isset() so the simplified-form path (where refund_total may legitimately be absent at validation time if fill_missing_refund_totals skipped) no longer raises an undefined-index notice. Adds: - Unit test: validate_line_items rejects all bad quantity shapes (missing, 0, -1, string, float) via data provider - Integration test: POST to /wc/v4/refunds with missing quantity returns 400 invalid_line_item, not the misleading invalid_refund_amount cascade Addresses review issue #1 (the only critical) from the PR woocommerce#65439 audit. * Treat refund_total null the same as missing; document zero semantics fill_missing_refund_totals now uses array_key_exists + null check instead of isset, so a client sending refund_total: null gets the same auto-computed value as omitting the field entirely. This removes a subtle behavioural split that was undocumented and easy to break in future refactors. Explicit refund_total: 0 is left untouched as before — calculate_ refund_amount treats 0 as "no contribution to the sum" via its existing \!empty() check, which may trip the under-refund validation if the total amount is greater. The schema description now documents both behaviours explicitly. Adds: - Unit test asserting null is treated as missing - Unit test asserting explicit 0 is preserved Addresses review issue #2 from the PR woocommerce#65439 audit. * Defensive InvalidArgumentException catch in create_item fill_missing_refund_totals pre-checks quantity before calling compute_line_item_refund_total, so the latter's InvalidArgumentException should be unreachable in normal flow. If a future refactor breaks that invariant the throw would bubble as a fatal 500 with no log entry. Mirror the preview_item pattern from PR woocommerce#65335: catch InvalidArgumentException, log via wc_get_logger() with source 'wc-v4-refunds' and the order id, return 500 with code 'invalid_refund_request' and a generic user message (do not leak the exception message to clients). Addresses review issue woocommerce#3 from the PR woocommerce#65439 audit. * Sync $request['line_items'] after fill so hooks see augmented data Pre-PR behaviour populated refund_total: 0 on every line item via the schema default. With the default removed, third-party listeners on the 'woocommerce_rest_api_v4_refunds_created' hook reading $request['line_items'] would see entries without refund_total when the original client request used the simplified form — a silent semantic change. Mirror the augmented array back onto the request with $request->set_param('line_items', $line_items) after the fill, so all downstream readers (the hook payload, any future code that inspects the request) see normalised data with refund_total populated. Adds an integration test that registers a listener on the 'created' hook and asserts the captured request['line_items'] contains the auto-computed refund_total. Addresses review issue woocommerce#4 from the PR woocommerce#65439 audit. * Type-design polish: PHPDoc array shape, tax-inclusive convention comment - fill_missing_refund_totals docblock now expresses the line_items array shape as a PHPDoc generic (list<array{line_item_id?, quantity?, refund_total?, refund_tax?}>) on both @param and @return. Zero runtime cost; PHPStan now narrows the type at the single call site in Controller::create_item. - Expand the docblock prose to document the explicit-0 vs missing/null semantics introduced in the previous commit, and the tax-inclusive convention shared with compute_line_item_refund_total. - Add a code comment in Controller::create_item explaining that auto-computed and explicit refund_total values use the same (tax-inclusive) convention, so summing across mixed entries is safe. Addresses review issue woocommerce#5 (type-design wins) from the PR woocommerce#65439 audit. * Add integration tests for fee auto-compute (positive and negative) - test_refunds_create_simplified_form_fee_line: POSTs the simplified form for a positive-total fee, asserts the auto-computed amount equals the full fee total. Mirrors the existing shipping integration test (which had no equivalent for fees). - test_refunds_create_simplified_form_negative_fee: discount-as-fee scenario from the spec. Asserts that whatever the platform's behaviour for negative-fee refunds, the result is NOT a misleading invalid_refund_amount cascade. Locks the contract regardless of whether wc_create_refund accepts the negative. Addresses review issue woocommerce#6 (fee auto-compute + negative-fee end-to-end) from the PR woocommerce#65439 audit. * Lint fixes: docblock formatting + comment style * Review fixes: zero-qty source error + narrowed exception catch Address three issues from the 3-agent review: 1. CRITICAL (silent failure): when a product line on the source order has quantity=0, fill_missing_refund_totals now skips it instead of letting compute_line_item_refund_total return 0.0 silently. validate_line_items surfaces a specific 'invalid_line_item' error telling the client to provide an explicit refund_total, replacing the misleading "must be greater than zero" cascade. 2. HIGH (broad catch): \InvalidArgumentException is now caught only around the fill_missing_refund_totals call rather than the whole create_item try block, so genuine exceptions from wc_create_refund, MetaDataUtil, prepare_item_for_response, or third-party 'created' hook listeners are no longer swallowed. The order resolution check is tightened to instanceof WC_Order (rejecting WC_Order_Refund). 3. Tests: add unit + integration coverage for the zero-qty source path, a tax-inclusive store (prices_include_tax=yes) integration test, and a cross-order line_item_id integration test. Also fix mis-indented trailing comments around lines 844-865 (PHPCS). * Fix CI: stale PHPStan baseline + flaky integration tests PHPStan baseline: - Remove 2 stale entries for DataUtils::convert_line_items_to_internal_format and DataUtils::validate_line_items 'expects WC_Order, got WC_Order|WC_Order_Refund'. The previous commit's instanceof WC_Order check in create_item resolved both. Test fixes (4 pre-existing flaky tests surfacing as CI failures): - test_refunds_create_simplified_form_with_tax: replace calculate_totals(false) with explicit set_total(110.00). The former does not reliably populate the order total in the test environment, so wc_create_refund rejected the refund with 'cannot_create_refund: Invalid refund amount'. - test_refunds_create_simplified_matches_explicit + hook test: use set_regular_price() instead of set_price(). set_price() only updates the in-memory derived price, so the order created via the REST API used the WC_Helper default ($10) instead of the test's $25. - test_refunds_create_simplified_form_negative_fee: tighten assertion to reflect actual platform behavior (negative-fee refunds inevitably trip the 0 > refund_amount guard and surface invalid_refund_amount). The previous assertion claimed this code must NOT appear, which was unrealistic. * Address Copilot review: schema null, comment indentation, dupe changelog - RefundSchema: refund_total now accepts ['number', 'null'] and drops the sanitize_text_field that was stripping null values. Aligns the actual schema with the documented behavior ('omitted OR null triggers backend computation'). - Reformat four mis-aligned inline comments inside line_items arrays in the integration tests (PHPCS-correct + readable). - Remove duplicate changelog file 65439-woomob-2685-simplify-refund-creation; the woomob-2685-simplify-refund-creation entry is the canonical one. * Add changefile(s) from automation for the following project(s): woocommerce * Preserve legacy explicit-refund_total path without quantity The PR's strict quantity check in validate_line_items was meant to guard the new auto-compute path but unintentionally rejected the legacy v3-style request shape `{line_item_id, refund_total}` (no quantity) — which POS clients will rely on when the v4 features port to v3. Backend: - validate_line_items: gate the positive-integer quantity check on refund_total being absent. When refund_total is provided explicitly, quantity stays optional / informational (legacy behavior). - Gate the over-quantity check on quantity being set. Schema: - Drop `default: 0` on quantity (removes the misleading default — the field is genuinely optional now, conditional on refund_total). - Document the conditional requirement in the field description. Tests: - New integration test: POST /wc/v4/refunds with {line_item_id, refund_total: 50} and no quantity returns 201. - New unit test: validate_line_items accepts missing/zero quantity when refund_total is provided. * Add changefile(s) from automation for the following project(s): woocommerce * Fix silent-failure paths: line-item drop + explicit-zero in calculate Address two silent-failure paths surfaced by the re-review hunter: B (CRITICAL) — Legacy {line_item_id, refund_total} (no quantity) was silently dropped from the refund record. The validator accepted the shape but convert_line_items_to_internal_format required all three keys, so the line was skipped and wc_create_refund got an empty line_items array — the refund had the right dollar amount but no line attribution, leaving per-unit accounting broken for that line. Fix: relax the converter to require line_item_id + (quantity OR refund_total) and default qty=0 when quantity is missing. Matches v3 semantics ("refunded $X of this line without consuming specific units"). Dollar accounting via get_remaining_refund_amount still gates over-refunding regardless of the per-unit looseness. A (HIGH) — calculate_refund_amount used !empty() which treats an explicit refund_total of 0 as missing, silently dropping the line from the sum. Replace with isset() + is_numeric() so the explicit- zero contract documented in the schema is honored. Tests added/augmented: - test_refunds_create_legacy_form_no_quantity_with_explicit_refund_total now asserts the refund line item is attached (count=1) with qty=0 and a -$30 total, AND that a follow-up simplified-form refund exceeding remaining dollars is correctly rejected by wc_create_refund. - test_refunds_create_legacy_form_api_restock_does_not_restock pins the no-restock semantic: qty=0 means no units to add back, so api_restock=true is a no-op for legacy refunds. - test_calculate_refund_amount_includes_explicit_zero (unit) — regression guard against the !empty() reintroduction. - test_convert_line_items_legacy_no_quantity_defaults_qty_zero (unit) — pins the converter's new behavior. Also: guard tax extraction in the converter with isset(refund_total) to avoid silently coercing missing refund_total to 0 via float cast. * Close the test-analyzer follow-ups - Add test_refunds_create_simplified_matches_explicit_tax_inclusive: simplified-vs-explicit equivalence on a tax-inclusive store (10% / $110). The previous equivalence test used an untaxed product, so a regression that yielded a tax-exclusive auto-computed value would have gone undetected. The new test asserts both top-level `amount` and per-line refund_total / refund_tax round-trip identically between the two paths. - Add test_refunds_create_invariant_violation_returns_500: partial-mock DataUtils so fill_missing_refund_totals throws InvalidArgumentException, inject into the DI-resolved RefundsController, dispatch a refund POST, and assert 500 + invalid_refund_request. Pins the scoped catch's response shape so a future refactor that broadens the catch (e.g. to \Throwable) or re-narrows fill's pre-check is caught. - Tighten test_refunds_create_simplified_form_negative_fee: replace the permissive assertLessThan(500, status) + assertArrayHasKey('code') with explicit assertEquals(400) + assertEquals('invalid_refund_amount'). Pins the current platform behavior (the controller's `0 > $refund_amount` guard fires for any negative auto-computed total) so a behavior change is loud rather than silent. * Address 3rd-pass review findings (test-side) Test-analyzer findings, in priority order: (#1, merge-blocker) Option leakage — two tests mutated woocommerce_calc_taxes / woocommerce_prices_include_tax without restoration. tearDown does not reset these, so any test running after them in the same process would see polluted state. Wrap both in try/finally that captures and restores the original values, mirroring the pattern already used in test_refunds_create_simplified_form_tax_inclusive_store. (woocommerce#4) test_refunds_create_simplified_matches_explicit_tax_inclusive was misnamed — it set prices_include_tax = no. Fix it to actually exercise a tax-inclusive store (set to yes; product entered with tax baked into regular_price). The name now matches reality. (#2) Augment test_refunds_create_legacy_form_no_quantity_with_explicit_refund_total with a Step 3: after the $30 initial refund and the rejected $100 follow-up, refund $40 — this MUST succeed and bring total_refunded to $70 / remaining $30. Guards against a regression where the first refund silently consumed the full $100 budget. (woocommerce#3) New test_refunds_create_legacy_form_tax_inclusive_store — exercises the converter's tax-extraction block on the legacy {line_item_id, refund_total} (no quantity) path under tax-inclusive prices. This combination is the one POS clients will actually exercise after the v3 port, but no other test reaches it. (woocommerce#5) New test_refunds_create_three_way_mixed_shapes — single create call with auto-compute + explicit-with-quantity + legacy-no-quantity entries. Verifies the controller's fill/convert pass handles all three shapes coexisting, and asserts each refund line item is attached with the expected qty (refunds store quantities negative, so request qty=1 lands as -1; legacy no-quantity lands as 0). (woocommerce#6) New test_refunds_create_hook_sees_explicit_refund_total_unchanged — pins that the request-mirroring step (set_param after fill_missing_refund_totals) does not overwrite client-supplied refund_total. Uses a deliberately different value from the auto-computed one so an overwrite regression would surface. * Address review comments CodeRabbit + codex review findings closed: (A) Per-line cap for partially-refunded shipping/fees in validate_preview_line_items. Previously only fully-refunded lines were rejected, so a $10 shipping line partially refunded by $5 would pass validation and let build_refund_preview return $10 even though only $5 was refundable. The new check computes the requested refund via compute_line_item_refund_total() and rejects with 'quantity_exceeds_refundable' (422) when it exceeds remaining. (B) Preserve the signed tax split for negative-tax discount fees. The tax-extraction filter in build_refund_preview previously dropped tax IDs where `amount > 0`, so a -$10 fee with -$1 stored tax previewed as subtotal -$11 / tax $0 — losing the breakdown. Switch the filter to `amount != 0` so non-zero (positive or negative) tax amounts are kept and WC_Tax::calc_inclusive_tax propagates the sign correctly. (C) @SInCE 10.8.0 → @SInCE 10.9.0 on all new public methods, and add @SInCE 10.9.0 to the newly-protected helpers (convert_line_item_taxes_to_internal_format, convert_proportional_taxes_to_schema_format, build_tax_rates_array). Per .ai/skills/woocommerce-backend-dev/code-entities.md, @SInCE is moved to the last line of each docblock. (D) Remove the duplicate changelog file 65334-woomob-2684-refund-preview-helpers — the unprefixed woomob-2684-refund-preview-helpers entry is the canonical one. (E) Fix the @testdox on test_validate_preview_line_items_shipping_fully_refunded to match the assertion (order_not_refundable). The order-level remaining-amount guard fires first when shipping is fully refunded. Tests added: - test_validate_preview_line_items_shipping_partial_remaining — pins the new per-line cap. Order has a $10 shipping line + $50 product; $5 of shipping is pre-refunded; previewing shipping at qty=1 must return quantity_exceeds_refundable rather than passing through to an oversized total. - test_build_refund_preview_negative_fee_with_negative_tax — pins the negative-tax breakdown fix. -$10 fee + -$1 stored tax must preview as subtotal -$10 / tax -$1 / total -$11. * Add changefile(s) from automation for the following project(s): woocommerce * Address review comments Codex findings closed: (#1, CRITICAL) Cap simplified-form refunds against the line's REMAINING refundable quantity rather than the original. validate_line_items previously compared $line_item['quantity'] to $item->get_quantity() (the unchanged original count), so once item A was refunded for the first time, a second simplified {line_item_id: A, quantity: 1} request would pass the per-line check and only be bounded by the order's dollar balance — if item B still had room, item A would be silently refunded twice. Hoist compute_refunded_quantities_and_totals outside the loop and cap product items by $item->get_quantity() + ($refund_data['qtys'][id] ?? 0), matching the pattern already used in validate_preview_line_items. Shipping/ fees keep the original simpler check (they don't track per-unit refund history). (#2, HIGH) Reject the ambiguous "omitted refund_total + explicit refund_tax" combination. fill_missing_refund_totals would have written a tax-inclusive refund_total (110 for a $100 item with $10 tax) and convert_line_items_to_internal_format would then skip tax extraction because refund_tax was already present — calculate_refund_amount summed both and emitted amount=120 (overstated by the tax amount). fill_missing_refund_totals now skips items with explicit refund_tax, and validate_line_items rejects the combination with 'invalid_line_item: refund_tax cannot be combined with auto-computed refund_total'. (woocommerce#3) Remove the duplicate changelog file 65439-woomob-2685-simplify-refund-creation — the unprefixed woomob-2685-simplify-refund-creation entry is the canonical one. (woocommerce#4) @SInCE 10.8.0 → @SInCE 10.9.0 on fill_missing_refund_totals; @SInCE moved to the last docblock line per .ai/skills/woocommerce-backend-dev/code-entities.md. The other @SInCE annotations on this file belong to methods introduced by woocommerce#65334 and were already updated on that branch (644f7c0); they'll re-flow to this branch via merge. Tests added: - test_refunds_create_simplified_form_rejects_already_refunded_product pins the new remaining-qty cap. Two-line order (A + B, each $50); refund A once → 201; refund A again → 400 invalid_line_item with "remaining refundable quantity" in the message. - test_refunds_create_rejects_auto_compute_with_explicit_refund_tax pins the rejection. $100 product with $10 stored tax; request with no refund_total + explicit refund_tax → 400 invalid_line_item with "refund_tax cannot be combined" in the message. Wrapped in try/finally for tax-option restoration. * Add changefile(s) from automation for the following project(s): woocommerce * Address review comments on validate_preview_line_items - Compare shipping/fee refunds on a tax-inclusive basis. The previous code pitted the tax-inclusive $requested_total against a tax-exclusive $remaining_total — for a $10 shipping line with $1.50 tax, the comparison was 11.50 > 10.00 and rejected a legitimate full refund. compute_refunded_quantities_and_totals() now records fee/shipping totals tax-inclusive so the validator can compare like-for-like. - Replace the hardcoded `'status' => 422` literals in validate_preview_line_items with WP_Http::UNPROCESSABLE_ENTITY, matching the convention used elsewhere in V4 routes. - Add a regression test (shipping line with tax, no prior refund) to lock in the tax-inclusive comparison. - Delete the duplicate changelog entry; keep the PR-prefixed one auto- generated by CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Align tax filter in convert_line_items_to_internal_format with preview side The creation-side filter dropped tax IDs whose stored amount was <= 0, so a negative-fee discount line (e.g. a -$10 fee with -$1 stored tax) had its tax breakdown stripped on save — refund_total stayed at -$11 and refund_tax was emitted as []. The preview side (build_refund_preview) keeps any non-zero stored tax and renders the signed split correctly, so a refund moving from preview to create lost the tax breakdown. Match the preview rule (non-zero, not strictly positive). Add a regression test that converts a negative fee with negative stored tax and verifies the signed split survives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Remove duplicate slug-only changelog entry The CI auto-added 65439-woomob-2685-simplify-refund-creation with a more detailed body. Keep the PR-prefixed file as the canonical entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add parity test: create amount matches build_refund_preview total Regression guard against the create vs preview drift mikejolley flagged on PRs woocommerce#65334 and woocommerce#65335. Builds an order with a product carrying 10% tax, calls build_refund_preview directly to capture the authoritative grand total, then posts the same line items (quantity only) to the create endpoint. The resulting refund amount must equal preview total exactly. A future change that subtly diverges create's auto compute from the preview calculation would fail this assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Lock fee/shipping quantity-ignored behavior in compute_line_item_refund_total The existing tests for shipping and positive fees pass quantity = 1, which does not prove the quantity argument is ignored. Add two short tests that pass quantity = 5 and assert the same line total + tax is returned. Catches a future refactor that wrongly applies unit_price * quantity to shipping or fee items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Improve `refund_total` description text Co-authored-by: Mike Jolley <mike.jolley@me.com> * Add multi-quantity rounding tests and fix refund total comparison * Fix duplicate refunds for fees and shipping * Remove refunds PHPStan baseline suppressions --------- Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Mike Jolley <mike.jolley@me.com>
vladolaru
pushed a commit
that referenced
this pull request
Jul 6, 2026
…5645) * Add DataUtils helpers for refund preview endpoint Adds three new public methods to Refunds\DataUtils to support an upcoming refund preview endpoint (POST /wc/v4/refunds/preview): - compute_line_item_refund_total(): tax-inclusive refund total for a given line item at a requested quantity. - build_refund_preview(): returns the structured refund breakdown (products/shipping/fees with per-section subtotal/tax/total plus top-level subtotal/tax/total/max_refundable). - validate_preview_line_items(): validates a preview request against the order — checks status (REFUNDABLE_STATUSES), remaining refundable amount, line item existence, and remaining refundable quantity/total. Reuses the same tax extraction path (WC_Tax::calc_inclusive_tax) as the create endpoint to guarantee preview/create equivalence. Relaxes visibility (private -> protected) on three existing helpers (build_tax_rates_array, convert_line_item_taxes_to_internal_format, convert_proportional_taxes_to_schema_format) so the new methods and tests can reuse them. Part of WOOMOB-2684. The endpoint that consumes these helpers will follow in a separate PR. * Add refund preview endpoint (POST /wc/v4/refunds/preview) Wires the v4 refund preview endpoint that returns authoritative totals/breakdowns for a proposed refund without writing any data. Request: - order_id + line_items[].line_item_id/quantity Response: - breakdown.{products, shipping, fees} (items + subtotal/tax/total) - top-level subtotal, tax, total, max_refundable The calculation, validation, and tax extraction live in Refunds\DataUtils (added in the helpers PR) — this PR is the HTTP surface: route registration, schema, controller handler, and integration tests. Key behaviors: - Read-only: no refund record, no stock reservation, no writes - Enforces REFUNDABLE_STATUSES order gate via the validation helper - Uses the same tax-extraction path as the create endpoint (WC_Tax::calc_inclusive_tax) to guarantee preview/create equivalence - Returns standard WP_Error responses (invalid_line_item, quantity_exceeds_refundable, order_not_refundable) - Gated behind the existing rest-api-v4 feature flag Part of WOOMOB-2684. Depends on the DataUtils helpers PR. * Add changefile(s) from automation for the following project(s): woocommerce * Tighten validate_preview_line_items input validation - Reject missing/non-int/non-positive quantity with new code invalid_quantity. Previously `quantity = $line_item['quantity'] ?? 0` silently passed for missing/string/float input, then downstream consumers saw 0 or a coerced value. - Require quantity === 1 for shipping/fee items (they aren't quantity-divisible — the remaining-total branch was already not scaled by quantity). - Switch shipping/fee remaining-total math to abs()-based so legitimately negative-total fees (discount-as-fee pattern) aren't rejected as "fully refunded". - Replace the catch-all invalid_line_item code with 4 distinct codes so clients can distinguish failure modes: invalid_line_item (empty array) -> missing_line_items invalid_line_item (missing id) -> missing_line_item_id invalid_line_item (not found) -> line_item_not_found invalid_line_item (unsupported) -> unsupported_item_type Addresses review issues #1, #3, and the lower-priority error-code split from the PR #65334 review. * Throw on missing item in build_refund_preview Replace silent `continue` with InvalidArgumentException so callers can't get a successful-looking empty preview when a line_item_id is invalid (e.g. typo, race with delete, validation bypassed). Document precondition in the docblock: callers must invoke validate_preview_line_items() first. Addresses review issue #2. * Accumulate raw floats in build_refund_preview section sums Previously the per-section subtotal/tax/total were accumulated by casting already-formatted decimal strings back to floats via `(float) $item['subtotal']`, which loses precision and can produce a 1-cent drift between `breakdown.products.total` and the sum of `breakdown.products.items[].total` on multi-line refunds. Refactor: keep running raw-float totals per section during the per-item loop, format once at section level. Item-level strings are unchanged. Addresses review issue #5. * Log malformed tax data and zero-quantity branches - compute_line_item_refund_total: emit a warning before returning 0.0 when a product item has zero original quantity. Indicates corrupted order data that would otherwise silently produce a $0 preview. - build_refund_preview: emit a warning when an item's taxes array is non-empty but all entries are filtered out by the is_numeric && > 0 check. Surfaces malformed tax metadata for ops without changing user-visible behavior. Both warnings use wc_get_logger() with source 'wc-v4-refunds'. Addresses review issues #6 and #8. * Add preconditions to compute_line_item_refund_total Guard $quantity >= 1 with an InvalidArgumentException at method entry. Document the precondition in the docblock plus a note that shipping and fee items ignore quantity, and that the return value can be negative for negative-total items (discount fees). The validator catches bad input at the request boundary; this guard protects direct callers since the method is public on an Internal\* class that may be reused by the create endpoint. Addresses review issue #7. * Expand DataUtils unit tests, drop reflection tests Delete the two reflection-based test_build_tax_rates_array_* tests. build_tax_rates_array is exercised indirectly by test_convert_line_items_extracts_tax_automatically and test_build_refund_preview_with_tax; the project convention is to test through public interfaces (see tests/php/src/CLAUDE.md). Add 19 unit tests for the helpers introduced in this PR: - compute_line_item_refund_total: * zero-original-quantity branch returns 0.0 * shipping item (full total + tax, quantity ignored) * fee item with positive total * fee item with negative total (sign preserved) * InvalidArgumentException for quantity < 1 (data provider) - build_refund_preview: * shipping-only order (products + fees sections empty) * fee-only order (products + shipping sections empty) * mixed sections (products + shipping + fees aggregate correctly) * multi-item fractional-price aggregation (no drift between section total and sum of item totals) * InvalidArgumentException for missing line_item_id - validate_preview_line_items: * empty array -> missing_line_items * order with no remaining refund amount -> order_not_refundable * missing line_item_id key -> missing_line_item_id * cross-order line_item_id -> line_item_not_found * unsupported item type (tax line) -> unsupported_item_type * invalid quantity values (data provider) -> invalid_quantity * shipping with quantity \!= 1 -> invalid_quantity * shipping fully refunded -> order_not_refundable * negative-total fee passes validation * Fix PHPCS issues in DataUtils.php (escape exception output, align assignments) * Apply PHPCBF auto-fixes to DataUtilsTest * Remove unused @var docblock to satisfy lint * Catch InvalidArgumentException in preview_item; add integration tests Controller: - preview_item now wraps build_refund_preview() in a try/catch for \InvalidArgumentException. The validator above should have rejected bad input, so any throw here is an invariant violation — surface as invalid_preview_request rather than letting it bubble as a fatal. Integration tests: - Update test_preview_invalid_line_item assertion to the new line_item_not_found error code (was the catch-all invalid_line_item). - Tighten test_preview_empty_line_items to assert missing_line_items. - Add test_preview_invalid_quantity_zero (asserts invalid_quantity). - Add test_preview_shipping_line (shipping-only order, breakdown.shipping). - Add test_preview_fee_line (fee-only order, breakdown.fees). - Add test_preview_mixed_sections (products + shipping + fees aggregate). - Add create_order_with_shipping / create_order_with_fee helpers. * Apply PHPCBF auto-fixes to PR2 files * Restore @var docblock with short description for PHPStan type narrowing * Add changefile(s) from automation for the following project(s): woocommerce * Tighten preview endpoint request validation - Add validate_callback to the top-level line_items arg so REST framework runs rest_validate_value_from_schema on the nested items[] (previously inert: the nested validate_callback / sanitize_callback keys on items.properties are only honored at the top level). - Drop absint sanitize_callbacks from order_id, line_item_id, quantity. absint silently rewrites negative or float inputs; use 'minimum' => 1 with rest_validate_request_arg to reject invalid input at the framework boundary. - Add 'minItems' => 1 to line_items so an empty array is rejected by the REST framework instead of bouncing through DataUtils. - Add 'additionalProperties' => false and explicit 'required' list to the items schema for stricter shape enforcement. - Reject non-shop_order post types (e.g. shop_subscription) in preview_item — previously only WC_Order_Refund was rejected, so a subscription ID would pass through and either compute nonsense totals or trigger an opaque InvalidArgumentException. - Add a code comment justifying create_item_permissions_check on a read-only endpoint (preview is part of refund-create flow). Addresses review issues #1 (line_items validation), #3 (order type gate), and minor #6 (absint masking) / #8 (minItems) from the PR #65335 audit. * Preserve WP_Error status and broaden preview_item catch list Validation errors: Switch from get_route_error_response (hardcoded 400) to get_route_error_response_from_object, reading the status data from the WP_Error so per-code statuses are honored (line_item_not_found can be 404, order_not_refundable can be 422, etc.). When the WP_Error has no status data, defaults to 400 — same behavior as before. The DataUtils-side change to populate status data per code lands in the helpers PR. Exception handling: Match create_item's catch list: - \WC_Data_Exception and \WC_REST_Exception now caught with the exception's own error code preserved. - \InvalidArgumentException now logged via wc_get_logger() with source 'wc-v4-refunds' + order_id context, returns HTTP 500 (was 400) since the code comment already identified this as a server-side invariant violation. The exception message is no longer leaked to the client — generic message returned instead. - Final \Throwable arm catches anything else (PHP TypeError, RuntimeException, etc.), logs it, returns 500 with code unexpected_preview_error. Addresses review issues #2 (status preservation), #5 (InvalidArgumentException as 500), #6 (broader catch list) from the PR #65335 audit. * Schema: split product/base item shapes; throw on unused stub - Split the single $item_schema (which advertised product_id and variation_id on every section) into get_base_item_schema() (id/name/quantity/subtotal/tax/total) and get_product_item_schema() (extends base with product_id/variation_id). The breakdown.products section uses the product variant; shipping and fees use the base variant. The public schema document now accurately reflects which fields appear in which section. - get_item_response() now throws \LogicException instead of being a silent no-op stub. The preview controller bypasses prepare_item_for_response and returns the data array directly, so this method should never be invoked. Throwing surfaces any accidental future call site immediately rather than silently returning incomplete or wrong-shaped data. Addresses review issues #4 (schema declares product fields on shipping/fees) and #7 (silent get_item_response stub). * Add integration tests for new validation, type gate, and invariant catch - test_preview_invalid_quantity (data provider) replaces the single test_preview_invalid_quantity_zero. Covers zero, negative, missing key, string, and float. Accepts either rest_invalid_param (when the REST framework rejects pre-handler via the new minimum/type constraints) or invalid_quantity (when DataUtils rejects), so the test documents the actual HTTP-level behavior without coupling to which layer rejects first. - test_preview_invalid_payload_shape — POSTs a malformed object (string line_item_id / quantity) and asserts rest_invalid_param. Locks the new rest_validate_request_arg on line_items. - test_preview_non_shop_order_returns_invalid_id — passes a refund ID to the preview endpoint and asserts 404. Locks the shop_order type gate. - test_preview_read_only_user_returns_forbidden — creates a customer-role user and asserts 401/403. Locks the create_item_permissions_check gate against accidental loosening. - test_preview_invariant_violation_returns_500 — replaces the controller's DataUtils dependency with an anonymous stub that validates true but throws on build. Asserts 500 with code invalid_preview_request. Locks the controller's InvalidArgumentException catch arm, which is otherwise dead code in normal flow. * Add schema/response parity test for refund preview test_schema_matches_response_shape walks the declared RefundPreviewSchema properties recursively against an actual preview response built from a mixed (product + shipping + fee) order. Asserts that every object/array section declared in the schema is present in the response, and every key in the response is declared in the schema. Catches future drift where new keys are added to build_refund_preview() but not to the schema (or vice versa) — which would silently mislead clients reading the autodoc at /wp-json/wc/v4/refunds/preview. * Tighten preview integration tests (quality nits) - Replace assertSame(array(), ...) with assertEmpty() in shipping and fee section assertions. The empty-array check still holds but is no longer brittle to a future schema change that might return null or omit the key. - test_preview_matches_create (P19): derive create's refund_total from $preview_data['total'] instead of hardcoding 110.00. A divergence between preview and create now produces an actual mismatch rather than passing by coincidence — which was the whole point of this regression guard. - Replace hardcoded 999999 invalid-id with $existing_item_id + 999 so the test is principled rather than statistically safe. - Move wp_insert_user from per-test setUp to setUpBeforeClass (using self::$user_id). Saves ~25 user inserts per run. * Populate HTTP status data on validate_preview_line_items WP_Errors Each WP_Error now carries a per-code 'status' key in its error data, so the REST controller can map to the right HTTP status instead of flattening everything to 400: missing_line_items -> 400 Bad Request missing_line_item_id -> 400 Bad Request invalid_quantity -> 400 Bad Request line_item_not_found -> 404 Not Found order_not_refundable -> 422 Unprocessable Entity unsupported_item_type -> 422 Unprocessable Entity quantity_exceeds_refundable -> 422 Unprocessable Entity The controller-side switch to get_route_error_response_from_object landed in the endpoint PR (#65335); this commit activates it by populating the data the helper reads. * Fix PHPCS warnings in PR2 changes * More PHPCS fixes: docblocks, elseif, ignore comments * Move phpcs:disable outside docblock so it applies * Make per-line refund_total optional on POST /wc/v4/refunds When a client sends a line item without `refund_total`, the backend now computes the tax-inclusive total from the order line item's unit price × quantity, via the existing DataUtils::compute_line_item_refund_total() helper (introduced in PR #65334 for the preview endpoint). This is the third v4 enhancement from the POS Refunds API spec (WOOMOB-2685). It eliminates the need for mobile POS clients to duplicate the backend's tax/rounding logic just to assemble a refund request. Changes: - DataUtils::fill_missing_refund_totals() — new helper that fills refund_total for any line item that omits it. Items that can't be resolved (missing id, item not on order, bad quantity, unsupported type) are left untouched; the existing validator surfaces the right error. - Refunds\Controller::create_item() — calls fill_missing_refund_totals after order resolution and before validation, so all downstream code (validator, converter, calculator) sees fully-populated input. - RefundSchema — removes the `default: 0` on line_items[].refund_total so "missing" is detectable downstream. Updates the field description to document the new optional behavior. Backward compatibility is preserved: clients that send refund_total explicitly continue to work unchanged, and clients that send the top-level `amount` continue to work unchanged. The existing under- refund check (amount must not be less than the line items' total) still runs against the auto-computed value. * Add tests for simplified refund creation Unit tests (DataUtilsTest) for fill_missing_refund_totals: - fills product line item from unit price × quantity - preserves explicit refund_total when present - skips items with line_item_id not on the order - skips bad quantity (data provider: null, 0, -1, "abc", 1.5) - fills shipping line item (full total, quantity ignored) - processes a mixed array (some with, some without refund_total) Integration tests (v4 refunds controller): - test_refunds_create_simplified_form_no_tax — auto-computed amount for a single product with no tax - test_refunds_create_simplified_form_with_tax — tax extraction works on the auto-computed total; per-line refund_tax is populated - test_refunds_create_simplified_matches_explicit — sending the computed refund_total explicitly produces the same amount as omitting it - test_refunds_create_mixed_with_and_without_refund_total — mixed request: one item auto-computed, one explicit - test_refunds_create_simplified_form_rejects_over_quantity — over- quantity is still rejected even when refund_total is auto-computed * Apply phpcbf auto-fixes to new tests * Fix inline comment punctuation in test_fill_missing_refund_totals_mixed * Add changefile(s) from automation for the following project(s): woocommerce * Reject missing/non-positive quantity with a specific error When a client sent {line_item_id: X} (no quantity), validate_line_items silently passed the existing checks (PHP's `int < null` evaluates to false) and the request cascaded into a misleading "Refund total must be greater than zero" error. The new method's docblock claimed "the downstream validator surfaces the right error" — until now, that claim was false for missing quantity. Add an explicit precondition in validate_line_items: quantity must be set, an integer, and >= 1. The error code stays the same (invalid_line_item) but the message names the actual problem. Also guards the existing $line_item['refund_total'] comparison with isset() so the simplified-form path (where refund_total may legitimately be absent at validation time if fill_missing_refund_totals skipped) no longer raises an undefined-index notice. Adds: - Unit test: validate_line_items rejects all bad quantity shapes (missing, 0, -1, string, float) via data provider - Integration test: POST to /wc/v4/refunds with missing quantity returns 400 invalid_line_item, not the misleading invalid_refund_amount cascade Addresses review issue #1 (the only critical) from the PR #65439 audit. * Treat refund_total null the same as missing; document zero semantics fill_missing_refund_totals now uses array_key_exists + null check instead of isset, so a client sending refund_total: null gets the same auto-computed value as omitting the field entirely. This removes a subtle behavioural split that was undocumented and easy to break in future refactors. Explicit refund_total: 0 is left untouched as before — calculate_ refund_amount treats 0 as "no contribution to the sum" via its existing \!empty() check, which may trip the under-refund validation if the total amount is greater. The schema description now documents both behaviours explicitly. Adds: - Unit test asserting null is treated as missing - Unit test asserting explicit 0 is preserved Addresses review issue #2 from the PR #65439 audit. * Defensive InvalidArgumentException catch in create_item fill_missing_refund_totals pre-checks quantity before calling compute_line_item_refund_total, so the latter's InvalidArgumentException should be unreachable in normal flow. If a future refactor breaks that invariant the throw would bubble as a fatal 500 with no log entry. Mirror the preview_item pattern from PR #65335: catch InvalidArgumentException, log via wc_get_logger() with source 'wc-v4-refunds' and the order id, return 500 with code 'invalid_refund_request' and a generic user message (do not leak the exception message to clients). Addresses review issue #3 from the PR #65439 audit. * Sync $request['line_items'] after fill so hooks see augmented data Pre-PR behaviour populated refund_total: 0 on every line item via the schema default. With the default removed, third-party listeners on the 'woocommerce_rest_api_v4_refunds_created' hook reading $request['line_items'] would see entries without refund_total when the original client request used the simplified form — a silent semantic change. Mirror the augmented array back onto the request with $request->set_param('line_items', $line_items) after the fill, so all downstream readers (the hook payload, any future code that inspects the request) see normalised data with refund_total populated. Adds an integration test that registers a listener on the 'created' hook and asserts the captured request['line_items'] contains the auto-computed refund_total. Addresses review issue #4 from the PR #65439 audit. * Type-design polish: PHPDoc array shape, tax-inclusive convention comment - fill_missing_refund_totals docblock now expresses the line_items array shape as a PHPDoc generic (list<array{line_item_id?, quantity?, refund_total?, refund_tax?}>) on both @param and @return. Zero runtime cost; PHPStan now narrows the type at the single call site in Controller::create_item. - Expand the docblock prose to document the explicit-0 vs missing/null semantics introduced in the previous commit, and the tax-inclusive convention shared with compute_line_item_refund_total. - Add a code comment in Controller::create_item explaining that auto-computed and explicit refund_total values use the same (tax-inclusive) convention, so summing across mixed entries is safe. Addresses review issue #5 (type-design wins) from the PR #65439 audit. * Add integration tests for fee auto-compute (positive and negative) - test_refunds_create_simplified_form_fee_line: POSTs the simplified form for a positive-total fee, asserts the auto-computed amount equals the full fee total. Mirrors the existing shipping integration test (which had no equivalent for fees). - test_refunds_create_simplified_form_negative_fee: discount-as-fee scenario from the spec. Asserts that whatever the platform's behaviour for negative-fee refunds, the result is NOT a misleading invalid_refund_amount cascade. Locks the contract regardless of whether wc_create_refund accepts the negative. Addresses review issue #6 (fee auto-compute + negative-fee end-to-end) from the PR #65439 audit. * Lint fixes: docblock formatting + comment style * Review fixes: zero-qty source error + narrowed exception catch Address three issues from the 3-agent review: 1. CRITICAL (silent failure): when a product line on the source order has quantity=0, fill_missing_refund_totals now skips it instead of letting compute_line_item_refund_total return 0.0 silently. validate_line_items surfaces a specific 'invalid_line_item' error telling the client to provide an explicit refund_total, replacing the misleading "must be greater than zero" cascade. 2. HIGH (broad catch): \InvalidArgumentException is now caught only around the fill_missing_refund_totals call rather than the whole create_item try block, so genuine exceptions from wc_create_refund, MetaDataUtil, prepare_item_for_response, or third-party 'created' hook listeners are no longer swallowed. The order resolution check is tightened to instanceof WC_Order (rejecting WC_Order_Refund). 3. Tests: add unit + integration coverage for the zero-qty source path, a tax-inclusive store (prices_include_tax=yes) integration test, and a cross-order line_item_id integration test. Also fix mis-indented trailing comments around lines 844-865 (PHPCS). * Fix CI: PHPStan errors + stale baseline + preview test status codes PHPStan code fixes: - Add use WC_Order_Refund to RefundSchema so docblock @param resolves to the global WC_Order_Refund instead of the unknown Schema\WC_Order_Refund. - Add use WC_Order to Controller and tighten preview_item's order check to instanceof WC_Order, so the WC_Order_Refund branch from wc_get_order is rejected with INVALID_ID instead of leaking into validate_preview_line_items / build_refund_preview (both expect WC_Order). - Remove dead WC_Data_Exception and WC_REST_Exception catches around build_refund_preview; only InvalidArgumentException and Throwable can fire. - Add a @phpstan-param annotation on RefundPreviewSchema::get_item_response to satisfy missingType.generics without breaking PHPCS (the latter rejects generics in @param). PHPStan baseline cleanup: - Remove 14 stale entries: 4 referencing the long-renamed Refunds\OrderSchema, 10 referencing Refunds\Schema\WC_Order_Refund that became stale once the WC_Order_Refund use was added to RefundSchema. Preview test status codes: - validate_preview_line_items emits status: 422 for order_not_refundable / quantity_exceeds_refundable / unsupported_item_type and status: 404 for line_item_not_found, but five tests still asserted 400. Align them. - test_preview_empty_line_items: the schema-level minItems check fires before the controller runs, so the response carries rest_invalid_param (not missing_line_items). Update the assertion and document why. * Fix CI: stale PHPStan baseline + flaky integration tests PHPStan baseline: - Remove 2 stale entries for DataUtils::convert_line_items_to_internal_format and DataUtils::validate_line_items 'expects WC_Order, got WC_Order|WC_Order_Refund'. The previous commit's instanceof WC_Order check in create_item resolved both. Test fixes (4 pre-existing flaky tests surfacing as CI failures): - test_refunds_create_simplified_form_with_tax: replace calculate_totals(false) with explicit set_total(110.00). The former does not reliably populate the order total in the test environment, so wc_create_refund rejected the refund with 'cannot_create_refund: Invalid refund amount'. - test_refunds_create_simplified_matches_explicit + hook test: use set_regular_price() instead of set_price(). set_price() only updates the in-memory derived price, so the order created via the REST API used the WC_Helper default ($10) instead of the test's $25. - test_refunds_create_simplified_form_negative_fee: tighten assertion to reflect actual platform behavior (negative-fee refunds inevitably trip the 0 > refund_amount guard and surface invalid_refund_amount). The previous assertion claimed this code must NOT appear, which was unrealistic. * Address Copilot review: schema null, comment indentation, dupe changelog - RefundSchema: refund_total now accepts ['number', 'null'] and drops the sanitize_text_field that was stripping null values. Aligns the actual schema with the documented behavior ('omitted OR null triggers backend computation'). - Reformat four mis-aligned inline comments inside line_items arrays in the integration tests (PHPCS-correct + readable). - Remove duplicate changelog file 65439-woomob-2685-simplify-refund-creation; the woomob-2685-simplify-refund-creation entry is the canonical one. * Add changefile(s) from automation for the following project(s): woocommerce * Preserve legacy explicit-refund_total path without quantity The PR's strict quantity check in validate_line_items was meant to guard the new auto-compute path but unintentionally rejected the legacy v3-style request shape `{line_item_id, refund_total}` (no quantity) — which POS clients will rely on when the v4 features port to v3. Backend: - validate_line_items: gate the positive-integer quantity check on refund_total being absent. When refund_total is provided explicitly, quantity stays optional / informational (legacy behavior). - Gate the over-quantity check on quantity being set. Schema: - Drop `default: 0` on quantity (removes the misleading default — the field is genuinely optional now, conditional on refund_total). - Document the conditional requirement in the field description. Tests: - New integration test: POST /wc/v4/refunds with {line_item_id, refund_total: 50} and no quantity returns 201. - New unit test: validate_line_items accepts missing/zero quantity when refund_total is provided. * Add changefile(s) from automation for the following project(s): woocommerce * Fix silent-failure paths: line-item drop + explicit-zero in calculate Address two silent-failure paths surfaced by the re-review hunter: B (CRITICAL) — Legacy {line_item_id, refund_total} (no quantity) was silently dropped from the refund record. The validator accepted the shape but convert_line_items_to_internal_format required all three keys, so the line was skipped and wc_create_refund got an empty line_items array — the refund had the right dollar amount but no line attribution, leaving per-unit accounting broken for that line. Fix: relax the converter to require line_item_id + (quantity OR refund_total) and default qty=0 when quantity is missing. Matches v3 semantics ("refunded $X of this line without consuming specific units"). Dollar accounting via get_remaining_refund_amount still gates over-refunding regardless of the per-unit looseness. A (HIGH) — calculate_refund_amount used !empty() which treats an explicit refund_total of 0 as missing, silently dropping the line from the sum. Replace with isset() + is_numeric() so the explicit- zero contract documented in the schema is honored. Tests added/augmented: - test_refunds_create_legacy_form_no_quantity_with_explicit_refund_total now asserts the refund line item is attached (count=1) with qty=0 and a -$30 total, AND that a follow-up simplified-form refund exceeding remaining dollars is correctly rejected by wc_create_refund. - test_refunds_create_legacy_form_api_restock_does_not_restock pins the no-restock semantic: qty=0 means no units to add back, so api_restock=true is a no-op for legacy refunds. - test_calculate_refund_amount_includes_explicit_zero (unit) — regression guard against the !empty() reintroduction. - test_convert_line_items_legacy_no_quantity_defaults_qty_zero (unit) — pins the converter's new behavior. Also: guard tax extraction in the converter with isset(refund_total) to avoid silently coercing missing refund_total to 0 via float cast. * Close the test-analyzer follow-ups - Add test_refunds_create_simplified_matches_explicit_tax_inclusive: simplified-vs-explicit equivalence on a tax-inclusive store (10% / $110). The previous equivalence test used an untaxed product, so a regression that yielded a tax-exclusive auto-computed value would have gone undetected. The new test asserts both top-level `amount` and per-line refund_total / refund_tax round-trip identically between the two paths. - Add test_refunds_create_invariant_violation_returns_500: partial-mock DataUtils so fill_missing_refund_totals throws InvalidArgumentException, inject into the DI-resolved RefundsController, dispatch a refund POST, and assert 500 + invalid_refund_request. Pins the scoped catch's response shape so a future refactor that broadens the catch (e.g. to \Throwable) or re-narrows fill's pre-check is caught. - Tighten test_refunds_create_simplified_form_negative_fee: replace the permissive assertLessThan(500, status) + assertArrayHasKey('code') with explicit assertEquals(400) + assertEquals('invalid_refund_amount'). Pins the current platform behavior (the controller's `0 > $refund_amount` guard fires for any negative auto-computed total) so a behavior change is loud rather than silent. * Address 3rd-pass review findings (test-side) Test-analyzer findings, in priority order: (#1, merge-blocker) Option leakage — two tests mutated woocommerce_calc_taxes / woocommerce_prices_include_tax without restoration. tearDown does not reset these, so any test running after them in the same process would see polluted state. Wrap both in try/finally that captures and restores the original values, mirroring the pattern already used in test_refunds_create_simplified_form_tax_inclusive_store. (#4) test_refunds_create_simplified_matches_explicit_tax_inclusive was misnamed — it set prices_include_tax = no. Fix it to actually exercise a tax-inclusive store (set to yes; product entered with tax baked into regular_price). The name now matches reality. (#2) Augment test_refunds_create_legacy_form_no_quantity_with_explicit_refund_total with a Step 3: after the $30 initial refund and the rejected $100 follow-up, refund $40 — this MUST succeed and bring total_refunded to $70 / remaining $30. Guards against a regression where the first refund silently consumed the full $100 budget. (#3) New test_refunds_create_legacy_form_tax_inclusive_store — exercises the converter's tax-extraction block on the legacy {line_item_id, refund_total} (no quantity) path under tax-inclusive prices. This combination is the one POS clients will actually exercise after the v3 port, but no other test reaches it. (#5) New test_refunds_create_three_way_mixed_shapes — single create call with auto-compute + explicit-with-quantity + legacy-no-quantity entries. Verifies the controller's fill/convert pass handles all three shapes coexisting, and asserts each refund line item is attached with the expected qty (refunds store quantities negative, so request qty=1 lands as -1; legacy no-quantity lands as 0). (#6) New test_refunds_create_hook_sees_explicit_refund_total_unchanged — pins that the request-mirroring step (set_param after fill_missing_refund_totals) does not overwrite client-supplied refund_total. Uses a deliberately different value from the auto-computed one so an overwrite regression would surface. * Address review comments CodeRabbit + codex review findings closed: (A) Per-line cap for partially-refunded shipping/fees in validate_preview_line_items. Previously only fully-refunded lines were rejected, so a $10 shipping line partially refunded by $5 would pass validation and let build_refund_preview return $10 even though only $5 was refundable. The new check computes the requested refund via compute_line_item_refund_total() and rejects with 'quantity_exceeds_refundable' (422) when it exceeds remaining. (B) Preserve the signed tax split for negative-tax discount fees. The tax-extraction filter in build_refund_preview previously dropped tax IDs where `amount > 0`, so a -$10 fee with -$1 stored tax previewed as subtotal -$11 / tax $0 — losing the breakdown. Switch the filter to `amount != 0` so non-zero (positive or negative) tax amounts are kept and WC_Tax::calc_inclusive_tax propagates the sign correctly. (C) @since 10.8.0 → @since 10.9.0 on all new public methods, and add @since 10.9.0 to the newly-protected helpers (convert_line_item_taxes_to_internal_format, convert_proportional_taxes_to_schema_format, build_tax_rates_array). Per .ai/skills/woocommerce-backend-dev/code-entities.md, @since is moved to the last line of each docblock. (D) Remove the duplicate changelog file 65334-woomob-2684-refund-preview-helpers — the unprefixed woomob-2684-refund-preview-helpers entry is the canonical one. (E) Fix the @testdox on test_validate_preview_line_items_shipping_fully_refunded to match the assertion (order_not_refundable). The order-level remaining-amount guard fires first when shipping is fully refunded. Tests added: - test_validate_preview_line_items_shipping_partial_remaining — pins the new per-line cap. Order has a $10 shipping line + $50 product; $5 of shipping is pre-refunded; previewing shipping at qty=1 must return quantity_exceeds_refundable rather than passing through to an oversized total. - test_build_refund_preview_negative_fee_with_negative_tax — pins the negative-tax breakdown fix. -$10 fee + -$1 stored tax must preview as subtotal -$10 / tax -$1 / total -$11. * Add changefile(s) from automation for the following project(s): woocommerce * Address review comments Codex review findings closed: (G) Endpoint-level grand-total guard. Even when per-line validation passes, the aggregate preview total can still exceed the order's remaining refundable amount — typical case: an amount-only partial refund applied earlier doesn't consume any per-line quantity, so per-line checks pass while the preview overstates remaining dollars. After build_refund_preview returns, compare preview.total against preview.max_refundable; return 422 preview_exceeds_max_refundable when exceeded. abs() lets negative-fee scenarios compare correctly. (H) @since 10.8.0 → @since 10.9.0 on: - Controller::preview_item - RefundPreviewSchema class - RefundPreviewSchema::get_item_response (was missing) - RefundPreviewSchema::get_item_schema_properties (was missing) Per .ai/skills/woocommerce-backend-dev/code-entities.md, @since is moved to the last line of each docblock. (I) Remove the duplicate changelog file 65335-woomob-2684-refund-preview-endpoint — the unprefixed woomob-2684-refund-preview-endpoint entry is the canonical one. Test added: test_preview_returns_422_when_total_exceeds_max_refundable pins the new endpoint guard. 2 × $100 order with a $50 amount-only refund applied → previewing qty 2 must return 422 preview_exceeds_max_refundable rather than a $200 total with $150 max_refundable in the response body. * Add changefile(s) from automation for the following project(s): woocommerce * Address review comments Codex findings closed: (#1, CRITICAL) Cap simplified-form refunds against the line's REMAINING refundable quantity rather than the original. validate_line_items previously compared $line_item['quantity'] to $item->get_quantity() (the unchanged original count), so once item A was refunded for the first time, a second simplified {line_item_id: A, quantity: 1} request would pass the per-line check and only be bounded by the order's dollar balance — if item B still had room, item A would be silently refunded twice. Hoist compute_refunded_quantities_and_totals outside the loop and cap product items by $item->get_quantity() + ($refund_data['qtys'][id] ?? 0), matching the pattern already used in validate_preview_line_items. Shipping/ fees keep the original simpler check (they don't track per-unit refund history). (#2, HIGH) Reject the ambiguous "omitted refund_total + explicit refund_tax" combination. fill_missing_refund_totals would have written a tax-inclusive refund_total (110 for a $100 item with $10 tax) and convert_line_items_to_internal_format would then skip tax extraction because refund_tax was already present — calculate_refund_amount summed both and emitted amount=120 (overstated by the tax amount). fill_missing_refund_totals now skips items with explicit refund_tax, and validate_line_items rejects the combination with 'invalid_line_item: refund_tax cannot be combined with auto-computed refund_total'. (#3) Remove the duplicate changelog file 65439-woomob-2685-simplify-refund-creation — the unprefixed woomob-2685-simplify-refund-creation entry is the canonical one. (#4) @since 10.8.0 → @since 10.9.0 on fill_missing_refund_totals; @since moved to the last docblock line per .ai/skills/woocommerce-backend-dev/code-entities.md. The other @since annotations on this file belong to methods introduced by #65334 and were already updated on that branch (644f7c0cd9); they'll re-flow to this branch via merge. Tests added: - test_refunds_create_simplified_form_rejects_already_refunded_product pins the new remaining-qty cap. Two-line order (A + B, each $50); refund A once → 201; refund A again → 400 invalid_line_item with "remaining refundable quantity" in the message. - test_refunds_create_rejects_auto_compute_with_explicit_refund_tax pins the rejection. $100 product with $10 stored tax; request with no refund_total + explicit refund_tax → 400 invalid_line_item with "refund_tax cannot be combined" in the message. Wrapped in try/finally for tax-option restoration. * Add changefile(s) from automation for the following project(s): woocommerce * Address review comments on validate_preview_line_items - Compare shipping/fee refunds on a tax-inclusive basis. The previous code pitted the tax-inclusive $requested_total against a tax-exclusive $remaining_total — for a $10 shipping line with $1.50 tax, the comparison was 11.50 > 10.00 and rejected a legitimate full refund. compute_refunded_quantities_and_totals() now records fee/shipping totals tax-inclusive so the validator can compare like-for-like. - Replace the hardcoded `'status' => 422` literals in validate_preview_line_items with WP_Http::UNPROCESSABLE_ENTITY, matching the convention used elsewhere in V4 routes. - Add a regression test (shipping line with tax, no prior refund) to lock in the tax-inclusive comparison. - Delete the duplicate changelog entry; keep the PR-prefixed one auto- generated by CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Align tax filter in convert_line_items_to_internal_format with preview side The creation-side filter dropped tax IDs whose stored amount was <= 0, so a negative-fee discount line (e.g. a -$10 fee with -$1 stored tax) had its tax breakdown stripped on save — refund_total stayed at -$11 and refund_tax was emitted as []. The preview side (build_refund_preview) keeps any non-zero stored tax and renders the signed split correctly, so a refund moving from preview to create lost the tax breakdown. Match the preview rule (non-zero, not strictly positive). Add a regression test that converts a negative fee with negative stored tax and verifies the signed split survives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use WP_Http constants in Refunds Controller Replace the 422 literal in preview_item with WP_Http::UNPROCESSABLE_ENTITY, and the 204 literal in delete_item with WP_Http::NO_CONTENT. Matches the convention already used elsewhere in V4 routes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rename refund preview totals to total + total_tax Match the V4 Orders convention. Items, sections, and the grand total block now expose `total` (excluding tax) and `total_tax` only. The including-tax figure is consumer-computed as `total + total_tax`. Controller's preview-exceeds-max-refundable check now compares the inclusive sum (`total + total_tax`) against `max_refundable`, which is still the order-side inclusive remaining amount. Tests cover the new shape positively and assert the dropped `subtotal` and `tax` keys are absent. The preview-to-create round trip in the integration test now feeds `total + total_tax` into `POST /wc/v4/refunds`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse product_id and variation_id on the preview product item schema Drop the separate variation_id field. product_id now carries the variation ID when present and the product ID otherwise, matching OrderItemSchema.php:181 (V4 Orders convention). Description text matches OrderItemSchema as "Product or variation ID.". Integration test asserts variation_id is absent. New unit test exercises the variation branch so product_id === variation_id is locked in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update negative-fee preview test to new total + total_tax shape The test inherited from the helpers merge was written against the old preview shape (subtotal + tax + including-tax total). Update assertions to match the new shape: section total = -10.00 (excluding tax), total_tax = -1.00. Item entries no longer carry subtotal or bare tax. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Remove duplicate slug-only changelog entry The CI auto-added 65335-woomob-2684-refund-preview-endpoint with the same content. Keep the PR-prefixed file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Remove duplicate slug-only changelog entry The CI auto-added 65439-woomob-2685-simplify-refund-creation with a more detailed body. Keep the PR-prefixed file as the canonical entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add parity test: create amount matches build_refund_preview total Regression guard against the create vs preview drift mikejolley flagged on PRs #65334 and #65335. Builds an order with a product carrying 10% tax, calls build_refund_preview directly to capture the authoritative grand total, then posts the same line items (quantity only) to the create endpoint. The resulting refund amount must equal preview total exactly. A future change that subtly diverges create's auto compute from the preview calculation would fail this assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Lock fee/shipping quantity-ignored behavior in compute_line_item_refund_total The existing tests for shipping and positive fees pass quantity = 1, which does not prove the quantity argument is ignored. Add two short tests that pass quantity = 5 and assert the same line total + tax is returned. Catches a future refactor that wrongly applies unit_price * quantity to shipping or fee items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add partial-amount form to POST /wc/v4/refunds/preview * Add changelog for partial-amount preview form * Fix partial-refund validation gaps found in review * Align preview schema, controller, and tests with subtotal/tax/total Schema fields renamed to subtotal (ex-tax), tax, and total (tax-inclusive) at item, section, and grand levels. Collapse variation_id into product_id for variation line items. Update controller guard and test assertions to match. * Update partial-amount preview tests to subtotal/tax/total field names * Fix test isolation: guard wp_insert_user and wrap stub restore in finally * Reject non-positive refund_total in preview with invalid_refund_total A present-but-zero refund_total was treated as absent by validate_preview_line_items (which then validated the quantity path) while build_refund_preview used the explicit 0 via isset(), so a request like {quantity: 2, refund_total: 0} returned a 200 preview with a $0.00 total. Validation now rejects any present non-positive refund_total with a dedicated invalid_refund_total error (400), and build_refund_preview only takes the explicit value when positive. Also aligns the get_public_preview_schema @since tag with the rest of the preview code (10.8.0 -> 10.9.0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Align v4 refund preview with create path and harden tax split Split tax-inclusive amounts by the line's stored total/tax ratio in a shared helper used by both build_refund_preview and the create converter, and normalize caller-supplied refund_total to currency precision in both flows, so a previewed amount matches the created refund to the cent (and charged tax is never dropped on zero-rate or non-proportional lines). Harden validation across both endpoints: reject duplicate line items, reject a refund_total whose sign is wrong for the line, cap each line against its remaining refundable amount, return 400 for bad line references on both paths, and reject an order-level over-refund up front with a clear error. Clamp the tax split when a line's stored total and tax nearly cancel. Add tests for partial multi-tax-ID distribution, rounding parity, non-2-decimal currencies, normalize_refund_totals, and the new guards. * Refine changelog wording for partial-amount refund preview * Align v4 refund create validation and errors with preview path Make the create endpoint reject the same invalid inputs the preview endpoint already rejects, with matching codes and HTTP status: - Reject non-refundable order statuses (order_not_refundable, 422). - Reject a refund_total that rounds to zero (invalid_refund_total). - Return 422 with distinct codes for per-line over-refunds (refund_total_exceeds_line, line_item_already_refunded, refund_total_exceeds_remaining) instead of a flattened 400. - Propagate the WP_Error status from validate_line_items. Also cap explicit refund_tax against the remaining per-tax-id amount by tracking already-refunded tax in compute_refunded_quantities_and_totals, preventing sequential refunds from over-refunding a single tax bucket. * Align v4 refund create errors with preview and add partial-amount tests Give validate_line_items granular error codes and HTTP statuses (missing_line_item_id, duplicate_line_item, line_item_not_found, unsupported_item_type, missing_quantity_or_refund_total, quantity_exceeds_refundable, invalid_refund_total) so create and preview reject the same input identically, reject a fully-refunded order up front, and round both sides of the per-tax-id cap to currency precision. Refresh the stale tax-split comment in the controller. Add tests: - preview/create breakdown parity for the explicit refund_total form (per-line net + tax) - two sequential partial-amount refunds summing to the line total, then rejection - direct unit coverage of split_inclusive_by_stored_ratio (rounding remainder, negative discount fee, zero-tax line, degenerate-ratio clamp) Remove the duplicate 65439-woomob-2685 changelog entry; woomob-3176 covers it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Align v4 refund preview validation with create validate_preview_line_items() was more lenient than validate_line_items(), letting a preview return 200 for inputs create then rejects: - Validate a supplied quantity even when refund_total is present, so {quantity: 2, refund_total: 1} on a 1-unit line is rejected, not previewed. - Cap the quantity-derived amount against the remaining line amount for products too (not just shipping/fees), so a product with a prior amount-only refund can no longer preview an over-refund. - Enforce refund_total sign parity: accept a negative refund_total on a discount-fee line, reject a positive one on a negative line, keep rejecting zero. build_refund_preview() now honors a non-zero explicit refund_total instead of falling through to the quantity path. Add 5 DataUtils unit tests and 3 preview/create parity endpoint tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make per-tax-id refund_tax cap sign-aware for negative tax buckets validate_line_items() compared the remaining tax bucket with a signed `<`, so for a discount fee with stored tax -1.00 a valid partial refund_tax of -0.50 was rejected while an over-refund of -2.00 passed. Cap on absolute magnitudes (stored bucket minus prior refunds, which are already accumulated as positive magnitudes) and reject a refund_tax whose sign is opposite the bucket, mirroring the refund_total caps. Add 3 DataUtils tests: partial negative refund_tax accepted, negative over-refund rejected, positive-on-negative wrong-sign rejected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reject non-positive aggregate total in v4 refund preview preview_item() only guarded abs(total) > max_refundable, so a preview of only a negative discount line, or a product plus discount that nets to zero, returned 200 while create_item() rejects it with invalid_refund_amount. Add the same non-positive guard to preview (total <= 0 -> invalid_refund_amount), plus two preview/create parity endpoint tests (negative-only and zero-net). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Cap gross line refund and document refund_total/refund_tax contract refund_tax presence is the discriminator: without it, refund_total is tax-inclusive and the server splits tax; with it, refund_total is the tax-exclusive subtotal and the gross line refund is refund_total + refund_tax (core Woo semantics). The schema and a controller comment wrongly called the explicit-tax refund_total tax-inclusive. - validate_line_items now caps the GROSS (refund_total + sum(refund_tax)) against the line's tax-inclusive total, so a client cannot push the overage into refund_tax and over-refund the line. With no refund_tax the gross equals refund_total, so the inclusive form and preview parity are unchanged. - Clarify the dual-mode contract in RefundSchema, the create_item comment, convert_line_items_to_internal_format, and calculate_refund_amount. Add 2 DataUtils tests (gross over line rejected, gross within line accepted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Correct v4 refund preview refund_total schema doc for signed amounts The preview request schema described refund_total as "Must be greater than zero" and the comment claimed negatives return invalid_refund_total, but validate_preview_line_items() accepts a negative refund_total for a discount or credit line (in a mixed refund) and rejects only zero and wrong-sign values. The stale schema could steer clients away from valid mixed refunds. Doc/comment only; behavior is unchanged and already covered by tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address CodeRabbit review: quantity validation, schema docs, test hygiene - validate_line_items: reject a negative or non-integer quantity supplied alongside refund_total (it would be stored verbatim as the refund line qty). An explicit 0 stays accepted as the dollars-only form, matching the existing loose-quantity contract; only negative/fractional values are rejected. - RefundSchema: correct refund_total ("a value that rounds to 0 is rejected", not "0 is a zero refund") and refund_tax (auto-split uses the line's stored total/tax ratio, not the order's current tax rates). - Fix a stale "explicit 0 is allowed" comment in validate_line_items. - Fix a preview testdox that said "returns 400" for a 422 case. - Reset woocommerce_calc_taxes/prices_include_tax in the controller test tearDown so a test that toggles them can't bleed into the suite. - Wrap an expected-exception preview test in try/finally so the order is always cleaned up. Add a data-provider test for the negative/non-integer quantity rejection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix tax-only explicit refunds * Remove stale PHPStan baseline entries * Align refund preview amount-cap errors * Restore refund auto-compute regression tests * Fix refund CI failures * Align v4 Orders can_be_refunded for fee/shipping with the refund validator WOOPLUG-6829: OrderSchema computed fee/shipping can_be_refunded as ( get_total() + get_total_tax() - refunded ) > 0, omitting the abs() and rounding the Refunds validator uses. So a discount (negative-total) fee reported can_be_refunded=false even though the refund endpoints accept it, and sub-cent float residue could report a fully-refunded line as refundable. Compare on a tax-inclusive absolute basis and round to currency precision, mirroring Refunds\DataUtils. Add a regression test for the negative-fee case; existing fully-refunded/partial tests guard the rounding path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Submission Review Guidelines:
Changes proposed in this Pull Request:
Closes # .
(For Bug Fixes) Bug introduced in PR # .
Screenshots or screen recordings:
How to test the changes in this Pull Request:
Using the WooCommerce Testing Instructions Guide, include your detailed testing instructions:
Testing that has already taken place:
Changelog entry
Changelog Entry Details
Significance
Type
Message
Changelog Entry Comment
Comment