/wp-content/plugins/google-listings-and-ads/src/Shipping/ShippingZone.php

https://gitlab.com/remyvianne/krowkaramel · PHP · 378 lines · 187 code · 48 blank · 143 comment · 25 complexity · f2110e36f9d078d238282523c279a691 MD5 · raw file

  1. <?php
  2. declare( strict_types=1 );
  3. namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;
  4. use Automattic\WooCommerce\GoogleListingsAndAds\GoogleHelper;
  5. use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
  6. use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
  7. use WC_Shipping_Method;
  8. use WC_Shipping_Zone;
  9. defined( 'ABSPATH' ) || exit;
  10. /**
  11. * Class ShippingZone
  12. *
  13. * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
  14. *
  15. * @since 1.9.0
  16. */
  17. class ShippingZone implements Service {
  18. public const METHOD_FLAT_RATE = 'flat_rate';
  19. public const METHOD_PICKUP = 'local_pickup';
  20. public const METHOD_FREE = 'free_shipping';
  21. use GoogleHelper;
  22. /**
  23. * @var WC
  24. */
  25. protected $wc;
  26. /**
  27. * @var array|null Array of shipping methods for each country.
  28. */
  29. protected $methods_countries = null;
  30. /**
  31. * ShippingZone constructor.
  32. *
  33. * @param WC $wc
  34. */
  35. public function __construct( WC $wc ) {
  36. $this->wc = $wc;
  37. }
  38. /**
  39. * Gets the shipping countries from the WooCommerce shipping zones.
  40. *
  41. * Note: This method only returns the countries that have at least one shipping method.
  42. *
  43. * @return string[]
  44. */
  45. public function get_shipping_countries(): array {
  46. $this->parse_shipping_zones();
  47. $countries = array_keys( $this->methods_countries ?: [] );
  48. // Match the list of shipping countries with the list of Merchant Center supported countries.
  49. $countries = array_intersect( $countries, $this->get_mc_supported_countries() );
  50. return array_values( $countries );
  51. }
  52. /**
  53. * Gets the shipping methods for the given country.
  54. *
  55. * @param string $country_code
  56. *
  57. * @return array[] Returns an array of shipping methods for the given country.
  58. *
  59. * @see ShippingZone::parse_method() for the format of the returned array.
  60. */
  61. public function get_shipping_methods_for_country( string $country_code ): array {
  62. $this->parse_shipping_zones();
  63. return ! empty( $this->methods_countries[ $country_code ] ) ? array_values( $this->methods_countries[ $country_code ] ) : [];
  64. }
  65. /**
  66. * Parses the WooCommerce shipping zones and maps them into the self::$methods_countries array.
  67. */
  68. protected function parse_shipping_zones(): void {
  69. // Don't parse if already parsed.
  70. if ( null !== $this->methods_countries ) {
  71. return;
  72. }
  73. $this->methods_countries = [];
  74. foreach ( $this->wc->get_shipping_zones() as $zone ) {
  75. $zone = $this->wc->get_shipping_zone( $zone['zone_id'] );
  76. /**
  77. * @var WC_Shipping_Method[] $methods
  78. */
  79. $methods = $zone->get_shipping_methods();
  80. // Skip if no shipping methods.
  81. if ( empty( $methods ) ) {
  82. continue;
  83. }
  84. foreach ( $methods as $method ) {
  85. // Check if the method is supported and return its properties.
  86. $method = $this->parse_method( $method );
  87. // Skip if method is not supported.
  88. if ( null === $method ) {
  89. continue;
  90. }
  91. // Add the method to the list of methods for each country in the zone.
  92. foreach ( $this->get_shipping_countries_from_zone( $zone ) as $country ) {
  93. // Initialize the shipping methods array if it doesn't exist.
  94. $this->methods_countries[ $country ] = $this->methods_countries[ $country ] ?? [];
  95. // Add the method to the array of shipping methods for the country if it doesn't exist.
  96. if ( ! isset( $this->methods_countries[ $country ][ $method['id'] ] ) ||
  97. self::should_method_be_replaced( $method, $this->methods_countries[ $country ][ $method['id'] ] )
  98. ) {
  99. $this->methods_countries[ $country ][ $method['id'] ] = $method;
  100. }
  101. }
  102. }
  103. }
  104. }
  105. /**
  106. * Checks whether the shipping method should be replaced with a more suitable one.
  107. *
  108. * @param array $method
  109. * @param array $existing_method
  110. *
  111. * @return bool
  112. */
  113. protected static function should_method_be_replaced( array $method, array $existing_method ): bool {
  114. if ( $method['id'] !== $existing_method['id'] ) {
  115. return false;
  116. }
  117. if (
  118. // If a flat-rate/local-pickup method already exists, we replace it with the one with the higher cost.
  119. self::does_method_have_higher_cost( $method, $existing_method ) ||
  120. // If a free-shipping method already exists, we replace it with the one with the higher required minimum order amount.
  121. self::does_method_have_higher_min_amount( $method, $existing_method )
  122. ) {
  123. return true;
  124. }
  125. return false;
  126. }
  127. /**
  128. * Checks whether the given method has a higher cost than the existing method.
  129. *
  130. * @param array $method A shipping method.
  131. * @param array $existing_method Another shipping method to compare with.
  132. *
  133. * @return bool
  134. */
  135. protected static function does_method_have_higher_cost( array $method, array $existing_method ): bool {
  136. if ( $method['id'] !== $existing_method['id'] ) {
  137. return false;
  138. }
  139. return in_array( $method['id'], [ self::METHOD_FLAT_RATE, self::METHOD_PICKUP ], true ) && $method['options']['cost'] > $existing_method['options']['cost'];
  140. }
  141. /**
  142. * Checks whether the given method has a minimum order amount higher than the existing method.
  143. *
  144. * @param array $method A shipping method.
  145. * @param array $existing_method Another shipping method to compare with.
  146. *
  147. * @return bool
  148. */
  149. protected static function does_method_have_higher_min_amount( array $method, array $existing_method ): bool {
  150. if ( self::METHOD_FREE !== $method['id'] || $method['id'] !== $existing_method['id'] ) {
  151. return false;
  152. }
  153. // If a free shipping method already exists but doesn't have a minimum order amount, we replace it with the one with the one that has one.
  154. if ( isset( $method['options']['min_amount'] ) && ! isset( $existing_method['options']['min_amount'] ) ) {
  155. return true;
  156. }
  157. return isset( $method['options']['min_amount'] ) && isset( $existing_method['options']['min_amount'] ) && $method['options']['min_amount'] > $existing_method['options']['min_amount'];
  158. }
  159. /**
  160. * Gets the list of countries defined in a shipping zone.
  161. *
  162. * @param WC_Shipping_Zone $zone
  163. *
  164. * @return string[] Array of country codes.
  165. */
  166. protected function get_shipping_countries_from_zone( WC_Shipping_Zone $zone ): array {
  167. $countries = [];
  168. foreach ( $zone->get_zone_locations() as $location ) {
  169. switch ( $location->type ) {
  170. case 'country':
  171. $countries[ $location->code ] = $location->code;
  172. break;
  173. case 'continent':
  174. $countries = array_merge( $countries, $this->get_countries_from_continent( $location->code ) );
  175. break;
  176. case 'state':
  177. $country_code = $this->get_country_of_state( $location->code );
  178. $countries[ $country_code ] = $country_code;
  179. break;
  180. default:
  181. break;
  182. }
  183. }
  184. return $countries;
  185. }
  186. /**
  187. * Parses the given shipping method and returns its properties if it's supported. Returns null otherwise.
  188. *
  189. * @param object|WC_Shipping_Method $method
  190. *
  191. * @return array|null Returns an array with the parsed shipping method or null if the shipping method is not supported. {
  192. * Array of shipping method arguments.
  193. *
  194. * @type string $id The shipping method ID.
  195. * @type string $title The user-defined title of the shipping method.
  196. * @type bool $enabled A boolean indicating whether the shipping method is enabled or not.
  197. * @type array $options Array of options for the shipping method (varies based on the method type). {
  198. * Array of options for the shipping method.
  199. *
  200. * @type string $cost The cost of the shipping method. Only if the method is flat-rate or local pickup.
  201. * @type array $class_costs An array of costs for each shipping class (with class names used as array keys). Only if the method is flat-rate.
  202. * @type string $min_amount The minimum order amount required to use the shipping method. Only if the method is free shipping.
  203. *
  204. * }
  205. * }
  206. */
  207. protected function parse_method( object $method ): ?array {
  208. $parsed_method = [
  209. 'id' => $method->id,
  210. 'title' => $method->title,
  211. 'enabled' => $method->is_enabled(),
  212. 'currency' => $this->wc->get_woocommerce_currency(),
  213. 'options' => [],
  214. ];
  215. switch ( $method->id ) {
  216. case self::METHOD_FLAT_RATE:
  217. $parsed_method['options'] = $this->get_flat_rate_method_options( $method );
  218. // If the flat-rate method has no cost AND no shipping classes, we don't return it.
  219. if ( empty( $parsed_method['options']['cost'] ) && empty( $parsed_method['options']['class_costs'] ) ) {
  220. return null;
  221. }
  222. break;
  223. case self::METHOD_PICKUP:
  224. $cost = $method->get_option( 'cost' );
  225. // Check if the cost is a numeric value (and not null or a math expression).
  226. if ( ! is_numeric( $cost ) ) {
  227. return null;
  228. }
  229. $parsed_method['options']['cost'] = (float) $cost;
  230. break;
  231. case self::METHOD_FREE:
  232. // Check if free shipping requires a minimum order amount.
  233. $requires = $method->get_option( 'requires' );
  234. if ( in_array( $requires, [ 'min_amount', 'either' ], true ) ) {
  235. $parsed_method['options']['min_amount'] = (float) $method->get_option( 'min_amount' );
  236. } elseif ( in_array( $requires, [ 'coupon', 'both' ], true ) ) {
  237. // We can't sync this method if free shipping requires a coupon.
  238. return null;
  239. }
  240. break;
  241. default:
  242. // We don't support other shipping methods.
  243. return null;
  244. }
  245. return $parsed_method;
  246. }
  247. /**
  248. * Get the array of options of the flat-rate shipping method.
  249. *
  250. * @param object $method
  251. *
  252. * @return array
  253. */
  254. protected function get_flat_rate_method_options( object $method ): array {
  255. $options = [
  256. 'cost' => null,
  257. ];
  258. $flat_cost = 0;
  259. $cost = $method->get_option( 'cost' );
  260. // Check if the cost is a numeric value (and not null or a math expression).
  261. if ( is_numeric( $cost ) ) {
  262. $flat_cost = (float) $cost;
  263. $options['cost'] = $flat_cost;
  264. }
  265. // Add the no class cost.
  266. $no_class_cost = $method->get_option( 'no_class_cost' );
  267. if ( is_numeric( $no_class_cost ) ) {
  268. $options['cost'] = $flat_cost + (float) $no_class_cost;
  269. }
  270. // Add shipping class costs.
  271. $shipping_classes = $this->wc->get_shipping_classes();
  272. foreach ( $shipping_classes as $shipping_class ) {
  273. // Initialize the array if it doesn't exist.
  274. $options['class_costs'] = $options['class_costs'] ?? [];
  275. $shipping_class_cost = $method->get_option( 'class_cost_' . $shipping_class->term_id );
  276. if ( is_numeric( $shipping_class_cost ) ) {
  277. // Add the flat rate cost to the shipping class cost.
  278. $options['class_costs'][ $shipping_class->slug ] = $flat_cost + $shipping_class_cost;
  279. }
  280. }
  281. return $options;
  282. }
  283. /**
  284. * Gets the list of countries from a continent.
  285. *
  286. * @param string $continent_code
  287. *
  288. * @return string[] Returns an array of country codes with each country code used both as the key and value.
  289. * For example: [ 'US' => 'US', 'DE' => 'DE' ].
  290. */
  291. protected function get_countries_from_continent( string $continent_code ): array {
  292. $countries = [];
  293. $continents = $this->wc->get_wc_countries()->get_continents();
  294. if ( isset( $continents[ $continent_code ] ) ) {
  295. $countries = $continents[ $continent_code ]['countries'];
  296. // Use the country code as array keys.
  297. $countries = array_combine( $countries, $countries );
  298. }
  299. return $countries;
  300. }
  301. /**
  302. * Gets the country code of a state defined in WooCommerce shipping zone.
  303. *
  304. * @param string $state_code
  305. *
  306. * @return string
  307. */
  308. protected function get_country_of_state( string $state_code ): string {
  309. $location_codes = explode( ':', $state_code );
  310. return $location_codes[0];
  311. }
  312. /**
  313. * Checks whether the given shipping method is valid.
  314. *
  315. * @param string $method
  316. *
  317. * @return bool
  318. */
  319. public static function is_shipping_method_valid( string $method ): bool {
  320. return in_array(
  321. $method,
  322. [
  323. self::METHOD_FLAT_RATE,
  324. self::METHOD_FREE,
  325. self::METHOD_PICKUP,
  326. ],
  327. true
  328. );
  329. }
  330. }