HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Shipping.tar
CountryRatesCollection.php000064400000006046151542470430011746 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;

defined( 'ABSPATH' ) || exit;

/**
 * Class CountryRatesCollection
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
class CountryRatesCollection extends LocationRatesCollection {
	/**
	 * @var string
	 */
	protected $country;

	/**
	 * @var ServiceRatesCollection[]
	 */
	protected $services_groups;

	/**
	 * CountryRatesCollection constructor.
	 *
	 * @param string         $country
	 * @param LocationRate[] $location_rates
	 */
	public function __construct( string $country, array $location_rates = [] ) {
		$this->country = $country;
		parent::__construct( $location_rates );
	}

	/**
	 * @return string
	 */
	public function get_country(): string {
		return $this->country;
	}

	/**
	 * Return collections of location rates grouped into shipping services.
	 *
	 * @return ServiceRatesCollection[]
	 */
	public function get_rates_grouped_by_service(): array {
		$this->group_rates_by_service();

		return array_values( $this->services_groups );
	}

	/**
	 * Groups the location rates into collections of rates based on how they fit into Merchant Center services.
	 */
	protected function group_rates_by_service(): void {
		if ( isset( $this->services_groups ) ) {
			return;
		}
		$this->services_groups = [];

		foreach ( $this->location_rates as $location_rate ) {
			$country          = $location_rate->get_location()->get_country();
			$shipping_area    = $location_rate->get_location()->get_applicable_area();
			$min_order_amount = $location_rate->get_shipping_rate()->get_min_order_amount();

			// Group rates by their applicable country and affecting shipping area
			$service_key = $country . $shipping_area;
			// If the rate has a min order amount constraint, then it should be under a new service
			if ( $location_rate->get_shipping_rate()->has_min_order_amount() ) {
				$service_key .= $min_order_amount;
			}

			if ( ! isset( $this->services_groups[ $service_key ] ) ) {
				$this->services_groups[ $service_key ] = new ServiceRatesCollection(
					$country,
					$shipping_area,
					$min_order_amount,
					[]
				);
			}

			$this->services_groups[ $service_key ]->add_location_rate( $location_rate );
		}
	}

	/**
	 * @param LocationRate $location_rate
	 *
	 * @throws InvalidValue If any of the location rates do not belong to the same country as the one provided for this class or if any of the rates are negative.
	 */
	protected function validate_rate( LocationRate $location_rate ) {
		if ( $this->country !== $location_rate->get_location()->get_country() ) {
			throw new InvalidValue( 'All location rates must be in the same country as the one provided for this collection.' );
		}
		if ( $location_rate->get_shipping_rate()->get_rate() < 0 ) {
			throw new InvalidValue( 'Shipping rates cannot be negative.' );
		}
	}

	/**
	 * Reset the internal mappings/groups
	 */
	protected function reset_rates_mappings(): void {
		unset( $this->services_groups );
	}
}
GoogleAdapter/AbstractRateGroupAdapter.php000064400000004343151542470430014700 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping\GoogleAdapter;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\LocationRate;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Price;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\RateGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Value;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractRateGroupAdapter
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
abstract class AbstractRateGroupAdapter extends RateGroup {
	/**
	 * Initialize this object's properties from an array.
	 *
	 * @param array $properties Used to seed this object's properties.
	 *
	 * @throws InvalidValue When the required parameters are not provided, or they are invalid.
	 */
	public function mapTypes( $properties ) {
		if ( empty( $properties['currency'] ) || ! is_string( $properties['currency'] ) ) {
			throw new InvalidValue( 'The value of "currency" must be a non empty string.' );
		}
		if ( empty( $properties['location_rates'] ) || ! is_array( $properties['location_rates'] ) ) {
			throw new InvalidValue( 'The value of "location_rates" must be a non empty array.' );
		}

		$this->map_location_rates( $properties['location_rates'], $properties['currency'] );

		// Remove the extra data before calling the parent method since it doesn't expect them.
		unset( $properties['currency'] );
		unset( $properties['location_rates'] );

		parent::mapTypes( $properties );
	}

	/**
	 * @param float  $rate
	 * @param string $currency
	 *
	 * @return Value
	 */
	protected function create_value_object( float $rate, string $currency ): Value {
		$price = new Price(
			[
				'currency' => $currency,
				'value'    => $rate,
			]
		);

		return new Value( [ 'flatRate' => $price ] );
	}

	/**
	 * Map the location rates to the class properties.
	 *
	 * @param LocationRate[] $location_rates
	 * @param string         $currency
	 *
	 * @return void
	 */
	abstract protected function map_location_rates( array $location_rates, string $currency ): void;
}
GoogleAdapter/AbstractShippingSettingsAdapter.php000064400000006327151542470430016276 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping\GoogleAdapter;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\DeliveryTime;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\ShippingSettings as GoogleShippingSettings;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractShippingSettingsAdapter
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
abstract class AbstractShippingSettingsAdapter extends GoogleShippingSettings {
	/**
	 * @var string
	 */
	protected $currency;

	/**
	 * @var array
	 */
	protected $delivery_times;

	/**
	 * Initialize this object's properties from an array.
	 *
	 * @param array $properties Used to seed this object's properties.
	 *
	 * @return void
	 *
	 * @throws InvalidValue When the required parameters are not provided, or they are invalid.
	 */
	public function mapTypes( $properties ) {
		$this->validate_gla_data( $properties );

		$this->currency       = $properties['currency'];
		$this->delivery_times = $properties['delivery_times'];

		$this->map_gla_data( $properties );

		$this->unset_gla_data( $properties );

		parent::mapTypes( $properties );
	}

	/**
	 * Return estimated delivery time for a given country in days.
	 *
	 * @param string $country
	 *
	 * @return DeliveryTime
	 *
	 * @throws InvalidValue If no delivery time can be found for the country.
	 */
	protected function get_delivery_time( string $country ): DeliveryTime {
		if ( ! array_key_exists( $country, $this->delivery_times ) ) {
			throw new InvalidValue( 'No estimated delivery time provided for country: ' . $country );
		}

		$time = new DeliveryTime();
		$time->setMinHandlingTimeInDays( 0 );
		$time->setMaxHandlingTimeInDays( 0 );
		$time->setMinTransitTimeInDays( (int) $this->delivery_times[ $country ]['time'] );
		$time->setMaxTransitTimeInDays( (int) $this->delivery_times[ $country ]['max_time'] );

		return $time;
	}

	/**
	 * Validates the input array provided to this class.
	 *
	 * @param array $data
	 *
	 * @throws InvalidValue When the required parameters are not provided, or they are invalid.
	 *
	 * @link AbstractShippingSettingsAdapter::mapTypes() The $data input comes from this method.
	 */
	protected function validate_gla_data( array $data ): void {
		if ( empty( $data['currency'] ) || ! is_string( $data['currency'] ) ) {
			throw new InvalidValue( 'The value of "currency" must be a non empty string.' );
		}
		if ( empty( $data['delivery_times'] ) || ! is_array( $data['delivery_times'] ) ) {
			throw new InvalidValue( 'The value of "delivery_times" must be a non empty array.' );
		}
	}

	/**
	 * Remove the extra data we added to the input array since the MC API doesn't expect them (and it will fail).
	 *
	 * @param array $data
	 */
	protected function unset_gla_data( array &$data ): void {
		unset( $data['currency'] );
		unset( $data['delivery_times'] );
	}

	/**
	 * Parses the already validated input data and maps the provided shipping rates into MC shipping settings.
	 *
	 * @param array $data Validated data.
	 */
	abstract protected function map_gla_data( array $data ): void;
}
GoogleAdapter/DBShippingSettingsAdapter.php000064400000012501151542470430015007 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping\GoogleAdapter;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Price;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\RateGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Service as GoogleShippingService;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Value;

defined( 'ABSPATH' ) || exit;

