Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 105 additions & 77 deletions includes/class-wc-amazon-payments-advanced-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,46 +139,6 @@ protected static function get_client() {
return self::$amazonpay_client;
}

/**
* Location type detection.
*
* @param object $location Location to check.
* @return boolean
*/
private static function location_is_continent( $location ) {
return 'continent' === $location->type;
}

/**
* Location type detection.
*
* @param object $location Location to check.
* @return boolean
*/
private static function location_is_country( $location ) {
return 'country' === $location->type;
}

/**
* Location type detection.
*
* @param object $location Location to check.
* @return boolean
*/
private static function location_is_state( $location ) {
return 'state' === $location->type;
}

/**
* Location type detection.
*
* @param object $location Location to check.
* @return boolean
*/
private static function location_is_postcode( $location ) {
return 'postcode' === $location->type;
}

/**
* Remove string from string letters.
*
Expand Down Expand Up @@ -285,57 +245,94 @@ protected static function get_shipping_restrictions() {
}
}

$current_loops_zones = array();
foreach ( $raw_zones as $raw_zone ) {
$zone = new WC_Shipping_Zone( $raw_zone );
$methods = $zone->get_shipping_methods( true, 'json' );
if ( empty( $methods ) ) {
continue; // If no shipping methods, we assume no support on this region.
}

$locations = $zone->get_zone_locations( 'json' );
$continents = array_filter( $locations, array( __CLASS__, 'location_is_continent' ) );
$countries = array_filter( $locations, array( __CLASS__, 'location_is_country' ) );
$states = array_filter( $locations, array( __CLASS__, 'location_is_state' ) );
$postcodes = array_filter( $locations, array( __CLASS__, 'location_is_postcode' ) ); // HARD TODO: Postcode wildcards can't be implemented afaik.

foreach ( $continents as $location ) {
foreach ( $all_continents[ $location->code ]['countries'] as $country ) {
if ( ! isset( $zones[ $country ] ) ) {
$zones[ $country ] = new stdClass(); // If we use an empty array it'll be treated as an array in JSON.
}
$locations = $zone->get_zone_locations( 'json' );

/**
* Post code rules will be applied to every country defined by the current zone locations.
* If there is no country available they will not be applied.
*/
$postcode_rules = array();

/**
* @see https://developer.amazon.com/docs/amazon-pay-api-v2/checkout-session.html#type-restriction
*/
foreach ( $locations as $location ) {
switch ( $location->type ) {
case 'continent':
foreach ( $all_continents[ $location->code ]['countries'] as $country ) {
if ( ! isset( $current_loops_zones[ $country ] ) ) {
$current_loops_zones[ $country ] = new stdClass(); // If we use an empty array it'll be treated as an array in JSON.
}
}
break;
case 'country':
$current_loops_zones[ $location->code ] = new stdClass(); // If we use an empty array it'll be treated as an array in JSON.
break;
case 'state':
$location_codes = explode( ':', $location->code );
$country = strtoupper( $location_codes[0] );
$state = $location_codes[1];
if ( ! isset( $current_loops_zones[ $country ] ) ) {
$current_loops_zones[ $country ] = new stdClass(); // If we use an empty array it'll be treated as an array in JSON.
$current_loops_zones[ $country ]->statesOrRegions = array(); // phpcs:ignore WordPress.NamingConventions
} else {
if ( ! isset( $current_loops_zones[ $country ]->statesOrRegions ) ) { // phpcs:ignore WordPress.NamingConventions
// Do not override anything if the country is allowed fully.
break;
}
}

$current_loops_zones[ $country ]->statesOrRegions[] = $state; // phpcs:ignore WordPress.NamingConventions
if ( 'US' !== $country ) {

$current_loops_zones[ $country ]->statesOrRegions[] = $all_states[ $country ][ $state ]; // phpcs:ignore WordPress.NamingConventions
$variation_state = self::remove_signs( $all_states[ $country ][ $state ] );
if ( $variation_state !== $all_states[ $country ][ $state ] ) {
$current_loops_zones[ $country ]->statesOrRegions[] = $variation_state; // phpcs:ignore WordPress.NamingConventions
}
}
break;
case 'postcode':
$postcode = $location->code;
if ( strstr( $postcode, '...' ) ) {
$postcode = explode( '...', $postcode );
$postcode_rules = array_merge( $postcode_rules, array_map( 'strval', range( (int) $postcode['0'], (int) $postcode['1'] ) ) );
} else {
$postcode_rules[] = $postcode;
}
break;
}
}

foreach ( $countries as $location ) {
$country = $location->code;
// We're forcing it to be an empty, since it will override if the full country is allowed anywhere.
$zones[ $country ] = new stdClass(); // If we use an empty array it'll be treated as an array in JSON.
}

