Commit b3ce23d8aa2 for woocommerce
commit b3ce23d8aa2e5b41d7df18187082f446a766f2d2
Author: Seghir Nadir <nadir.seghir@gmail.com>
Date: Wed Oct 7 12:58:27 2026 +0200
Revert "Fix conditional checkout field rules for fields with no value yet" (#69521)
* Revert "Fix conditional checkout field rules for fields with no value yet (#68220)"
This reverts commit a0a5fdd5dce661de88a8520908019c811086d722.
* Keep PHPStan baseline unchanged in CheckoutOrder revert
diff --git a/plugins/woocommerce/changelog/revert-68220-conditional-checkout-fields b/plugins/woocommerce/changelog/revert-68220-conditional-checkout-fields
new file mode 100644
index 00000000000..91d2b0227fd
--- /dev/null
+++ b/plugins/woocommerce/changelog/revert-68220-conditional-checkout-fields
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Revert recent changes to how conditional checkout field rules handle missing values.
diff --git a/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md b/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md
index ef5d5dd77ab..70fec879e22 100644
--- a/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md
+++ b/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md
@@ -15,7 +15,7 @@ WooCommerce currently uses two feature-flag systems in Blocks:
| Flag | Defaults | Current Blocks usage |
| --- | --- | --- |
-| `experimental-blocks` | Enabled in [development](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/config/development.json) and disabled in [core builds](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/config/core.json). | Exposed to the editor through [`isExperimentalBlocksEnabled()`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/settings/blocks/feature-flags.ts). It currently gates the **Disable product descriptions** editor control in Checkout Order Summary Cart Items. It does not determine which block scripts webpack builds or which general block types are registered. |
+| `experimental-blocks` | Enabled in [development](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/config/development.json) and disabled in [core builds](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/config/core.json). | Exposed to the editor through [`isExperimentalBlocksEnabled()`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/settings/blocks/feature-flags.ts). It currently gates the **Disable product descriptions** editor control in Checkout Order Summary Cart Items and conditional checkout-field processing in the Store API. It does not determine which block scripts webpack builds or which general block types are registered. |
| `rest-api-v4` | Disabled in both development and core build configurations. | Exposed through [`isExperimentalWcRestApiV4Enabled()`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/settings/blocks/feature-flags.ts). It switches product entities from `/wc/v3/products` to `/wc/v4/products`, registers the settings entity, and enables the v4 product-data paths used by Product Price and Product Button in the editor. |
### Runtime feature flags
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index 013d662ef3e..cb53a67a90d 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -280,11 +280,10 @@ class Checkout extends AbstractCartRoute {
$invalid_details = [];
$is_partial = in_array( $request->get_method(), [ 'PUT', 'PATCH' ], true );
- $document_object = $this->get_document_object_from_rest_request( $request );
-
foreach ( $validate_contexts as $context => $context_data ) {
$errors = new \WP_Error();
+ $document_object = $this->get_document_object_from_rest_request( $request );
$document_object->set_context( $context );
$additional_fields = $this->additional_fields_controller->get_contextual_fields_for_location( $context_data['location'], $document_object );
@@ -926,10 +925,9 @@ class Checkout extends AbstractCartRoute {
],
];
- $document_object = $this->get_document_object_from_rest_request( $request );
-
foreach ( $additional_field_contexts as $context => $context_data ) {
+ $document_object = $this->get_document_object_from_rest_request( $request );
$document_object->set_context( $context );
$additional_fields = $this->additional_fields_controller->get_contextual_fields_for_location( $context_data['location'], $document_object );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CheckoutOrder.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CheckoutOrder.php
index 87868393ce3..c798e747130 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CheckoutOrder.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CheckoutOrder.php
@@ -31,7 +31,7 @@ class CheckoutOrder extends AbstractCartRoute {
/**
* Holds the current order being processed.
*
- * @var \WC_Order|null
+ * @var \WC_Order
*/
private $order = null;
@@ -201,7 +201,6 @@ class CheckoutOrder extends AbstractCartRoute {
* @param \WP_REST_Request $request Full details about the request.
*/
private function update_billing_address( \WP_REST_Request $request ) {
- $order = $this->get_order_or_throw();
$customer = wc()->customer;
// Billing address is a required field.
@@ -210,9 +209,9 @@ class CheckoutOrder extends AbstractCartRoute {
// If shipping address (optional field) was not provided, set it to the given billing address (required field).
$shipping = $request['shipping_address'] ?? $billing;
- $order->set_billing_address( $billing );
- $order->set_shipping_address( $shipping );
- $this->order_controller->validate_existing_order_before_update( $order );
+ $this->order->set_billing_address( $billing );
+ $this->order->set_shipping_address( $shipping );
+ $this->order_controller->validate_existing_order_before_update( $this->order );
// Update customer object with validated order addresses.
foreach ( $billing as $key => $value ) {
@@ -238,8 +237,8 @@ class CheckoutOrder extends AbstractCartRoute {
do_action( 'woocommerce_store_api_checkout_update_customer_from_request', $customer, $request );
$customer->save();
- $order->save();
- $order->calculate_totals();
+ $this->order->save();
+ $this->order->calculate_totals();
}
/**
@@ -253,7 +252,7 @@ class CheckoutOrder extends AbstractCartRoute {
$request_payment_method = wc_clean( wp_unslash( $request['payment_method'] ?? '' ) );
if ( empty( $request_payment_method ) ) {
- if ( $this->get_order_or_throw()->needs_payment() ) {
+ if ( $this->order->needs_payment() ) {
throw new RouteException(
'woocommerce_rest_checkout_missing_payment_method',
__( 'No payment method provided.', 'woocommerce' ),
@@ -287,6 +286,6 @@ class CheckoutOrder extends AbstractCartRoute {
* @param \WP_REST_Request $request Request object.
*/
private function process_customer( \WP_REST_Request $request ) {
- $this->order_controller->sync_customer_data_with_order( $this->get_order_or_throw() );
+ $this->order_controller->sync_customer_data_with_order( $this->order );
}
}
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php
index e79e79634c3..427dfc13910 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php
@@ -324,17 +324,21 @@ class CheckoutSchema extends AbstractSchema {
)
: $this->additional_fields_controller->get_all_fields_from_object( $wc_object, 'other' );
- $response = [];
- foreach ( $this->get_additional_fields_schema() as $key => $field_schema ) {
+ $additional_field_schema = $this->get_additional_fields_schema();
+ foreach ( $fields as $key => $value ) {
+ if ( ! isset( $additional_field_schema[ $key ] ) ) {
+ unset( $fields[ $key ] );
+ continue;
+ }
// This makes sure we're casting checkboxes from "1" and "0" to boolean. In the frontend, "0" is treated as truthy.
- if ( isset( $field_schema['type'] ) && 'boolean' === $field_schema['type'] ) {
- $response[ $key ] = (bool) ( $fields[ $key ] ?? false );
+ if ( isset( $additional_field_schema[ $key ]['type'] ) && 'boolean' === $additional_field_schema[ $key ]['type'] ) {
+ $fields[ $key ] = (bool) $value;
} else {
- $response[ $key ] = $this->prepare_html_response( $fields[ $key ] ?? '' );
+ $fields[ $key ] = $this->prepare_html_response( $value );
}
}
- return (object) $response;
+ return (object) $fields;
}
/**
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
index b2cf9d6ca57..bc5400652f8 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
@@ -7,6 +7,7 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\DocumentObject;
+use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Enums\OrderStatus;
use WC_Customer;
@@ -483,12 +484,19 @@ trait CheckoutTrait {
* @param callable $persist Callback invoked as `$persist( string $key, mixed $value )` for each field.
*/
private function resolve_and_persist_additional_fields( \WP_REST_Request $request, callable $persist ): void {
- $document_object = $this->get_document_object_from_rest_request( $request );
- $document_object->set_context( 'order' );
- $additional_fields = array_merge(
- $this->additional_fields_controller->get_contextual_fields_for_location( 'order', $document_object ),
- $this->additional_fields_controller->get_contextual_fields_for_location( 'contact', $document_object )
- );
+ if ( Features::is_enabled( 'experimental-blocks' ) ) {
+ $document_object = $this->get_document_object_from_rest_request( $request );
+ $document_object->set_context( 'order' );
+ $additional_fields = array_merge(
+ $this->additional_fields_controller->get_contextual_fields_for_location( 'order', $document_object ),
+ $this->additional_fields_controller->get_contextual_fields_for_location( 'contact', $document_object )
+ );
+ } else {
+ $additional_fields = array_merge(
+ $this->additional_fields_controller->get_fields_for_location( 'order' ),
+ $this->additional_fields_controller->get_fields_for_location( 'contact' )
+ );
+ }
$field_values = isset( $request['additional_fields'] ) ? (array) $request['additional_fields'] : array();
@@ -510,47 +518,27 @@ trait CheckoutTrait {
* Returns a document object from a REST request.
*
* @param \WP_REST_Request $request The REST request.
- * @return DocumentObject The document object.
+ * @return DocumentObject The document object or null if experimental blocks are not enabled.
*/
public function get_document_object_from_rest_request( \WP_REST_Request $request ) {
- // Keep the order local so validation errors do not release its stock or coupon holds.
- $order = $this->order ?? $this->get_draft_order();
- $saved_fields = wc()->customer instanceof WC_Customer
- ? $this->additional_fields_controller->get_all_fields_from_object( wc()->customer, 'other' )
- : [];
- if ( $order instanceof \WC_Order ) {
- $saved_fields = wp_parse_args(
- $this->additional_fields_controller->get_all_fields_from_object( $order, 'other' ),
- $saved_fields
- );
- }
-
- $field_values = wp_parse_args( $request['additional_fields'] ?? [], $saved_fields );
- $additional_fields = [];
- $registered_fields = array_merge(
- $this->additional_fields_controller->get_fields_for_location( 'contact' ),
- $this->additional_fields_controller->get_fields_for_location( 'order' )
- );
-
- // Conditions need raw values and explicit empty values for missing fields.
- foreach ( $registered_fields as $key => $field ) {
- $additional_fields[ $key ] = 'checkbox' === $field['type']
- ? (bool) ( $field_values[ $key ] ?? false )
- : ( $field_values[ $key ] ?? '' );
- }
-
return new DocumentObject(
[
'customer' => [
'billing_address' => $request['billing_address'],
'shipping_address' => $request['shipping_address'],
- 'additional_fields' => $this->additional_fields_controller->filter_fields_for_location( $additional_fields, 'contact' ),
+ 'additional_fields' => array_intersect_key(
+ $request['additional_fields'] ?? [],
+ array_flip( $this->additional_fields_controller->get_contact_fields_keys() )
+ ),
],
'checkout' => [
'payment_method' => $request['payment_method'],
'create_account' => $request['create_account'],
'customer_note' => $request['customer_note'],
- 'additional_fields' => $this->additional_fields_controller->filter_fields_for_location( $additional_fields, 'order' ),
+ 'additional_fields' => array_intersect_key(
+ $request['additional_fields'] ?? [],
+ array_flip( $this->additional_fields_controller->get_order_fields_keys() )
+ ),
],
]
);
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php
index 0b98bd7f5b9..ae2bfcf214d 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php
@@ -325,38 +325,6 @@ class Additional_Checkout_Fields_Test_Helper {
),
)
);
-
- // Only shown once "How did you hear about us?" is set to Facebook. Covers a rule that
- // reads another additional field, which needs that field to have a value from the start.
- woocommerce_register_additional_checkout_field(
- array(
- 'id' => 'third-plugin-namespace/facebook-page',
- 'label' => 'Which Facebook page did you see us on?',
- 'location' => 'order',
- 'type' => 'text',
- 'hidden' => array(
- 'not' => array(
- 'type' => 'object',
- 'properties' => array(
- 'checkout' => array(
- 'type' => 'object',
- 'properties' => array(
- 'additional_fields' => array(
- 'type' => 'object',
- 'properties' => array(
- 'third-plugin-namespace/how-did-you-hear-about-us' => array(
- 'type' => 'string',
- 'const' => 'facebook',
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- )
- );
}
}
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
index cf45a55174d..626908309ce 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
@@ -368,32 +368,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
).toBeHidden();
} );
- test( 'Conditional field stays hidden while the field it depends on has no value', async ( {
- checkoutPageObject,
- frontendUtils,
- } ) => {
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
-
- const facebookPage = checkoutPageObject.page.getByLabel(
- 'Which Facebook page did you see us on?'
- );
- const howDidYouHear = checkoutPageObject.page.getByLabel(
- 'How did you hear about us?'
- );
-
- // Nothing has been picked yet, so the rule must not match.
- await expect( howDidYouHear ).toHaveValue( '' );
- await expect( facebookPage ).toBeHidden();
-
- await howDidYouHear.selectOption( 'facebook' );
- await expect( facebookPage ).toBeVisible();
-
- await howDidYouHear.selectOption( 'google' );
- await expect( facebookPage ).toBeHidden();
- } );
-
test( 'Conditional fields are shown/hidden based on cart state', async ( {
checkoutPageObject,
frontendUtils,
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php
index 9d6448583c7..061771273b0 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php
@@ -6,7 +6,6 @@ namespace Automattic\WooCommerce\Tests\Blocks\Domain\Services\CheckoutFieldsSche
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\DocumentObject;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
-use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
use Automattic\WooCommerce\Blocks\Package;
use Opis\JsonSchema\{
@@ -28,7 +27,6 @@ class DocumentObjectTests extends \WC_Unit_Test_Case {
* to test the DocumentObject class.
*/
use CheckoutTrait;
- use DraftOrderTrait;
/**
* Checkout fields controller.
@@ -36,12 +34,6 @@ class DocumentObjectTests extends \WC_Unit_Test_Case {
*/
protected $additional_fields_controller;
- /**
- * Current order, needed for the trait.
- * @var \WC_Order|null
- */
- private $order = null;
-
/**
* Fixture data.
* @var FixtureData
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
index 1257a1e2122..b481075eebb 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
@@ -10,7 +10,6 @@ use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Package;
use WC_Gateway_BACS;
use Automattic\WooCommerce\Enums\ProductStockStatus;
-use Automattic\WooCommerce\Enums\OrderStatus;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
/**
@@ -2461,7 +2460,7 @@ class AdditionalFields extends \WP_Test_REST_TestCase {
$this->assertEquals( 'billing-saved-gov-id', ( (array) $data['billing_address'] )['plugin-namespace/gov-id'], print_r( $data, true ) );
$this->assertEquals( 'shipping-saved-gov-id', ( (array) $data['shipping_address'] )['plugin-namespace/gov-id'], print_r( $data, true ) );
$this->assertEquals( 'engineering', $additional_fields['plugin-namespace/job-function'], print_r( $data, true ) );
- $this->assertFalse( $additional_fields['plugin-namespace/leave-on-porch'], print_r( $data, true ) );
+ $this->assertArrayNotHasKey( 'plugin-namespace/leave-on-porch', $additional_fields, print_r( $data, true ) );
}
/**
@@ -2953,310 +2952,4 @@ class AdditionalFields extends \WP_Test_REST_TestCase {
\__internal_woocommerce_blocks_deregister_checkout_field( $id );
$this->assertFalse( $this->controller->is_field( $id ), sprintf( '%s is still registered', $id ) );
}
-
- /**
- * Returns a rule matching the referral-source value at the given path.
- *
- * @param string[] $path Property path to the object holding the field (e.g. [ 'checkout', 'additional_fields' ]).
- * @param string $value The value to match.
- * @return array The rule schema.
- */
- private function get_referral_source_rule( array $path = array( 'checkout', 'additional_fields' ), string $value = 'other' ) {
- $rule = array(
- 'type' => 'object',
- 'properties' => array(
- 'plugin-namespace/referral-source' => array(
- 'type' => 'string',
- 'const' => $value,
- ),
- ),
- );
- foreach ( array_reverse( $path ) as $segment ) {
- $rule = array(
- 'type' => 'object',
- 'properties' => array( $segment => $rule ),
- );
- }
- return $rule;
- }
-
- /**
- * Registers a referral-source select field and a referral-detail field with a conditional rule on it.
- *
- * @param string $rule_prop Which conditional rule to give the detail field (hidden|required).
- * @param string $location The location to register both fields in (order|contact|address).
- */
- private function register_conditional_referral_fields( $rule_prop, $location = 'order' ) {
- $rule_paths = array(
- 'order' => array( 'checkout', 'additional_fields' ),
- 'contact' => array( 'customer', 'additional_fields' ),
- 'address' => array( 'customer', 'billing_address' ),
- );
- $rule = $this->get_referral_source_rule( $rule_paths[ $location ] );
-
- \woocommerce_register_additional_checkout_field(
- array(
- 'id' => 'plugin-namespace/referral-source',
- 'label' => 'How did you find us?',
- 'location' => $location,
- 'type' => 'select',
- 'options' => array(
- array(
- 'value' => 'search',
- 'label' => 'Search engine',
- ),
- array(
- 'value' => 'other',
- 'label' => 'Other',
- ),
- ),
- )
- );
- \woocommerce_register_additional_checkout_field(
- array(
- 'id' => 'plugin-namespace/referral-detail',
- 'label' => 'Please specify',
- 'location' => $location,
- 'type' => 'text',
- $rule_prop => 'hidden' === $rule_prop
- ? array( 'not' => $rule )
- : $rule,
- )
- );
- }
-
- /**
- * Builds a checkout request with a valid address and the given additional field values.
- *
- * @param string $method The request method (POST|PUT|PATCH).
- * @param array $additional_fields The additional field values to send.
- * @param array $address_fields Additional address field values, merged into both addresses.
- * @return \WP_REST_Request The request.
- */
- private function get_checkout_request_with_fields( $method, array $additional_fields, array $address_fields = array() ) {
- $address = array_merge(
- array(
- 'first_name' => 'test',
- 'last_name' => 'test',
- 'address_1' => 'test',
- 'city' => 'test',
- 'state' => '',
- 'postcode' => 'cb241ab',
- 'country' => 'GB',
- 'email' => 'testaccount@test.com',
- ),
- $address_fields
- );
- $request = new \WP_REST_Request( $method, '/wc/store/v1/checkout' );
- $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
- $request->set_body_params(
- array(
- 'billing_address' => (object) $address,
- 'shipping_address' => (object) $address,
- 'payment_method' => WC_Gateway_BACS::ID,
- 'additional_fields' => $additional_fields,
- )
- );
- return $request;
- }
-
- /**
- * @testdox A conditionally hidden field stays hidden while the field it depends on has no value, so its posted value is cleared.
- */
- public function test_conditional_hidden_field_cleared_when_dependency_has_no_value() {
- $this->unregister_fields();
- $this->register_conditional_referral_fields( 'hidden' );
-
- $request = $this->get_checkout_request_with_fields( 'POST', array( 'plugin-namespace/referral-detail' => 'posted while hidden' ) );
- $response = rest_get_server()->dispatch( $request );
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status(), print_r( $data, true ) );
- $fields = (array) $data['additional_fields'];
- $this->assertSame( '', $fields['plugin-namespace/referral-detail'], 'The detail field is hidden while referral-source has no value, so its posted value must be cleared' );
- }
-
- /**
- * @testdox A conditionally required field is not required while the field it depends on has no value, even when that field's key is absent from the request.
- */
- public function test_conditional_required_field_ignores_missing_dependency_key() {
- $this->unregister_fields();
- $this->register_conditional_referral_fields( 'required' );
-
- // Dependency never given a value: the detail field must not be required.
- $response = rest_get_server()->dispatch( $this->get_checkout_request_with_fields( 'POST', array() ) );
- $this->assertEquals( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->reset_session();
-
- // Dependency set to the trigger value: the detail field is required.
- $response = rest_get_server()->dispatch( $this->get_checkout_request_with_fields( 'POST', array( 'plugin-namespace/referral-source' => 'other' ) ) );
- $this->assertEquals( 400, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->reset_session();
-
- $response = rest_get_server()->dispatch(
- $this->get_checkout_request_with_fields(
- 'POST',
- array(
- 'plugin-namespace/referral-source' => 'other',
- 'plugin-namespace/referral-detail' => 'a friend',
- )
- )
- );
- $this->assertEquals( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- }
-
- /**
- * @testdox A conditional rule reads the persisted dependency value when the request omits its key, so omitting the trigger field does not bypass a required field.
- */
- public function test_conditional_required_field_uses_persisted_dependency_value() {
- $this->unregister_fields();
- $this->register_conditional_referral_fields( 'required' );
-
- $response = rest_get_server()->dispatch( $this->get_checkout_request_with_fields( 'PUT', array( 'plugin-namespace/referral-source' => 'other' ) ) );
- $this->assertEquals( 200, $response->get_status(), print_r( $response->get_data(), true ) );
-
- // The POST omits referral-source, but its persisted value still makes the detail field required.
- $response = rest_get_server()->dispatch( $this->get_checkout_request_with_fields( 'POST', array() ) );
- $this->assertEquals( 400, $response->get_status(), print_r( $response->get_data(), true ) );
-
- // The customer object survives reset_session, so clear the value this test persisted.
- $this->controller->persist_field_for_customer( 'plugin-namespace/referral-source', '', wc()->customer, 'other' );
- wc()->customer->save();
- }
-
- /**
- * @testdox A payment retry cannot clear a field required by a value saved on the order.
- * @testWith ["POST"]
- * ["PUT"]
- * ["PATCH"]
- * @param string $method The request method.
- */
- public function test_conditional_required_field_uses_pending_order_value( string $method ): void {
- $this->unregister_fields();
- $this->register_conditional_referral_fields( 'required' );
- update_option( 'woocommerce_manage_stock', 'yes' );
- $this->products[0]->set_manage_stock( true );
- $this->products[0]->set_stock_quantity( 10 );
- $this->products[0]->save();
-
- $order = wc_create_order();
- $order->add_product( $this->products[0], 1 );
- $order->set_status( OrderStatus::PENDING );
- $order->set_total( 30 );
- $order->set_cart_hash( wc()->cart->get_cart_hash() );
- $this->controller->persist_field_for_order( 'plugin-namespace/referral-source', 'other', $order, 'other', false );
- $this->controller->persist_field_for_order( 'plugin-namespace/referral-detail', 'a friend', $order, 'other', false );
- $order->save();
- wc_reserve_stock_for_order( $order );
- $this->assertSame( 1, wc_get_held_stock_quantity( $this->products[0] ), 'The pending order must hold stock before the retry.' );
- wc()->session->set( 'store_api_draft_order', $order->get_id() );
-
- $request = $this->get_checkout_request_with_fields( $method, array( 'plugin-namespace/referral-detail' => '' ) );
- $response = rest_get_server()->dispatch( $request );
-
- $this->assertSame( 400, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->assertSame( 'a friend', $this->controller->get_field_from_object( 'plugin-namespace/referral-detail', wc_get_order( $order->get_id() ) ), 'Rejected requests must leave the saved field unchanged.' );
- $this->assertSame( 1, wc_get_held_stock_quantity( $this->products[0] ), 'Field validation errors must leave the existing stock hold unchanged.' );
- }
-
- /**
- * @testdox A partial update matches saved text without changing its punctuation.
- * @testWith ["order", "checkout"]
- * ["contact", "customer"]
- * @param string $location The field location.
- * @param string $context The document object property holding the fields.
- */
- public function test_conditional_hidden_field_uses_raw_saved_text( string $location, string $context ): void {
- $this->unregister_fields();
- \woocommerce_register_additional_checkout_field(
- array(
- 'id' => 'plugin-namespace/referral-source',
- 'label' => 'Source',
- 'location' => $location,
- 'type' => 'text',
- )
- );
- \woocommerce_register_additional_checkout_field(
- array(
- 'id' => 'plugin-namespace/referral-detail',
- 'label' => 'Please specify',
- 'location' => $location,
- 'type' => 'text',
- 'hidden' => array( 'not' => $this->get_referral_source_rule( array( $context, 'additional_fields' ), "John's" ) ),
- )
- );
-
- $request = $this->get_checkout_request_with_fields(
- 'PUT',
- array(
- 'plugin-namespace/referral-source' => "John's",
- 'plugin-namespace/referral-detail' => 'first value',
- )
- );
- $response = rest_get_server()->dispatch( $request );
- $this->assertSame( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->assertSame( 'first value', ( (array) $response->get_data()['additional_fields'] )['plugin-namespace/referral-detail'] );
-
- $request = $this->get_checkout_request_with_fields( 'PUT', array( 'plugin-namespace/referral-detail' => 'second value' ) );
- $response = rest_get_server()->dispatch( $request );
- $this->assertSame( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->assertSame( 'second value', ( (array) $response->get_data()['additional_fields'] )['plugin-namespace/referral-detail'], 'The saved dependency must match the same rule as its posted value.' );
- }
-
- /**
- * @testdox A conditionally required contact field follows the same rules as order fields when its dependency has no value.
- */
- public function test_conditional_required_contact_field_ignores_missing_dependency_key() {
- $this->unregister_fields();
- $this->register_conditional_referral_fields( 'required', 'contact' );
-
- $response = rest_get_server()->dispatch( $this->get_checkout_request_with_fields( 'POST', array() ) );
- $this->assertEquals( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->reset_session();
-
- $response = rest_get_server()->dispatch( $this->get_checkout_request_with_fields( 'POST', array( 'plugin-namespace/referral-source' => 'other' ) ) );
- $this->assertEquals( 400, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->reset_session();
-
- $response = rest_get_server()->dispatch(
- $this->get_checkout_request_with_fields(
- 'POST',
- array(
- 'plugin-namespace/referral-source' => 'other',
- 'plugin-namespace/referral-detail' => 'a friend',
- )
- )
- );
- $this->assertEquals( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- }
-
- /**
- * @testdox A conditionally required address field follows the same rules as order fields when its dependency has no value.
- */
- public function test_conditional_required_address_field_ignores_missing_dependency_key() {
- $this->unregister_fields();
- $this->register_conditional_referral_fields( 'required', 'address' );
-
- $response = rest_get_server()->dispatch( $this->get_checkout_request_with_fields( 'POST', array() ) );
- $this->assertEquals( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->reset_session();
-
- $response = rest_get_server()->dispatch(
- $this->get_checkout_request_with_fields( 'POST', array(), array( 'plugin-namespace/referral-source' => 'other' ) )
- );
- $this->assertEquals( 400, $response->get_status(), print_r( $response->get_data(), true ) );
- $this->reset_session();
-
- $response = rest_get_server()->dispatch(
- $this->get_checkout_request_with_fields(
- 'POST',
- array(),
- array(
- 'plugin-namespace/referral-source' => 'other',
- 'plugin-namespace/referral-detail' => 'a friend',
- )
- )
- );
- $this->assertEquals( 200, $response->get_status(), print_r( $response->get_data(), true ) );
- }
}