/**
 * Class DBShippingSettingsAdapter
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
class DBShippingSettingsAdapter extends AbstractShippingSettingsAdapter {
	/**
	 * Parses the already validated input data and maps the provided shipping rates into MC shipping settings.
	 *
	 * @param array $data Validated data.
	 */
	protected function map_gla_data( array $data ): void {
		$this->map_db_rates( $data['db_rates'] );
	}

	/**
	 * Validates the input array provided to this class.
	 *
	 * @param array $data
	 *
	 * @throws InvalidValue When the required parameters are not provided, or they are invalid.
	 *
	 * @link AbstractShippingSettingsAdapter::mapTypes() The $data input comes from this method.
	 */
	protected function validate_gla_data( array $data ): void {
		parent::validate_gla_data( $data );

		if ( empty( $data['db_rates'] ) || ! is_array( $data['db_rates'] ) ) {
			throw new InvalidValue( 'The value of "db_rates" must be a non empty associated array of shipping rates.' );
		}
	}

	/**
	 * Remove the extra data we added to the input array since the MC API doesn't expect them (and it will fail).
	 *
	 * @param array $data
	 */
	protected function unset_gla_data( array &$data ): void {
		unset( $data['db_rates'] );
		parent::unset_gla_data( $data );
	}

	/**
	 * Map the shipping rates stored for each country in DB to MC shipping settings.
	 *
	 * @param array[] $db_rates
	 *
	 * @return void
	 */
	protected function map_db_rates( array $db_rates ) {
		$services = [];
		foreach ( $db_rates as ['country' => $country, 'rate' => $rate, 'options' => $options] ) {
			// No negative rates.
			if ( $rate < 0 ) {
				continue;
			}

			$service = $this->create_shipping_service( $country, $this->currency, (float) $rate );

			if ( isset( $options['free_shipping_threshold'] ) ) {
				$minimum_order_value = (float) $options['free_shipping_threshold'];

				if ( $rate > 0 ) {
					// Add a conditional free-shipping service if the current rate is not free.
					$services[] = $this->create_conditional_free_shipping_service( $country, $this->currency, $minimum_order_value );
				} else {
					// Set the minimum order value if the current rate is free.
					$service->setMinimumOrderValue(
						new Price(
							[
								'value'    => $minimum_order_value,
								'currency' => $this->currency,
							]
						)
					);
				}
			}

			$services[] = $service;
		}

		$this->setServices( $services );
	}

	/**
	 * Create a rate group object for the shopping settings.
	 *
	 * @param string $currency
	 * @param float  $rate
	 *
	 * @return RateGroup
	 */
	protected function create_rate_group_object( string $currency, float $rate ): RateGroup {
		$price = new Price();
		$price->setCurrency( $currency );
		$price->setValue( $rate );

		$value = new Value();
		$value->setFlatRate( $price );

		$rate_group = new RateGroup();

		$rate_group->setSingleValue( $value );

		$name = sprintf(
		/* translators: %1 is the shipping rate, %2 is the currency (e.g. USD) */
			__( 'Flat rate - %1$s %2$s', 'google-listings-and-ads' ),
			$rate,
			$currency
		);

		$rate_group->setName( $name );

		return $rate_group;
	}

	/**
	 * Create a shipping service object.
	 *
	 * @param string $country
	 * @param string $currency
	 * @param float  $rate
	 *
	 * @return GoogleShippingService
	 */
	protected function create_shipping_service( string $country, string $currency, float $rate ): GoogleShippingService {
		$unique  = sprintf( '%04x', wp_rand( 0, 0xffff ) );
		$service = new GoogleShippingService();
		$service->setActive( true );
		$service->setDeliveryCountry( $country );
		$service->setCurrency( $currency );
		$service->setName(
			sprintf(
			/* translators: %1 is a random 4-digit string, %2 is the rate, %3 is the currency, %4 is the country code  */
				__( '[%1$s] Google for WooCommerce generated service - %2$s %3$s to %4$s', 'google-listings-and-ads' ),
				$unique,
				$rate,
				$currency,
				$country
			)
		);

		$service->setRateGroups( [ $this->create_rate_group_object( $currency, $rate ) ] );
		$service->setDeliveryTime( $this->get_delivery_time( $country ) );

		return $service;
	}

	/**
	 * Create a free shipping service.
	 *
	 * @param string $country
	 * @param string $currency
	 * @param float  $minimum_order_value
	 *
	 * @return GoogleShippingService
	 */
	protected function create_conditional_free_shipping_service( string $country, string $currency, float $minimum_order_value ): GoogleShippingService {
		$service = $this->create_shipping_service( $country, $currency, 0 );

		// Set the minimum order value to be eligible for free shipping.
		$service->setMinimumOrderValue(
			new Price(
				[
					'value'    => $minimum_order_value,
					'currency' => $currency,
				]
			)
		);

		return $service;
	}
}
GoogleAdapter/PostcodesRateGroupAdapter.php000064400000003147151542470430015101 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping\GoogleAdapter;

use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\LocationRate;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Headers;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Row;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Table;

defined( 'ABSPATH' ) || exit;

/**
 * Class PostcodesRateGroupAdapter
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
class PostcodesRateGroupAdapter extends AbstractRateGroupAdapter {
	/**
	 * Map the location rates to the class properties.
	 *
	 * @param LocationRate[] $location_rates
	 * @param string         $currency
	 *
	 * @return void
	 */
	protected function map_location_rates( array $location_rates, string $currency ): void {
		$postal_codes = [];
		$rows         = [];
		foreach ( $location_rates as $location_rate ) {
			$region = $location_rate->get_location()->get_shipping_region();
			if ( empty( $region ) ) {
				continue;
			}

			$postcode_name                  = $region->get_id();
			$postal_codes[ $postcode_name ] = $postcode_name;

			$rows[ $postcode_name ] = new Row( [ 'cells' => [ $this->create_value_object( $location_rate->get_shipping_rate()->get_rate(), $currency ) ] ] );
		}

		$table = new Table(
			[
				'rowHeaders' => new Headers( [ 'postalCodeGroupNames' => array_values( $postal_codes ) ] ),
				'rows'       => array_values( $rows ),
			]
		);

		$this->setMainTable( $table );
	}
}
GoogleAdapter/StatesRateGroupAdapter.php000064400000003236151542470430014400 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping\GoogleAdapter;

use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\LocationRate;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Headers;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\LocationIdSet;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Row;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Table;

defined( 'ABSPATH' ) || exit;

/**
 * Class StatesRateGroupAdapter
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
class StatesRateGroupAdapter extends AbstractRateGroupAdapter {
	/**
	 * Map the location rates to the class properties.
	 *
	 * @param LocationRate[] $location_rates
	 * @param string         $currency
	 *
	 * @return void
	 */
	protected function map_location_rates( array $location_rates, string $currency ): void {
		$location_id_sets = [];
		$rows             = [];
		foreach ( $location_rates as $location_rate ) {
			$location_id                      = $location_rate->get_location()->get_google_id();
			$location_id_sets[ $location_id ] = new LocationIdSet( [ 'locationIds' => [ $location_id ] ] );

			$rows[ $location_id ] = new Row( [ 'cells' => [ $this->create_value_object( $location_rate->get_shipping_rate()->get_rate(), $currency ) ] ] );
		}

		$table = new Table(
			[
				'rowHeaders' => new Headers( [ 'locations' => array_values( $location_id_sets ) ] ),
				'rows'       => array_values( $rows ),
			]
		);

		$this->setMainTable( $table );
	}
}
GoogleAdapter/WCShippingSettingsAdapter.php000064400000020520151542470430015033 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping\GoogleAdapter;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidArgument;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidClass;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\CountryRatesCollection;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\LocationRate;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\ServiceRatesCollection;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\ShippingLocation;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\PostalCodeGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\PostalCodeRange;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Price;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\RateGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Service as GoogleShippingService;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Value;

defined( 'ABSPATH' ) || exit;

/**
 * Class WCShippingSettingsAdapter
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
class WCShippingSettingsAdapter extends AbstractShippingSettingsAdapter {
	/**
	 * Parses the already validated input data and maps the provided shipping rates into MC shipping settings.
	 *
	 * @param array $data Validated data.
	 */
	protected function map_gla_data( array $data ): void {
		$this->map_rates_collections( $data['rates_collections'] );
	}

	/**
	 * Validates the input array provided to this class.
	 *
	 * @param array $data
	 *
	 * @throws InvalidValue When the required parameters are not provided, or they are invalid.
	 *
	 * @link AbstractShippingSettingsAdapter::mapTypes() The $data input comes from this method.
	 */
	protected function validate_gla_data( array $data ): void {
		parent::validate_gla_data( $data );

		if ( empty( $data['rates_collections'] ) || ! is_array( $data['rates_collections'] ) ) {
			throw new InvalidValue( 'The value of "rates_collections" must be a non empty array of CountryRatesCollection objects.' );
		} else {
			$this->validate_rates_collections( $data['rates_collections'] );
		}
	}

	/**
	 * Remove the extra data we added to the input array since the MC API doesn't expect them (and it will fail).
	 *
	 * @param array $data
	 */
	protected function unset_gla_data( array &$data ): void {
		unset( $data['rates_collections'] );
		parent::unset_gla_data( $data );
	}

	/**
	 * Map the collections of location rates for each country to the shipping settings.
	 *
	 * @param CountryRatesCollection[] $rates_collections
	 *
	 * @return void
	 */
	protected function map_rates_collections( array $rates_collections ) {
		$postcode_groups = [];
		$services        = [];
		foreach ( $rates_collections as $rates_collection ) {
			$postcode_groups = array_merge( $postcode_groups, $this->get_location_rates_postcode_groups( $rates_collection->get_location_rates() ) );

			foreach ( $rates_collection->get_rates_grouped_by_service() as $service_collection ) {
				$services[] = $this->create_shipping_service( $service_collection );
			}
		}

		$this->setServices( $services );
		$this->setPostalCodeGroups( array_values( $postcode_groups ) );
	}

	/**
	 * @param LocationRate[] $location_rates
	 * @param string         $shipping_area
	 * @param array          $applicable_classes
	 *
	 * @return RateGroup
	 *
	 * @throws InvalidArgument If an invalid value is provided for the shipping_area argument.
	 */
	protected function create_rate_group( array $location_rates, string $shipping_area, array $applicable_classes = [] ): RateGroup {
		switch ( $shipping_area ) {
			case ShippingLocation::COUNTRY_AREA:
				// Each country can only have one global rate.
				$country_rate = $location_rates[ array_key_first( $location_rates ) ];
				$rate_group   = $this->create_single_value_rate_group( $country_rate, $applicable_classes );
				break;
			case ShippingLocation::POSTCODE_AREA:
				$rate_group = new PostcodesRateGroupAdapter(
					[
						'location_rates'           => $location_rates,
						'currency'                 => $this->currency,
						'applicableShippingLabels' => $applicable_classes,
					]
				);
				break;
			case ShippingLocation::STATE_AREA:
				$rate_group = new StatesRateGroupAdapter(
					[
						'location_rates'           => $location_rates,
						'currency'                 => $this->currency,
						'applicableShippingLabels' => $applicable_classes,
					]
				);
				break;
			default:
				throw new InvalidArgument( 'Invalid shipping area.' );
		}

		return $rate_group;
	}

	/**
	 * Create a shipping service object.
	 *
	 * @param ServiceRatesCollection $service_collection
	 *
	 * @return GoogleShippingService
	 */
	protected function create_shipping_service( ServiceRatesCollection $service_collection ): GoogleShippingService {
		$rate_groups   = [];
		$shipping_area = $service_collection->get_shipping_area();
		foreach ( $service_collection->get_rates_grouped_by_shipping_class() as $class => $location_rates ) {
			$applicable_classes    = ! empty( $class ) ? [ $class ] : [];
			$rate_groups[ $class ] = $this->create_rate_group( $location_rates, $shipping_area, $applicable_classes );
		}

		$country = $service_collection->get_country();
		$name    = sprintf(
		/* translators: %1 is a random 4-digit string, %2 is the country code  */
			__( '[%1$s] Google for WooCommerce generated service - %2$s', 'google-listings-and-ads' ),
			sprintf( '%04x', wp_rand( 0, 0xffff ) ),
			$country
		);

		$service = new GoogleShippingService(
			[
				'active'          => true,
				'deliveryCountry' => $country,
				'currency'        => $this->currency,
				'name'            => $name,
				'deliveryTime'    => $this->get_delivery_time( $country ),
				'rateGroups'      => array_values( $rate_groups ),
			]
		);

		$min_order_amount = $service_collection->get_min_order_amount();
		if ( $min_order_amount ) {
			$min_order_value = new Price(
				[
					'currency' => $this->currency,
					'value'    => $min_order_amount,
				]
			);
			$service->setMinimumOrderValue( $min_order_value );
		}

		return $service;
	}

	/**
	 * Extract and return the postcode groups for the given location rates.
	 *
	 * @param LocationRate[] $location_rates
	 *
	 * @return PostalCodeGroup[]
	 */
	protected function get_location_rates_postcode_groups( array $location_rates ): array {
		$postcode_groups = [];

		foreach ( $location_rates as $location_rate ) {
			$location = $location_rate->get_location();
			if ( empty( $location->get_shipping_region() ) ) {
				continue;
			}
			$region = $location->get_shipping_region();

			$postcode_ranges = [];
			foreach ( $region->get_postcode_ranges() as $postcode_range ) {
				$postcode_ranges[] = new PostalCodeRange(
					[
						'postalCodeRangeBegin' => $postcode_range->get_start_code(),
						'postalCodeRangeEnd'   => $postcode_range->get_end_code(),
					]
				);
			}

			$postcode_groups[ $region->get_id() ] = new PostalCodeGroup(
				[
					'name'             => $region->get_id(),
					'country'          => $location->get_country(),
					'postalCodeRanges' => $postcode_ranges,
				]
			);
		}

		return $postcode_groups;
	}

	/**
	 * @param LocationRate $location_rate
	 * @param string[]     $shipping_classes
	 *
	 * @return RateGroup
	 */
	protected function create_single_value_rate_group( LocationRate $location_rate, array $shipping_classes = [] ): RateGroup {
		$price = new Price(
			[
				'currency' => $this->currency,
				'value'    => $location_rate->get_shipping_rate()->get_rate(),
			]
		);

		return new RateGroup(
			[
				'singleValue'              => new Value( [ 'flatRate' => $price ] ),
				'applicableShippingLabels' => $shipping_classes,
			]
		);
	}

	/**
	 * @param array $rates_collections
	 *
	 * @throws InvalidClass If any of the objects in the array is not an instance of CountryRatesCollection.
	 */
	protected function validate_rates_collections( array $rates_collections ) {
		array_walk(
			$rates_collections,
			function ( $obj ) {
				if ( ! $obj instanceof CountryRatesCollection ) {
					throw new InvalidValue( 'All values of the "rates_collections" array must be an instance of CountryRatesCollection.' );
				}
			}
		);
	}
}
LocationRate.php000064400000002347151542470430007654 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use JsonSerializable;

