Skip to content

Commit 40e5cb9

Browse files
authored
fix: port non-tokenized ECE massaging for HK-based addresses (#10815)
1 parent b81cadc commit 40e5cb9

File tree

4 files changed

+229
-123
lines changed

4 files changed

+229
-123
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: update
3+
4+
refactor: add data massaging from legacy google pay/apple pay for HK-based addresses

includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,15 +285,145 @@ private function transform_ece_address_state_data( $address ) {
285285
return $address;
286286
}
287287

288-
// states from Apple Pay or Google Pay might be in long format, we need their short format.
288+
// Due to a bug in Apple Pay, the "Region" part of a Hong Kong address is delivered in
289+
// `shipping_postcode`, so we need some special case handling for that. According to
290+
// our sources at Apple Pay people will sometimes use the district or even sub-district
291+
// for this value. As such we check against all regions, districts, and sub-districts
292+
// with both English and Mandarin spelling.
293+
//
294+
// @reykjalin: The check here is quite elaborate in an attempt to make sure this doesn't break once
295+
// Apple Pay fixes the bug that causes address values to be in the wrong place. Because of that the
296+
// algorithm becomes:
297+
// 1. Use the supplied state if it's valid (in case Apple Pay bug is fixed)
298+
// 2. Use the value supplied in the postcode if it's a valid HK region (equivalent to a WC state).
299+
// 3. Fall back to the value supplied in the state. This will likely cause a validation error, in
300+
// which case a merchant can reach out to us so we can either: 1) add whatever the customer used
301+
// as a state to our list of valid states; or 2) let them know the customer must spell the state
302+
// in some way that matches our list of valid states.
303+
//
304+
// @reykjalin: This HK specific sanitazation *should be removed* once Apple Pay fix
305+
// the address bug. More info on that in pc4etw-bY-p2.
306+
if ( Country_Code::HONG_KONG === $country ) {
307+
include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-hong-kong-states.php';
308+
309+
$state = $address['state'] ?? '';
310+
if ( ! \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $state ) ) ) {
311+
$postcode = $address['postcode'] ?? '';
312+
if ( strtolower( $postcode ) === 'hongkong' ) {
313+
$postcode = 'hong kong';
314+
}
315+
if ( \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $postcode ) ) ) {
316+
$address['state'] = $postcode;
317+
}
318+
}
319+
}
320+
321+
// States from Apple Pay or Google Pay are in long format, we need their short format.
289322
$state = $address['state'] ?? '';
290323
if ( ! empty( $state ) ) {
291-
$address['state'] = $this->express_checkout_button_helper->get_normalized_state( $state, $country );
324+
$address['state'] = $this->get_normalized_state( $state, $country );
292325
}
293326

294327
return $address;
295328
}
296329

