Skip to content

Commit cbfc976

Browse files
committed
fix: port non-tokenized ECE massaging for HK-based addresses
1 parent 65c9e45 commit cbfc976

File tree

3 files changed

+198
-124
lines changed

3 files changed

+198
-124
lines changed

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

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public function maybe_skip_postcode_validation( $valid, $postcode, $country ) {
262262
return $valid;
263263
}
264264

265-
// We padded the string with `0` in the `get_normalized_postal_code` method.
265+
// We padded the string with `0` in the express_checkout_button_helper's get_normalized_postal_code method.
266266
// It's a flimsy check, but better than nothing.
267267
// Plus, this check is only made for the scenarios outlined in the `tokenized_cart_store_api_address_normalization` method.
268268
if ( substr( $postcode, - 1 ) === '0' ) {
@@ -285,15 +285,142 @@ 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 ( ! empty( $state ) && ! \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $state ) ) ) {
311+
$postcode = $address['postcode'] ?? '';
312+
if ( ! empty( $postcode ) && \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $postcode ) ) ) {
313+
$address['state'] = $postcode;
314+
}
315+
}
316+
}
317+
318+
// States from Apple Pay or Google Pay are in long format, we need their short format.
289319
$state = $address['state'] ?? '';
290320
if ( ! empty( $state ) ) {
291-
$address['state'] = $this->express_checkout_button_helper->get_normalized_state( $state, $country );
321+
$address['state'] = $this->get_normalized_state( $state, $country );
292322
}
293323

294324
return $address;
295325
}
296326

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

316443
return $address;
317444
}
318445

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

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,49 @@ public function test_tokenized_cart_avoid_address_postcode_normalization_if_rout
186186
$this->assertSame( 'H3B', $billing_address['postcode'] );
187187
}
188188

189+
/**
190+
* Test 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+
}
209+
210+
/**
211+
* Test when Hong Kong regions/districts are delivered in the postcode field due to an Apple Pay bug.
212+
*/
213+
public function test_tokenized_cart_hk_postcode_with_region() {
214+
$request = new WP_REST_Request();
215+
$request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' );
216+
$request->set_header( 'X-WooPayments-Tokenized-Cart-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
217+
$request->set_header( 'Content-Type', 'application/json' );
218+
$request->set_param(
219+
'shipping_address',
220+
[
221+
'country' => Country_Code::HONG_KONG,
222+
'state' => 'invalid-state',
223+
'postcode' => 'kowloon',
224+
]
225+
);
226+
227+
$this->ajax_handler->tokenized_cart_store_api_address_normalization( null, null, $request );
228+
$shipping_address = $request->get_param( 'shipping_address' );
229+
$this->assertEquals( Country_Code::HONG_KONG, $shipping_address['country'] );
230+
}
231+
189232
public function test_tokenized_cart_italy_state_venezia_normalization() {
190233
$request = new WP_REST_Request();
191234
$request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' );

0 commit comments

Comments
 (0)