File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Shipping.tar
CountryRatesCollection.php 0000644 00000006046 15154247043 0011746 0 ustar 00 <?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.php 0000644 00000004343 15154247043 0014700 0 ustar 00 <?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.php 0000644 00000006327 15154247043 0016276 0 ustar 00 <?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.php 0000644 00000012501 15154247043 0015007 0 ustar 00 <?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.php 0000644 00000003147 15154247043 0015101 0 ustar 00 <?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.php 0000644 00000003236 15154247043 0014400 0 ustar 00 <?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.php 0000644 00000020520 15154247043 0015033 0 ustar 00 <?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.php 0000644 00000002347 15154247043 0007654 0 ustar 00 <?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.php 0000644 00000003451 15154247044 0012051 0 ustar 00 <?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.php 0000644 00000004320 15154247044 0011731 0 ustar 00 <?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.php 0000644 00000003225 15154247044 0010022 0 ustar 00 <?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.php 0000644 00000005057 15154247044 0011705 0 ustar 00 <?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.php 0000644 00000005306 15154247044 0010541 0 ustar 00 <?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.php 0000644 00000004742 15154247045 0007670 0 ustar 00 <?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.php 0000644 00000003307 15154247045 0010214 0 ustar 00 <?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.php 0000644 00000005320 15154247045 0012436 0 ustar 00 <?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.php 0000644 00000012005 15154247045 0007677 0 ustar 00 <?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.php 0000644 00000012174 15154247045 0007540 0 ustar 00 <?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.php 0000644 00000010145 15154247045 0011231 0 ustar 00 <?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.php 0000644 00000012730 15154247045 0010703 0 ustar 00 <?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.php 0000644 00000010722 15155067126 0010213 0 ustar 00 <?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.php 0000644 00000037714 15155067126 0011126 0 ustar 00 <?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;
}
}