foreach ( $states as $location ) {
$location_codes = explode( ':', $location->code );
$country = strtoupper( $location_codes[0] );
$state = $location_codes[1];
if ( ! isset( $zones[ $country ] ) ) {
$zones[ $country ] = new stdClass(); // If we use an empty array it'll be treated as an array in JSON.
$zones[ $country ]->statesOrRegions = array();
} else {
if ( ! isset( $zones[ $country ]->statesOrRegions ) ) {
// Do not override anything if the country is allowed fully.
continue;
$postcode_rules = array_unique( $postcode_rules );

foreach ( $current_loops_zones as $country => $object ) {
if ( ! empty( $postcode_rules ) ) {
$object->zipCodes = $postcode_rules; // phpcs:ignore WordPress.NamingConventions
/**
* If one of the rules was restricting a Region (or more) with postcodes.
* We need to unset the statesOrRegions added, cause Amazon would allow
* the whole Region for shipping if we don't.
*/
if ( isset( $object->statesOrRegions ) ) { // phpcs:ignore WordPress.NamingConventions
unset( $object->statesOrRegions ); // phpcs:ignore WordPress.NamingConventions
}
}

$zones[ $country ]->statesOrRegions[] = $state;
if ( 'US' !== $country ) {

$zones[ $country ]->statesOrRegions[] = $all_states[ $country ][ $state ];
$variation_state = self::remove_signs( $all_states[ $country ][ $state ] );
if ( $variation_state !== $all_states[ $country ][ $state ] ) {
$zones[ $country ]->statesOrRegions[] = $variation_state;
}
if ( ! empty( $zones[ $country ] ) ) {
$zones[ $country ] = self::pick_most_generic_option( $zones[ $country ], $object );
} else {
$zones[ $country ] = $object;
}
}
$current_loops_zones = array();
}

$zones = array_intersect_key( $zones, $shipping_countries );
Expand Down Expand Up @@ -962,4 +959,35 @@ public static function get_secret_key_page_url() {
return isset( $urls[ $region ] ) ? $urls[ $region ] : false;
}

/**
* Returns the most generic restrictions of the 2.
*
* The most generic is the one with the less specifications so the most
* possible generic is the one with empty statesOrRegions and zipCodes properties.
*
* @param stdClass $primary
* @param stdClass $secondary
* @return stdClass
*/
private static function pick_most_generic_option( stdClass $primary, stdClass $secondary ) {
if ( empty( $secondary->statesOrRegions ) && empty( $secondary->zipCodes ) ) { // phpcs:ignore WordPress.NamingConventions
return $secondary;
}

if ( empty( $primary->statesOrRegions ) && empty( $primary->zipCodes ) ) { // phpcs:ignore WordPress.NamingConventions
return $primary;
}

$primary_states = ! empty( $primary->statesOrRegions ) ? $primary->statesOrRegions : array(); // phpcs:ignore WordPress.NamingConventions
$secondary_states = ! empty( $secondary->statesOrRegions ) ? $secondary->statesOrRegions : array(); // phpcs:ignore WordPress.NamingConventions
$primary_zips = ! empty( $primary->zipCodes ) ? $primary->zipCodes : array(); // phpcs:ignore WordPress.NamingConventions
$secondary_zips = ! empty( $secondary->zipCodes ) ? $secondary->zipCodes : array(); // phpcs:ignore WordPress.NamingConventions

$return = new stdClass();

$return->statesOrRegions = array_intersect( $primary_states, $secondary_states ); // phpcs:ignore WordPress.NamingConventions
$return->zipCodes = array_intersect( $primary_zips, $secondary_zips ); // phpcs:ignore WordPress.NamingConventions

return $return;
}
}
48 changes: 48 additions & 0 deletions includes/class-wc-gateway-amazon-payments-advanced-abstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ public function __construct() {

add_action( 'woocommerce_amazon_checkout_init', array( $this, 'checkout_init_common' ) );

add_action( 'woocommerce_shipping_zone_before_methods_table', array( $this, 'postcode_explanation_message' ) );

}

/**
Expand Down Expand Up @@ -969,4 +971,50 @@ public function has_other_gateways_enabled() {
}
return false;
}

/**
* Displays an explanation/warning in order to make it clear how Amazon Pay
* treats postcode restrictions.
*
* @param WC_Shipping_Zone $zone
* @return void
*/
public function postcode_explanation_message( $zone ) {
$pcodes = false;

foreach ( $zone->get_zone_locations() as $location ) {
if ( 'postcode' === $location->type ) {
$pcodes = true;
break;
}
}

if ( $pcodes ) :
?>
<div class="notice notice-warning">
<p>
<strong><?php esc_html_e( 'Amazon Pay Notice:', 'woocommerce-gateway-amazon-payments-advanced' ); ?></strong>
<?php
/* translators: 1) Opening b tag. 2) Closing b tag. */
printf( esc_html__( 'When using postcodes to restrict shipping locations, a selected zone is required for them to apply and they will apply to %1$sall%2$s the selected zones. So please, restrict accordingly.', 'woocommerce-gateway-amazon-payments-advanced' ), '<b>', '</b>' );
?>
</p>
<p>
<?php
/* translators: 1) Opening b tag. 2) Closing b tag. */
printf( esc_html__( 'Additionally, be careful when specifying ranges. By default WooCommerce supports only %1$sfully numeric ranges%2$s. The same applies for Amazon Pay.', 'woocommerce-gateway-amazon-payments-advanced' ), '<b>', '</b>' );
?>
</p>
<p>
<?php
esc_html_e(
'Ranges covering thousands or more of postcodes may slow down your site, since Amazon Pay does not support postcode ranges we loop from the minimum until we reach the maximum in order to include them all. Try to use the asterisk (*) wildcard when possible for faster loading times.',
Copy link
Member

Choose a reason for hiding this comment

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

We should test with a large number of postcodes (99k ?) to make sure it works.
If it ends up clogging the payload & making API requests fail, it may be better to try something different.

Copy link
Author

Choose a reason for hiding this comment

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

You are correct, when using 99k zip codes as range Amazon API fails. Will try to think of an alternative.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, that's potentially a blocker.

One option would be to notify the user that their settings are "incompatible" with what Amazon supports, and not include those postcodes in the API call - so that it doesn't fail.
But again we would need to know/find what the threshold should be at which we are triggering that scenario.

Copy link
Author

Choose a reason for hiding this comment

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

Implemented an algorithm where ranges are converted to wildcards using the '?' symbol supported by the Amazon API. Let me know your thoughts

'woocommerce-gateway-amazon-payments-advanced'
);
?>
</p>
</div>
<?php
endif;
}
}