Skip to content

fix: prevent POS price modifications from persisting to product database#509

Merged
kilbot merged 2 commits intomainfrom
fix/sale-price-persistence
Feb 11, 2026
Merged

fix: prevent POS price modifications from persisting to product database#509
kilbot merged 2 commits intomainfrom
fix/sale-price-persistence

Conversation

@kilbot
Copy link
Copy Markdown
Contributor

@kilbot kilbot commented Feb 11, 2026

Summary

  • When order_item_product() clones a product and sets POS prices on it, WC stock functions (wc_reduce_stock_levels, wc_increase_stock_levels) can later call save() on the clone, inadvertently writing sale_price, price, regular_price, and tax_status to the DB
  • Fix uses apply_changes() to clear the $changes tracking array, plus an update_post_metadata filter to block the !metadata_exists() fallback in WC's get_props_to_update() (which writes _sale_price when no DB row exists, regardless of $changes)
  • The clone keeps its real product ID so WC stock management works normally — no need to detach with set_id(0)

Test plan

  • Create a simple product with regular_price=25 and no sale_price
  • Create a POS order with that product, setting a POS price of 20.00 and regular_price of 25.00 in _woocommerce_pos_data meta
  • Verify the filtered product (from $item->get_product()) has sale_price=20.00 and is_on_sale()=true — in-memory values work correctly
  • Call save() on the filtered product (simulating what WC stock functions do)
  • Reload the product from DB and verify sale_price is still empty and price is still 25 — no price data leaked to the database
  • Verify existing coupon tests still pass (the is_on_sale() behavior for exclude_sale_items is preserved)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added protection mechanism for product prices modified through the POS system. Temporary price adjustments are now prevented from being permanently saved to the database while preserving original base prices, ensuring data integrity.
  • Tests

    • Added comprehensive test coverage to verify price protection functionality and prevent unintended price persistence.

When order_item_product() clones a product and sets POS prices (price,
sale_price, regular_price, tax_status), WC stock functions can later call
save() on the clone, writing those temporary values to the DB.

Fix uses two mechanisms:
- apply_changes() clears the $changes array so get_props_to_update()
  won't include price fields from its normal tracking
- update_post_metadata filter blocks the !metadata_exists() fallback
  that would still write _sale_price when no DB row exists

The clone keeps its real ID so WC stock management works normally.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Introduces in-memory protection for POS-modified product price metadata in the Orders class. A new protect_product_price_meta method hooks into WordPress's update_post_metadata filter to prevent persistence of specific price-related meta keys for products tracked during order item processing.

Changes

Cohort / File(s) Summary
POS Price Meta Protection
includes/Orders.php
Adds protect_product_price_meta method, price_protected_ids tracking list, and PROTECTED_META_KEYS constant to prevent persistence of POS-modified price metadata; hooks filter in constructor.
Test Coverage
tests/includes/Test_Orders.php
Adds test_order_item_product_save_does_not_persist_sale_price to verify POS price meta is not persisted, and extends test_direct_constructor_registers_filters to confirm protect_product_price_meta filter registration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 In memory's realm where prices play,
A rabbit guards them night and day,
Protected from the database's keep,
Safe in RAM, their secrets deep! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically summarizes the main objective of the changeset—preventing POS price modifications from persisting to the product database.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sale-price-persistence

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@includes/Orders.php`:
- Around line 262-269: The protect_product_price_meta method currently has an
unused parameter $meta_value which triggers PHPMD; rename the parameter to
$_meta_value (or prefix with an underscore) in the function signature and all
internal references to silence the unused-parameter warning, keeping the
function name protect_product_price_meta and its behavior unchanged (return true
when $object_id is in $this->price_protected_ids and $meta_key in
self::PROTECTED_META_KEYS, otherwise return $check).
🧹 Nitpick comments (1)
includes/Orders.php (1)

28-48: Consider set-style tracking for protected IDs.

Using an associative array avoids duplicates and makes membership checks O(1), which helps for large orders.

♻️ Proposed refactor (multi-location)
-	/**
-	 * `@var` array<int>
-	 */
-	private $price_protected_ids = array();
+	/**
+	 * `@var` array<int, bool>
+	 */
+	private $price_protected_ids = array();
-				if ( $product->get_id() ) {
-					$this->price_protected_ids[] = $product->get_id();
-				}
+				if ( $product->get_id() ) {
+					$this->price_protected_ids[ $product->get_id() ] = true;
+				}
-		if ( \in_array( $object_id, $this->price_protected_ids, true )
+		if ( isset( $this->price_protected_ids[ $object_id ] )
 			&& \in_array( $meta_key, self::PROTECTED_META_KEYS, true )
 		) {

Comment thread includes/Orders.php
Comment on lines +262 to +269
public function protect_product_price_meta( $check, $object_id, $meta_key, $meta_value ) {
if ( \in_array( $object_id, $this->price_protected_ids, true )
&& \in_array( $meta_key, self::PROTECTED_META_KEYS, true )
) {
return true;
}

return $check;
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.

⚠️ Potential issue | 🟡 Minor

Silence the unused-parameter warning.

PHPMD flags $meta_value as unused; rename it to $_meta_value (or suppress) to keep the signal clean.

🧹 Proposed fix
-public function protect_product_price_meta( $check, $object_id, $meta_key, $meta_value ) {
+public function protect_product_price_meta( $check, $object_id, $meta_key, $_meta_value ) {
📝 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
public function protect_product_price_meta( $check, $object_id, $meta_key, $meta_value ) {
if ( \in_array( $object_id, $this->price_protected_ids, true )
&& \in_array( $meta_key, self::PROTECTED_META_KEYS, true )
) {
return true;
}
return $check;
public function protect_product_price_meta( $check, $object_id, $meta_key, $_meta_value ) {
if ( \in_array( $object_id, $this->price_protected_ids, true )
&& \in_array( $meta_key, self::PROTECTED_META_KEYS, true )
) {
return true;
}
return $check;
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 262-262: Avoid unused parameters such as '$meta_value'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In `@includes/Orders.php` around lines 262 - 269, The protect_product_price_meta
method currently has an unused parameter $meta_value which triggers PHPMD;
rename the parameter to $_meta_value (or prefix with an underscore) in the
function signature and all internal references to silence the unused-parameter
warning, keeping the function name protect_product_price_meta and its behavior
unchanged (return true when $object_id is in $this->price_protected_ids and
$meta_key in self::PROTECTED_META_KEYS, otherwise return $check).

@github-actions
Copy link
Copy Markdown
Contributor

PHPUnit Test Results

704 tests   696 ✅  1m 1s ⏱️
 39 suites    8 💤
  1 files      0 ❌

Results for commit 13842a1.

@kilbot kilbot merged commit 2d8ca2f into main Feb 11, 2026
15 checks passed
@kilbot kilbot deleted the fix/sale-price-persistence branch February 11, 2026 17:54
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