defined( 'ABSPATH' ) || exit;

/**
 * Class LocationRate
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class LocationRate implements JsonSerializable {
	/**
	 * @var ShippingLocation
	 */
	protected $location;

	/**
	 * @var ShippingRate
	 */
	protected $shipping_rate;

	/**
	 * LocationRate constructor.
	 *
	 * @param ShippingLocation $location
	 * @param ShippingRate     $shipping_rate
	 */
	public function __construct( ShippingLocation $location, ShippingRate $shipping_rate ) {
		$this->location      = $location;
		$this->shipping_rate = $shipping_rate;
	}

	/**
	 * @return ShippingLocation
	 */
	public function get_location(): ShippingLocation {
		return $this->location;
	}

	/**
	 * @return ShippingRate
	 */
	public function get_shipping_rate(): ShippingRate {
		return $this->shipping_rate;
	}

	/**
	 * Specify data which should be serialized to JSON
	 */
	public function jsonSerialize(): array {
		$rate_serialized = $this->shipping_rate->jsonSerialize();

		return array_merge(
			$rate_serialized,
			[
				'country' => $this->location->get_country(),
			]
		);
	}
}
LocationRatesCollection.php000064400000003451151542470440012051 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;

defined( 'ABSPATH' ) || exit;

/**
 * Class LocationRatesCollection
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
abstract class LocationRatesCollection {
	/**
	 * @var LocationRate[]
	 */
	protected $location_rates = [];

	/**
	 * LocationRatesCollection constructor.
	 *
	 * @param LocationRate[] $location_rates
	 */
	public function __construct( array $location_rates = [] ) {
		$this->set_location_rates( $location_rates );
	}

	/**
	 * @return LocationRate[]
	 */
	public function get_location_rates(): array {
		return $this->location_rates;
	}

	/**
	 * @param LocationRate[] $location_rates
	 *
	 * @return LocationRatesCollection
	 */
	public function set_location_rates( array $location_rates ): LocationRatesCollection {
		foreach ( $location_rates as $location_rate ) {
			$this->validate_rate( $location_rate );
		}

		$this->location_rates = $location_rates;
		$this->reset_rates_mappings();

		return $this;
	}

	/**
	 * @param LocationRate $location_rate
	 *
	 * @return LocationRatesCollection
	 */
	public function add_location_rate( LocationRate $location_rate ): LocationRatesCollection {
		$this->validate_rate( $location_rate );

		$this->location_rates[] = $location_rate;
		$this->reset_rates_mappings();

		return $this;
	}

	/**
	 * @param LocationRate $location_rate
	 *
	 * @throws InvalidValue If any of the location rates do not belong to the same country as the one provided for this class.
	 */
	abstract protected function validate_rate( LocationRate $location_rate );

	/**
	 * Reset the internal mappings/groups
	 */
	abstract protected function reset_rates_mappings(): void;
}
LocationRatesProcessor.php000064400000004320151542470440011731 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

defined( 'ABSPATH' ) || exit;

/**
 * Class LocationRatesProcessor
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class LocationRatesProcessor {

	/**
	 * Process the shipping rates data for output.
	 *
	 * @param LocationRate[] $location_rates Array of shipping rates belonging to a specific location.
	 *
	 * @return LocationRate[] Array of processed location rates.
	 */
	public function process( array $location_rates ): array {
		/** @var LocationRate[] $grouped_rates */
		$grouped_rates = [];
		foreach ( $location_rates as $location_rate ) {
			$shipping_rate = $location_rate->get_shipping_rate();

			$type = 'flat_rate';
			// If there are conditional free shipping rates, we need to group and compare them together.
			if ( $shipping_rate->is_free() && $shipping_rate->has_min_order_amount() ) {
				$type = 'conditional_free';
			}

			// Append the shipping class names to the type key to group and compare the class rates together.
			$classes = ! empty( $shipping_rate->get_applicable_classes() ) ? join( ',', $shipping_rate->get_applicable_classes() ) : '';
			$type   .= $classes;

			if ( ! isset( $grouped_rates[ $type ] ) || $this->should_rate_be_replaced( $shipping_rate, $grouped_rates[ $type ]->get_shipping_rate() ) ) {
				$grouped_rates[ $type ] = $location_rate;
			}
		}

		// Ignore the conditional free rate if there are no flat rate or if the existing flat rate is free.
		if ( ! isset( $grouped_rates['flat_rate'] ) || $grouped_rates['flat_rate']->get_shipping_rate()->is_free() ) {
			unset( $grouped_rates['conditional_free'] );
		}

		return array_values( $grouped_rates );
	}

	/**
	 * Checks whether the existing shipping rate should be replaced with a more suitable one. Used when grouping the rates.
	 *
	 * @param ShippingRate $new_rate
	 * @param ShippingRate $existing_rate
	 *
	 * @return bool
	 */
	protected function should_rate_be_replaced( ShippingRate $new_rate, ShippingRate $existing_rate ): bool {
		return $new_rate->get_rate() > $existing_rate->get_rate() ||
			(float) $new_rate->get_min_order_amount() > (float) $existing_rate->get_min_order_amount();
	}
}
PostcodeRange.php000064400000003225151542470440010022 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

defined( 'ABSPATH' ) || exit;

/**
 * Class PostcodeRange
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class PostcodeRange {

	/**
	 * @var string
	 */
	protected $start_code;

	/**
	 * @var string
	 */
	protected $end_code;

	/**
	 * PostcodeRange constructor.
	 *
	 * @param string      $start_code Beginning of the range.
	 * @param string|null $end_code   End of the range.
	 */
	public function __construct( string $start_code, ?string $end_code = null ) {
		$this->start_code = $start_code;
		$this->end_code   = $end_code;
	}

	/**
	 * @return string
	 */
	public function get_start_code(): string {
		return $this->start_code;
	}

	/**
	 * @return string|null
	 */
	public function get_end_code(): ?string {
		return $this->end_code;
	}

	/**
	 * Returns a PostcodeRange object from a string representation of the postcode.
	 *
	 * @param string $postcode String representation of the postcode. If it's a range it should be separated by "...". E.g. "12345...12345".
	 *
	 * @return PostcodeRange
	 */
	public static function from_string( string $postcode ): PostcodeRange {
		$postcode_range = explode( '...', $postcode );
		if ( 2 === count( $postcode_range ) ) {
			return new PostcodeRange( $postcode_range[0], $postcode_range[1] );
		}

		return new PostcodeRange( $postcode );
	}

	/**
	 * Returns the string representation of this postcode.
	 *
	 * @return string
	 */
	public function __toString() {
		if ( ! empty( $this->end_code ) ) {
			return "$this->start_code...$this->end_code";
		}

		return $this->start_code;
	}
}
ServiceRatesCollection.php000064400000005057151542470440011705 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

defined( 'ABSPATH' ) || exit;