330+
/**
331+
* Gets the normalized state/county field because in some
332+
* cases, the state/county field is formatted differently from
333+
* what WC is expecting and throws an error. An example
334+
* for Ireland, the county dropdown in Chrome shows "Co. Clare" format.
335+
*
336+
* @param string $state Full state name or an already normalized abbreviation.
337+
* @param string $country Two-letter country code.
338+
*
339+
* @return string Normalized state abbreviation.
340+
*/
341+
private function get_normalized_state( $state, $country ) {
342+
// If it's empty or already normalized, skip.
343+
if ( ! $state || $this->is_normalized_state( $state, $country ) ) {
344+
return $state;
345+
}
346+
347+
// Try to match state from the Express Checkout API list of states.
348+
$state = $this->get_normalized_state_from_ece_states( $state, $country );
349+
350+
// If it's normalized, return.
351+
if ( $this->is_normalized_state( $state, $country ) ) {
352+
return $state;
353+
}
354+
355+
// If the above doesn't work, fallback to matching against the list of translated
356+
// states from WooCommerce.
357+
return $this->get_normalized_state_from_wc_states( $state, $country );
358+
}
359+
360+
/**
361+
* Checks if given state is normalized.
362+
*
363+
* @param string $state State.
364+
* @param string $country Two-letter country code.
365+
*
366+
* @return bool Whether state is normalized or not.
367+
*/
368+
private function is_normalized_state( $state, $country ) {
369+
$wc_states = WC()->countries->get_states( $country );
370+
return is_array( $wc_states ) && array_key_exists( $state, $wc_states );
371+
}
372+
373+
/**
374+
* Get normalized state from Express Checkout API dropdown list of states.
375+
*
376+
* @param string $state Full state name or state code.
377+
* @param string $country Two-letter country code.
378+
*
379+
* @return string Normalized state or original state input value.
380+
*/
381+
private function get_normalized_state_from_ece_states( $state, $country ) {
382+
// Include Express Checkout Element API State list for compatibility with WC countries/states.
383+
include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-element-states.php';
384+
$pr_states = \WCPay\Constants\Express_Checkout_Element_States::STATES;
385+
386+
if ( ! isset( $pr_states[ $country ] ) ) {
387+
return $state;
388+
}
389+
390+
foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) {
391+
$sanitized_state_string = $this->express_checkout_button_helper->sanitize_string( $state );
392+
// Checks if input state matches with Express Checkout state code (0), name (1) or localName (2).
393+
if (
394+
( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->express_checkout_button_helper->sanitize_string( $pr_state[0] ) ) ||
395+
( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->express_checkout_button_helper->sanitize_string( $pr_state[1] ) ) ||
396+
( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->express_checkout_button_helper->sanitize_string( $pr_state[2] ) )
397+
) {
398+
return $wc_state_abbr;
399+
}
400+
}
401+
402+
return $state;
403+
}
404+
405+
/**
406+
* Get normalized state from WooCommerce list of translated states.
407+
*
408+
* @param string $state Full state name or state code.
409+
* @param string $country Two-letter country code.
410+
*
411+
* @return string Normalized state or original state input value.
412+
*/
413+
private function get_normalized_state_from_wc_states( $state, $country ) {
414+
$wc_states = WC()->countries->get_states( $country );
415+
416+
if ( is_array( $wc_states ) ) {
417+
foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) {
418+
if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) {
419+
return $wc_state_abbr;
420+
}
421+
}
422+
}
423+
424+
return $state;
425+
}
426+
297427
/**
298428
* Transform a Google Pay/Apple Pay postcode address data fields into values that are valid for WooCommerce.
299429
*
@@ -310,12 +440,36 @@ private function transform_ece_address_postcode_data( $address ) {
310440
// Normalizes postal code in case of redacted data from Apple Pay or Google Pay.
311441
$postcode = $address['postcode'] ?? '';
312442
if ( ! empty( $postcode ) ) {
313-
$address['postcode'] = $this->express_checkout_button_helper->get_normalized_postal_code( $postcode, $country );
443+
$address['postcode'] = $this->get_normalized_postal_code( $postcode, $country );
314444
}
315445

316446
return $address;
317447
}
318448

449+
/**
450+
* Normalizes postal code in case of redacted data from Apple Pay.
451+
*
452+
* @param string $postcode Postal code.
453+
* @param string $country Country.
454+
*/
455+
private function get_normalized_postal_code( $postcode, $country ) {
456+
/**
457+
* Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively
458+
* when passing it back from the shippingcontactselected object. This causes WC to invalidate
459+
* the postal code and not calculate shipping zones correctly.
460+
*/
461+
if ( Country_Code::UNITED_KINGDOM === $country ) {
462+
// Replaces a redacted string with something like N1C0000.
463+
return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '0' );
464+
}
465+
if ( Country_Code::CANADA === $country ) {
466+
// Replaces a redacted string with something like H3B000.
467+
return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '0' );
468+
}
469+
470+
return $postcode;
471+
}
472+
319473
/**
320474
* Modify country locale settings to handle express checkout address requirements.
321475
*

includes/express-checkout/class-wc-payments-express-checkout-button-helper.php

Lines changed: 0 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -792,126 +792,6 @@ public function get_taxes_like_cart( $product, $price ) {
792792
return WC_Tax::calc_tax( $price, $rates, false );
793793
}
794794

795-
/**
796-
* Gets the normalized state/county field because in some
797-
* cases, the state/county field is formatted differently from
798-
* what WC is expecting and throws an error. An example
799-
* for Ireland, the county dropdown in Chrome shows "Co. Clare" format.
800-
*
801-
* @param string $state Full state name or an already normalized abbreviation.
802-
* @param string $country Two-letter country code.
803-
*
804-
* @return string Normalized state abbreviation.
805-
*/
806-
public function get_normalized_state( $state, $country ) {
807-
// If it's empty or already normalized, skip.
808-
if ( ! $state || $this->is_normalized_state( $state, $country ) ) {
809-
return $state;
810-
}
811-
812-
// Try to match state from the Express Checkout API list of states.
813-
$state = $this->get_normalized_state_from_ece_states( $state, $country );
814-
815-
// If it's normalized, return.
816-
if ( $this->is_normalized_state( $state, $country ) ) {
817-
return $state;
818-
}
819-
820-
// If the above doesn't work, fallback to matching against the list of translated
821-
// states from WooCommerce.
822-
return $this->get_normalized_state_from_wc_states( $state, $country );
823-
}
824-
825-
/**
826-
* Checks if given state is normalized.
827-
*
828-
* @param string $state State.
829-
* @param string $country Two-letter country code.
830-
*
831-
* @return bool Whether state is normalized or not.
832-
*/
833-
public function is_normalized_state( $state, $country ) {
834-
$wc_states = WC()->countries->get_states( $country );
835-
return is_array( $wc_states ) && array_key_exists( $state, $wc_states );
836-
}
837-
838-
/**
839-
* Get normalized state from Express Checkout API dropdown list of states.
840-
*
841-
* @param string $state Full state name or state code.
842-
* @param string $country Two-letter country code.
843-
*
844-
* @return string Normalized state or original state input value.
845-
*/
846-
public function get_normalized_state_from_ece_states( $state, $country ) {
847-
// Include Express Checkout Element API State list for compatibility with WC countries/states.
848-
include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-element-states.php';
849-
$pr_states = \WCPay\Constants\Express_Checkout_Element_States::STATES;
850-
851-
if ( ! isset( $pr_states[ $country ] ) ) {
852-
return $state;
853-
}
854-
855-
foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) {
856-
$sanitized_state_string = $this->sanitize_string( $state );
857-
// Checks if input state matches with Express Checkout state code (0), name (1) or localName (2).
858-
if (
859-
( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) ||
860-
( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) ||
861-
( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) )
862-
) {
863-
return $wc_state_abbr;
864-
}
865-
}
866-
867-
return $state;
868-
}
869-
870-
/**
871-
* Get normalized state from WooCommerce list of translated states.
872-
*
873-
* @param string $state Full state name or state code.
874-
* @param string $country Two-letter country code.
875-
*
876-
* @return string Normalized state or original state input value.
877-
*/
878-
public function get_normalized_state_from_wc_states( $state, $country ) {
879-
$wc_states = WC()->countries->get_states( $country );
880-
881-
if ( is_array( $wc_states ) ) {
882-
foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) {
883-
if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) {
884-
return $wc_state_abbr;
885-
}
886-
}
887-
}
888-
889-
return $state;
890-
}
891-
892-
/**
893-
* Normalizes postal code in case of redacted data from Apple Pay.
894-
*
895-
* @param string $postcode Postal code.
896-
* @param string $country Country.
897-
*/
898-
public function get_normalized_postal_code( $postcode, $country ) {
899-
/**
900-
* Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively
901-
* when passing it back from the shippingcontactselected object. This causes WC to invalidate
902-
* the postal code and not calculate shipping zones correctly.
903-
*/
904-
if ( Country_Code::UNITED_KINGDOM === $country ) {
905-
// Replaces a redacted string with something like N1C0000.
906-
return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '0' );
907-
}
908-
if ( Country_Code::CANADA === $country ) {
909-
// Replaces a redacted string with something like H3B000.
910-
return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '0' );
911-
}
912-
913-
return $postcode;
914-
}
915795

