diff --git a/assets/wp/js/form-billing-address.js b/assets/wp/js/form-billing-address.js index 3d87087..5cdea19 100644 --- a/assets/wp/js/form-billing-address.js +++ b/assets/wp/js/form-billing-address.js @@ -825,9 +825,7 @@ if (window.console) console.log('[KiriminAja] Persist district selection', data); var updateKey = kiriofBuildBlockCartUpdateKey(data); if (updateKey && updateKey === kiriofLastBlockCartUpdateKey) { - if (window.console) console.log('[KiriminAja] District already persisted (dedup), refreshing rates'); - kiriofScheduleBlockShippingRatesRefresh(160); - kiriofForceBlockCartUpdate(data.destination_name || '', data.destination_id || ''); + if (window.console) console.log('[KiriminAja] District already persisted (dedup)'); return; } @@ -835,9 +833,10 @@ function kiriofAfterBlockDistrictPersist() { kiriofScheduleBlockShippingRatesRefresh(80); + kiriofSyncBlockDistrictWarningState(); + } - // Save session data (insurance, payment_method, destination) via AJAX - // as fallback when extensionCartUpdate is not available. + function kiriofPersistBlockDistrictFallback() { var ajaxUrl = (typeof kiriofAjax !== 'undefined' && kiriofAjax.ajaxurl) ? kiriofAjax.ajaxurl : kiriofBillingAddressConfig.ajaxUrl || ''; @@ -874,11 +873,10 @@ result.then(function() { kiriofAfterBlockDistrictPersist(); }).catch(function() { - kiriofScheduleBlockShippingRatesRefresh(160); - kiriofForceBlockCartUpdate(data.destination_name || '', data.destination_id || ''); + kiriofPersistBlockDistrictFallback(); }); } else { - kiriofAfterBlockDistrictPersist(); + kiriofPersistBlockDistrictFallback(); } } @@ -926,7 +924,8 @@ // Direct change handler on the select for reliability. // The delegated handler on `document` may not fire in WooCommerce // blocks (React 18 captures native events before they reach document). - $select.on('change.kiriofBlockDistrictDirect', function() { + $select.on('change.kiriofBlockDistrictDirect', function(event) { + event.kiriofDistrictHandled = true; if (window.console) console.log('[KiriminAja] Select changed', jQuery(this).val()); try { var districtVal = jQuery(this).val(); @@ -1561,7 +1560,8 @@ } jQuery(document).off('change.kiriofBlockDistrict', '[name="' + kiriofFieldId + '"], .kiriof-block-district-select') - .on('change.kiriofBlockDistrict', '[name="' + kiriofFieldId + '"], .kiriof-block-district-select', function() { + .on('change.kiriofBlockDistrict', '[name="' + kiriofFieldId + '"], .kiriof-block-district-select', function(event) { + if (event.kiriofDistrictHandled) return; var val = jQuery(this).val(); var label = jQuery(this).find('option:selected').text(); var postcode = kiriofGetCurrentPostcodeKey(); diff --git a/inc/Base/KiriminAjaApi.php b/inc/Base/KiriminAjaApi.php index 9a97db5..f45e766 100644 --- a/inc/Base/KiriminAjaApi.php +++ b/inc/Base/KiriminAjaApi.php @@ -204,14 +204,17 @@ public function get($endpoint, $body = array(), $log_context = array()) return $this->finalize_response( $response, $request_meta ); } - public function post($endpoint, $body = array(), $log_context = array()) + public function post($endpoint, $body = array(), $log_context = array(), $request_args = array()) { $requestBody = $body; if ( is_array( $body ) && empty( $body ) ) { $requestBody = (object) array(); } - $args = wp_parse_args(array('body' => wp_json_encode($requestBody)), $this->default_args); + $args = wp_parse_args( + array_merge( $request_args, array( 'body' => wp_json_encode( $requestBody ) ) ), + $this->default_args + ); $request_meta = $this->build_request_log_context( 'POST', $endpoint, $body, $log_context ); $response = wp_remote_post($this->base_url . $endpoint, $args); diff --git a/inc/Controllers/AccountAddressController.php b/inc/Controllers/AccountAddressController.php index 74a5d1f..03cdf98 100644 --- a/inc/Controllers/AccountAddressController.php +++ b/inc/Controllers/AccountAddressController.php @@ -20,6 +20,7 @@ public function addDistrictFields( array $fields, string $address_type ): array return $fields; } + $fields = $this->removeBlocksDistrictFields( $fields, $address_type ); $district = ( new CustomerDistrictService() )->get( get_current_user_id(), $address_type ); $field_key = $address_type . '_kiriof_destination_area'; $name_key = $address_type . '_kiriof_destination_area_name'; @@ -49,6 +50,17 @@ public function addDistrictFields( array $fields, string $address_type ): array return $fields; } + private function removeBlocksDistrictFields( array $fields, string $address_type ): array { + $canonical_key = $address_type . '_kiriof_destination_area'; + foreach ( array_keys( $fields ) as $key ) { + if ( $key !== $canonical_key && false !== strpos( (string) $key, 'kiriof_destination_area' ) ) { + unset( $fields[ $key ] ); + } + } + + return $fields; + } + public function validateDistrict( int $user_id, string $address_type, array $address, $customer ): void { unset( $user_id, $address, $customer ); if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) { diff --git a/inc/Controllers/CheckoutController.php b/inc/Controllers/CheckoutController.php index bd07def..9922d6a 100644 --- a/inc/Controllers/CheckoutController.php +++ b/inc/Controllers/CheckoutController.php @@ -2079,14 +2079,8 @@ public function kiriof_store_api_update_checkout( $data ) { WC()->session->set( 'kiriof_destination_postcode_map', $saved_destination_map ); } - if ( - ( $destination_id > 0 || '' !== $shipping_method || '' !== $payment_method ) - && isset( WC()->cart ) - && is_object( WC()->cart ) - && method_exists( WC()->cart, 'calculate_totals' ) - ) { - WC()->cart->calculate_totals(); - } + // WooCommerce recalculates the Store API cart after this callback returns. + // Calling calculate_totals() here duplicates shipping and pricing work. } /** diff --git a/inc/Repositories/KiriminajaApiRepository.php b/inc/Repositories/KiriminajaApiRepository.php index e653673..fffb2d0 100644 --- a/inc/Repositories/KiriminajaApiRepository.php +++ b/inc/Repositories/KiriminajaApiRepository.php @@ -69,6 +69,9 @@ public function getPricing($payload){ ], array( 'source' => 'kiriminaja_shipping', 'operation' => 'get_pricing', + ), array( + 'timeout' => (int) apply_filters( 'kiriof_pricing_api_timeout', 8 ), + 'httpversion' => '1.1', )); } diff --git a/tests/CheckoutShippingPerformanceTest.php b/tests/CheckoutShippingPerformanceTest.php index b829626..a2e37e2 100644 --- a/tests/CheckoutShippingPerformanceTest.php +++ b/tests/CheckoutShippingPerformanceTest.php @@ -182,4 +182,35 @@ public function plugin_ajax_handlers_defer_totals_recalculation_to_woocommerce_u 'Fee AJAX should only cache calculated fees; the following update_checkout request renders them' ); } + + #[Test] + public function pricing_api_uses_a_short_filterable_timeout(): void + { + $repository = file_get_contents( PLUGIN_DIR . '/inc/Repositories/KiriminajaApiRepository.php' ); + $api = file_get_contents( PLUGIN_DIR . '/inc/Base/KiriminAjaApi.php' ); + + $this->assertStringContainsString("apply_filters( 'kiriof_pricing_api_timeout', 8 )", $repository); + $this->assertStringContainsString("'httpversion' => '1.1'", $repository); + $this->assertStringContainsString('$request_args = array()', $api); + $this->assertStringContainsString('array_merge( $request_args', $api); + } + + #[Test] + public function block_district_persistence_uses_ajax_only_as_fallback(): void + { + $script = file_get_contents( PLUGIN_DIR . '/assets/wp/js/form-billing-address.js' ); + $start = strpos( $script, 'function kiriofPersistBlockDistrictSelection' ); + $end = strpos( $script, 'var kiriofLastDistrictResults', $start ); + $body = substr( $script, $start, $end - $start ); + + $this->assertStringContainsString('function kiriofPersistBlockDistrictFallback()', $body); + $this->assertStringContainsString('result.then(function()', $body); + $this->assertStringContainsString('kiriofAfterBlockDistrictPersist();', $body); + $this->assertStringContainsString('kiriofPersistBlockDistrictFallback();', $body); + $this->assertLessThan( + strpos( $body, 'kiriofPersistBlockDistrictFallback();' ), + strpos( $body, 'kiriofAfterBlockDistrictPersist();' ), + 'Successful extensionCartUpdate should finish without the fallback AJAX and raw Store API update chain' + ); + } } diff --git a/tests/MyAccountDistrictTest.php b/tests/MyAccountDistrictTest.php index b852d5e..703576b 100644 --- a/tests/MyAccountDistrictTest.php +++ b/tests/MyAccountDistrictTest.php @@ -17,6 +17,16 @@ public function account_address_controller_registers_account_only_district_lifec $this->assertStringContainsString("\$address_type . '_kiriof_destination_area'", $content); } + #[Test] + public function account_address_controller_removes_blocks_duplicate_district_fields(): void + { + $content = file_get_contents(PLUGIN_DIR . '/inc/Controllers/AccountAddressController.php'); + + $this->assertStringContainsString('removeBlocksDistrictFields', $content); + $this->assertStringContainsString("strpos( (string) \$key, 'kiriof_destination_area' )", $content); + $this->assertStringContainsString("\$key !== \$canonical_key", $content); + } + #[Test] public function account_district_service_reads_and_writes_canonical_and_legacy_metadata(): void { diff --git a/tests/ShopVerseBlockCheckoutCompatibilityTest.php b/tests/ShopVerseBlockCheckoutCompatibilityTest.php index 37bcc94..4490618 100644 --- a/tests/ShopVerseBlockCheckoutCompatibilityTest.php +++ b/tests/ShopVerseBlockCheckoutCompatibilityTest.php @@ -1925,7 +1925,7 @@ public function store_api_update_callback_does_not_overwrite_visible_block_postc } #[Test] - public function store_api_update_callback_recalculates_totals_after_block_destination_changes(): void + public function store_api_update_callback_defers_totals_to_woocommerce(): void { $content = file_get_contents(PLUGIN_DIR . '/inc/Controllers/CheckoutController.php'); $start = strpos($content, 'public function kiriof_store_api_update_checkout'); @@ -1934,21 +1934,15 @@ public function store_api_update_callback_recalculates_totals_after_block_destin $this->assertNotFalse($end, 'Store API update callback boundary must exist'); $methodBody = substr($content, $start, $end - $start); - $destinationPosition = strpos($methodBody, "WC()->session->set( 'kiriof_destination_area', \$destination_id );"); - $calculatePosition = strpos($methodBody, 'WC()->cart->calculate_totals();'); - - $this->assertNotFalse( - $destinationPosition, + $this->assertStringContainsString( + "WC()->session->set( 'kiriof_destination_area', \$destination_id );", + $methodBody, 'Store API callback must persist the selected District before rates are recalculated' ); - $this->assertNotFalse( - $calculatePosition, - 'Store API callback must recalculate totals after District/payment/shipping context changes so block checkout receives fresh rates' - ); - $this->assertLessThan( - $calculatePosition, - $destinationPosition, - 'Destination must be in session before Woo recalculates shipping totals' + $this->assertStringNotContainsString( + 'WC()->cart->calculate_totals();', + $methodBody, + 'Store API already recalculates the cart after extension callbacks; nested totals duplicate pricing calls' ); }