/**
 * Class ServiceRatesCollection
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
class ServiceRatesCollection extends CountryRatesCollection {
	/**
	 * @var string
	 */
	protected $shipping_area;

	/**
	 * @var float|null
	 */
	protected $min_order_amount;

	/**
	 * @var LocationRate[][]
	 */
	protected $class_groups;

	/**
	 * ServiceRatesCollection constructor.
	 *
	 * @param string     $country
	 * @param string     $shipping_area
	 * @param float|null $min_order_amount
	 * @param array      $location_rates
	 */
	public function __construct( string $country, string $shipping_area, ?float $min_order_amount = null, array $location_rates = [] ) {
		$this->shipping_area    = $shipping_area;
		$this->min_order_amount = $min_order_amount;
		parent::__construct( $country, $location_rates );
	}

	/**
	 * @return float|null
	 */
	public function get_min_order_amount(): ?float {
		return $this->min_order_amount;
	}

	/**
	 * @return string
	 */
	public function get_shipping_area(): string {
		return $this->shipping_area;
	}

	/**
	 * Return array of location rates grouped by their applicable shipping classes. Multiple rates might be returned per class.
	 *
	 * @return LocationRate[][] Arrays of location rates grouped by their applicable shipping class. Shipping class name is used as array keys.
	 */
	public function get_rates_grouped_by_shipping_class(): array {
		$this->group_rates_by_shipping_class();

		return $this->class_groups;
	}

	/**
	 * Group the location rates by their applicable shipping classes.
	 */
	public function group_rates_by_shipping_class(): void {
		if ( isset( $this->class_groups ) ) {
			return;
		}
		$this->class_groups = [];

		foreach ( $this->location_rates as $location_rate ) {
			if ( ! empty( $location_rate->get_shipping_rate()->get_applicable_classes() ) ) {
				// For every rate defined in the location_rate, create a new shipping rate and add it to the array
				foreach ( $location_rate->get_shipping_rate()->get_applicable_classes() as $class ) {
					$this->class_groups[ $class ][] = $location_rate;
				}
			} else {
				$this->class_groups[''][] = $location_rate;
			}
		}

		// Sort the groups so that the rate with no shipping class is placed at the end.
		krsort( $this->class_groups );
	}

	/**
	 * Reset the internal mappings/groups
	 */
	protected function reset_rates_mappings(): void {
		parent::reset_rates_mappings();
		unset( $this->class_groups );
	}
}
ShippingLocation.php000064400000005306151542470440010541 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingLocation
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since   2.1.0
 */
class ShippingLocation {
	public const COUNTRY_AREA  = 'country_area';
	public const STATE_AREA    = 'state_area';
	public const POSTCODE_AREA = 'postcode_area';

	/**
	 * @var int
	 */
	protected $google_id;

	/**
	 * @var string
	 */
	protected $country;

	/**
	 * @var string
	 */
	protected $state;

	/**
	 * @var ShippingRegion
	 */
	protected $shipping_region;

	/**
	 * ShippingLocation constructor.
	 *
	 * @param int                 $google_id
	 * @param string              $country
	 * @param string|null         $state
	 * @param ShippingRegion|null $shipping_region
	 */
	public function __construct( int $google_id, string $country, ?string $state = null, ?ShippingRegion $shipping_region = null ) {
		$this->google_id       = $google_id;
		$this->country         = $country;
		$this->state           = $state;
		$this->shipping_region = $shipping_region;
	}

	/**
	 * @return int
	 */
	public function get_google_id(): int {
		return $this->google_id;
	}

	/**
	 * @return string
	 */
	public function get_country(): string {
		return $this->country;
	}

	/**
	 * @return string|null
	 */
	public function get_state(): ?string {
		return $this->state;
	}

	/**
	 * @return ShippingRegion|null
	 */
	public function get_shipping_region(): ?ShippingRegion {
		return $this->shipping_region;
	}

	/**
	 * Return the applicable shipping area for this shipping location. e.g. whether it applies to a whole country, state, or postcodes.
	 *
	 * @return string
	 */
	public function get_applicable_area(): string {
		if ( ! empty( $this->get_shipping_region() ) ) {
			// ShippingLocation applies to a select postal code ranges of a country
			return self::POSTCODE_AREA;
		} elseif ( ! empty( $this->get_state() ) ) {
			// ShippingLocation applies to a state/province of a country
			return self::STATE_AREA;
		} else {
			// ShippingLocation applies to a whole country
			return self::COUNTRY_AREA;
		}
	}

	/**
	 * Returns the string representation of this ShippingLocation.
	 *
	 * @return string
	 */
	public function __toString() {
		$code = $this->get_country();
		if ( ! empty( $this->get_shipping_region() ) ) {
			// We assume that each postcode is unique within any supported country (a requirement set by Google API).
			// Therefore, there is no need to include the state name in the location string even if it's provided.
			$code .= '::' . $this->get_shipping_region();
		} elseif ( ! empty( $this->get_state() ) ) {
			$code .= '_' . $this->get_state();
		}

		return $code;
	}
}
ShippingRate.php000064400000004742151542470450007670 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use JsonSerializable;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingRate
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class ShippingRate implements JsonSerializable {

	/**
	 * @var float
	 */
	protected $rate;

	/**
	 * @var float|null
	 */
	protected $min_order_amount;

	/**
	 * @var array
	 */
	protected $applicable_classes = [];

	/**
	 * ShippingRate constructor.
	 *
	 * @param float $rate The shipping cost in store currency.
	 */
	public function __construct( float $rate ) {
		// Google only accepts rates with two decimal places.
		// We avoid using wc_format_decimal or number_format_i18n because these functions format numbers according to locale settings, which may include thousands separators and different decimal separators.
		// At this stage, we want to ensure the number is formatted strictly as a float, with no thousands separators and a dot as the decimal separator.
		$this->rate = (float) number_format( $rate, 2 );
	}

	/**
	 * @return float
	 */
	public function get_rate(): float {
		return $this->rate;
	}

	/**
	 * @param float $rate
	 *
	 * @return ShippingRate
	 */
	public function set_rate( float $rate ): ShippingRate {
		$this->rate = $rate;

		return $this;
	}

	/**
	 * Returns whether the shipping rate is free.
	 *
	 * @return bool
	 */
	public function is_free(): bool {
		return 0.0 === $this->get_rate();
	}

	/**
	 * @return float|null
	 */
	public function get_min_order_amount(): ?float {
		return $this->min_order_amount;
	}

	/**
	 * @param float|null $min_order_amount
	 */
	public function set_min_order_amount( ?float $min_order_amount ): void {
		$this->min_order_amount = $min_order_amount;
	}

	/**
	 * Returns whether the shipping rate has a minimum order amount constraint.
	 *
	 * @return bool
	 */
	public function has_min_order_amount(): bool {
		return ! is_null( $this->get_min_order_amount() );
	}

	/**
	 * @return string[]
	 */
	public function get_applicable_classes(): array {
		return $this->applicable_classes;
	}

	/**
	 * @param string[] $applicable_classes
	 *
	 * @return ShippingRate
	 */
	public function set_applicable_classes( array $applicable_classes ): ShippingRate {
		$this->applicable_classes = $applicable_classes;

		return $this;
	}

	/**
	 * Specify data which should be serialized to JSON
	 */
	public function jsonSerialize(): array {
		return [
			'rate' => $this->get_rate(),
		];
	}
}
ShippingRegion.php000064400000003307151542470450010214 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingRegion
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class ShippingRegion {

	/**
	 * @var string
	 */
	protected $id;

	/**
	 * @var string
	 */
	protected $country;

	/**
	 * @var PostcodeRange[]
	 */
	protected $postcode_ranges;

	/**
	 * ShippingRegion constructor.
	 *
	 * @param string          $id
	 * @param string          $country
	 * @param PostcodeRange[] $postcode_ranges
	 */
	public function __construct( string $id, string $country, array $postcode_ranges ) {
		$this->id              = $id;
		$this->country         = $country;
		$this->postcode_ranges = $postcode_ranges;
	}

	/**
	 * @return string
	 */
	public function get_id(): string {
		return $this->id;
	}

	/**
	 * @return string
	 */
	public function get_country(): string {
		return $this->country;
	}

	/**
	 * @return PostcodeRange[]
	 */
	public function get_postcode_ranges(): array {
		return $this->postcode_ranges;
	}

	/**
	 * Generate a random ID for the region.
	 *
	 * For privacy reasons, the region ID value must be a randomized set of numbers (minimum 6 digits)
	 *
	 * @return string
	 *
	 * @throws \Exception If generating a random ID fails.
	 *
	 * @link https://support.google.com/merchants/answer/9698880?hl=en#requirements
	 */
	public static function generate_random_id(): string {
		return (string) random_int( 100000, PHP_INT_MAX );
	}

	/**
	 * Returns the string representation of this object.
	 *
	 * @return string
	 */
	public function __toString() {
		return $this->get_country() . join( ',', $this->get_postcode_ranges() );
	}
}
ShippingSuggestionService.php000064400000005320151542470450012436 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingSuggestionService
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class ShippingSuggestionService implements Service {

	/**
	 * @var ShippingZone
	 */
	protected $shipping_zone;

	/**
	 * @var WC
	 */
	protected $wc;

	/**
	 * ShippingSuggestionService constructor.
	 *
	 * @param ShippingZone $shipping_zone
	 * @param WC           $wc
	 */
	public function __construct( ShippingZone $shipping_zone, WC $wc ) {
		$this->shipping_zone = $shipping_zone;
		$this->wc            = $wc;
	}

	/**
	 * Get shipping rate suggestions.
	 *
	 * @param string $country_code
	 *
	 * @return array A multidimensional array of shipping rate suggestions. {
	 *     Array of shipping rate suggestion arguments.
	 *
	 *     @type string $country  The shipping country.
	 *     @type string $currency The suggested rate currency (this is the same as the store's currency).
	 *     @type float  $rate     The cost of the shipping method.
	 *     @type array  $options  Array of options for the shipping method.
	 * }
	 */
	public function get_suggestions( string $country_code ): array {
		$location_rates = $this->shipping_zone->get_shipping_rates_grouped_by_country( $country_code );

		$suggestions    = [];
		$free_threshold = null;
		foreach ( $location_rates as $location_rate ) {
			$serialized = $location_rate->jsonSerialize();

			// Check if there is a conditional free shipping rate (with minimum order amount).
			// We will set the minimum order amount as the free shipping threshold for other rates.
			$shipping_rate = $location_rate->get_shipping_rate();

			// Ignore rates with shipping classes.
			if ( ! empty( $shipping_rate->get_applicable_classes() ) ) {
				continue;
			}

			if ( $shipping_rate->is_free() && $shipping_rate->has_min_order_amount() ) {
				$free_threshold = $shipping_rate->get_min_order_amount();

				// Ignore the conditional free rate if there are other rates.
				if ( count( $location_rates ) > 1 ) {
					continue;
				}
			}

			// Add the store currency to each rate.
			$serialized['currency'] = $this->wc->get_woocommerce_currency();

			$suggestions[] = $serialized;
		}

		if ( null !== $free_threshold ) {
			// Set the free shipping threshold for all suggestions if there is one.
			foreach ( $suggestions as $key => $suggestion ) {
				$suggestion['options'] = [
					'free_shipping_threshold' => $free_threshold,
				];

				$suggestions[ $key ] = $suggestion;
			}
		}

		return $suggestions;
	}
}
ShippingZone.php000064400000012005151542470450007677 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingZone
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 1.9.0
 */