916796
/**
917797
* Sanitize string for comparison.

tests/unit/express-checkout/test-class-wc-payments-express-checkout-ajax-handler.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,74 @@ public function test_tokenized_cart_avoid_address_postcode_normalization_if_rout
186186
$this->assertSame( 'H3B', $billing_address['postcode'] );
187187
}
188188

189+
/**
190+
* When Hong Kong has an invalid state, it should remain unchanged.
191+
*/
192+
public function test_tokenized_cart_hk_invalid_state() {
193+
$request = new WP_REST_Request();
194+
$request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' );
195+
$request->set_header( 'X-WooPayments-Tokenized-Cart-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
196+
$request->set_header( 'Content-Type', 'application/json' );
197+
$request->set_param(
198+
'shipping_address',
199+
[
200+
'country' => Country_Code::HONG_KONG,
201+
'state' => 'invalid-state',
202+
]
203+
);
204+
205+
$this->ajax_handler->tokenized_cart_store_api_address_normalization( null, null, $request );
206+
$shipping_address = $request->get_param( 'shipping_address' );
207+
$this->assertEquals( Country_Code::HONG_KONG, $shipping_address['country'] );
208+
$this->assertEquals( 'invalid-state', $shipping_address['state'] );
209+
}
210+
211+
/**
212+
* When Hong Kong regions/districts are delivered in the postcode field due to an Apple Pay bug, they should be adjusted.
213+
*/
214+
public function test_tokenized_cart_hk_postcode_with_region() {
215+
$request = new WP_REST_Request();
216+
$request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' );
217+
$request->set_header( 'X-WooPayments-Tokenized-Cart-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
218+
$request->set_header( 'Content-Type', 'application/json' );
219+
$request->set_param(
220+
'shipping_address',
221+
[
222+
'country' => Country_Code::HONG_KONG,
223+
'state' => 'invalid-state',
224+
'postcode' => 'kowloon',
225+
]
226+
);
227+
228+
$this->ajax_handler->tokenized_cart_store_api_address_normalization( null, null, $request );
229+
$shipping_address = $request->get_param( 'shipping_address' );
230+
$this->assertEquals( Country_Code::HONG_KONG, $shipping_address['country'] );
231+
$this->assertEquals( 'KOWLOON', $shipping_address['state'] );
232+
}
233+
234+
/**
235+
* When the `九龍` Hong Kong region is delivered in the postcode field, it should be adjusted for WooCommerce to be able to handle it.
236+
*/
237+
public function test_tokenized_cart_hk_postcode_with_九龍_region() {
238+
$request = new WP_REST_Request();
239+
$request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' );
240+
$request->set_header( 'X-WooPayments-Tokenized-Cart-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
241+
$request->set_header( 'Content-Type', 'application/json' );
242+
$request->set_param(
243+
'shipping_address',
244+
[
245+
'country' => Country_Code::HONG_KONG,
246+
'state' => 'invalid-state',
247+
'postcode' => '九龍',
248+
]
249+
);
250+
251+
$this->ajax_handler->tokenized_cart_store_api_address_normalization( null, null, $request );
252+
$shipping_address = $request->get_param( 'shipping_address' );
253+
$this->assertEquals( Country_Code::HONG_KONG, $shipping_address['country'] );
254+
$this->assertEquals( 'KOWLOON', $shipping_address['state'] );
255+
}
256+
189257
public function test_tokenized_cart_italy_state_venezia_normalization() {
190258
$request = new WP_REST_Request();
191259
$request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' );

0 commit comments

Comments
 (0)