class ShippingZone implements Service {

	/**
	 * @var WC
	 */
	protected $wc;

	/**
	 * @var ZoneLocationsParser
	 */
	protected $locations_parser;

	/**
	 * @var ZoneMethodsParser
	 */
	protected $methods_parser;

	/**
	 * @var LocationRatesProcessor
	 */
	protected $rates_processor;

	/**
	 * @var array[][]|null Array of shipping rates for each location.
	 */
	protected $location_rates = null;

	/**
	 * ShippingZone constructor.
	 *
	 * @param WC                     $wc
	 * @param ZoneLocationsParser    $location_parser
	 * @param ZoneMethodsParser      $methods_parser
	 * @param LocationRatesProcessor $rates_processor
	 */
	public function __construct(
		WC $wc,
		ZoneLocationsParser $location_parser,
		ZoneMethodsParser $methods_parser,
		LocationRatesProcessor $rates_processor
	) {
		$this->wc               = $wc;
		$this->locations_parser = $location_parser;
		$this->methods_parser   = $methods_parser;
		$this->rates_processor  = $rates_processor;
	}

	/**
	 * Gets the shipping countries from the WooCommerce shipping zones.
	 *
	 * Note: This method only returns the countries that have at least one shipping method.
	 *
	 * @return string[]
	 */
	public function get_shipping_countries(): array {
		$this->parse_shipping_zones();

		$countries = array_keys( $this->location_rates );

		return array_values( $countries );
	}

	/**
	 * Get the number of shipping rates enable in WooCommerce.
	 *
	 * @return int
	 */
	public function get_shipping_rates_count(): int {
		$this->parse_shipping_zones();
		return count( $this->location_rates ?? [] );
	}

	/**
	 * Returns the available shipping rates for a country and its subdivisions.
	 *
	 * @param string $country_code
	 *
	 * @return LocationRate[]
	 */
	public function get_shipping_rates_for_country( string $country_code ): array {
		$this->parse_shipping_zones();

		if ( empty( $this->location_rates[ $country_code ] ) ) {
			return [];
		}

		// Process the rates for each country subdivision separately.
		$location_rates = array_map( [ $this->rates_processor, 'process' ], $this->location_rates[ $country_code ] );

		// Convert the string array keys to integers.
		$country_rates = array_values( $location_rates );

		// Flatten and merge the country shipping rates.
		$country_rates = array_merge( [], ...$country_rates );

		return array_values( $country_rates );
	}

	/**
	 * Returns the available shipping rates for a country.
	 *
	 * If there are separate rates for the country's subdivisions (e.g. state,province, postcode etc.), they will be
	 * grouped by their parent country.
	 *
	 * @param string $country_code
	 *
	 * @return LocationRate[]
	 */
	public function get_shipping_rates_grouped_by_country( string $country_code ): array {
		$this->parse_shipping_zones();

		if ( empty( $this->location_rates[ $country_code ] ) ) {
			return [];
		}

		// Convert the string array keys to integers.
		$country_rates = array_values( $this->location_rates[ $country_code ] );

		// Flatten and merge the country shipping rates.
		$country_rates = array_merge( [], ...$country_rates );

		return $this->rates_processor->process( $country_rates );
	}

	/**
	 * Parses the WooCommerce shipping zones.
	 */
	protected function parse_shipping_zones(): void {
		// Don't parse if already parsed.
		if ( null !== $this->location_rates ) {
			return;
		}
		$this->location_rates = [];

		foreach ( $this->wc->get_shipping_zones() as $zone ) {
			$zone           = $this->wc->get_shipping_zone( $zone['zone_id'] );
			$zone_locations = $this->locations_parser->parse( $zone );
			$shipping_rates = $this->methods_parser->parse( $zone );
			$this->map_rates_to_locations( $shipping_rates, $zone_locations );
		}
	}

	/**
	 * Maps each shipping method to its related shipping locations.
	 *
	 * @param ShippingRate[]     $shipping_rates The shipping rates.
	 * @param ShippingLocation[] $locations      The shipping locations.
	 *
	 * @since 2.1.0
	 */
	protected function map_rates_to_locations( array $shipping_rates, array $locations ): void {
		if ( empty( $shipping_rates ) || empty( $locations ) ) {
			return;
		}

		foreach ( $locations as $location ) {
			$location_rates = [];
			foreach ( $shipping_rates as $shipping_rate ) {
				$location_rates[] = new LocationRate( $location, $shipping_rate );
			}

			$country_code = $location->get_country();

			// Initialize the array if it doesn't exist.
			$this->location_rates[ $country_code ] = $this->location_rates[ $country_code ] ?? [];

			$location_key = (string) $location;

			// Group the rates by their parent country and a location key. The location key is used to prevent duplicate rates for the same location.
			$this->location_rates[ $country_code ][ $location_key ] = $location_rates;
		}
	}
}
SyncerHooks.php000064400000012174151542470450007540 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Settings as GoogleSettings;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\ShippingNotificationJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateShippingSettings;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;

defined( 'ABSPATH' ) || exit;

/**
 * Class SyncerHooks
 *
 * Hooks to various WooCommerce and WordPress actions to automatically sync shipping settings.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class SyncerHooks implements Service, Registerable {

	/**
	 * This property is used to avoid scheduling duplicate jobs in the same request.
	 *
	 * @var bool
	 */
	protected $already_scheduled = false;

	/**
	 * @var GoogleSettings
	 */
	protected $google_settings;

	/**
	 * @var MerchantCenterService
	 */
	protected $merchant_center;

	/**
	 * @var JobRepository
	 */
	protected $job_repository;

	/**
	 * @var NotificationsService $notifications_service
	 */
	protected $notifications_service;

	/**
	 * SyncerHooks constructor.
	 *
	 * @param MerchantCenterService $merchant_center
	 * @param GoogleSettings        $google_settings
	 * @param JobRepository         $job_repository
	 * @param NotificationsService  $notifications_service
	 */
	public function __construct( MerchantCenterService $merchant_center, GoogleSettings $google_settings, JobRepository $job_repository, NotificationsService $notifications_service ) {
		$this->google_settings       = $google_settings;
		$this->merchant_center       = $merchant_center;
		$this->job_repository        = $job_repository;
		$this->notifications_service = $notifications_service;
	}

	/**
	 * Register the service.
	 */
	public function register(): void {
		// only register the hooks if Merchant Center account is connected and the user has chosen for the shipping rates to be synced from WooCommerce settings.
		if ( ! $this->merchant_center->is_connected() || ! $this->google_settings->should_get_shipping_rates_from_woocommerce() ) {
			return;
		}

		$update_settings = function () {
			$this->handle_update_shipping_settings();
		};

		// After a shipping zone object is saved to database.
		add_action( 'woocommerce_after_shipping_zone_object_save', $update_settings, 90 );

		// After a shipping zone is deleted.
		add_action( 'woocommerce_delete_shipping_zone', $update_settings, 90 );

		// After a shipping method is added to or deleted from a shipping zone.
		add_action( 'woocommerce_shipping_zone_method_added', $update_settings, 90 );
		add_action( 'woocommerce_shipping_zone_method_deleted', $update_settings, 90 );

		// After a shipping method is enabled or disabled.
		add_action( 'woocommerce_shipping_zone_method_status_toggled', $update_settings, 90 );

		// After a shipping class is updated/deleted.
		add_action( 'woocommerce_shipping_classes_save_class', $update_settings, 90 );
		add_action( 'saved_product_shipping_class', $update_settings, 90 );
		add_action( 'delete_product_shipping_class', $update_settings, 90 );

		// After free_shipping and flat_rate method options are updated.
		add_action( 'woocommerce_update_options_shipping_free_shipping', $update_settings, 90 );
		add_action( 'woocommerce_update_options_shipping_flat_rate', $update_settings, 90 );

		// The shipping options can also be updated using other methods (e.g. by calling WC_Shipping_Method::process_admin_options).
		// Those methods may not fire any hooks, so we need to watch the base WordPress hooks for when those options are updated.
		$on_option_change = function ( $option ) {
			/**
			 * This Regex checks for the shipping options key generated by the `WC_Shipping_Method::get_instance_option_key` method.
			 * We check for the shipping method IDs supported by GLA (flat_rate or free_shipping), and an integer instance_id.
			 *
			 * @see \WC_Shipping_Method::get_instance_option_key for more information about this key.
			 */
			if ( preg_match( '/^woocommerce_(flat_rate|free_shipping)_\d+_settings$/', $option ) ) {
				$this->handle_update_shipping_settings();
			}
		};
		add_action(
			'updated_option',
			$on_option_change,
			90
		);
		add_action(
			'added_option',
			$on_option_change,
			90
		);
	}

	/**
	 * Handle updating of Merchant Center shipping settings.
	 *
	 * @return void
	 */
	protected function handle_update_shipping_settings() {
		// Bail if an event is already scheduled in the current request
		if ( $this->already_scheduled ) {
			return;
		}

		if ( $this->notifications_service->is_ready() ) {
			$this->job_repository->get( ShippingNotificationJob::class )->schedule( [ 'topic' => NotificationsService::TOPIC_SHIPPING_UPDATED ] );
		}

		$this->job_repository->get( UpdateShippingSettings::class )->schedule();

		$this->already_scheduled = true;
	}
}
ZoneLocationsParser.php000064400000010145151542470450011231 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use WC_Shipping_Zone;

defined( 'ABSPATH' ) || exit;

/**
 * Class ZoneLocationsParser
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class ZoneLocationsParser implements Service {

	/**
	 * @var GoogleHelper
	 */
	protected $google_helper;

	/**
	 * ZoneLocationsParser constructor.
	 *
	 * @param GoogleHelper $google_helper
	 */
	public function __construct( GoogleHelper $google_helper ) {
		$this->google_helper = $google_helper;
	}

	/**
	 * Returns the supported locations for the given WooCommerce shipping zone.
	 *
	 * @param WC_Shipping_Zone $zone
	 *
	 * @return ShippingLocation[] Array of supported locations.
	 */
	public function parse( WC_Shipping_Zone $zone ): array {
		$locations = [];
		$postcodes = $this->get_postcodes_from_zone( $zone );

		foreach ( $zone->get_zone_locations() as $location ) {
			switch ( $location->type ) {
				case 'country':
					$country = $location->code;
					if ( $this->google_helper->is_country_supported( $country ) ) {
						$google_id = $this->google_helper->find_country_id_by_code( $country );
						$region    = $this->maybe_create_region_for_postcodes( $country, $postcodes );

						$locations[ $location->code ] = new ShippingLocation( $google_id, $country, null, $region );
					}
					break;
				case 'continent':
					foreach ( $this->google_helper->get_supported_countries_from_continent( $location->code ) as $country ) {
						$google_id = $this->google_helper->find_country_id_by_code( $country );
						$region    = $this->maybe_create_region_for_postcodes( $country, $postcodes );

						$locations[ $country ] = new ShippingLocation( $google_id, $country, null, $region );
					}
					break;
				case 'state':
					[ $country, $state ] = explode( ':', $location->code );

					// Ignore if the country is not supported.
					if ( ! $this->google_helper->is_country_supported( $country ) ) {
						break;
					}

					$region = $this->maybe_create_region_for_postcodes( $country, $postcodes );

					// Only add the state if the regional shipping is supported for the country.
					if ( $this->google_helper->does_country_support_regional_shipping( $country ) ) {
						$google_id = $this->google_helper->find_subdivision_id_by_code( $state, $country );

						if ( ! is_null( $google_id ) ) {
							$locations[ $location->code ] = new ShippingLocation( $google_id, $country, $state, $region );
						}
					} else {
						$google_id             = $this->google_helper->find_country_id_by_code( $country );
						$locations[ $country ] = new ShippingLocation( $google_id, $country, null, $region );
					}
					break;
				default:
					break;
			}
		}

		return array_values( $locations );
	}

	/**
	 * Returns the applicable postcodes for the given WooCommerce shipping zone.
	 *
	 * @param WC_Shipping_Zone $zone
	 *
	 * @return PostcodeRange[] Array of postcodes.
	 */
	protected function get_postcodes_from_zone( WC_Shipping_Zone $zone ): array {
		$postcodes = array_filter(
			$zone->get_zone_locations(),
			function ( $location ) {
				return 'postcode' === $location->type;
			}
		);

		return array_map(
			function ( $postcode ) {
				return PostcodeRange::from_string( $postcode->code );
			},
			$postcodes
		);
	}

	/**
	 * Returns the applicable shipping region including postcodes for the given WooCommerce shipping zone.
	 *
	 * @param string $country
	 * @param array  $postcode_ranges
	 *
	 * @return ShippingRegion|null
	 */
	protected function maybe_create_region_for_postcodes( string $country, array $postcode_ranges ): ?ShippingRegion {
		// Do not return a region if the country does not support regional shipping, or if no postcode ranges provided.
		if ( ! $this->google_helper->does_country_support_regional_shipping( $country ) || empty( $postcode_ranges ) ) {
			return null;
		}

		$region_id = ShippingRegion::generate_random_id();

		return new ShippingRegion( $region_id, $country, $postcode_ranges );
	}
}
ZoneMethodsParser.php000064400000012730151542470450010703 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Shipping;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use WC_Shipping_Method;
use WC_Shipping_Zone;

defined( 'ABSPATH' ) || exit;

/**
 * Class ZoneMethodsParser
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Shipping
 *
 * @since 2.1.0
 */
class ZoneMethodsParser implements Service {

	use PluginHelper;

	public const METHOD_FLAT_RATE = 'flat_rate';
	public const METHOD_FREE      = 'free_shipping';

	/**
	 * @var WC
	 */
	private $wc;

	/**
	 * ZoneMethodsParser constructor.
	 *
	 * @param WC $wc
	 */
	public function __construct( WC $wc ) {
		$this->wc = $wc;
	}

	/**
	 * Parses the given shipping method and returns its properties if it's supported. Returns null otherwise.
	 *
	 * @param WC_Shipping_Zone $zone
	 *
	 * @return ShippingRate[] Returns an array of parsed shipping rates, or null if the shipping method is not supported.
	 */
	public function parse( WC_Shipping_Zone $zone ): array {
		$parsed_rates = [];
		foreach ( $zone->get_shipping_methods( true ) as $method ) {
			$parsed_rates = array_merge( $parsed_rates, $this->shipping_method_to_rates( $method ) );
		}

		return $parsed_rates;
	}

	/**
	 * Parses the given shipping method and returns its properties if it's supported. Returns null otherwise.
	 *
	 * @param object|WC_Shipping_Method $method
	 *
	 * @return ShippingRate[] Returns an array of parsed shipping rates, or empty if the shipping method is not supported.
	 */
	protected function shipping_method_to_rates( object $method ): array {
		$shipping_rates = [];
		switch ( $method->id ) {
			case self::METHOD_FLAT_RATE:
				$flat_rate            = $this->get_flat_rate_method_rate( $method );
				$shipping_class_rates = $this->get_flat_rate_method_class_rates( $method );

				// If the flat-rate method has no rate AND no shipping classes, we don't return it.
				if ( null === $flat_rate && empty( $shipping_class_rates ) ) {
					return [];
				}

				$shipping_rates[] = new ShippingRate( (float) $flat_rate );

				if ( ! empty( $shipping_class_rates ) ) {
					foreach ( $shipping_class_rates as ['class' => $class, 'rate' => $rate] ) {
						$shipping_rate = new ShippingRate( $rate );
						$shipping_rate->set_applicable_classes( [ $class ] );
						$shipping_rates[] = $shipping_rate;
					}
				}

				break;
			case self::METHOD_FREE:
				$shipping_rate = new ShippingRate( 0 );

				// Check if free shipping requires a minimum order amount.
				$requires = $method->get_option( 'requires' );
				if ( in_array( $requires, [ 'min_amount', 'either' ], true ) ) {
					$shipping_rate->set_min_order_amount( (float) $method->get_option( 'min_amount' ) );
				} elseif ( in_array( $requires, [ 'coupon', 'both' ], true ) ) {
					// We can't sync this method if free shipping requires a coupon.
					return [];
				}
				$shipping_rates[] = $shipping_rate;
				break;
			default:
				/**
				 * Filter the shipping rates for a shipping method that is not supported.
				 *
				 * @param ShippingRate[] $shipping_rates The shipping rates.
				 * @param object|WC_Shipping_Method $method The shipping method.
				 */
				return apply_filters(
					'woocommerce_gla_handle_shipping_method_to_rates',
					$shipping_rates,
					$method
				);
		}

		return $shipping_rates;
	}

	/**
	 * Get the flat-rate shipping method rate.
	 *
	 * @param object|WC_Shipping_Method $method
	 *
	 * @return float|null
	 */
	protected function get_flat_rate_method_rate( object $method ): ?float {
		$rate = null;

		$flat_cost = 0;
		$cost      = $this->convert_to_standard_decimal( (string) $method->get_option( 'cost' ) );
		// Check if the cost is a numeric value (and not null or a math expression).
		if ( is_numeric( $cost ) ) {
			$flat_cost = (float) $cost;
			$rate      = $flat_cost;
		}

		// Add the no class cost.
		$no_class_cost = $this->convert_to_standard_decimal( (string) $method->get_option( 'no_class_cost' ) );
		if ( is_numeric( $no_class_cost ) ) {
			$rate = $flat_cost + (float) $no_class_cost;
		}

		return $rate;
	}

	/**
	 * Get the array of options of the flat-rate shipping method.
	 *
	 * @param object|WC_Shipping_Method $method
	 *
	 * @return array A multidimensional array of shipping class rates. {
	 *     Array of shipping method arguments.
	 *
	 *     @type string $class The shipping class slug/id.
	 *     @type float  $rate  The cost of the shipping method for the class in WooCommerce store currency.
	 * }
	 */
	protected function get_flat_rate_method_class_rates( object $method ): array {
		$class_rates = [];

		$flat_cost = 0;
		$cost      = $this->convert_to_standard_decimal( (string) $method->get_option( 'cost' ) );
		// Check if the cost is a numeric value (and not null or a math expression).
		if ( is_numeric( $cost ) ) {
			$flat_cost = (float) $cost;
		}

		// Add shipping class costs.
		$shipping_classes = $this->wc->get_shipping_classes();
		foreach ( $shipping_classes as $shipping_class ) {
			$shipping_class_cost = $this->convert_to_standard_decimal( (string) $method->get_option( 'class_cost_' . $shipping_class->term_id ) );
			if ( is_numeric( $shipping_class_cost ) ) {
				// Add the flat rate cost to the shipping class cost.
				$class_rates[ $shipping_class->slug ] = [
					'class' => $shipping_class->slug,
					'rate'  => $flat_cost + (float) $shipping_class_cost,
				];
			}
		}

		return array_values( $class_rates );
	}
}
PickupLocation.php000064400000010722151550671260010213 0ustar00<?php
namespace Automattic\WooCommerce\Blocks\Shipping;

use WC_Shipping_Method;

/**
 * Local Pickup Shipping Method.
 */
class PickupLocation extends WC_Shipping_Method {

	/**
	 * Pickup locations.
	 *
	 * @var array
	 */
	protected $pickup_locations = [];

	/**
	 * Cost
	 *
	 * @var string
	 */
	protected $cost = '';

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id                 = 'pickup_location';
		$this->method_title       = __( 'Local pickup', 'woocommerce' );
		$this->method_description = __( 'Allow customers to choose a local pickup location during checkout.', 'woocommerce' );
		$this->init();
	}

	/**
	 * Init function.
	 */
	public function init() {
		$this->enabled          = $this->get_option( 'enabled' );
		$this->title            = $this->get_option( 'title' );
		$this->tax_status       = $this->get_option( 'tax_status' );
		$this->cost             = $this->get_option( 'cost' );
		$this->supports         = [ 'settings', 'local-pickup' ];
		$this->pickup_locations = get_option( $this->id . '_pickup_locations', [] );
		add_filter( 'woocommerce_attribute_label', array( $this, 'translate_meta_data' ), 10, 3 );
	}

	/**
	 * Checks if a given address is complete.
	 *
	 * @param array $address Address.
	 * @return bool
	 */
	protected function has_valid_pickup_location( $address ) {
		// Normalize address.
		$address_fields = wp_parse_args(
			(array) $address,
			array(
				'city'     => '',
				'postcode' => '',
				'state'    => '',
				'country'  => '',
			)
		);

		// Country is always required.
		if ( empty( $address_fields['country'] ) ) {
			return false;
		}

		// If all fields are provided, we can skip further checks.
		if ( ! empty( $address_fields['city'] ) && ! empty( $address_fields['postcode'] ) && ! empty( $address_fields['state'] ) ) {
			return true;
		}

		// Check validity based on requirements for the country.
		$country_address_fields = wc()->countries->get_address_fields( $address_fields['country'], 'shipping_' );

		foreach ( $country_address_fields as $field_name => $field ) {
			$key = str_replace( 'shipping_', '', $field_name );

			if ( isset( $address_fields[ $key ] ) && true === $field['required'] && empty( $address_fields[ $key ] ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Calculate shipping.
	 *
	 * @param array $package Package information.
	 */
	public function calculate_shipping( $package = array() ) {
		if ( $this->pickup_locations ) {
			foreach ( $this->pickup_locations as $index => $location ) {
				if ( ! $location['enabled'] ) {
					continue;
				}
				$this->add_rate(
					array(
						'id'        => $this->id . ':' . $index,
						// This is the label shown in shipping rate/method context e.g. London (Local Pickup).
						'label'     => wp_kses_post( $this->title . ' (' . $location['name'] . ')' ),
						'package'   => $package,
						'cost'      => $this->cost,
						'meta_data' => array(
							'pickup_location' => wp_kses_post( $location['name'] ),
							'pickup_address'  => $this->has_valid_pickup_location( $location['address'] ) ? wc()->countries->get_formatted_address( $location['address'], ', ' ) : '',
							'pickup_details'  => wp_kses_post( $location['details'] ),
						),
					)
				);
			}
		}
	}

	/**
	 * See if the method is available.
	 *
	 * @param array $package Package information.
	 * @return bool
	 */
	public function is_available( $package ) {
		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', 'yes' === $this->enabled, $package, $this );
	}

	/**
	 * Translates meta data for the shipping method.
	 *
	 * @param string $label Meta label.
	 * @param string $name Meta key.
	 * @param mixed  $product Product if applicable.
	 * @return string
	 */
	public function translate_meta_data( $label, $name, $product ) {
		if ( $product ) {
			return $label;
		}
		switch ( $name ) {
			case 'pickup_location':
				return __( 'Pickup location', 'woocommerce' );
			case 'pickup_address':
				return __( 'Pickup address', 'woocommerce' );
		}
		return $label;
	}

	/**
	 * Admin options screen.
	 *
	 * See also WC_Shipping_Method::admin_options().
	 */
	public function admin_options() {
		global $hide_save_button;
		$hide_save_button = true;

		wp_enqueue_script( 'wc-shipping-method-pickup-location' );

		echo '<h2>' . esc_html__( 'Local pickup', 'woocommerce' ) . '</h2>';
		echo '<div class="wrap"><div id="wc-shipping-method-pickup-location-settings-container"></div></div>';
	}
}
ShippingController.php000064400000037714151550671260011126 0ustar00<?php
namespace Automattic\WooCommerce\Blocks\Shipping;

use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
use Automattic\WooCommerce\Utilities\ArrayUtil;

/**
 * ShippingController class.
 *
 * @internal
 */
class ShippingController {
	/**
	 * Instance of the asset API.
	 *
	 * @var AssetApi
	 */
	protected $asset_api;

	/**
	 * Instance of the asset data registry.
	 *
	 * @var AssetDataRegistry
	 */
	protected $asset_data_registry;

	/**
	 * Whether local pickup is enabled.
	 *
	 * @var bool
	 */
	private $local_pickup_enabled;

	/**
	 * Constructor.
	 *
	 * @param AssetApi          $asset_api Instance of the asset API.
	 * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
	 */
	public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
		$this->asset_api           = $asset_api;
		$this->asset_data_registry = $asset_data_registry;

		$this->local_pickup_enabled = LocalPickupUtils::is_local_pickup_enabled();
	}

	/**
	 * Initialization method.
	 */
	public function init() {
		if ( is_admin() ) {
			$this->asset_data_registry->add(
				'countryStates',
				function() {
					return WC()->countries->get_states();
				},
				true
			);
		}

		$this->asset_data_registry->add( 'collectableMethodIds', array( 'Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils', 'get_local_pickup_method_ids' ), true );
		$this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' );
		add_action( 'rest_api_init', [ $this, 'register_settings' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'hydrate_client_settings' ] );
		add_action( 'woocommerce_load_shipping_methods', array( $this, 'register_local_pickup' ) );
		add_filter( 'woocommerce_local_pickup_methods', array( $this, 'register_local_pickup_method' ) );
		add_filter( 'woocommerce_order_hide_shipping_address', array( $this, 'hide_shipping_address_for_local_pickup' ), 10 );
		add_filter( 'woocommerce_customer_taxable_address', array( $this, 'filter_taxable_address' ) );
		add_filter( 'woocommerce_shipping_packages', array( $this, 'filter_shipping_packages' ) );
		add_filter( 'pre_update_option_woocommerce_pickup_location_settings', array( $this, 'flush_cache' ) );
		add_filter( 'pre_update_option_pickup_location_pickup_locations', array( $this, 'flush_cache' ) );
		add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) );
		add_filter( 'wc_shipping_enabled', array( $this, 'force_shipping_enabled' ), 100, 1 );
		add_filter( 'woocommerce_order_shipping_to_display', array( $this, 'show_local_pickup_details' ), 10, 2 );

		// This is required to short circuit `show_shipping` from class-wc-cart.php - without it, that function
		// returns based on the option's value in the DB and we can't override it any other way.
		add_filter( 'option_woocommerce_shipping_cost_requires_address', array( $this, 'override_cost_requires_address_option' ) );
	}

	/**
	 * Overrides the option to force shipping calculations NOT to wait until an address is entered, but only if the
	 * Checkout page contains the Checkout Block.
	 *
	 * @param boolean $value Whether shipping cost calculation requires address to be entered.
	 * @return boolean Whether shipping cost calculation should require an address to be entered before calculating.
	 */
	public function override_cost_requires_address_option( $value ) {
		if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
			return 'no';
		}
		return $value;
	}

	/**
	 * Force shipping to be enabled if the Checkout block is in use on the Checkout page.
	 *
	 * @param boolean $enabled Whether shipping is currently enabled.
	 * @return boolean Whether shipping should continue to be enabled/disabled.
	 */
	public function force_shipping_enabled( $enabled ) {
		if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
			return true;
		}
		return $enabled;
	}

	/**
	 * Inject collection details onto the order received page.
	 *
	 * @param string    $return Return value.
	 * @param \WC_Order $order Order object.
	 * @return string
	 */
	public function show_local_pickup_details( $return, $order ) {
		// Confirm order is valid before proceeding further.
		if ( ! $order instanceof \WC_Order ) {
			return $return;
		}

		$shipping_method_ids = ArrayUtil::select( $order->get_shipping_methods(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
		$shipping_method_id  = current( $shipping_method_ids );

		// Ensure order used pickup location method, otherwise bail.
		if ( 'pickup_location' !== $shipping_method_id ) {
			return $return;
		}

		$shipping_method = current( $order->get_shipping_methods() );
		$details         = $shipping_method->get_meta( 'pickup_details' );
		$location        = $shipping_method->get_meta( 'pickup_location' );
		$address         = $shipping_method->get_meta( 'pickup_address' );

		if ( ! $address ) {
			return $return;
		}

		return sprintf(
			// Translators: %s location name.
			__( 'Collection from <strong>%s</strong>:', 'woocommerce' ),
			$location
		) . '<br/><address>' . str_replace( ',', ',<br/>', $address ) . '</address><br/>' . $details;
	}

	/**
	 * If the Checkout block Remove shipping settings from WC Core's admin panels that are now block settings.
	 *
	 * @param array $settings The default WC shipping settings.
	 * @return array|mixed The filtered settings with relevant items removed.
	 */
	public function remove_shipping_settings( $settings ) {

		// Do not add the shipping calculator setting if the Cart block is not used on the WC cart page.
		if ( CartCheckoutUtils::is_cart_block_default() ) {

			// Ensure the 'Calculations' title is added to the `woocommerce_shipping_cost_requires_address` options
			// group, since it is attached to the `woocommerce_enable_shipping_calc` option that gets removed if the
			// Cart block is in use.
			$calculations_title = '';

			// Get Calculations title so we can add it to 'Hide shipping costs until an address is entered' option.
			foreach ( $settings as $setting ) {
				if ( 'woocommerce_enable_shipping_calc' === $setting['id'] ) {
					$calculations_title = $setting['title'];
					break;
				}
			}

			// Add Calculations title to 'Hide shipping costs until an address is entered' option.
			foreach ( $settings as $index => $setting ) {
				if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {
					$settings[ $index ]['title']         = $calculations_title;
					$settings[ $index ]['checkboxgroup'] = 'start';
					break;
				}
			}

			$settings = array_filter(
				$settings,
				function( $setting ) {
					return ! in_array(
						$setting['id'],
						array(
							'woocommerce_enable_shipping_calc',
						),
						true
					);
				}
			);
		}

		if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
			foreach ( $settings as $index => $setting ) {
				if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {
					$settings[ $index ]['desc']    .= ' (' . __( 'Not available when using WooCommerce Blocks Local Pickup', 'woocommerce' ) . ')';
					$settings[ $index ]['disabled'] = true;
					$settings[ $index ]['value']    = 'no';
					break;
				}
			}
		}

		return $settings;
	}

	/**
	 * Register Local Pickup settings for rest api.
	 */
	public function register_settings() {
		register_setting(
			'options',
			'woocommerce_pickup_location_settings',
			[
				'type'         => 'object',
				'description'  => 'WooCommerce Local Pickup Method Settings',
				'default'      => [],
				'show_in_rest' => [
					'name'   => 'pickup_location_settings',
					'schema' => [
						'type'       => 'object',
						'properties' => array(
							'enabled'    => [
								'description' => __( 'If enabled, this method will appear on the block based checkout.', 'woocommerce' ),
								'type'        => 'string',
								'enum'        => [ 'yes', 'no' ],
							],
							'title'      => [
								'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
								'type'        => 'string',
							],
							'tax_status' => [
								'description' => __( 'If a cost is defined, this controls if taxes are applied to that cost.', 'woocommerce' ),
								'type'        => 'string',
								'enum'        => [ 'taxable', 'none' ],
							],
							'cost'       => [
								'description' => __( 'Optional cost to charge for local pickup.', 'woocommerce' ),
								'type'        => 'string',
							],
						),
					],
				],
			]
		);
		register_setting(
			'options',
			'pickup_location_pickup_locations',
			[
				'type'         => 'array',
				'description'  => 'WooCommerce Local Pickup Locations',
				'default'      => [],
				'show_in_rest' => [
					'name'   => 'pickup_locations',
					'schema' => [
						'type'  => 'array',
						'items' => [
							'type'       => 'object',
							'properties' => array(
								'name'    => [
									'type' => 'string',
								],
								'address' => [
									'type'       => 'object',
									'properties' => array(
										'address_1' => [
											'type' => 'string',
										],
										'city'      => [
											'type' => 'string',
										],
										'state'     => [
											'type' => 'string',
										],
										'postcode'  => [
											'type' => 'string',
										],
										'country'   => [
											'type' => 'string',
										],
									),
								],
								'details' => [
									'type' => 'string',
								],
								'enabled' => [
									'type' => 'boolean',
								],
							),
						],
					],
				],
			]
		);
	}

	/**
	 * Hydrate client settings
	 */
	public function hydrate_client_settings() {
		$locations = get_option( 'pickup_location_pickup_locations', [] );

		$formatted_pickup_locations = [];
		foreach ( $locations as $location ) {
			$formatted_pickup_locations[] = [
				'name'    => $location['name'],
				'address' => $location['address'],
				'details' => $location['details'],
				'enabled' => wc_string_to_bool( $location['enabled'] ),
			];
		}

		$has_legacy_pickup = false;

		// Get all shipping zones.
		$shipping_zones              = \WC_Shipping_Zones::get_zones( 'admin' );
		$international_shipping_zone = new \WC_Shipping_Zone( 0 );

		// Loop through each shipping zone.
		foreach ( $shipping_zones as $shipping_zone ) {
			// Get all registered rates for this shipping zone.
			$shipping_methods = $shipping_zone['shipping_methods'];
			// Loop through each registered rate.
			foreach ( $shipping_methods as $shipping_method ) {
				if ( 'local_pickup' === $shipping_method->id && 'yes' === $shipping_method->enabled ) {
					$has_legacy_pickup = true;
					break 2;
				}
			}
		}

		foreach ( $international_shipping_zone->get_shipping_methods( true ) as $shipping_method ) {
			if ( 'local_pickup' === $shipping_method->id ) {
				$has_legacy_pickup = true;
				break;
			}
		}

		$settings = array(
			'pickupLocationSettings' => get_option( 'woocommerce_pickup_location_settings', [] ),
			'pickupLocations'        => $formatted_pickup_locations,
			'readonlySettings'       => array(
				'hasLegacyPickup' => $has_legacy_pickup,
				'storeCountry'    => WC()->countries->get_base_country(),
				'storeState'      => WC()->countries->get_base_state(),
			),
		);

		wp_add_inline_script(
			'wc-shipping-method-pickup-location',
			sprintf(
				'var hydratedScreenSettings = %s;',
				wp_json_encode( $settings )
			),
			'before'
		);
	}
	/**
	 * Load admin scripts.
	 */
	public function admin_scripts() {
		$this->asset_api->register_script( 'wc-shipping-method-pickup-location', 'build/wc-shipping-method-pickup-location.js', [], true );
	}

	/**
	 * Registers the Local Pickup shipping method used by the Checkout Block.
	 */
	public function register_local_pickup() {
		if ( CartCheckoutUtils::is_checkout_block_default() ) {
			wc()->shipping->register_shipping_method( new PickupLocation() );
		}
	}

	/**
	 * Declares the Pickup Location shipping method as a Local Pickup method for WooCommerce.
	 *
	 * @param array $methods Shipping method ids.
	 * @return array
	 */
	public function register_local_pickup_method( $methods ) {
		$methods[] = 'pickup_location';
		return $methods;
	}

	/**
	 * Hides the shipping address on the order confirmation page when local pickup is selected.
	 *
	 * @param array $pickup_methods Method ids.
	 * @return array
	 */
	public function hide_shipping_address_for_local_pickup( $pickup_methods ) {
		return array_merge( $pickup_methods, LocalPickupUtils::get_local_pickup_method_ids() );
	}

	/**
	 * Everytime we save or update local pickup settings, we flush the shipping
	 * transient group.
	 *
	 * @param array $settings The setting array we're saving.
	 * @return array $settings The setting array we're saving.
	 */
	public function flush_cache( $settings ) {
		\WC_Cache_Helper::get_transient_version( 'shipping', true );
		return $settings;
	}
	/**
	 * Filter the location used for taxes based on the chosen pickup location.
	 *
	 * @param array $address Location args.
	 * @return array
	 */
	public function filter_taxable_address( $address ) {

		if ( null === WC()->session ) {
			return $address;
		}
		// We only need to select from the first package, since pickup_location only supports a single package.
		$chosen_method          = current( WC()->session->get( 'chosen_shipping_methods', array() ) ) ?? '';
		$chosen_method_id       = explode( ':', $chosen_method )[0];
		$chosen_method_instance = explode( ':', $chosen_method )[1] ?? 0;

		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
		if ( $chosen_method_id && true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && in_array( $chosen_method_id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) {
			$pickup_locations = get_option( 'pickup_location_pickup_locations', [] );
			$pickup_location  = $pickup_locations[ $chosen_method_instance ] ?? [];

			if ( isset( $pickup_location['address'], $pickup_location['address']['country'] ) && ! empty( $pickup_location['address']['country'] ) ) {
				$address = array(
					$pickup_locations[ $chosen_method_instance ]['address']['country'],
					$pickup_locations[ $chosen_method_instance ]['address']['state'],
					$pickup_locations[ $chosen_method_instance ]['address']['postcode'],
					$pickup_locations[ $chosen_method_instance ]['address']['city'],
				);
			}
		}

		return $address;
	}

	/**
	 * Local Pickup requires all packages to support local pickup. This is because the entire order must be picked up
	 * so that all packages get the same tax rates applied during checkout.
	 *
	 * If a shipping package does not support local pickup (e.g. if disabled by an extension), this filters the option
	 * out for all packages. This will in turn disable the "pickup" toggle in Block Checkout.
	 *
	 * @param array $packages Array of shipping packages.
	 * @return array
	 */
	public function filter_shipping_packages( $packages ) {
		// Check all packages for an instance of a collectable shipping method.
		$valid_packages = array_filter(
			$packages,
			function( $package ) {
				$shipping_method_ids = ArrayUtil::select( $package['rates'] ?? [], 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
				return ! empty( array_intersect( LocalPickupUtils::get_local_pickup_method_ids(), $shipping_method_ids ) );
			}
		);

		// Remove pickup location from rates arrays.
		if ( count( $valid_packages ) !== count( $packages ) ) {
			$packages = array_map(
				function( $package ) {
					if ( ! is_array( $package['rates'] ) ) {
						$package['rates'] = [];
						return $package;
					}
					$package['rates'] = array_filter(
						$package['rates'],
						function( $rate ) {
							return ! in_array( $rate->get_method_id(), LocalPickupUtils::get_local_pickup_method_ids(), true );
						}
					);
					return $package;
				},
				$packages
			);
		}

		return $packages;
	}
}