File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/MerchantCenter.tar
AccountService.php 0000644 00000055711 15154165034 0010207 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\Jetpack\Connection\Client;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Middleware;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\SiteVerification;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\OAuthService;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\MerchantIssueTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingRateTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingTimeTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ApiNotReady;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\CleanupSyncedProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\AdsAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\MerchantAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerInterface;
use Exception;
use Jetpack_Options;
defined( 'ABSPATH' ) || exit;
/**
* Class AccountService
*
* Container used to access:
* - Ads
* - AdsAccountState
* - JobRepository
* - Merchant
* - MerchantCenterService
* - MerchantIssueTable
* - MerchantStatuses
* - Middleware
* - SiteVerification
* - ShippingRateTable
* - ShippingTimeTable
*
* @since 1.12.0
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
class AccountService implements ContainerAwareInterface, OptionsAwareInterface, Service {
use ContainerAwareTrait;
use OptionsAwareTrait;
use PluginHelper;
/**
* @var MerchantAccountState
*/
protected $state;
/**
* Perform a website claim with overwrite.
*
* @var bool
*/
protected $overwrite_claim = false;
/**
* Allow switching the existing website URL.
*
* @var bool
*/
protected $allow_switch_url = false;
/**
* AccountService constructor.
*
* @param MerchantAccountState $state
*/
public function __construct( MerchantAccountState $state ) {
$this->state = $state;
}
/**
* Get all Merchant Accounts associated with the connected account.
*
* @return array
* @throws Exception When an API error occurs.
*/
public function get_accounts(): array {
return $this->container->get( Middleware::class )->get_merchant_accounts();
}
/**
* Use an existing MC account. Mark the 'set_id' step as done, update the MC account's website URL,
* and sets the Merchant ID.
*
* @param int $account_id The merchant ID to use.
*
* @throws ExceptionWithResponseData If there's a website URL conflict, or account data can't be retrieved.
*/
public function use_existing_account_id( int $account_id ): void {
// Reset the process if the provided ID isn't the same as the one stored in options.
$merchant_id = $this->options->get_merchant_id();
if ( $merchant_id && $merchant_id !== $account_id ) {
$this->reset_account_setup();
}
$state = $this->state->get();
// Don't do anything if this step was already finished.
if ( MerchantAccountState::STEP_DONE === $state['set_id']['status'] ) {
return;
}
try {
// Make sure the existing account has the correct website URL (or fail).
$this->maybe_add_merchant_center_url( $account_id );
// Re-fetch state as it might have changed.
$state = $this->state->get();
$middleware = $this->container->get( Middleware::class );
// Maybe the existing account is a sub-account!
$state['set_id']['data']['from_mca'] = false;
foreach ( $middleware->get_merchant_accounts() as $existing_account ) {
if ( $existing_account['id'] === $account_id ) {
$state['set_id']['data']['from_mca'] = $existing_account['subaccount'];
break;
}
}
$middleware->link_merchant_account( $account_id );
$state['set_id']['status'] = MerchantAccountState::STEP_DONE;
$this->state->update( $state );
} catch ( ExceptionWithResponseData $e ) {
throw $e;
} catch ( Exception $e ) {
throw $this->prepare_exception( $e->getMessage(), [], $e->getCode() );
}
}
/**
* Run the process for setting up a Merchant Center account (sub-account or standalone).
*
* @param int $account_id
*
* @return array The account ID if setup has completed.
* @throws ExceptionWithResponseData When the account is already connected or a setup error occurs.
*/
public function setup_account( int $account_id ) {
// Reset the process if the provided ID isn't the same as the one stored in options.
$merchant_id = $this->options->get_merchant_id();
if ( $merchant_id && $merchant_id !== $account_id ) {
$this->reset_account_setup();
}
try {
return $this->setup_account_steps();
} catch ( ExceptionWithResponseData | ApiNotReady $e ) {
throw $e;
} catch ( Exception $e ) {
throw $this->prepare_exception( $e->getMessage(), [], $e->getCode() );
}
}
/**
* Create or link an account, switching the URL during the set_id step.
*
* @param int $account_id
*
* @return array
* @throws ExceptionWithResponseData When a setup error occurs.
*/
public function switch_url( int $account_id ): array {
$state = $this->state->get();
$switch_necessary = ! empty( $state['set_id']['data']['old_url'] );
$set_id_status = $state['set_id']['status'] ?? MerchantAccountState::STEP_PENDING;
if ( ! $account_id || MerchantAccountState::STEP_DONE === $set_id_status || ! $switch_necessary ) {
throw $this->prepare_exception(
__( 'Attempting invalid URL switch.', 'google-listings-and-ads' )
);
}
$this->allow_switch_url = true;
$this->use_existing_account_id( $account_id );
return $this->setup_account( $account_id );
}
/**
* Create or link an account, overwriting the website claim during the claim step.
*
* @param int $account_id
*
* @return array
* @throws ExceptionWithResponseData When a setup error occurs.
*/
public function overwrite_claim( int $account_id ): array {
$state = $this->state->get( false );
$overwrite_necessary = ! empty( $state['claim']['data']['overwrite_required'] );
$claim_status = $state['claim']['status'] ?? MerchantAccountState::STEP_PENDING;
if ( MerchantAccountState::STEP_DONE === $claim_status || ! $overwrite_necessary ) {
throw $this->prepare_exception(
__( 'Attempting invalid claim overwrite.', 'google-listings-and-ads' )
);
}
$this->overwrite_claim = true;
return $this->setup_account( $account_id );
}
/**
* Get the connected merchant account.
*
* @return array
*/
public function get_connected_status(): array {
/** @var NotificationsService $notifications_service */
$notifications_service = $this->container->get( NotificationsService::class );
$id = $this->options->get_merchant_id();
$wpcom_rest_api_status = $this->options->get( OptionsInterface::WPCOM_REST_API_STATUS );
// If token is revoked outside the extension. Set the status as error to force the merchant to grant access again.
if ( $wpcom_rest_api_status === 'approved' && ! $this->is_wpcom_api_status_healthy() ) {
$wpcom_rest_api_status = OAuthService::STATUS_ERROR;
$this->options->update( OptionsInterface::WPCOM_REST_API_STATUS, $wpcom_rest_api_status );
}
$status = [
'id' => $id,
'status' => $id ? 'connected' : 'disconnected',
'notification_service_enabled' => $notifications_service->is_enabled(),
'wpcom_rest_api_status' => $wpcom_rest_api_status,
];
$incomplete = $this->state->last_incomplete_step();
if ( ! empty( $incomplete ) ) {
$status['status'] = 'incomplete';
$status['step'] = $incomplete;
}
return $status;
}
/**
* Return the setup status to determine what step to continue at.
*
* @return array
*/
public function get_setup_status(): array {
return $this->container->get( MerchantCenterService::class )->get_setup_status();
}
/**
* Disconnect Merchant Center account
*/
public function disconnect() {
$this->options->delete( OptionsInterface::CONTACT_INFO_SETUP );
$this->options->delete( OptionsInterface::MC_SETUP_COMPLETED_AT );
$this->options->delete( OptionsInterface::MERCHANT_ACCOUNT_STATE );
$this->options->delete( OptionsInterface::MERCHANT_CENTER );
$this->options->delete( OptionsInterface::SITE_VERIFICATION );
$this->options->delete( OptionsInterface::TARGET_AUDIENCE );
$this->options->delete( OptionsInterface::MERCHANT_ID );
$this->options->delete( OptionsInterface::CLAIMED_URL_HASH );
$this->container->get( MerchantStatuses::class )->delete();
$this->container->get( MerchantIssueTable::class )->truncate();
$this->container->get( ShippingRateTable::class )->truncate();
$this->container->get( ShippingTimeTable::class )->truncate();
$this->container->get( JobRepository::class )->get( CleanupSyncedProducts::class )->schedule();
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_ACCOUNT_REVIEW );
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::URL_MATCHES );
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_IS_SUBACCOUNT );
}
/**
* Performs the steps necessary to initialize a Merchant Center account.
* Should always resume up at the last pending or unfinished step. If the Merchant Center account
* has already been created, the ID is simply returned.
*
* @return array The newly created (or pre-existing) Merchant account data.
* @throws ExceptionWithResponseData If an error occurs during any step.
* @throws Exception If the step is unknown.
* @throws ApiNotReady If we should wait to complete the next step.
*/
private function setup_account_steps() {
$state = $this->state->get();
$merchant_id = $this->options->get_merchant_id();
$merchant = $this->container->get( Merchant::class );
$middleware = $this->container->get( Middleware::class );
foreach ( $state as $name => &$step ) {
if ( MerchantAccountState::STEP_DONE === $step['status'] ) {
continue;
}
if ( 'link' === $name ) {
$time_to_wait = $this->state->get_seconds_to_wait_after_created();
if ( $time_to_wait ) {
sleep( $time_to_wait );
}
}
try {
switch ( $name ) {
case 'set_id':
// Just in case, don't create another merchant ID.
if ( ! empty( $merchant_id ) ) {
break;
}
$merchant_id = $middleware->create_merchant_account();
$step['data']['from_mca'] = true;
$step['data']['created_timestamp'] = time();
break;
case 'verify':
// Skip if previously verified.
if ( $this->state->is_site_verified() ) {
break;
}
$site_url = esc_url_raw( $this->get_site_url() );
$this->container->get( SiteVerification::class )->verify_site( $site_url );
break;
case 'link':
$middleware->link_merchant_to_mca();
break;
case 'claim':
// At this step, the website URL is assumed to be correct.
// If the URL is already claimed, no claim should be attempted.
if ( $merchant->get_accountstatus( $merchant_id )->getWebsiteClaimed() ) {
break;
}
if ( $this->overwrite_claim ) {
$middleware->claim_merchant_website( true );
} else {
$merchant->claimwebsite();
}
break;
case 'link_ads':
// Continue to next step if Ads account is not connected yet.
if ( ! $this->options->get_ads_id() ) {
// Save step as pending and continue the foreach loop with `continue 2`.
$state[ $name ]['status'] = MerchantAccountState::STEP_PENDING;
$this->state->update( $state );
continue 2;
}
$this->link_ads_account();
break;
default:
throw new Exception(
sprintf(
/* translators: 1: is a string representing an unknown step name */
__( 'Unknown merchant account creation step %1$s', 'google-listings-and-ads' ),
$name
)
);
}
$step['status'] = MerchantAccountState::STEP_DONE;
$step['message'] = '';
$this->state->update( $state );
} catch ( Exception $e ) {
$step['status'] = MerchantAccountState::STEP_ERROR;
$step['message'] = $e->getMessage();
// URL already claimed.
if ( 'claim' === $name && 403 === $e->getCode() ) {
$data = [
'id' => $merchant_id,
'website_url' => $this->strip_url_protocol(
esc_url_raw( $this->get_site_url() )
),
];
// Sub-account: request overwrite confirmation.
if ( $state['set_id']['data']['from_mca'] ?? true ) {
do_action( 'woocommerce_gla_site_claim_overwrite_required', [] );
$step['data']['overwrite_required'] = true;
$e = $this->prepare_exception( $e->getMessage(), $data, $e->getCode() );
} else {
do_action( 'woocommerce_gla_site_claim_failure', [ 'details' => 'independent_account' ] );
// Independent account: overwrite not possible.
$e = $this->prepare_exception(
__( 'Unable to claim website URL with this Merchant Center Account.', 'google-listings-and-ads' ),
$data,
406
);
}
} elseif ( 'link' === $name && 401 === $e->getCode() ) {
// New sub-account not yet manipulable.
$state['set_id']['data']['created_timestamp'] = time();
$e = ApiNotReady::retry_after( MerchantAccountState::MC_DELAY_AFTER_CREATE );
}
$this->state->update( $state );
throw $e;
}
}
return [ 'id' => $merchant_id ];
}
/**
* Restart the account setup when we are connecting with a different account ID.
* Do not allow reset when the full setup process has completed.
*
* @throws ExceptionWithResponseData When the full setup process is completed.
*/
private function reset_account_setup() {
// Can't reset if the MC connection process has been completed previously.
if ( $this->container->get( MerchantCenterService::class )->is_setup_complete() ) {
throw $this->prepare_exception(
sprintf(
/* translators: 1: is a numeric account ID */
__( 'Merchant Center account already connected: %d', 'google-listings-and-ads' ),
$this->options->get_merchant_id()
)
);
}
$this->disconnect();
}
/**
* Ensure the Merchant Center account's Website URL matches the site URL. Update an empty value or
* a different, unclaimed URL value. Throw a 409 exception if a different, claimed URL is found.
*
* @param int $merchant_id The Merchant Center account to update.
*
* @throws ExceptionWithResponseData If the account URL doesn't match the site URL or the URL is invalid.
*/
private function maybe_add_merchant_center_url( int $merchant_id ) {
$site_url = esc_url_raw( $this->get_site_url() );
if ( ! wc_is_valid_url( $site_url ) ) {
throw $this->prepare_exception( __( 'Invalid site URL.', 'google-listings-and-ads' ) );
}
/** @var Merchant $merchant */
$merchant = $this->container->get( Merchant::class );
/** @var Account $account */
$account = $merchant->get_account( $merchant_id );
$account_url = $account->getWebsiteUrl() ?: '';
if ( untrailingslashit( $site_url ) !== untrailingslashit( $account_url ) ) {
$is_website_claimed = $merchant->get_accountstatus( $merchant_id )->getWebsiteClaimed();
if ( ! empty( $account_url ) && $is_website_claimed && ! $this->allow_switch_url ) {
$state = $this->state->get();
$state['set_id']['data']['old_url'] = $account_url;
$state['set_id']['status'] = MerchantAccountState::STEP_ERROR;
$this->state->update( $state );
$clean_account_url = $this->strip_url_protocol( $account_url );
$clean_site_url = $this->strip_url_protocol( $site_url );
do_action( 'woocommerce_gla_url_switch_required', [] );
throw $this->prepare_exception(
sprintf(
/* translators: 1: is a website URL (without the protocol) */
__( 'This Merchant Center account already has a verified and claimed URL, %1$s', 'google-listings-and-ads' ),
$clean_account_url
),
[
'id' => $merchant_id,
'claimed_url' => $clean_account_url,
'new_url' => $clean_site_url,
],
409
);
}
$account->setWebsiteUrl( $site_url );
$merchant->update_account( $account );
// Clear previous hashed URL.
$this->options->delete( OptionsInterface::CLAIMED_URL_HASH );
do_action( 'woocommerce_gla_url_switch_success', [] );
}
}
/**
* Get the callback function for linking an Ads account.
*
* @throws Exception When the merchant account hasn't been set yet.
*/
private function link_ads_account() {
if ( ! $this->options->get_merchant_id() ) {
throw new Exception( 'A Merchant Center account must be connected' );
}
$ads_state = $this->container->get( AdsAccountState::class );
// Create link for Merchant and accept it in Ads.
$waiting_acceptance = $this->container->get( Merchant::class )->link_ads_id( $this->options->get_ads_id() );
if ( $waiting_acceptance ) {
$this->container->get( Ads::class )->accept_merchant_link( $this->options->get_merchant_id() );
}
$ads_state->complete_step( 'link_merchant' );
}
/**
* Prepares an Exception to be thrown with Merchant data:
* - Ensure it has the merchant_id value
* - Default to a 400 error code
*
* @param string $message
* @param array $data
* @param int|null $code
*
* @return ExceptionWithResponseData
*/
private function prepare_exception( string $message, array $data = [], ?int $code = null ): ExceptionWithResponseData {
$merchant_id = $this->options->get_merchant_id();
if ( $merchant_id && ! isset( $data['id'] ) ) {
$data['id'] = $merchant_id;
}
return new ExceptionWithResponseData( $message, $code ?: 400, null, $data );
}
/**
* Delete the option regarding WPCOM authorization
*
* @return bool
*/
public function reset_wpcom_api_authorization_data(): bool {
$this->delete_wpcom_api_auth_nonce();
$this->delete_wpcom_api_status_transient();
return $this->options->delete( OptionsInterface::WPCOM_REST_API_STATUS );
}
/**
* Update the status of the merchant granting access to Google's WPCOM app in the database.
* Before updating the status in the DB it will compare the nonce stored in the DB with the nonce passed to the API.
*
* @param string $status The status of the merchant granting access to Google's WPCOM app.
* @param string $nonce The nonce provided by Google in the URL query parameter when Google redirects back to merchant's site.
*
* @return bool
* @throws ExceptionWithResponseData If the stored nonce / nonce from query param is not provided, or the nonces mismatch.
*/
public function update_wpcom_api_authorization( string $status, string $nonce ): bool {
try {
$stored_nonce = $this->options->get( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE );
if ( empty( $stored_nonce ) ) {
throw $this->prepare_exception(
__( 'No stored nonce found in the database, skip updating auth status.', 'google-listings-and-ads' )
);
}
if ( empty( $nonce ) ) {
throw $this->prepare_exception(
__( 'Nonce is not provided, skip updating auth status.', 'google-listings-and-ads' )
);
}
if ( $stored_nonce !== $nonce ) {
$this->delete_wpcom_api_auth_nonce();
throw $this->prepare_exception(
__( 'Nonces mismatch, skip updating auth status.', 'google-listings-and-ads' )
);
}
$this->delete_wpcom_api_auth_nonce();
/**
* When the WPCOM Authorization status has been updated.
*
* @event update_wpcom_api_authorization
* @property string status The status of the request.
* @property int|null blog_id The blog ID.
*/
do_action(
'woocommerce_gla_track_event',
'update_wpcom_api_authorization',
[
'status' => $status,
'blog_id' => Jetpack_Options::get_option( 'id' ),
]
);
$this->delete_wpcom_api_status_transient();
return $this->options->update( OptionsInterface::WPCOM_REST_API_STATUS, $status );
} catch ( ExceptionWithResponseData $e ) {
/**
* When the WPCOM Authorization status has been updated with errors.
*
* @event update_wpcom_api_authorization
* @property string status The status of the request.
* @property int|null blog_id The blog ID.
*/
do_action(
'woocommerce_gla_track_event',
'update_wpcom_api_authorization',
[
'status' => $e->getMessage(),
'blog_id' => Jetpack_Options::get_option( 'id' ),
]
);
throw $e;
}
}
/**
* Delete the nonce of "verifying Google is the one redirect back to merchant site and set the auth status" in the database.
*
* @return bool
*/
public function delete_wpcom_api_auth_nonce(): bool {
return $this->options->delete( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE );
}
/**
* Deletes the transient storing the WPCOM Status data.
*/
public function delete_wpcom_api_status_transient(): void {
$transients = $this->container->get( TransientsInterface::class );
$transients->delete( TransientsInterface::WPCOM_API_STATUS );
}
/**
* Check if the WPCOM API Status is healthy by doing a request to /wc/partners/google/remote-site-status endpoint in WPCOM.
*
* @return bool True when the status is healthy, false otherwise.
*/
public function is_wpcom_api_status_healthy() {
/** @var TransientsInterface $transients */
$transients = $this->container->get( TransientsInterface::class );
$status = $transients->get( TransientsInterface::WPCOM_API_STATUS );
if ( ! $status ) {
$integration_status_args = [
'method' => 'GET',
'timeout' => 30,
'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/' . Jetpack_Options::get_option( 'id' ) . '/wc/partners/google/remote-site-status',
'user_id' => get_current_user_id(),
];
$integration_remote_request_response = Client::remote_request( $integration_status_args, null );
if ( is_wp_error( $integration_remote_request_response ) ) {
$status = [ 'is_healthy' => false ];
} else {
$status = json_decode( wp_remote_retrieve_body( $integration_remote_request_response ), true ) ?? [ 'is_healthy' => false ];
}
$transients->set( TransientsInterface::WPCOM_API_STATUS, $status, MINUTE_IN_SECONDS * 30 );
}
return isset( $status['is_healthy'] ) && $status['is_healthy'] && $status['is_wc_rest_api_healthy'] && $status['is_partner_token_healthy'];
}
}
ContactInformation.php 0000644 00000005416 15154165034 0011070 0 ustar 00 <?php
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Settings;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\AccountBusinessInformation;
/**
* Class ContactInformation.
*
* @since 1.4.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
class ContactInformation implements Service {
/**
* @var Merchant
*/
protected $merchant;
/**
* @var Settings
*/
protected $settings;
/**
* ContactInformation constructor.
*
* @param Merchant $merchant
* @param Settings $settings
*/
public function __construct( Merchant $merchant, Settings $settings ) {
$this->merchant = $merchant;
$this->settings = $settings;
}
/**
* Get the contact information for the connected Merchant Center account.
*
* @return AccountBusinessInformation|null The contact information associated with the Merchant Center account or
* null.
*
* @throws ExceptionWithResponseData If the Merchant Center account can't be retrieved.
*/
public function get_contact_information(): ?AccountBusinessInformation {
$business_information = $this->merchant->get_account()->getBusinessInformation();
return $business_information ?: null;
}
/**
* Update the address for the connected Merchant Center account to the store address set in WooCommerce
* settings.
*
* @return AccountBusinessInformation The contact information associated with the Merchant Center account.
*
* @throws ExceptionWithResponseData If the Merchant Center account can't be retrieved or updated.
*/
public function update_address_based_on_store_settings(): AccountBusinessInformation {
$business_information = $this->get_contact_information() ?: new AccountBusinessInformation();
$store_address = $this->settings->get_store_address();
$business_information->setAddress( $store_address );
$this->update_contact_information( $business_information );
return $business_information;
}
/**
* Update the contact information for the connected Merchant Center account.
*
* @param AccountBusinessInformation $business_information
*
* @throws ExceptionWithResponseData If the Merchant Center account can't be retrieved or updated.
*/
protected function update_contact_information( AccountBusinessInformation $business_information ): void {
$account = $this->merchant->get_account();
$account->setBusinessInformation( $business_information );
$this->merchant->update_account( $account );
}
}
MerchantCenterAwareInterface.php 0000644 00000000711 15154165034 0012763 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
defined( 'ABSPATH' ) || exit;
/**
* Interface MerchantCenterAwareInterface
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
interface MerchantCenterAwareInterface {
/**
* @param MerchantCenterService $merchant_center
*/
public function set_merchant_center_object( MerchantCenterService $merchant_center ): void;
}
MerchantCenterAwareTrait.php 0000644 00000001134 15154165034 0012146 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
defined( 'ABSPATH' ) || exit;
/**
* Trait MerchantCenterAwareTrait
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
trait MerchantCenterAwareTrait {
/**
* The MerchantCenterService object.
*
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* @param MerchantCenterService $merchant_center
*/
public function set_merchant_center_object( MerchantCenterService $merchant_center ): void {
$this->merchant_center = $merchant_center;
}
}
MerchantCenterService.php 0000644 00000032364 15154165034 0011514 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AdsService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Settings;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\ShippingRateQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\ShippingTimeQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\MerchantAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\AddressUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\AccountAddress;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\AccountBusinessInformation;
use DateTime;
defined( 'ABSPATH' ) || exit;
/**
* Class MerchantCenterService
*
* ContainerAware used to access:
* - AddressUtility
* - AdsService
* - ContactInformation
* - Merchant
* - MerchantAccountState
* - MerchantStatuses
* - Settings
* - ShippingRateQuery
* - ShippingTimeQuery
* - TransientsInterface
* - WC
* - WP
* - TargetAudience
* - GoogleHelper
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
class MerchantCenterService implements ContainerAwareInterface, OptionsAwareInterface, Service {
use ContainerAwareTrait;
use OptionsAwareTrait;
use PluginHelper;
/**
* MerchantCenterService constructor.
*/
public function __construct() {
add_filter(
'woocommerce_gla_custom_merchant_issues',
function ( array $issues, DateTime $cache_created_time ) {
return $this->maybe_add_contact_info_issue( $issues, $cache_created_time );
},
10,
2
);
}
/**
* Get whether Merchant Center setup is completed.
*
* @return bool
*/
public function is_setup_complete(): bool {
return boolval( $this->options->get( OptionsInterface::MC_SETUP_COMPLETED_AT, false ) );
}
/**
* Get whether Merchant Center is connected.
*
* @return bool
*/
public function is_connected(): bool {
return $this->is_google_connected() && $this->is_setup_complete();
}
/**
* Get whether the dependent Google account is connected.
*
* @return bool
*/
public function is_google_connected(): bool {
return boolval( $this->options->get( OptionsInterface::GOOGLE_CONNECTED, false ) );
}
/**
* Whether we are able to sync data to the Merchant Center account.
* Account must be connected and the URL we claimed with must match the site URL.
* URL matches is stored in a transient to prevent it from being refetched in cases
* where the site is unable to access account data.
*
* @since 1.13.0
* @return boolean
*/
public function is_ready_for_syncing(): bool {
if ( ! $this->is_connected() ) {
return false;
}
/** @var TransientsInterface $transients */
$transients = $this->container->get( TransientsInterface::class );
$url_matches = $transients->get( TransientsInterface::URL_MATCHES );
if ( null === $url_matches ) {
$claimed_url_hash = $this->container->get( Merchant::class )->get_claimed_url_hash();
$site_url_hash = md5( untrailingslashit( $this->get_site_url() ) );
$url_matches = apply_filters( 'woocommerce_gla_ready_for_syncing', $claimed_url_hash === $site_url_hash ) ? 'yes' : 'no';
$transients->set( TransientsInterface::URL_MATCHES, $url_matches, HOUR_IN_SECONDS * 12 );
}
return 'yes' === $url_matches;
}
/**
* Whether we should push data into MC. Only if:
* - MC is ready for syncing {@see is_ready_for_syncing}
* - Notifications Service is not enabled
*
* @return bool
* @since 2.8.0
*/
public function should_push(): bool {
return $this->is_ready_for_syncing();
}
/**
* Get whether the country is supported by the Merchant Center.
*
* @return bool True if the country is in the list of MC-supported countries.
*
* @since 1.9.0
*/
public function is_store_country_supported(): bool {
$country = $this->container->get( WC::class )->get_base_country();
/** @var GoogleHelper $google_helper */
$google_helper = $this->container->get( GoogleHelper::class );
return $google_helper->is_country_supported( $country );
}
/**
* Get whether the language is supported by the Merchant Center.
*
* @param string $language Optional - to check a language other than the site language.
* @return bool True if the language is in the list of MC-supported languages.
*/
public function is_language_supported( string $language = '' ): bool {
// Default to base site language
if ( empty( $language ) ) {
$language = substr( $this->container->get( WP::class )->get_locale(), 0, 2 );
}
/** @var GoogleHelper $google_helper */
$google_helper = $this->container->get( GoogleHelper::class );
return array_key_exists(
strtolower( $language ),
$google_helper->get_mc_supported_languages()
);
}
/**
* Get whether the contact information has been setup.
*
* @since 1.4.0
*
* @return bool
*/
public function is_contact_information_setup(): bool {
if ( true === boolval( $this->options->get( OptionsInterface::CONTACT_INFO_SETUP, false ) ) ) {
return true;
}
// Additional check for users that have already gone through on-boarding.
if ( $this->is_setup_complete() ) {
$is_mc_setup = $this->is_mc_contact_information_setup();
$this->options->update( OptionsInterface::CONTACT_INFO_SETUP, $is_mc_setup );
return $is_mc_setup;
}
return false;
}
/**
* Return if the given country is supported to have promotions on Google.
*
* @param string $country
*
* @return bool
*/
public function is_promotion_supported_country( string $country = '' ): bool {
// Default to WooCommerce store country
if ( empty( $country ) ) {
$country = $this->container->get( WC::class )->get_base_country();
}
/** @var GoogleHelper $google_helper */
$google_helper = $this->container->get( GoogleHelper::class );
return in_array( $country, $google_helper->get_mc_promotion_supported_countries(), true );
}
/**
* Return the setup status to determine what step to continue at.
*
* @return array
*/
public function get_setup_status(): array {
if ( $this->is_setup_complete() ) {
return [ 'status' => 'complete' ];
}
$step = 'accounts';
if (
$this->connected_account() &&
$this->container->get( AdsService::class )->connected_account() &&
$this->is_mc_contact_information_setup()
) {
$step = 'product_listings';
if ( $this->saved_target_audience() && $this->saved_shipping_and_tax_options() ) {
$step = 'paid_ads';
}
}
return [
'status' => 'incomplete',
'step' => $step,
];
}
/**
* Check if account has been connected.
*
* @return bool
*/
protected function connected_account(): bool {
$id = $this->options->get_merchant_id();
return $id && ! $this->container->get( MerchantAccountState::class )->last_incomplete_step();
}
/**
* Check if target audience has been saved (with a valid selection of countries).
*
* @return bool
*/
protected function saved_target_audience(): bool {
$audience = $this->options->get( OptionsInterface::TARGET_AUDIENCE );
if ( empty( $audience ) || ! isset( $audience['location'] ) ) {
return false;
}
$empty_selection = 'selected' === $audience['location'] && empty( $audience['countries'] );
return ! $empty_selection;
}
/**
* Checks if we should add an issue when the contact information is not setup.
*
* @since 1.4.0
*
* @param array $issues The current array of custom issues
* @param DateTime $cache_created_time The time of the cache/issues generation.
*
* @return array
*/
protected function maybe_add_contact_info_issue( array $issues, DateTime $cache_created_time ): array {
if ( $this->is_setup_complete() && ! $this->is_contact_information_setup() ) {
$issues[] = [
'product_id' => 0,
'product' => 'All products',
'code' => 'missing_contact_information',
'issue' => __( 'No contact information.', 'google-listings-and-ads' ),
'action' => __( 'Add store contact information', 'google-listings-and-ads' ),
'action_url' => $this->get_settings_url(),
'created_at' => $cache_created_time->format( 'Y-m-d H:i:s' ),
'type' => MerchantStatuses::TYPE_ACCOUNT,
'severity' => 'error',
'source' => 'filter',
];
}
return $issues;
}
/**
* Check if the Merchant Center contact information has been setup already.
*
* @since 1.4.0
*
* @return boolean
*/
protected function is_mc_contact_information_setup(): bool {
$is_setup = [
'address' => false,
];
try {
$contact_info = $this->container->get( ContactInformation::class )->get_contact_information();
} catch ( ExceptionWithResponseData $exception ) {
do_action(
'woocommerce_gla_debug_message',
'Error retrieving Merchant Center account\'s business information.',
__METHOD__
);
return false;
}
if ( $contact_info instanceof AccountBusinessInformation ) {
/** @var Settings $settings */
$settings = $this->container->get( Settings::class );
if ( $contact_info->getAddress() instanceof AccountAddress && $settings->get_store_address() instanceof AccountAddress ) {
$is_setup['address'] = $this->container->get( AddressUtility::class )->compare_addresses(
$contact_info->getAddress(),
$settings->get_store_address()
);
}
}
return $is_setup['address'];
}
/**
* Check if the taxes + shipping rate and time + free shipping settings have been saved.
*
* @return bool If all required settings have been provided.
*
* @since 1.4.0
*/
protected function saved_shipping_and_tax_options(): bool {
$merchant_center_settings = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] );
$target_countries = $this->container->get( TargetAudience::class )->get_target_countries();
// Tax options saved if: not US (no taxes) or tax_rate has been set
if ( in_array( 'US', $target_countries, true ) && empty( $merchant_center_settings['tax_rate'] ) ) {
return false;
}
// Shipping time saved if: 'manual' OR records for all countries
if ( isset( $merchant_center_settings['shipping_time'] ) && 'manual' === $merchant_center_settings['shipping_time'] ) {
$saved_shipping_time = true;
} else {
$shipping_time_rows = $this->container->get( ShippingTimeQuery::class )->get_results();
// Get the name of countries that have saved shipping times.
$saved_time_countries = array_column( $shipping_time_rows, 'country' );
// Check if all target countries have a shipping time.
$saved_shipping_time = count( $shipping_time_rows ) === count( $target_countries ) &&
empty( array_diff( $target_countries, $saved_time_countries ) );
}
// Shipping rates saved if: 'manual', 'automatic', OR there are records for all countries
if (
isset( $merchant_center_settings['shipping_rate'] ) &&
in_array( $merchant_center_settings['shipping_rate'], [ 'manual', 'automatic' ], true )
) {
$saved_shipping_rate = true;
} else {
// Get the list of saved shipping rates grouped by country.
/**
* @var ShippingRateQuery $shipping_rate_query
*/
$shipping_rate_query = $this->container->get( ShippingRateQuery::class );
$shipping_rate_query->group_by( 'country' );
$shipping_rate_rows = $shipping_rate_query->get_results();
// Get the name of countries that have saved shipping rates.
$saved_rates_countries = array_column( $shipping_rate_rows, 'country' );
// Check if all target countries have a shipping rate.
$saved_shipping_rate = count( $shipping_rate_rows ) === count( $target_countries ) &&
empty( array_diff( $target_countries, $saved_rates_countries ) );
}
return $saved_shipping_rate && $saved_shipping_time;
}
/**
* Determine whether there are any account-level issues.
*
* @since 1.11.0
* @return bool
*/
public function has_account_issues(): bool {
$issues = $this->container->get( MerchantStatuses::class )->get_issues( MerchantStatuses::TYPE_ACCOUNT );
return isset( $issues['issues'] ) && count( $issues['issues'] ) >= 1;
}
/**
* Determine whether there is at least one synced product.
*
* @since 1.11.0
* @return bool
*/
public function has_at_least_one_synced_product(): bool {
$statuses = $this->container->get( MerchantStatuses::class )->get_product_statistics();
return isset( $statuses['statistics']['active'] ) && $statuses['statistics']['active'] >= 1;
}
}
MerchantStatuses.php 0000644 00000111362 15154165034 0010562 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\ProductMetaQueryHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\MerchantIssueQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\MerchantIssueTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\Transients;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductMetaHandler;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\MCStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\ProductStatus as GoogleProductStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\DeleteAllProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateAllProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateMerchantProductStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use DateTime;
use Exception;
use WC_Product;
/**
* Class MerchantStatuses.
* Note: this class uses vanilla WP methods get_post, get_post_meta, update_post_meta
*
* ContainerAware used to retrieve
* - JobRepository
* - Merchant
* - MerchantCenterService
* - MerchantIssueQuery
* - MerchantIssueTable
* - ProductHelper
* - ProductRepository
* - TransientsInterface
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
class MerchantStatuses implements Service, ContainerAwareInterface, OptionsAwareInterface {
use OptionsAwareTrait;
use ContainerAwareTrait;
use PluginHelper;
/**
* The lifetime of the status-related data.
*/
public const STATUS_LIFETIME = 12 * HOUR_IN_SECONDS;
/**
* The types of issues.
*/
public const TYPE_ACCOUNT = 'account';
public const TYPE_PRODUCT = 'product';
/**
* Issue severity levels.
*/
public const SEVERITY_WARNING = 'warning';
public const SEVERITY_ERROR = 'error';
/**
* @var DateTime $cache_created_time For cache age operations.
*/
protected $cache_created_time;
/**
* @var array Reference array of countries associated to each product+issue combo.
*/
protected $product_issue_countries = [];
/**
* @var array Transient with timestamp and product statuses as reported by Merchant Center.
*/
protected $mc_statuses;
/**
* @var array Statuses for each product id and parent id.
*/
protected $product_statuses = [
'products' => [],
'parents' => [],
];
/**
* @var array Default product stats.
*/
public const DEFAULT_PRODUCT_STATS = [
MCStatus::APPROVED => 0,
MCStatus::PARTIALLY_APPROVED => 0,
MCStatus::EXPIRING => 0,
MCStatus::PENDING => 0,
MCStatus::DISAPPROVED => 0,
MCStatus::NOT_SYNCED => 0,
'parents' => [],
];
/**
* @var array Initial intermediate data for product status counts.
*/
protected $initial_intermediate_data = self::DEFAULT_PRODUCT_STATS;
/**
* @var WC_Product[] Lookup of WooCommerce Product Objects.
*/
protected $product_data_lookup = [];
/**
* MerchantStatuses constructor.
*/
public function __construct() {
$this->cache_created_time = new DateTime();
}
/**
* Get the Product Statistics (updating caches if necessary). This is the
* number of product IDs with each status (approved and partially approved are combined as active).
*
* @param bool $force_refresh Force refresh of all product status data.
*
* @return array The product status statistics.
* @throws Exception If no Merchant Center account is connected, or account status is not retrievable.
*/
public function get_product_statistics( bool $force_refresh = false ): array {
$job = $this->maybe_refresh_status_data( $force_refresh );
$failure_rate_msg = $job->get_failure_rate_message();
$this->mc_statuses = $this->container->get( TransientsInterface::class )->get( Transients::MC_STATUSES );
// If the failure rate is too high, return an error message so the UI can stop polling.
if ( $failure_rate_msg && null === $this->mc_statuses ) {
return [
'timestamp' => $this->cache_created_time->getTimestamp(),
'statistics' => null,
'loading' => false,
'error' => __( 'The scheduled job has been paused due to a high failure rate.', 'google-listings-and-ads' ),
];
}
if ( $job->is_scheduled() || null === $this->mc_statuses ) {
return [
'timestamp' => $this->cache_created_time->getTimestamp(),
'statistics' => null,
'loading' => true,
'error' => null,
];
}
if ( ! empty( $this->mc_statuses['error'] ) ) {
return $this->mc_statuses;
}
$counting_stats = $this->mc_statuses['statistics'];
$counting_stats = array_merge(
[ 'active' => $counting_stats[ MCStatus::PARTIALLY_APPROVED ] + $counting_stats[ MCStatus::APPROVED ] ],
$counting_stats
);
unset( $counting_stats[ MCStatus::PARTIALLY_APPROVED ], $counting_stats[ MCStatus::APPROVED ] );
return array_merge(
$this->mc_statuses,
[ 'statistics' => $counting_stats ]
);
}
/**
* Retrieve the Merchant Center issues and total count. Refresh if the cache issues have gone stale.
* Issue details are reduced, and for products, grouped by type.
* Issues can be filtered by type, severity and searched by name or ID (if product type) and paginated.
* Count takes into account the type filter, but not the pagination.
*
* In case there are issues with severity Error we hide the other issues with lower severity.
*
* @param string|null $type To filter by issue type if desired.
* @param int $per_page The number of issues to return (0 for no limit).
* @param int $page The page to start on (1-indexed).
* @param bool $force_refresh Force refresh of all product status data.
*
* @return array With two indices, results (may be paged), count (considers type) and loading (indicating whether the data is loading).
* @throws Exception If the account state can't be retrieved from Google.
*/
public function get_issues( ?string $type = null, int $per_page = 0, int $page = 1, bool $force_refresh = false ): array {
$job = $this->maybe_refresh_status_data( $force_refresh );
// Get only error issues
$severity_error_issues = $this->fetch_issues( $type, $per_page, $page, true );
// In case there are error issues we show only those, otherwise we show all the issues.
$issues = $severity_error_issues['total'] > 0 ? $severity_error_issues : $this->fetch_issues( $type, $per_page, $page );
$issues['loading'] = $job->is_scheduled();
return $issues;
}
/**
* Clears the status cache data.
*
* @since 1.1.0
*/
public function clear_cache(): void {
$job_repository = $this->container->get( JobRepository::class );
$update_all_products_job = $job_repository->get( UpdateAllProducts::class );
$delete_all_products_job = $job_repository->get( DeleteAllProducts::class );
// Clear the cache if we are not in the middle of updating/deleting all products. Otherwise, we might update the product stats for each individual batch.
// See: ClearProductStatsCache::register
if ( $update_all_products_job->can_schedule( null ) && $delete_all_products_job->can_schedule( null ) ) {
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_STATUSES );
}
}
/**
* Delete the intermediate product status count data.
*
* @since 2.6.4
*/
protected function delete_product_statuses_count_intermediate_data(): void {
$this->options->delete( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA );
}
/**
* Delete the stale issues from the database.
*
* @since 2.6.4
*/
protected function delete_stale_issues(): void {
$this->container->get( MerchantIssueTable::class )->delete_stale( $this->cache_created_time );
}
/**
* Delete the stale mc statuses from the database.
*
* @since 2.6.4
*/
protected function delete_stale_mc_statuses(): void {
$product_meta_query_helper = $this->container->get( ProductMetaQueryHelper::class );
$product_meta_query_helper->delete_all_values( ProductMetaHandler::KEY_MC_STATUS );
}
/**
* Clear the product statuses cache and delete stale issues.
*
* @since 2.6.4
*/
public function clear_product_statuses_cache_and_issues(): void {
$this->delete_stale_issues();
$this->delete_stale_mc_statuses();
$this->delete_product_statuses_count_intermediate_data();
}
/**
* Check if the Merchant Center account is connected and throw an exception if it's not.
*
* @since 2.6.4
*
* @throws Exception If the Merchant Center account is not connected.
*/
protected function check_mc_is_connected() {
$mc_service = $this->container->get( MerchantCenterService::class );
if ( ! $mc_service->is_connected() ) {
// Return a 401 to redirect to reconnect flow if the Google account is not connected.
if ( ! $mc_service->is_google_connected() ) {
throw new Exception( __( 'Google account is not connected.', 'google-listings-and-ads' ), 401 );
}
throw new Exception( __( 'Merchant Center account is not set up.', 'google-listings-and-ads' ) );
}
}
/**
* Maybe start the job to refresh the status and issues data.
*
* @param bool $force_refresh Force refresh of all status-related data.
*
* @return UpdateMerchantProductStatuses The job to update the statuses.
*
* @throws Exception If no Merchant Center account is connected, or account status is not retrievable.
* @throws NotFoundExceptionInterface If the class is not found in the container.
* @throws ContainerExceptionInterface If the container throws an exception.
*/
public function maybe_refresh_status_data( bool $force_refresh = false ): UpdateMerchantProductStatuses {
$this->check_mc_is_connected();
// Only refresh if the current data has expired.
$this->mc_statuses = $this->container->get( TransientsInterface::class )->get( Transients::MC_STATUSES );
$job = $this->container->get( JobRepository::class )->get( UpdateMerchantProductStatuses::class );
// If force_refresh is true or if not transient, return empty array and scheduled the job to update the statuses.
if ( ! $job->is_scheduled() && ( $force_refresh || ( ! $force_refresh && null === $this->mc_statuses ) ) ) {
// Delete the transient before scheduling the job because some errors, like the failure rate message, can occur before the job is executed.
$this->clear_cache();
// Schedule job to update the statuses. If the failure rate is too high, the job will not be scheduled.
$job->schedule();
}
return $job;
}
/**
* Delete the cached statistics and issues.
*/
public function delete(): void {
$this->container->get( TransientsInterface::class )->delete( Transients::MC_STATUSES );
$this->container->get( MerchantIssueTable::class )->truncate();
}
/**
* Fetch the cached issues from the database.
*
* @param string|null $type To filter by issue type if desired.
* @param int $per_page The number of issues to return (0 for no limit).
* @param int $page The page to start on (1-indexed).
* @param bool $only_errors Filters only the issues with error and critical severity.
*
* @return array The requested issues and the total count of issues.
* @throws InvalidValue If the type filter is invalid.
*/
protected function fetch_issues( ?string $type = null, int $per_page = 0, int $page = 1, bool $only_errors = false ): array {
/** @var MerchantIssueQuery $issue_query */
$issue_query = $this->container->get( MerchantIssueQuery::class );
// Ensure account issues are shown first.
$issue_query->set_order( 'type' );
$issue_query->set_order( 'product' );
$issue_query->set_order( 'issue' );
// Filter by type if valid.
if ( in_array( $type, $this->get_valid_issue_types(), true ) ) {
$issue_query->where( 'type', $type );
} elseif ( null !== $type ) {
throw InvalidValue::not_in_allowed_list( 'type filter', $this->get_valid_issue_types() );
}
// Result pagination.
if ( $per_page > 0 ) {
$issue_query->set_limit( $per_page );
$issue_query->set_offset( $per_page * ( $page - 1 ) );
}
if ( $only_errors ) {
$issue_query->where( 'severity', [ 'error', 'critical' ], 'IN' );
}
$issues = [];
foreach ( $issue_query->get_results() as $row ) {
$issue = [
'type' => $row['type'],
'product_id' => intval( $row['product_id'] ),
'product' => $row['product'],
'issue' => $row['issue'],
'code' => $row['code'],
'action' => $row['action'],
'action_url' => $row['action_url'],
'severity' => $this->get_issue_severity( $row ),
];
if ( $issue['product_id'] ) {
$issue['applicable_countries'] = json_decode( $row['applicable_countries'], true );
} else {
unset( $issue['product_id'] );
}
$issues[] = $issue;
}
return [
'issues' => $issues,
'total' => $issue_query->get_count(),
];
}
/**
* Get MC product issues from a list of Product View statuses.
*
* @param array $statuses The list of Product View statuses.
* @throws NotFoundExceptionInterface If the class is not found in the container.
* @throws ContainerExceptionInterface If the container throws an exception.
*
* @return array The list of product issues.
*/
protected function get_product_issues( array $statuses ): array {
/** @var Merchant $merchant */
$merchant = $this->container->get( Merchant::class );
/** @var ProductHelper $product_helper */
$product_helper = $this->container->get( ProductHelper::class );
$visibility_meta_key = $this->prefix_meta_key( ProductMetaHandler::KEY_VISIBILITY );
$google_ids = array_column( $statuses, 'mc_id' );
$product_issues = [];
$created_at = $this->cache_created_time->format( 'Y-m-d H:i:s' );
$entries = $merchant->get_productstatuses_batch( $google_ids )->getEntries() ?? [];
foreach ( $entries as $response_entry ) {
/** @var GoogleProductStatus $mc_product_status */
$mc_product_status = $response_entry->getProductStatus();
$mc_product_id = $mc_product_status->getProductId();
$wc_product_id = $product_helper->get_wc_product_id( $mc_product_id );
$wc_product = $this->product_data_lookup[ $wc_product_id ] ?? null;
// Skip products not synced by this extension.
if ( ! $wc_product ) {
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Merchant Center product %s not found in this WooCommerce store.', $mc_product_id ),
__METHOD__ . ' in remove_invalid_statuses()',
);
continue;
}
// Unsynced issues shouldn't be shown.
if ( ChannelVisibility::DONT_SYNC_AND_SHOW === $wc_product->get_meta( $visibility_meta_key ) ) {
continue;
}
// Confirm there are issues for this product.
if ( empty( $mc_product_status->getItemLevelIssues() ) ) {
continue;
}
$product_issue_template = [
'product' => html_entity_decode( $wc_product->get_name(), ENT_QUOTES ),
'product_id' => $wc_product_id,
'created_at' => $created_at,
'applicable_countries' => [],
'source' => 'mc',
];
foreach ( $mc_product_status->getItemLevelIssues() as $item_level_issue ) {
if ( 'merchant_action' !== $item_level_issue->getResolution() ) {
continue;
}
$hash_key = $wc_product_id . '__' . md5( $item_level_issue->getDescription() );
$this->product_issue_countries[ $hash_key ] = array_merge(
$this->product_issue_countries[ $hash_key ] ?? [],
$item_level_issue->getApplicableCountries()
);
$product_issues[ $hash_key ] = $product_issue_template + [
'code' => $item_level_issue->getCode(),
'issue' => $item_level_issue->getDescription(),
'action' => $item_level_issue->getDetail(),
'action_url' => $item_level_issue->getDocumentation(),
'severity' => $item_level_issue->getServability(),
];
}
}
return $product_issues;
}
/**
* Refresh the account , pre-sync product validation and custom merchant issues.
*
* @since 2.6.4
*
* @throws Exception If the account state can't be retrieved from Google.
*/
public function refresh_account_and_presync_issues(): void {
// Update account-level issues.
$this->refresh_account_issues();
// Update pre-sync product validation issues.
$this->refresh_presync_product_issues();
// Include any custom merchant issues.
$this->refresh_custom_merchant_issues();
}
/**
* Retrieve all account-level issues and store them in the database.
*
* @throws Exception If the account state can't be retrieved from Google.
*/
protected function refresh_account_issues(): void {
/** @var Merchant $merchant */
$merchant = $this->container->get( Merchant::class );
$account_issues = [];
$created_at = $this->cache_created_time->format( 'Y-m-d H:i:s' );
$issues = $merchant->get_accountstatus()->getAccountLevelIssues() ?? [];
foreach ( $issues as $issue ) {
$key = md5( $issue->getTitle() );
if ( isset( $account_issues[ $key ] ) ) {
$account_issues[ $key ]['applicable_countries'][] = $issue->getCountry();
} else {
$account_issues[ $key ] = [
'product_id' => 0,
'product' => __( 'All products', 'google-listings-and-ads' ),
'code' => $issue->getId(),
'issue' => $issue->getTitle(),
'action' => $issue->getDetail(),
'action_url' => $issue->getDocumentation(),
'created_at' => $created_at,
'type' => self::TYPE_ACCOUNT,
'severity' => $issue->getSeverity(),
'source' => 'mc',
'applicable_countries' => [ $issue->getCountry() ],
];
$account_issues[ $key ] = $this->maybe_override_issue_values( $account_issues[ $key ] );
}
}
// Sort and encode countries
$account_issues = array_map(
function ( $issue ) {
sort( $issue['applicable_countries'] );
$issue['applicable_countries'] = wp_json_encode(
array_unique(
$issue['applicable_countries']
)
);
return $issue;
},
$account_issues
);
/** @var MerchantIssueQuery $issue_query */
$issue_query = $this->container->get( MerchantIssueQuery::class );
$issue_query->update_or_insert( $account_issues );
}
/**
* Custom issues can be added to the merchant issues table.
*
* @since 1.2.0
*/
protected function refresh_custom_merchant_issues() {
$custom_issues = apply_filters( 'woocommerce_gla_custom_merchant_issues', [], $this->cache_created_time );
if ( empty( $custom_issues ) ) {
return;
}
/** @var MerchantIssueQuery $issue_query */
$issue_query = $this->container->get( MerchantIssueQuery::class );
$issue_query->update_or_insert( $custom_issues );
}
/**
* Refresh product issues in the merchant issues table.
*
* @param array $product_issues Array of product issues.
* @throws InvalidQuery If an invalid column name is provided.
* @throws NotFoundExceptionInterface If the class is not found in the container.
* @throws ContainerExceptionInterface If the container throws an exception.
*/
protected function refresh_product_issues( array $product_issues ): void {
// Alphabetize all product/issue country lists.
array_walk(
$this->product_issue_countries,
function ( &$countries ) {
sort( $countries );
}
);
// Product issue cleanup: sorting (by product ID) and encode applicable countries.
ksort( $product_issues );
$product_issues = array_map(
function ( $unique_key, $issue ) {
$issue['applicable_countries'] = wp_json_encode( $this->product_issue_countries[ $unique_key ] );
return $issue;
},
array_keys( $product_issues ),
$product_issues
);
/** @var MerchantIssueQuery $issue_query */
$issue_query = $this->container->get( MerchantIssueQuery::class );
$issue_query->update_or_insert( array_values( $product_issues ) );
}
/**
* Include local presync product validation issues in the merchant issues table.
*/
protected function refresh_presync_product_issues(): void {
/** @var MerchantIssueQuery $issue_query */
$issue_query = $this->container->get( MerchantIssueQuery::class );
$created_at = $this->cache_created_time->format( 'Y-m-d H:i:s' );
$issue_action = __( 'Update this attribute in your product data', 'google-listings-and-ads' );
/** @var ProductMetaQueryHelper $product_meta_query_helper */
$product_meta_query_helper = $this->container->get( ProductMetaQueryHelper::class );
// Get all MC statuses.
$all_errors = $product_meta_query_helper->get_all_values( ProductMetaHandler::KEY_ERRORS );
$chunk_size = apply_filters( 'woocommerce_gla_merchant_status_presync_issues_chunk', 500 );
$product_issues = [];
foreach ( $all_errors as $product_id => $presync_errors ) {
// Don't create issues with empty descriptions
// or for variable parents (they contain issues of all children).
$error = $presync_errors[ array_key_first( $presync_errors ) ];
if ( empty( $error ) || ! is_string( $error ) ) {
continue;
}
$product = get_post( $product_id );
// Don't store pre-sync errors for unpublished (draft, trashed) products.
if ( 'publish' !== get_post_status( $product ) ) {
continue;
}
foreach ( $presync_errors as $text ) {
$issue_parts = $this->parse_presync_issue_text( $text );
$product_issues[] = [
'product' => $product->post_title,
'product_id' => $product_id,
'code' => $issue_parts['code'],
'severity' => self::SEVERITY_ERROR,
'issue' => $issue_parts['issue'],
'action' => $issue_action,
'action_url' => 'https://support.google.com/merchants/answer/10538362?hl=en&ref_topic=6098333',
'applicable_countries' => '["all"]',
'source' => 'pre-sync',
'created_at' => $created_at,
];
}
// Do update-or-insert in chunks.
if ( count( $product_issues ) >= $chunk_size ) {
$issue_query->update_or_insert( $product_issues );
$product_issues = [];
}
}
// Handle any leftover issues.
$issue_query->update_or_insert( $product_issues );
}
/**
* Process product status statistics.
*
* @param array $product_view_statuses Product View statuses.
* @see MerchantReport::get_product_view_report
*
* @throws NotFoundExceptionInterface If the class is not found in the container.
* @throws ContainerExceptionInterface If the container throws an exception.
*/
public function process_product_statuses( array $product_view_statuses ): void {
$this->mc_statuses = [];
$product_repository = $this->container->get( ProductRepository::class );
$this->product_data_lookup = $product_repository->find_by_ids_as_associative_array( array_column( $product_view_statuses, 'product_id' ) );
$this->product_statuses = [
'products' => [],
'parents' => [],
];
foreach ( $product_view_statuses as $product_status ) {
$wc_product_id = $product_status['product_id'];
$mc_product_status = $product_status['status'];
$wc_product = $this->product_data_lookup[ $wc_product_id ] ?? null;
if ( ! $wc_product || ! $wc_product_id ) {
// Skip if the product does not exist in WooCommerce.
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Merchant Center product %s not found in this WooCommerce store.', $wc_product_id ),
__METHOD__,
);
continue;
}
if ( $this->product_is_expiring( $product_status['expiration_date'] ) ) {
$mc_product_status = MCStatus::EXPIRING;
}
// Products is used later for global product status statistics.
$this->product_statuses['products'][ $wc_product_id ][ $mc_product_status ] = 1 + ( $this->product_statuses['products'][ $wc_product_id ][ $mc_product_status ] ?? 0 );
// Aggregate parent statuses for mc_status postmeta.
$wc_parent_id = $wc_product->get_parent_id();
if ( ! $wc_parent_id ) {
continue;
}
$this->product_statuses['parents'][ $wc_parent_id ][ $mc_product_status ] = 1 + ( $this->product_statuses['parents'][ $wc_parent_id ][ $mc_product_status ] ?? 0 );
}
$parent_keys = array_values( array_keys( $this->product_statuses['parents'] ) );
$parent_products = $product_repository->find_by_ids_as_associative_array( $parent_keys );
$this->product_data_lookup = $this->product_data_lookup + $parent_products;
// Update each product's mc_status and then update the global statistics.
$this->update_products_meta_with_mc_status();
$this->update_intermediate_product_statistics();
$product_issues = $this->get_product_issues( $product_view_statuses );
$this->refresh_product_issues( $product_issues );
}
/**
* Whether a product is expiring.
*
* @param DateTime $expiration_date
*
* @return bool Whether the product is expiring.
*/
protected function product_is_expiring( DateTime $expiration_date ): bool {
if ( ! $expiration_date ) {
return false;
}
// Products are considered expiring if they will expire within 3 days.
return time() + 3 * DAY_IN_SECONDS > $expiration_date->getTimestamp();
}
/**
* Sum and update the intermediate product status statistics. It will group
* the variations for the same parent.
*
* For the case that one variation is approved and the other disapproved:
* 1. Give each status a priority.
* 2. Store the last highest priority status in `$parent_statuses`.
* 3. Compare if a higher priority status is found for that variable product.
* 4. Loop through the `$parent_statuses` array at the end to add the final status counts.
*
* @return array Product status statistics.
*/
protected function update_intermediate_product_statistics(): array {
$product_statistics = self::DEFAULT_PRODUCT_STATS;
// If the option is set, use it to sum the total quantity.
$product_statistics_intermediate_data = $this->options->get( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA );
if ( $product_statistics_intermediate_data ) {
$product_statistics = $product_statistics_intermediate_data;
$this->initial_intermediate_data = $product_statistics;
}
$product_statistics_priority = [
MCStatus::APPROVED => 6,
MCStatus::PARTIALLY_APPROVED => 5,
MCStatus::EXPIRING => 4,
MCStatus::PENDING => 3,
MCStatus::DISAPPROVED => 2,
MCStatus::NOT_SYNCED => 1,
];
$parent_statuses = [];
foreach ( $this->product_statuses['products'] as $product_id => $statuses ) {
foreach ( $statuses as $status => $num_products ) {
$product = $this->product_data_lookup[ $product_id ] ?? null;
if ( ! $product ) {
continue;
}
$parent_id = $product->get_parent_id();
if ( ! $parent_id ) {
$product_statistics[ $status ] += $num_products;
} elseif ( ! isset( $parent_statuses[ $parent_id ] ) ) {
$parent_statuses[ $parent_id ] = $status;
} else {
$current_parent_status = $parent_statuses[ $parent_id ];
if ( $product_statistics_priority[ $status ] < $product_statistics_priority[ $current_parent_status ] ) {
$parent_statuses[ $parent_id ] = $status;
}
}
}
}
foreach ( $parent_statuses as $parent_id => $new_parent_status ) {
$current_parent_intermediate_data_status = $product_statistics_intermediate_data['parents'][ $parent_id ] ?? null;
if ( $current_parent_intermediate_data_status === $new_parent_status ) {
continue;
}
if ( ! $current_parent_intermediate_data_status ) {
$product_statistics[ $new_parent_status ] += 1;
$product_statistics['parents'][ $parent_id ] = $new_parent_status;
continue;
}
// Check if the new parent status has higher priority than the previous one.
if ( $product_statistics_priority[ $new_parent_status ] < $product_statistics_priority[ $current_parent_intermediate_data_status ] ) {
$product_statistics[ $current_parent_intermediate_data_status ] -= 1;
$product_statistics[ $new_parent_status ] += 1;
$product_statistics['parents'][ $parent_id ] = $new_parent_status;
} else {
$product_statistics['parents'][ $parent_id ] = $current_parent_intermediate_data_status;
}
}
$this->options->update( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA, $product_statistics );
return $product_statistics;
}
/**
* Calculate the total count of products in the MC using the statistics.
*
* @since 2.6.4
*
* @param array $statistics
*
* @return int
*/
protected function calculate_total_synced_product_statistics( array $statistics ): int {
if ( ! count( $statistics ) ) {
return 0;
}
$synced_status_values = array_values( array_diff( $statistics, [ $statistics[ MCStatus::NOT_SYNCED ] ] ) );
return array_sum( $synced_status_values );
}
/**
* Handle the failure of the Merchant Center statuses fetching.
*
* @since 2.6.4
*
* @param string $error_message The error message.
*
* @throws NotFoundExceptionInterface If the class is not found in the container.
* @throws ContainerExceptionInterface If the container throws an exception.
*/
public function handle_failed_mc_statuses_fetching( string $error_message = '' ): void {
// Reset the intermediate data to the initial state when starting the job.
$this->options->update( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA, $this->initial_intermediate_data );
// Let's remove any issue created during the failed fetch.
$this->container->get( MerchantIssueTable::class )->delete_specific_product_issues( array_keys( $this->product_data_lookup ) );
$mc_statuses = [
'timestamp' => $this->cache_created_time->getTimestamp(),
'statistics' => null,
'loading' => false,
'error' => $error_message,
];
$this->container->get( TransientsInterface::class )->set(
Transients::MC_STATUSES,
$mc_statuses,
$this->get_status_lifetime()
);
}
/**
* Handle the completion of the Merchant Center statuses fetching.
*
* @since 2.6.4
*/
public function handle_complete_mc_statuses_fetching() {
$intermediate_data = $this->options->get( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA, self::DEFAULT_PRODUCT_STATS );
unset( $intermediate_data['parents'] );
$total_synced_products = $this->calculate_total_synced_product_statistics( $intermediate_data );
/** @var ProductRepository $product_repository */
$product_repository = $this->container->get( ProductRepository::class );
$intermediate_data[ MCStatus::NOT_SYNCED ] = count(
$product_repository->find_all_product_ids()
) - $total_synced_products;
$mc_statuses = [
'timestamp' => $this->cache_created_time->getTimestamp(),
'statistics' => $intermediate_data,
'loading' => false,
'error' => null,
];
$this->container->get( TransientsInterface::class )->set(
Transients::MC_STATUSES,
$mc_statuses,
$this->get_status_lifetime()
);
$this->delete_product_statuses_count_intermediate_data();
}
/**
* Update the Merchant Center status for each product.
*/
protected function update_products_meta_with_mc_status() {
// Generate a product_id=>mc_status array.
$new_product_statuses = [];
foreach ( $this->product_statuses as $types ) {
foreach ( $types as $product_id => $statuses ) {
if ( isset( $statuses[ MCStatus::PENDING ] ) ) {
$new_product_statuses[ $product_id ] = MCStatus::PENDING;
} elseif ( isset( $statuses[ MCStatus::EXPIRING ] ) ) {
$new_product_statuses[ $product_id ] = MCStatus::EXPIRING;
} elseif ( isset( $statuses[ MCStatus::APPROVED ] ) ) {
if ( count( $statuses ) > 1 ) {
$new_product_statuses[ $product_id ] = MCStatus::PARTIALLY_APPROVED;
} else {
$new_product_statuses[ $product_id ] = MCStatus::APPROVED;
}
} else {
$new_product_statuses[ $product_id ] = array_key_first( $statuses );
}
}
}
foreach ( $new_product_statuses as $product_id => $new_status ) {
$product = $this->product_data_lookup[ $product_id ] ?? null;
// At this point, the product should exist in WooCommerce but in the case that product is not found, log it.
if ( ! $product ) {
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Merchant Center product with WooCommerce ID %d is not found in this store.', $product_id ),
__METHOD__,
);
continue;
}
$product->add_meta_data( $this->prefix_meta_key( ProductMetaHandler::KEY_MC_STATUS ), $new_status, true );
// We use save_meta_data so we don't trigger the woocommerce_update_product hook and the Syncer Hooks.
$product->save_meta_data();
}
}
/**
* Allows a hook to modify the lifetime of the statuses data.
*
* @return int
*/
protected function get_status_lifetime(): int {
return apply_filters( 'woocommerce_gla_mc_status_lifetime', self::STATUS_LIFETIME );
}
/**
* Valid issues types for issue type filter.
*
* @return string[]
*/
protected function get_valid_issue_types(): array {
return [
self::TYPE_ACCOUNT,
self::TYPE_PRODUCT,
];
}
/**
* Parse the code and formatted issue text out of the presync validation error text.
*
* Converts the error strings:
* "[attribute] Error message." > "Error message [attribute]"
*
* Note:
* If attribute is an array the name can be "[attribute[0]]".
* So we need to match the additional set of square brackets.
*
* @param string $text
*
* @return string[] With indexes `code` and `issue`
*/
protected function parse_presync_issue_text( string $text ): array {
$matches = [];
preg_match( '/^\[([^\]]+\]?)\]\s*(.+)$/', $text, $matches );
if ( count( $matches ) !== 3 ) {
return [
'code' => 'presync_error_attrib_' . md5( $text ),
'issue' => $text,
];
}
// Convert attribute name "imageLink" to "image".
if ( 'imageLink' === $matches[1] ) {
$matches[1] = 'image';
}
// Convert attribute name "additionalImageLinks[]" to "galleryImage".
if ( str_starts_with( $matches[1], 'additionalImageLinks' ) ) {
$matches[1] = 'galleryImage';
}
$matches[2] = trim( $matches[2], ' .' );
return [
'code' => 'presync_error_' . $matches[1],
'issue' => "{$matches[2]} [{$matches[1]}]",
];
}
/**
* Return a standardized Merchant Issue severity value.
*
* @param array $row
*
* @return string
*/
protected function get_issue_severity( array $row ): string {
$is_warning = in_array(
$row['severity'],
[
'warning',
'suggestion',
'demoted',
'unaffected',
],
true
);
return $is_warning ? self::SEVERITY_WARNING : self::SEVERITY_ERROR;
}
/**
* In very rare instances, issue values need to be overridden manually.
*
* @param array $issue
*
* @return array The original issue with any possibly overridden values.
*/
private function maybe_override_issue_values( array $issue ): array {
/**
* Code 'merchant_quality_low' for matching the original issue.
* Ref: https://developers.google.com/shopping-content/guides/account-issues#merchant_quality_low
*
* Issue string "Account isn't eligible for free listings" for matching
* the updated copy after Free and Enhanced Listings merge.
*
* TODO: Remove the condition of matching the $issue['issue']
* if its issue code is the same as 'merchant_quality_low'
* after Google replaces the issue title on their side.
*/
if ( 'merchant_quality_low' === $issue['code'] || "Account isn't eligible for free listings" === $issue['issue'] ) {
$issue['issue'] = 'Show products on additional surfaces across Google through free listings';
$issue['severity'] = self::SEVERITY_WARNING;
$issue['action_url'] = 'https://support.google.com/merchants/answer/9199328?hl=en';
}
/**
* Reference: https://github.com/woocommerce/google-listings-and-ads/issues/1688
*/
if ( 'home_page_issue' === $issue['code'] ) {
$issue['issue'] = 'Website claim is lost, need to re verify and claim your website. Please reference the support link';
$issue['action_url'] = 'https://woocommerce.com/document/google-for-woocommerce/faq/#reverify-website';
}
return $issue;
}
/**
* Getter for get_cache_created_time
*
* @return DateTime The DateTime stored in cache_created_time
*/
public function get_cache_created_time(): DateTime {
return $this->cache_created_time;
}
}
PhoneVerification.php 0000644 00000012127 15154165034 0010700 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\ISOUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\PhoneNumber;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\Exception as GoogleServiceException;
defined( 'ABSPATH' ) || exit;
/**
* Class PhoneVerification
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*
* @since 1.5.0
*/
class PhoneVerification implements Service {
public const VERIFICATION_METHOD_SMS = 'SMS';
public const VERIFICATION_METHOD_PHONE_CALL = 'PHONE_CALL';
/**
* @var Merchant
*/
protected $merchant;
/**
* @var WP
*/
protected $wp;
/**
* @var ISOUtility
*/
protected $iso_utility;
/**
* PhoneVerification constructor.
*
* @param Merchant $merchant
* @param WP $wp
* @param ISOUtility $iso_utility
*/
public function __construct( Merchant $merchant, WP $wp, ISOUtility $iso_utility ) {
$this->merchant = $merchant;
$this->wp = $wp;
$this->iso_utility = $iso_utility;
}
/**
* Request verification code to start phone verification.
*
* @param string $region_code Two-letter country code (ISO 3166-1 alpha-2) for the phone number, for
* example CA for Canadian numbers.
* @param PhoneNumber $phone_number Phone number to be verified.
* @param string $verification_method Verification method to receive verification code.
*
* @return string The verification ID to use in subsequent calls to
* `PhoneVerification::verify_phone_number`.
*
* @throws PhoneVerificationException If there are any errors requesting verification.
* @throws InvalidValue If an invalid input provided.
*/
public function request_phone_verification( string $region_code, PhoneNumber $phone_number, string $verification_method ): string {
$this->validate_verification_method( $verification_method );
$this->validate_phone_region( $region_code );
try {
return $this->merchant->request_phone_verification( $region_code, $phone_number->get(), $verification_method, $this->get_language_code() );
} catch ( GoogleServiceException $e ) {
throw $this->map_google_exception( $e );
}
}
/**
* Validates verification code to verify phone number for the account.
*
* @param string $verification_id The verification ID returned by
* `PhoneVerification::request_phone_verification`.
* @param string $verification_code The verification code that was sent to the phone number for validation.
* @param string $verification_method Verification method used to receive verification code.
*
* @return void
*
* @throws PhoneVerificationException If there are any errors verifying the phone number.
* @throws InvalidValue If an invalid input provided.
*/
public function verify_phone_number( string $verification_id, string $verification_code, string $verification_method ): void {
$this->validate_verification_method( $verification_method );
try {
$this->merchant->verify_phone_number( $verification_id, $verification_code, $verification_method );
} catch ( GoogleServiceException $e ) {
throw $this->map_google_exception( $e );
}
}
/**
* @param string $method
*
* @throws InvalidValue If the verification method is invalid.
*/
protected function validate_verification_method( string $method ) {
$allowed = [ self::VERIFICATION_METHOD_SMS, self::VERIFICATION_METHOD_PHONE_CALL ];
if ( ! in_array( $method, $allowed, true ) ) {
throw InvalidValue::not_in_allowed_list( $method, $allowed );
}
}
/**
* @param string $region_code
*
* @throws InvalidValue If the phone region code is not a valid ISO 3166-1 alpha-2 country code.
*/
protected function validate_phone_region( string $region_code ) {
if ( ! $this->iso_utility->is_iso3166_alpha2_country_code( $region_code ) ) {
throw new InvalidValue( 'Invalid phone region! Phone region must be a two letter ISO 3166-1 alpha-2 country code.' );
}
}
/**
* @return string
*/
protected function get_language_code(): string {
return $this->iso_utility->wp_locale_to_bcp47( $this->wp->get_user_locale() );
}
/**
* @param GoogleServiceException $exception
*
* @return PhoneVerificationException
*/
protected function map_google_exception( GoogleServiceException $exception ): PhoneVerificationException {
$code = $exception->getCode();
$message = $exception->getMessage();
$reason = '';
$errors = $exception->getErrors();
if ( ! empty( $errors ) ) {
$error = $errors[ array_key_first( $errors ) ];
$message = $error['message'] ?? '';
$reason = $error['reason'] ?? '';
}
return new PhoneVerificationException( $message, $code, $exception, [ 'reason' => $reason ] );
}
}
PhoneVerificationException.php 0000644 00000000646 15154165034 0012562 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
defined( 'ABSPATH' ) || exit;
/**
* Class PhoneVerificationException
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Product
*
* @since 1.5.0
*/
class PhoneVerificationException extends ExceptionWithResponseData {}
PolicyComplianceCheck.php 0000644 00000012444 15154165034 0011456 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\TargetAudience;
defined( 'ABSPATH' ) || exit;
/**
* Class PolicyComplianceCheck
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*
* @since 2.1.4
*/
class PolicyComplianceCheck implements Service {
use PluginHelper;
/**
* The WC proxy object.
*
* @var wc
*/
protected $wc;
/**
* @var GoogleHelper
*/
protected $google_helper;
/**
* @var TargetAudience
*/
protected $target_audience;
/**
* PolicyComplianceCheck constructor.
*
* @param WC $wc
* @param GoogleHelper $google_helper
* @param TargetAudience $target_audience
*/
public function __construct( WC $wc, GoogleHelper $google_helper, TargetAudience $target_audience ) {
$this->wc = $wc;
$this->google_helper = $google_helper;
$this->target_audience = $target_audience;
}
/**
* Check if the store website is accessed by all users for the controller.
*
* @return bool
*/
public function is_accessible(): bool {
$all_allowed_countries = $this->wc->get_allowed_countries();
$target_countries = $this->target_audience->get_target_countries();
foreach ( $target_countries as $country ) {
if ( ! array_key_exists( $country, $all_allowed_countries ) ) {
return false;
}
}
return true;
}
/**
* Check if the store sample product landing pages lead to a 404 error.
*
* @return bool
*/
public function has_page_not_found_error(): bool {
$url = $this->get_landing_page_url();
$response = wp_remote_get( $url );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return true;
}
return false;
}
/**
* Check if the store sample product landing pages has redirects through 3P domains.
*
* @return bool
*/
public function has_redirects(): bool {
$url = $this->get_landing_page_url();
$response = wp_remote_get( $url, [ 'redirection' => 0 ] );
$code = wp_remote_retrieve_response_code( $response );
if ( $code >= 300 && $code <= 399 ) {
return true;
}
return false;
}
/**
* Returns a product page URL, uses homepage as a fallback.
*
* @return string Landing page URL.
*/
private function get_landing_page_url(): string {
$products = wc_get_products(
[
'limit' => 1,
'status' => 'publish',
]
);
if ( ! empty( $products ) ) {
return $products[0]->get_permalink();
}
return $this->get_site_url();
}
/**
* Check if the merchant set the restrictions in robots.txt or not in the store.
*
* @return bool
*/
public function has_restriction(): bool {
return ! $this->robots_allowed( $this->get_site_url() );
}
/**
* Check if the robots.txt has restrictions or not in the store.
*
* @param string $url
* @return bool
*/
private function robots_allowed( $url ) {
$agents = [ preg_quote( '*', '/' ) ];
$agents = implode( '|', $agents );
// location of robots.txt file
$response = wp_remote_get( trailingslashit( $url ) . 'robots.txt' );
if ( is_wp_error( $response ) ) {
return true;
}
$body = wp_remote_retrieve_body( $response );
$robotstxt = preg_split( "/\r\n|\n|\r/", $body );
if ( empty( $robotstxt ) ) {
return true;
}
$rule_applies = false;
foreach ( $robotstxt as $line ) {
$line = trim( $line );
if ( ! $line ) {
continue;
}
// following rules only apply if User-agent matches '*'
if ( preg_match( '/^\s*User-agent:\s*(.*)/i', $line, $match ) ) {
$rule_applies = '*' === $match[1];
}
if ( $rule_applies && preg_match( '/^\s*Disallow:\s*(.*)/i', $line, $regs ) ) {
if ( ! $regs[1] ) {
return true;
}
if ( '/' === trim( $regs[1] ) ) {
return false;
}
}
}
return true;
}
/**
* Check if the payment gateways is empty or not for the controller.
*
* @return bool
*/
public function has_payment_gateways(): bool {
$gateways = $this->wc->get_available_payment_gateways();
if ( empty( $gateways ) ) {
return false;
}
return true;
}
/**
* Check if the store is using SSL for the controller.
*
* @return bool
*/
public function get_is_store_ssl(): bool {
return 'https' === wp_parse_url( $this->get_site_url(), PHP_URL_SCHEME );
}
/**
* Check if the store has refund return policy page for the controller.
*
* @return bool
*/
public function has_refund_return_policy_page(): bool {
// Check the slug as it's translated by the "woocommerce" text domain name.
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
if ( $this->the_slug_exists( _x( 'refund_returns', 'Page slug', 'woocommerce' ) ) ) {
return true;
}
return false;
}
/**
* Check if the slug exists or not.
*
* @param string $post_name
* @return bool
*/
protected function the_slug_exists( string $post_name ): bool {
$args = [
'name' => $post_name,
'post_type' => 'page',
'post_status' => 'publish',
'numberposts' => 1,
];
if ( get_posts( $args ) ) {
return true;
}
return false;
}
}
TargetAudience.php 0000644 00000004317 15154165034 0010152 0 ustar 00 <?php
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
/**
* Class TargetAudience.
*
* @since 1.12.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
class TargetAudience implements Service {
/**
* @var WC
*/
protected $wc;
/**
* @var OptionsInterface
*/
protected $options;
/**
* @var GoogleHelper
*/
protected $google_helper;
/**
* TargetAudience constructor.
*
* @param WC $wc
* @param OptionsInterface $options
* @param GoogleHelper $google_helper
*/
public function __construct( WC $wc, OptionsInterface $options, GoogleHelper $google_helper ) {
$this->wc = $wc;
$this->options = $options;
$this->google_helper = $google_helper;
}
/**
* @return string[] List of target countries specified in options. Defaults to WooCommerce store base country.
*/
public function get_target_countries(): array {
$target_countries = [ $this->wc->get_base_country() ];
$target_audience = $this->options->get( OptionsInterface::TARGET_AUDIENCE );
if ( empty( $target_audience['location'] ) && empty( $target_audience['countries'] ) ) {
return $target_countries;
}
$location = strtolower( $target_audience['location'] );
if ( 'all' === $location ) {
$target_countries = $this->google_helper->get_mc_supported_countries();
} elseif ( 'selected' === $location && ! empty( $target_audience['countries'] ) ) {
$target_countries = $target_audience['countries'];
}
return $target_countries;
}
/**
* Return the main target country (default Store country).
* If the store country is not included then use the first target country.
*
* @return string
*/
public function get_main_target_country(): string {
$target_countries = $this->get_target_countries();
$shop_country = $this->wc->get_base_country();
return in_array( $shop_country, $target_countries, true ) ? $shop_country : $target_countries[0];
}
}
AccountController.php 0000644 00000017167 15155646463 0010750 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ApiNotReady;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\AccountService;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class AccountController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class AccountController extends BaseController {
/**
* Service used to access / update Ads account data.
*
* @var AccountService
*/
protected $account;
/**
* AccountController constructor.
*
* @param RESTServer $server
* @param AccountService $account
*/
public function __construct( RESTServer $server, AccountService $account ) {
parent::__construct( $server );
$this->account = $account;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/accounts',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_accounts_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->setup_account_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'mc/accounts/claim-overwrite',
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->overwrite_claim_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'mc/accounts/switch-url',
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->switch_url_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'mc/connection',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_connected_merchant_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::DELETABLE,
'callback' => $this->disconnect_merchant_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
$this->register_route(
'mc/setup',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_setup_merchant_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
}
/**
* Get the callback function for the list accounts request.
*
* @return callable
*/
protected function get_accounts_callback(): callable {
return function ( Request $request ) {
try {
return array_map(
function ( $account ) use ( $request ) {
$data = $this->prepare_item_for_response( $account, $request );
return $this->prepare_response_for_collection( $data );
},
$this->account->get_accounts()
);
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback for creating or linking an account, overwriting the website claim during the claim step.
*
* @return callable
*/
protected function overwrite_claim_callback(): callable {
return $this->setup_account_callback( 'overwrite_claim' );
}
/**
* Get the callback for creating or linking an account, switching the URL during the set_id step.
*
* @return callable
*/
protected function switch_url_callback(): callable {
return $this->setup_account_callback( 'switch_url' );
}
/**
* Get the callback function for creating or linking an account.
*
* @param string $action Action to call while setting up account (default is normal setup).
* @return callable
*/
protected function setup_account_callback( string $action = 'setup_account' ): callable {
return function ( Request $request ) use ( $action ) {
try {
$account_id = absint( $request['id'] );
if ( $account_id && 'setup_account' === $action ) {
$this->account->use_existing_account_id( $account_id );
}
$account = $this->account->{$action}( $account_id );
return $this->prepare_item_for_response( $account, $request );
} catch ( ApiNotReady $e ) {
return $this->get_time_to_wait_response( $e );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for the connected merchant account.
*
* @return callable
*/
protected function get_connected_merchant_callback(): callable {
return function () {
return $this->account->get_connected_status();
};
}
/**
* Get the callback function for the merchant setup status.
*
* @return callable
*/
protected function get_setup_merchant_callback(): callable {
return function () {
return $this->account->get_setup_status();
};
}
/**
* Get the callback function for disconnecting a merchant.
*
* @return callable
*/
protected function disconnect_merchant_callback(): callable {
return function () {
$this->account->disconnect();
return [
'status' => 'success',
'message' => __( 'Merchant Center account successfully disconnected.', 'google-listings-and-ads' ),
];
};
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'id' => [
'type' => 'number',
'description' => __( 'Merchant Center Account ID.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'required' => false,
],
'subaccount' => [
'type' => 'boolean',
'description' => __( 'Is a MCA sub account.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'name' => [
'type' => 'string',
'description' => __( 'The Merchant Center Account name.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'required' => false,
],
'domain' => [
'type' => 'string',
'description' => __( 'The domain registered with the Merchant Center Account.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'account';
}
/**
* Return a 503 Response with Retry-After header and message.
*
* @param ApiNotReady $wait Exception containing the time to wait.
*
* @return Response
*/
private function get_time_to_wait_response( ApiNotReady $wait ): Response {
$data = $wait->get_response_data( true );
return new Response(
$data,
$wait->getCode() ?: 503,
[
'Retry-After' => $data['retry_after'],
]
);
}
}
AttributeMappingCategoriesController.php 0000644 00000006520 15155646463 0014630 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class for handling API requests for getting category tree in Attribute Mapping
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class AttributeMappingCategoriesController extends BaseController {
/**
* AttributeMappingCategoriesController constructor.
*
* @param RESTServer $server
*/
public function __construct( RESTServer $server ) {
parent::__construct( $server );
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/mapping/categories',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_categories_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
],
);
}
/**
* Callback function for getting the category tree
*
* @return callable
*/
protected function get_categories_callback(): callable {
return function ( Request $request ) {
try {
$cats = $this->get_category_tree();
return array_map(
function ( $cats ) use ( $request ) {
$response = $this->prepare_item_for_response( $cats, $request );
return $this->prepare_response_for_collection( $response );
},
$cats
);
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the item schema properties for the controller.
*
* @return array The Schema properties
*/
protected function get_schema_properties(): array {
return [
'id' => [
'description' => __( 'The Category ID.', 'google-listings-and-ads' ),
'type' => 'integer',
'validate_callback' => 'rest_validate_request_arg',
'readonly' => true,
],
'name' => [
'description' => __( 'The category name.', 'google-listings-and-ads' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
'readonly' => true,
],
'parent' => [
'description' => __( 'The category parent.', 'google-listings-and-ads' ),
'type' => 'integer',
'validate_callback' => 'rest_validate_request_arg',
'readonly' => true,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'attribute_mapping_categories';
}
/**
* Function to get all the categories
*
* @return array The categories
*/
private function get_category_tree(): array {
$categories = get_categories(
[
'taxonomy' => 'product_cat',
'hide_empty' => false,
]
);
return array_map(
function ( $category ) {
return [
'id' => $category->term_id,
'name' => $category->name,
'parent' => $category->parent,
];
},
$categories
);
}
}
BatchShippingTrait.php 0000644 00000002273 15155646463 0011027 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Trait BatchShippingTrait
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
trait BatchShippingTrait {
/**
* Get the callback for deleting shipping items via batch.
*
* @return callable
*/
protected function get_batch_delete_shipping_callback(): callable {
return function ( Request $request ) {
$country_codes = $request->get_param( 'country_codes' );
$responses = [];
$errors = [];
foreach ( $country_codes as $country_code ) {
$route = "/{$this->get_namespace()}/{$this->route_base}/{$country_code}";
$delete_request = new Request( 'DELETE', $route );
$response = $this->server->dispatch_request( $delete_request );
if ( 200 !== $response->get_status() ) {
$errors[] = $response->get_data();
} else {
$responses[] = $response->get_data();
}
}
return new Response(
[
'errors' => $errors,
'success' => $responses,
],
);
};
}
}
ConnectionController.php 0000644 00000003307 15155646463 0011442 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
defined( 'ABSPATH' ) || exit;
/**
* Class ConnectionController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ConnectionController extends BaseController {
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/connect',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_connect_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for the connection request.
*
* @return callable
*/
protected function get_connect_callback(): callable {
return function () {
return [
'url' => 'example.com',
];
};
}
/**
* Get the schema for settings endpoints.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'url' => [
'description' => __( 'Action that should be completed after connection.', 'google-listings-and-ads' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'merchant_center_connection';
}
}
ContactInformationController.php 0000644 00000023506 15155646463 0013147 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Settings;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\ContactInformation;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\AddressUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\PhoneNumber;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\AccountAddress;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\AccountBusinessInformation;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class ContactInformationController
*
* @since 1.4.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ContactInformationController extends BaseOptionsController {
/**
* @var ContactInformation $contact_information
*/
protected $contact_information;
/**
* @var Settings
*/
protected $settings;
/**
* @var AddressUtility
*/
protected $address_utility;
/**
* ContactInformationController constructor.
*
* @param RESTServer $server
* @param ContactInformation $contact_information
* @param Settings $settings
* @param AddressUtility $address_utility
*/
public function __construct( RESTServer $server, ContactInformation $contact_information, Settings $settings, AddressUtility $address_utility ) {
parent::__construct( $server );
$this->contact_information = $contact_information;
$this->settings = $settings;
$this->address_utility = $address_utility;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/contact-information',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_contact_information_endpoint_read_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_contact_information_endpoint_edit_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_update_args(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get a callback for the contact information endpoint.
*
* @return callable
*/
protected function get_contact_information_endpoint_read_callback(): callable {
return function ( Request $request ) {
try {
return $this->get_contact_information_response(
$this->contact_information->get_contact_information(),
$request
);
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get a callback for the edit contact information endpoint.
*
* @return callable
*/
protected function get_contact_information_endpoint_edit_callback(): callable {
return function ( Request $request ) {
try {
return $this->get_contact_information_response(
$this->contact_information->update_address_based_on_store_settings(),
$request
);
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the schema for contact information endpoints.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'id' => [
'type' => 'integer',
'description' => __( 'The Merchant Center account ID.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
],
'phone_number' => [
'type' => 'string',
'description' => __( 'The phone number associated with the Merchant Center account.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'phone_verification_status' => [
'type' => 'string',
'description' => __( 'The verification status of the phone number associated with the Merchant Center account.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'enum' => [ 'verified', 'unverified' ],
],
'mc_address' => [
'type' => 'object',
'description' => __( 'The address associated with the Merchant Center account.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'properties' => $this->get_address_schema(),
],
'wc_address' => [
'type' => 'object',
'description' => __( 'The WooCommerce store address.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'properties' => $this->get_address_schema(),
],
'is_mc_address_different' => [
'type' => 'boolean',
'description' => __( 'Whether the Merchant Center account address is different than the WooCommerce store address.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'wc_address_errors' => [
'type' => 'array',
'description' => __( 'The errors associated with the WooCommerce address', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
];
}
/**
* Get the schema for addresses returned by the contact information endpoints.
*
* @return array[]
*/
protected function get_address_schema(): array {
return [
'street_address' => [
'description' => __( 'Street-level part of the address.', 'google-listings-and-ads' ),
'type' => 'string',
'context' => [ 'view' ],
],
'locality' => [
'description' => __( 'City, town or commune. May also include dependent localities or sublocalities (e.g. neighborhoods or suburbs).', 'google-listings-and-ads' ),
'type' => 'string',
'context' => [ 'view' ],
],
'region' => [
'description' => __( 'Top-level administrative subdivision of the country. For example, a state like California ("CA") or a province like Quebec ("QC").', 'google-listings-and-ads' ),
'type' => 'string',
'context' => [ 'view' ],
],
'postal_code' => [
'description' => __( 'Postal code or ZIP (e.g. "94043").', 'google-listings-and-ads' ),
'type' => 'string',
'context' => [ 'view' ],
],
'country' => [
'description' => __( 'CLDR country code (e.g. "US").', 'google-listings-and-ads' ),
'type' => 'string',
'context' => [ 'view' ],
],
];
}
/**
* Get the arguments for the update endpoint.
*
* @return array
*/
public function get_update_args(): array {
return [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
];
}
/**
* Get the prepared REST response with Merchant Center account ID and contact information.
*
* @param AccountBusinessInformation|null $contact_information
* @param Request $request
*
* @return Response
*/
protected function get_contact_information_response( ?AccountBusinessInformation $contact_information, Request $request ): Response {
$phone_number = null;
$phone_verification_status = null;
$mc_address = null;
$wc_address = null;
$is_address_diff = false;
if ( $this->settings->get_store_address() instanceof AccountAddress ) {
$wc_address = $this->settings->get_store_address();
$is_address_diff = true;
}
if ( $contact_information instanceof AccountBusinessInformation ) {
if ( ! empty( $contact_information->getPhoneNumber() ) ) {
try {
$phone_number = PhoneNumber::cast( $contact_information->getPhoneNumber() )->get();
$phone_verification_status = strtolower( $contact_information->getPhoneVerificationStatus() );
} catch ( InvalidValue $exception ) {
// log and fail silently
do_action( 'woocommerce_gla_exception', $exception, __METHOD__ );
}
}
if ( $contact_information->getAddress() instanceof AccountAddress ) {
$mc_address = $contact_information->getAddress();
$is_address_diff = true;
}
if ( null !== $mc_address && null !== $wc_address ) {
$is_address_diff = ! $this->address_utility->compare_addresses( $contact_information->getAddress(), $this->settings->get_store_address() );
}
}
$wc_address_errors = $this->settings->wc_address_errors( $wc_address );
return $this->prepare_item_for_response(
[
'id' => $this->options->get_merchant_id(),
'phone_number' => $phone_number,
'phone_verification_status' => $phone_verification_status,
'mc_address' => self::serialize_address( $mc_address ),
'wc_address' => self::serialize_address( $wc_address ),
'is_mc_address_different' => $is_address_diff,
'wc_address_errors' => $wc_address_errors,
],
$request
);
}
/**
* @param AccountAddress|null $address
*
* @return array|null
*/
protected static function serialize_address( ?AccountAddress $address ): ?array {
if ( null === $address ) {
return null;
}
return [
'street_address' => $address->getStreetAddress(),
'locality' => $address->getLocality(),
'region' => $address->getRegion(),
'postal_code' => $address->getPostalCode(),
'country' => $address->getCountry(),
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'merchant_center_contact_information';
}
}
IssuesController.php 0000644 00000015561 15155646463 0010623 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Exception;
use WP_REST_Request as Request;
defined( 'ABSPATH' ) || exit;
/**
* Class IssuesController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class IssuesController extends BaseOptionsController {
/**
* @var MerchantStatuses
*/
protected $merchant_statuses;
/**
* @var ProductHelper
*/
protected $product_helper;
/**
* IssuesController constructor.
*
* @param RESTServer $server
* @param MerchantStatuses $merchant_statuses
* @param ProductHelper $product_helper
*/
public function __construct( RESTServer $server, MerchantStatuses $merchant_statuses, ProductHelper $product_helper ) {
parent::__construct( $server );
$this->merchant_statuses = $merchant_statuses;
$this->product_helper = $product_helper;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/issues(/(?P<type_filter>[a-z]+))?',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_issues_read_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
'schema' => $this->get_api_response_schema_callback(),
],
);
}
/**
* Get the callback function for returning account and product issues.
*
* @return callable
*/
protected function get_issues_read_callback(): callable {
return function ( Request $request ) {
$type_filter = $request['type_filter'];
$per_page = intval( $request['per_page'] );
$page = max( 1, intval( $request['page'] ) );
try {
$results = $this->merchant_statuses->get_issues( $type_filter, $per_page, $page );
$results['page'] = $page;
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
// Replace variation IDs with parent ID (for Edit links).
foreach ( $results['issues'] as &$issue ) {
$issue = apply_filters( 'woocommerce_gla_merchant_issue_override', $issue );
if ( empty( $issue['product_id'] ) ) {
continue;
}
try {
$issue['product_id'] = $this->product_helper->maybe_swap_for_parent_id( $issue['product_id'] );
} catch ( InvalidValue $e ) {
// Don't include invalid products
do_action(
'woocommerce_gla_debug_message',
sprintf( 'Merchant Center product ID %s not found in this WooCommerce store.', $issue['product_id'] ),
__METHOD__,
);
continue;
}
}
return $this->prepare_item_for_response( $results, $request );
};
}
/**
* Get the item schema properties for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'issues' => [
'type' => 'array',
'description' => __( 'The issues related to the Merchant Center account.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'type' => [
'type' => 'string',
'description' => __( 'Issue type.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'product' => [
'type' => 'string',
'description' => __( 'Affected product.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'product_id' => [
'type' => 'numeric',
'description' => __( 'The WooCommerce product ID.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'code' => [
'type' => 'string',
'description' => __( 'Internal Google code for issue.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'issue' => [
'type' => 'string',
'description' => __( 'Descriptive text of the issue.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'action' => [
'type' => 'string',
'description' => __( 'Descriptive text of action to take.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'action_url' => [
'type' => 'string',
'description' => __( 'Documentation URL for issue and/or action.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'severity' => [
'type' => 'string',
'description' => __( 'Severity level of the issue: warning or error.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'applicable_countries' => [
'type' => 'array',
'description' => __( 'Country codes of the product audience.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
],
],
],
'total' => [
'type' => 'numeric',
'context' => [ 'view' ],
'readonly' => true,
],
'page' => [
'type' => 'numeric',
'context' => [ 'view' ],
'readonly' => true,
],
'loading' => [
'type' => 'boolean',
'description' => __( 'Whether the product issues are loading.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
];
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params(): array {
return [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
'page' => [
'description' => __( 'Page of data to retrieve.', 'google-listings-and-ads' ),
'type' => 'integer',
'default' => 1,
'minimum' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
],
'per_page' => [
'description' => __( 'Maximum number of rows to be returned in result data.', 'google-listings-and-ads' ),
'type' => 'integer',
'default' => 0,
'minimum' => 0,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'merchant_issues';
}
}
PhoneVerificationController.php 0000644 00000012051 15155646463 0012753 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\EmptySchemaPropertiesTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\PhoneVerification;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\PhoneNumber;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class PhoneVerificationController
*
* @since 1.5.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class PhoneVerificationController extends BaseOptionsController {
use EmptySchemaPropertiesTrait;
/**
* @var PhoneVerification
*/
protected $phone_verification;
/**
* PhoneVerificationController constructor.
*
* @param RESTServer $server
* @param PhoneVerification $phone_verification Phone verification service.
*/
public function __construct( RESTServer $server, PhoneVerification $phone_verification ) {
parent::__construct( $server );
$this->phone_verification = $phone_verification;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$verification_method = [
'description' => __( 'Method used to verify the phone number.', 'google-listings-and-ads' ),
'enum' => [
PhoneVerification::VERIFICATION_METHOD_SMS,
PhoneVerification::VERIFICATION_METHOD_PHONE_CALL,
],
'required' => true,
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
];
$this->register_route(
'/mc/phone-verification/request',
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_request_phone_verification_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => [
'phone_region_code' => [
'description' => __( 'Two-letter country code (ISO 3166-1 alpha-2) for the phone number.', 'google-listings-and-ads' ),
'required' => true,
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
],
'phone_number' => [
'description' => __( 'The phone number to verify.', 'google-listings-and-ads' ),
'required' => true,
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
],
'verification_method' => $verification_method,
],
],
]
);
$this->register_route(
'/mc/phone-verification/verify',
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_verify_phone_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => [
'verification_id' => [
'description' => __( 'The verification ID returned by the /request call.', 'google-listings-and-ads' ),
'required' => true,
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
],
'verification_code' => [
'description' => __( 'The verification code that was sent to the phone number for validation.', 'google-listings-and-ads' ),
'required' => true,
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
],
'verification_method' => $verification_method,
],
],
]
);
}
/**
* Get callback for requesting phone verification endpoint.
*
* @return callable
*/
protected function get_request_phone_verification_callback(): callable {
return function ( Request $request ) {
try {
$verification_id = $this->phone_verification->request_phone_verification(
$request->get_param( 'phone_region_code' ),
new PhoneNumber( $request->get_param( 'phone_number' ) ),
$request->get_param( 'verification_method' ),
);
return [
'verification_id' => $verification_id,
];
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get callback for verifying a phone number.
*
* @return callable
*/
protected function get_verify_phone_callback(): callable {
return function ( Request $request ) {
try {
$this->phone_verification->verify_phone_number(
$request->get_param( 'verification_id' ),
$request->get_param( 'verification_code' ),
$request->get_param( 'verification_method' ),
);
return new Response( null, 204 );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the item schema name for the controller.
*
* @return string
*/
protected function get_schema_title(): string {
return 'phone_verification';
}
}
PolicyComplianceCheckController.php 0000644 00000011104 15155646463 0013525 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\CountryCodeTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\PolicyComplianceCheck;
use Exception;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class PolicyComplianceCheckController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class PolicyComplianceCheckController extends BaseController {
use CountryCodeTrait;
/**
* The PolicyComplianceCheck object.
*
* @var PolicyComplianceCheck
*/
protected $policy_compliance_check;
/**
* PolicyComplianceCheckController constructor.
*
* @param RESTServer $server
* @param PolicyComplianceCheck $policy_compliance_check
*/
public function __construct( RESTServer $server, PolicyComplianceCheck $policy_compliance_check ) {
parent::__construct( $server );
$this->policy_compliance_check = $policy_compliance_check;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/policy_check',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_policy_check_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
}
/**
* Get the allowed countries, payment gateways info, store ssl and refund return policy page for the controller.
*
* @return callable
*/
protected function get_policy_check_callback(): callable {
return function () {
try {
return new Response(
[
'allowed_countries' => $this->policy_compliance_check->is_accessible(),
'robots_restriction' => $this->policy_compliance_check->has_restriction(),
'page_not_found_error' => $this->policy_compliance_check->has_page_not_found_error(),
'page_redirects' => $this->policy_compliance_check->has_redirects(),
'payment_gateways' => $this->policy_compliance_check->has_payment_gateways(),
'store_ssl' => $this->policy_compliance_check->get_is_store_ssl(),
'refund_returns' => $this->policy_compliance_check->has_refund_return_policy_page(),
]
);
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the schema for policy compliance check endpoints.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'allowed_countries' => [
'type' => 'boolean',
'description' => __( 'The store website could be accessed or not by all users.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'robots_restriction' => [
'type' => 'boolean',
'description' => __( 'The merchant set the restrictions in robots.txt or not in the store.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'page_not_found_error' => [
'type' => 'boolean',
'description' => __( 'The sample of product landing pages leads to a 404 error.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'page_redirects' => [
'type' => 'boolean',
'description' => __( 'The sample of product landing pages have redirects through 3P domains.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'payment_gateways' => [
'type' => 'boolean',
'description' => __( 'The payment gateways associated with onboarding policy checking.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'store_ssl' => [
'type' => 'boolean',
'description' => __( 'The store ssl associated with onboarding policy checking.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'refund_returns' => [
'type' => 'boolean',
'description' => __( 'The refund returns policy associated with onboarding policy checking.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'schema' => $this->get_api_response_schema_callback(),
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'policy_check';
}
}
ProductFeedController.php 0000644 00000014342 15155646463 0011550 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\ProductFeedQueryHelper;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductFeedController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ProductFeedController extends BaseController {
/**
* @var ProductFeedQueryHelper
*/
protected $query_helper;
/**
* ProductFeedController constructor.
*
* @param RESTServer $server
* @param ProductFeedQueryHelper $query_helper
*/
public function __construct( RESTServer $server, ProductFeedQueryHelper $query_helper ) {
parent::__construct( $server );
$this->query_helper = $query_helper;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/product-feed',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_product_feed_read_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
],
);
}
/**
* Get the callback function for returning the product feed.
*
* @return callable
*/
protected function get_product_feed_read_callback(): callable {
return function ( Request $request ) {
try {
return [
'products' => $this->query_helper->get( $request ),
'total' => $this->query_helper->count( $request ),
'page' => $request['per_page'] > 0 && $request['page'] > 0 ? $request['page'] : 1,
];
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the item schema properties for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'products' => [
'type' => 'array',
'description' => __( 'The store\'s products.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'numeric',
'description' => __( 'Product ID.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'title' => [
'type' => 'string',
'description' => __( 'Product title.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'visible' => [
'type' => 'boolean',
'description' => __( 'Whether the product is set to be visible in the Merchant Center', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'status' => [
'type' => 'string',
'description' => __( 'The current sync status of the product.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'image_url' => [
'type' => 'string',
'description' => __( 'The image url of the product.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'price' => [
'type' => 'string',
'description' => __( 'The price of the product.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'errors' => [
'type' => 'array',
'description' => __( 'Errors preventing the product from being synced to the Merchant Center.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
],
],
],
'total' => [
'type' => 'numeric',
'context' => [ 'view' ],
'readonly' => true,
],
'page' => [
'type' => 'numeric',
'context' => [ 'view' ],
'readonly' => true,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'product_feed';
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params(): array {
return [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
'page' => [
'description' => __( 'Page of data to retrieve.', 'google-listings-and-ads' ),
'type' => 'integer',
'default' => 1,
'minimum' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
],
'per_page' => [
'description' => __( 'Maximum number of rows to be returned in result data.', 'google-listings-and-ads' ),
'type' => 'integer',
'default' => 0,
'minimum' => 0,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
],
'search' => [
'description' => __( 'Text to search for in product names.', 'google-listings-and-ads' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
],
'ids' => [
'description' => __( 'Limit result to items with specified ids (comma-separated).', 'google-listings-and-ads' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => [
'type' => 'integer',
],
],
'orderby' => [
'description' => __( 'Sort collection by attribute.', 'google-listings-and-ads' ),
'type' => 'string',
'default' => 'title',
'enum' => [ 'title', 'id', 'visible', 'status' ],
'validate_callback' => 'rest_validate_request_arg',
],
'order' => [
'description' => __( 'Order sort attribute ascending or descending.', 'google-listings-and-ads' ),
'type' => 'string',
'default' => 'ASC',
'enum' => [ 'ASC', 'DESC' ],
'validate_callback' => 'rest_validate_request_arg',
],
];
}
}
ProductStatisticsController.php 0000644 00000013475 15155646463 0013045 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\ProductSyncStats;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use WP_REST_Response as Response;
use WP_REST_Request as Request;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductStatisticsController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ProductStatisticsController extends BaseOptionsController {
/**
* The MerchantProducts object.
*
* @var MerchantStatuses
*/
protected $merchant_statuses;
/**
* Helper class to count scheduled sync jobs.
*
* @var ProductSyncStats
*/
protected $sync_stats;
/**
* ProductStatisticsController constructor.
*
* @param RESTServer $server
* @param MerchantStatuses $merchant_statuses
* @param ProductSyncStats $sync_stats
*/
public function __construct( RESTServer $server, MerchantStatuses $merchant_statuses, ProductSyncStats $sync_stats ) {
parent::__construct( $server );
$this->merchant_statuses = $merchant_statuses;
$this->sync_stats = $sync_stats;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/product-statistics',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_product_statistics_read_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
],
);
$this->register_route(
'mc/product-statistics/refresh',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_product_statistics_refresh_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
],
);
}
/**
* Get the callback function for returning product statistics.
*
* @return callable
*/
protected function get_product_statistics_read_callback(): callable {
return function ( Request $request ) {
return $this->get_product_status_stats( $request );
};
}
/**
* Get the callback function for getting re-calculated product statistics.
*
* @return callable
*/
protected function get_product_statistics_refresh_callback(): callable {
return function ( Request $request ) {
return $this->get_product_status_stats( $request, true );
};
}
/**
* Get the overall product status statistics array.
*
* @param Request $request
* @param bool $force_refresh True to force a refresh of the product status statistics.
*
* @return Response
*/
protected function get_product_status_stats( Request $request, bool $force_refresh = false ): Response {
try {
$response = $this->merchant_statuses->get_product_statistics( $force_refresh );
$response['scheduled_sync'] = $this->sync_stats->get_count();
return $this->prepare_item_for_response( $response, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
}
/**
* Get the item schema properties for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'timestamp' => [
'type' => 'number',
'description' => __( 'Timestamp reflecting when the product status statistics were last generated.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'statistics' => [
'type' => 'object',
'description' => __( 'Merchant Center product status statistics.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
'properties' => [
'active' => [
'type' => 'integer',
'description' => __( 'Active products.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'expiring' => [
'type' => 'integer',
'description' => __( 'Expiring products.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'pending' => [
'type' => 'number',
'description' => __( 'Pending products.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'disapproved' => [
'type' => 'number',
'description' => __( 'Disapproved products.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'not_synced' => [
'type' => 'number',
'description' => __( 'Products not uploaded.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
],
],
'scheduled_sync' => [
'type' => 'number',
'description' => __( 'Amount of scheduled jobs which will sync products to Google.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'loading' => [
'type' => 'boolean',
'description' => __( 'Whether the product statistics are loading.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'error' => [
'type' => 'string',
'description' => __( 'Error message in case of failure', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
'default' => null,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'product_statistics';
}
}
ProductVisibilityController.php 0000644 00000013050 15155646463 0013027 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\MerchantIssueQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductMetaHandler;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class ProductVisibilityController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ProductVisibilityController extends BaseController {
use PluginHelper;
/**
* @var ProductHelper $product_helper
*/
protected $product_helper;
/**
* @var MerchantIssueQuery $issue_query
*/
protected $issue_query;
/**
* ProductVisibilityController constructor.
*
* @param RESTServer $server
* @param ProductHelper $product_helper
* @param MerchantIssueQuery $issue_query
*/
public function __construct( RESTServer $server, ProductHelper $product_helper, MerchantIssueQuery $issue_query ) {
parent::__construct( $server );
$this->product_helper = $product_helper;
$this->issue_query = $issue_query;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/product-visibility',
[
[
'methods' => TransportMethods::EDITABLE,
'callback' => $this->get_update_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_update_args(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get a callback for updating products' channel visibility.
*
* @return callable
*/
protected function get_update_callback(): callable {
return function ( Request $request ) {
$ids = $request->get_param( 'ids' );
$visible = $request->get_param( 'visible' );
$success = [];
$errors = [];
foreach ( $ids as $product_id ) {
$product_id = intval( $product_id );
if ( ! $this->change_product_visibility( $product_id, $visible ) ) {
$errors[] = $product_id;
continue;
}
if ( ! $visible ) {
$this->issue_query->delete( 'product_id', $product_id );
}
$success[] = $product_id;
}
sort( $success );
sort( $errors );
return new Response(
[
'success' => $success,
'errors' => $errors,
],
count( $errors ) ? 400 : 200
);
};
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'success' => [
'type' => 'array',
'description' => __( 'Products whose visibility was changed successfully.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'validate_callback' => 'rest_validate_request_arg',
'items' => [
'type' => 'numeric',
],
],
'errors' => [
'type' => 'array',
'description' => __( 'Products whose visibility was not changed.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'validate_callback' => 'rest_validate_request_arg',
'items' => [
'type' => 'numeric',
],
],
];
}
/**
* Get the arguments for the update endpoint.
*
* @return array
*/
public function get_update_args(): array {
return [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
'ids' => [
'description' => __( 'IDs of the products to update.', 'google-listings-and-ads' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => [
'type' => 'integer',
],
],
'visible' => [
'description' => __( 'New Visibility status for the specified products.', 'google-listings-and-ads' ),
'type' => 'boolean',
'validate_callback' => 'rest_validate_request_arg',
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'product_visibility';
}
/**
* Update a product's Merchant Center visibility setting (or parent product, for variations).
*
* @param int $product_id
* @param bool $new_visibility True for visible, false for not visible.
*
* @return bool True if the product was found and updated correctly.
*/
protected function change_product_visibility( int $product_id, bool $new_visibility ): bool {
try {
$product = $this->product_helper->get_wc_product( $product_id );
$product = $this->product_helper->maybe_swap_for_parent( $product );
// Use $product->save() instead of ProductMetaHandler to trigger MC sync.
$product->update_meta_data(
$this->prefix_meta_key( ProductMetaHandler::KEY_VISIBILITY ),
$new_visibility ? ChannelVisibility::SYNC_AND_SHOW : ChannelVisibility::DONT_SYNC_AND_SHOW
);
$product->save();
return true;
} catch ( Exception $e ) {
do_action( 'woocommerce_gla_exception', $e, __METHOD__ );
return false;
}
}
}
ReportsController.php 0000644 00000012522 15155646463 0011000 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\MerchantReport;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseReportsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Exception;
use WP_REST_Request as Request;
defined( 'ABSPATH' ) || exit;
/**
* Class ReportsController
*
* ContainerAware used for:
* - MerchantReport
* - WP (in parent class)
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ReportsController extends BaseReportsController {
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/reports/programs',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_programs_report_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'mc/reports/products',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_products_report_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for the programs report request.
*
* @return callable
*/
protected function get_programs_report_callback(): callable {
return function ( Request $request ) {
try {
/** @var MerchantReport $merchant */
$merchant = $this->container->get( MerchantReport::class );
$data = $merchant->get_report_data( 'free_listings', $this->prepare_query_arguments( $request ) );
return $this->prepare_item_for_response( $data, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for the products report request.
*
* @return callable
*/
protected function get_products_report_callback(): callable {
return function ( Request $request ) {
try {
/** @var MerchantReport $merchant */
$merchant = $this->container->get( MerchantReport::class );
$data = $merchant->get_report_data( 'products', $this->prepare_query_arguments( $request ) );
return $this->prepare_item_for_response( $data, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params(): array {
$params = parent::get_collection_params();
$params['interval'] = [
'description' => __( 'Time interval to use for segments in the returned data.', 'google-listings-and-ads' ),
'type' => 'string',
'enum' => [
'day',
],
'validate_callback' => 'rest_validate_request_arg',
];
return $params;
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'free_listings' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'subtotals' => $this->get_totals_schema(),
],
],
],
'products' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
'description' => __( 'Product ID.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'name' => [
'type' => 'string',
'description' => __( 'Product name.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
],
'subtotals' => $this->get_totals_schema(),
],
],
],
'intervals' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'interval' => [
'type' => 'string',
'description' => __( 'ID of this report segment.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'subtotals' => $this->get_totals_schema(),
],
],
],
'totals' => $this->get_totals_schema(),
'next_page' => [
'type' => 'string',
'description' => __( 'Token to retrieve the next page of results.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
];
}
/**
* Return schema for total fields.
*
* @return array
*/
protected function get_totals_schema(): array {
return [
'type' => 'object',
'properties' => [
'clicks' => [
'type' => 'integer',
'description' => __( 'Clicks.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'impressions' => [
'type' => 'integer',
'description' => __( 'Impressions.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'reports';
}
}
RequestReviewController.php 0000644 00000024531 15155646463 0012157 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Middleware;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\RequestReviewStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class RequestReviewController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class RequestReviewController extends BaseOptionsController {
/**
* @var TransientsInterface
*/
private $transients;
/**
* RequestReviewController constructor.
*
* @param RESTServer $server
* @param Middleware $middleware
* @param Merchant $merchant
* @param RequestReviewStatuses $request_review_statuses
* @param TransientsInterface $transients
*/
public function __construct( RESTServer $server, Middleware $middleware, Merchant $merchant, RequestReviewStatuses $request_review_statuses, TransientsInterface $transients ) {
parent::__construct( $server );
$this->middleware = $middleware;
$this->merchant = $merchant;
$this->request_review_statuses = $request_review_statuses;
$this->transients = $transients;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
/**
* GET information regarding the current Account Status
*/
$this->register_route(
'mc/review',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_review_read_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
],
);
/**
* POST a request review for the current account
*/
$this->register_route(
'mc/review',
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->post_review_request_callback(),
'permission_callback' => $this->get_permission_callback(),
],
],
);
}
/**
* Get the callback function for returning the review status.
*
* @return callable
*/
protected function get_review_read_callback(): callable {
return function ( Request $request ) {
try {
return $this->prepare_item_for_response( $this->get_review_status(), $request );
} catch ( Exception $e ) {
return new Response( [ 'message' => $e->getMessage() ], $e->getCode() ?: 400 );
}
};
}
/**
* Get the callback function after requesting a review.
*
* @return callable
*/
protected function post_review_request_callback(): callable {
return function () {
try {
// getting the current account status
$account_review_status = $this->get_review_status();
// Abort if it's in cool down period
if ( $account_review_status['cooldown'] ) {
do_action(
'woocommerce_gla_request_review_failure',
[
'error' => 'cooldown',
'account_review_status' => $account_review_status,
]
);
throw new Exception( __( 'Your account is under cool down period and cannot request a new review.', 'google-listings-and-ads' ), 400 );
}
// Abort if there is no eligible region available
if ( ! count( $account_review_status['reviewEligibleRegions'] ) ) {
do_action(
'woocommerce_gla_request_review_failure',
[
'error' => 'ineligible',
'account_review_status' => $account_review_status,
]
);
throw new Exception( __( 'Your account is not eligible for a new request review.', 'google-listings-and-ads' ), 400 );
}
$this->account_request_review( $account_review_status['reviewEligibleRegions'] );
return $this->set_under_review_status();
} catch ( Exception $e ) {
/**
* Catch potential errors in any specific region API call.
*
* Notice due some inconsistencies with Google API we are not considering [Bad Request -> ...already under review...]
* as an exception. This is because we suspect that calling the API of a region is triggering other regions requests as well.
* This makes all the calls after the first to fail as they will be under review.
*
* The undesired call of this function for accounts under review is already prevented in a previous stage, so, there is no danger doing this.
*/
if ( strpos( $e->getMessage(), 'under review' ) !== false ) {
return $this->set_under_review_status();
}
return new Response( [ 'message' => $e->getMessage() ], $e->getCode() ?: 400 );
}
};
}
/**
* Set Under review Status in the cache and return the response
*
* @return Response With the Under review status
*/
private function set_under_review_status() {
$new_status = [
'issues' => [],
'cooldown' => 0,
'status' => $this->request_review_statuses::UNDER_REVIEW,
'reviewEligibleRegions' => [],
];
// Update Account status when successful response
$this->set_cached_review_status( $new_status );
return new Response( $new_status );
}
/**
* Get the item schema properties for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'status' => [
'type' => 'string',
'description' => __( 'The status of the last review.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'cooldown' => [
'type' => 'integer',
'description' => __( 'Timestamp indicating if the user is in cool down period.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'issues' => [
'type' => 'array',
'description' => __( 'The issues related to the Merchant Center to be reviewed and addressed before approval.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
'items' => [
'type' => 'string',
],
],
'reviewEligibleRegions' => [
'type' => 'array',
'description' => __( 'The region codes in which is allowed to request a new review.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
'items' => [
'type' => 'string',
],
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'merchant_account_review';
}
/**
* Save the Account Review Status data inside a transient for caching purposes.
*
* @param array $value The Account Review Status data to save in the transient
*/
private function set_cached_review_status( $value ): void {
$this->transients->set(
TransientsInterface::MC_ACCOUNT_REVIEW,
$value,
$this->request_review_statuses->get_account_review_lifetime()
);
}
/**
* Get the Account Review Status data inside a transient for caching purposes.
*
* @return null|array Returns NULL in case no data is available or an array with the Account Review Status data otherwise.
*/
private function get_cached_review_status(): ?array {
return $this->transients->get(
TransientsInterface::MC_ACCOUNT_REVIEW,
);
}
/**
* Get the Account Review Status. We attempt to get the cached version or create a request otherwise.
*
* @return null|array Returns NULL in case no data is available or an array with the Account Review Status data otherwise.
* @throws Exception If the get_account_review_status API call fails.
*/
private function get_review_status(): ?array {
$review_status = $this->get_cached_review_status();
if ( is_null( $review_status ) ) {
$response = $this->get_account_review_status();
$review_status = $this->request_review_statuses->get_statuses_from_response( $response );
$this->set_cached_review_status( $review_status );
}
return $review_status;
}
/**
* Get Account Review Status
*
* @return array the response data
* @throws Exception When there is an invalid response.
*/
public function get_account_review_status() {
try {
if ( ! $this->middleware->is_subaccount() ) {
return [];
}
$response = $this->merchant->get_account_review_status();
do_action( 'woocommerce_gla_request_review_response', $response );
return $response;
} catch ( Exception $e ) {
do_action( 'woocommerce_gla_exception', $e, __METHOD__ );
throw new Exception(
$e->getMessage() ?? __( 'Error getting account review status.', 'google-listings-and-ads' ),
$e->getCode()
);
}
}
/**
* Request a new account review
*
* @param array $regions Regions to request a review.
* @return array With a successful message
* @throws Exception When there is an invalid response.
*/
public function account_request_review( $regions ) {
try {
// For each region we request a new review
foreach ( $regions as $region_code => $region_types ) {
$result = $this->merchant->account_request_review( $region_code, $region_types );
if ( 200 !== $result->getStatusCode() ) {
do_action(
'woocommerce_gla_request_review_failure',
[
'error' => 'response',
'region_code' => $region_code,
'response' => $result,
]
);
do_action( 'woocommerce_gla_guzzle_invalid_response', $result, __METHOD__ );
$error = $response['message'] ?? __( 'Invalid response getting requesting a new review.', 'google-listings-and-ads' );
throw new Exception( $error, $result->getStatusCode() );
}
}
// Otherwise, return a successful message and update the account status
return [
'message' => __( 'A new review has been successfully requested', 'google-listings-and-ads' ),
];
} catch ( Exception $e ) {
do_action( 'woocommerce_gla_exception', $e, __METHOD__ );
throw new Exception(
$e->getMessage() ?? __( 'Error requesting a new review.', 'google-listings-and-ads' ),
$e->getCode()
);
}
}
}
SettingsController.php 0000644 00000012231 15155646463 0011137 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\ShippingZone;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
defined( 'ABSPATH' ) || exit;
/**
* Class SettingsController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class SettingsController extends BaseOptionsController {
/**
* @var ShippingZone
*/
protected $shipping_zone;
/**
* SettingsController constructor.
*
* @param RESTServer $server
* @param ShippingZone $shipping_zone
*/
public function __construct( RESTServer $server, ShippingZone $shipping_zone ) {
parent::__construct( $server );
$this->shipping_zone = $shipping_zone;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/settings',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_settings_endpoint_read_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::EDITABLE,
'callback' => $this->get_settings_endpoint_edit_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get a callback for the settings endpoint.
*
* @return callable
*/
protected function get_settings_endpoint_read_callback(): callable {
return function () {
$data = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] );
$data['shipping_rates_count'] = $this->shipping_zone->get_shipping_rates_count();
$schema = $this->get_schema_properties();
$items = [];
foreach ( $schema as $key => $property ) {
$items[ $key ] = $data[ $key ] ?? $property['default'] ?? null;
}
return $items;
};
}
/**
* Get a callback for editing the settings endpoint.
*
* @return callable
*/
protected function get_settings_endpoint_edit_callback(): callable {
return function ( Request $request ) {
$schema = $this->get_schema_properties();
$options = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] );
if ( ! is_array( $options ) ) {
$options = [];
}
foreach ( $schema as $key => $property ) {
if ( ! in_array( 'edit', $property['context'] ?? [], true ) ) {
continue;
}
$options[ $key ] = $request->get_param( $key ) ?? $options[ $key ] ?? $property['default'] ?? null;
}
$this->options->update( OptionsInterface::MERCHANT_CENTER, $options );
return [
'status' => 'success',
'message' => __( 'Merchant Center Settings successfully updated.', 'google-listings-and-ads' ),
'data' => $options,
];
};
}
/**
* Get the schema for settings endpoints.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'shipping_rate' => [
'type' => 'string',
'description' => __(
'Whether shipping rate is a simple flat rate or needs to be configured manually in the Merchant Center.',
'google-listings-and-ads'
),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'enum' => [
'automatic',
'flat',
'manual',
],
],
'shipping_time' => [
'type' => 'string',
'description' => __(
'Whether shipping time is a simple flat time or needs to be configured manually in the Merchant Center.',
'google-listings-and-ads'
),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'enum' => [
'flat',
'manual',
],
],
'tax_rate' => [
'type' => 'string',
'description' => __(
'Whether tax rate is destination based or need to be configured manually in the Merchant Center.',
'google-listings-and-ads'
),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'enum' => [
'destination',
'manual',
],
'default' => 'destination',
],
'shipping_rates_count' => [
'type' => 'number',
'description' => __(
'The number of shipping rates in WC ready to be used in the Merchant Center.',
'google-listings-and-ads'
),
'context' => [ 'view' ],
'validate_callback' => 'rest_validate_request_arg',
'default' => 0,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'merchant_center_settings';
}
}
SettingsSyncController.php 0000644 00000007063 15155646463 0012003 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Settings;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\EmptySchemaPropertiesTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\WPErrorTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class SettingsSyncController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class SettingsSyncController extends BaseController {
use EmptySchemaPropertiesTrait;
use WPErrorTrait;
/** @var Settings */
protected $settings;
/**
* SettingsSyncController constructor.
*
* @param RESTServer $server
* @param Settings $settings
*/
public function __construct( RESTServer $server, Settings $settings ) {
parent::__construct( $server );
$this->settings = $settings;
}
/**
* Registers the routes for the objects of the controller.
*/
public function register_routes() {
$this->register_route(
'mc/settings/sync',
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_sync_endpoint_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
}
/**
* Get the callback for syncing shipping.
*
* @return callable
*/
protected function get_sync_endpoint_callback(): callable {
return function ( Request $request ) {
try {
$this->settings->sync_taxes();
$this->settings->sync_shipping();
do_action( 'woocommerce_gla_mc_settings_sync' );
/**
* MerchantCenter onboarding has been successfully completed.
*
* @event gla_mc_setup_completed
* @property string shipping_rate Shipping rate setup `automatic`, `manual`, `flat`.
* @property bool offers_free_shipping Free Shipping is available.
* @property float free_shipping_threshold Minimum amount to avail of free shipping.
* @property string shipping_time Shipping time setup `flat`, `manual`.
* @property string tax_rate Tax rate setup `destination`, `manual`.
* @property string target_countries List of target countries or `all`.
*/
do_action(
'woocommerce_gla_track_event',
'mc_setup_completed',
$this->settings->get_settings_for_tracking()
);
return new Response(
[
'status' => 'success',
'message' => __( 'Successfully synchronized settings with Google.', 'google-listings-and-ads' ),
],
201
);
} catch ( Exception $e ) {
do_action( 'woocommerce_gla_exception', $e, __METHOD__ );
try {
$decoded = $this->json_decode_message( $e->getMessage() );
$data = [
'status' => $decoded['code'] ?? 500,
'message' => $decoded['message'] ?? '',
'data' => $decoded,
];
} catch ( Exception $e2 ) {
$data = [
'status' => 500,
];
}
return $this->error_from_exception(
$e,
'gla_setting_sync_error',
$data
);
}
};
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'settings_sync';
}
}
ShippingRateBatchController.php 0000644 00000010127 15155646463 0012700 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class ShippingRateBatchController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ShippingRateBatchController extends ShippingRateController {
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
"{$this->route_base}/batch",
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_batch_create_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_batch_create_args_schema(),
],
[
'methods' => TransportMethods::DELETABLE,
'callback' => $this->get_batch_delete_shipping_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_batch_delete_args_schema(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback for creating items via batch.
*
* @return callable
*/
protected function get_batch_create_callback(): callable {
return function ( Request $request ) {
$rates = $request->get_param( 'rates' );
$responses = [];
$errors = [];
foreach ( $rates as $rate ) {
$new_request = new Request( 'POST', "/{$this->get_namespace()}/{$this->route_base}" );
$new_request->set_body_params( $rate );
$response = $this->server->dispatch_request( $new_request );
if ( 201 !== $response->get_status() ) {
$errors[] = $response->get_data();
} else {
$responses[] = $response->get_data();
}
}
return new Response(
[
'errors' => $errors,
'success' => $responses,
],
201
);
};
}
/**
* Get the callback for deleting shipping items via batch.
*
* @return callable
*
* @since 1.12.0
*/
protected function get_batch_delete_shipping_callback(): callable {
return function ( Request $request ) {
$ids = $request->get_param( 'ids' );
$responses = [];
$errors = [];
foreach ( $ids as $id ) {
$route = "/{$this->get_namespace()}/{$this->route_base}/{$id}";
$delete_request = new Request( 'DELETE', $route );
$response = $this->server->dispatch_request( $delete_request );
if ( 200 !== $response->get_status() ) {
$errors[] = $response->get_data();
} else {
$responses[] = $response->get_data();
}
}
return new Response(
[
'errors' => $errors,
'success' => $responses,
],
);
};
}
/**
* Get the argument schema for a batch create request.
*
* @return array
*
* @since 1.12.0
*/
protected function get_batch_create_args_schema(): array {
return [
'rates' => [
'type' => 'array',
'minItems' => 1,
'uniqueItems' => true,
'description' => __( 'Array of shipping rates to create.', 'google-listings-and-ads' ),
'validate_callback' => 'rest_validate_request_arg',
'items' => [
'type' => 'object',
'additionalProperties' => false,
'properties' => $this->get_schema_properties(),
],
],
];
}
/**
* Get the argument schema for a batch delete request.
*
* @return array
*
* @since 1.12.0
*/
protected function get_batch_delete_args_schema(): array {
return [
'ids' => [
'type' => 'array',
'description' => __( 'Array of unique shipping rate identification numbers.', 'google-listings-and-ads' ),
'context' => [ 'edit' ],
'minItems' => 1,
'required' => true,
'uniqueItems' => true,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'batch_shipping_rates';
}
}
ShippingRateController.php 0000644 00000020133 15155646463 0011734 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\ShippingRateSchemaTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\ShippingRateQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ISO3166AwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class ShippingRateController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ShippingRateController extends BaseController implements ISO3166AwareInterface {
use ShippingRateSchemaTrait;
/**
* The base for routes in this controller.
*
* @var string
*/
protected $route_base = 'mc/shipping/rates';
/**
* @var ShippingRateQuery
*/
protected $query;
/**
* ShippingRateController constructor.
*
* @param RESTServer $server
* @param ShippingRateQuery $query
*/
public function __construct( RESTServer $server, ShippingRateQuery $query ) {
parent::__construct( $server );
$this->query = $query;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
$this->route_base,
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_read_all_rates_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_create_rate_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
"{$this->route_base}/(?P<id>[\d]+)",
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_read_rate_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => [ 'id' => $this->get_schema_properties()['id'] ],
],
[
'methods' => TransportMethods::EDITABLE,
'callback' => $this->get_update_rate_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
[
'methods' => TransportMethods::DELETABLE,
'callback' => $this->get_delete_rate_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => [ 'id' => $this->get_schema_properties()['id'] ],
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for returning the endpoint results.
*
* @return callable
*/
protected function get_read_all_rates_callback(): callable {
return function ( Request $request ) {
$rates = $this->get_all_shipping_rates();
return array_map(
function ( $rate ) use ( $request ) {
$response = $this->prepare_item_for_response( $rate, $request );
return $this->prepare_response_for_collection( $response );
},
$rates
);
};
}
/**
* @return callable
*/
protected function get_read_rate_callback(): callable {
return function ( Request $request ) {
$id = (string) $request->get_param( 'id' );
$rate = $this->get_shipping_rate_by_id( $id );
if ( empty( $rate ) ) {
return new Response(
[
'message' => __( 'No rate available.', 'google-listings-and-ads' ),
'id' => $id,
],
404
);
}
return $this->prepare_item_for_response( $rate, $request );
};
}
/**
* @return callable
*
* @since 1.12.0
*/
protected function get_update_rate_callback(): callable {
return function ( Request $request ) {
$id = (string) $request->get_param( 'id' );
$rate = $this->get_shipping_rate_by_id( $id );
if ( empty( $rate ) ) {
return new Response(
[
'message' => __( 'No rate found with the given ID.', 'google-listings-and-ads' ),
'id' => $id,
],
404
);
}
$data = $this->prepare_item_for_database( $request );
$this->create_query()->update(
$data,
[
'id' => $id,
]
);
return new Response( '', 204 );
};
}
/**
* Get the callback function for creating a new shipping rate.
*
* @return callable
*/
protected function get_create_rate_callback(): callable {
return function ( Request $request ) {
$shipping_rate_query = $this->create_query();
try {
$data = $this->prepare_item_for_database( $request );
$country = $data['country'];
$existing_query = $this->create_query()->where( 'country', $country );
$existing = ! empty( $existing_query->get_results() );
if ( $existing ) {
$rate_id = $existing_query->get_results()[0]['id'];
$shipping_rate_query->update( $data, [ 'id' => $rate_id ] );
} else {
$shipping_rate_query->insert( $data );
$rate_id = $shipping_rate_query->last_insert_id();
}
} catch ( InvalidQuery $e ) {
return $this->error_from_exception(
$e,
'gla_error_creating_shipping_rate',
[
'code' => 400,
'message' => $e->getMessage(),
]
);
}
// Fetch updated/inserted rate to return in response.
$rate_response = $this->prepare_item_for_response(
$this->get_shipping_rate_by_id( (string) $rate_id ),
$request
);
return new Response(
[
'status' => 'success',
'message' => sprintf(
/* translators: %s is the country code in ISO 3166-1 alpha-2 format. */
__( 'Successfully added rate for country: "%s".', 'google-listings-and-ads' ),
$country
),
'rate' => $rate_response->get_data(),
],
201
);
};
}
/**
* @return callable
*/
protected function get_delete_rate_callback(): callable {
return function ( Request $request ) {
try {
$id = (string) $request->get_param( 'id' );
$rate = $this->get_shipping_rate_by_id( $id );
if ( empty( $rate ) ) {
return new Response(
[
'message' => __( 'No rate found with the given ID.', 'google-listings-and-ads' ),
'id' => $id,
],
404
);
}
$this->create_query()->delete( 'id', $id );
return [
'status' => 'success',
'message' => __( 'Successfully deleted rate.', 'google-listings-and-ads' ),
];
} catch ( InvalidQuery $e ) {
return $this->error_from_exception(
$e,
'gla_error_deleting_shipping_rate',
[
'code' => 400,
'message' => $e->getMessage(),
]
);
}
};
}
/**
* Returns the list of all shipping rates stored in the database grouped by their respective country code.
*
* @return array Array of shipping rates grouped by country code.
*/
protected function get_all_shipping_rates(): array {
return $this->create_query()
->set_order( 'country', 'ASC' )
->get_results();
}
/**
* @param string $id
*
* @return array|null The shipping rate properties as an array or null if it doesn't exist.
*/
protected function get_shipping_rate_by_id( string $id ): ?array {
$results = $this->create_query()->where( 'id', $id )->get_results();
return ! empty( $results ) ? $results[0] : null;
}
/**
* Return a new instance of the shipping rate query object.
*
* @return ShippingRateQuery
*/
protected function create_query(): ShippingRateQuery {
return clone $this->query;
}
/**
* @return array
*/
protected function get_schema_properties(): array {
return $this->get_shipping_rate_schema();
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'shipping_rates';
}
}
ShippingRateSuggestionsController.php 0000644 00000007755 15155646463 0014206 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\ShippingRateSchemaTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ISO3166AwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\ShippingSuggestionService;
use WP_REST_Request as Request;
defined( 'ABSPATH' ) || exit;
/**
* Class ShippingRateSuggestionsController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*
* @since 1.12.0
*/
class ShippingRateSuggestionsController extends BaseController implements ISO3166AwareInterface {
use ShippingRateSchemaTrait;
/**
* The base for routes in this controller.
*
* @var string
*/
protected $route_base = 'mc/shipping/rates/suggestions';
/**
* @var ShippingSuggestionService
*/
protected $shipping_suggestion;
/**
* ShippingRateSuggestionsController constructor.
*
* @param RESTServer $server
* @param ShippingSuggestionService $shipping_suggestion
*/
public function __construct( RESTServer $server, ShippingSuggestionService $shipping_suggestion ) {
parent::__construct( $server );
$this->shipping_suggestion = $shipping_suggestion;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
"{$this->route_base}",
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_suggestions_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => [
'country_codes' => [
'type' => 'array',
'description' => __( 'Array of country codes in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ),
'context' => [ 'edit' ],
'sanitize_callback' => $this->get_country_code_sanitize_callback(),
'validate_callback' => $this->get_country_code_validate_callback(),
'minItems' => 1,
'required' => true,
'uniqueItems' => true,
'items' => [
'type' => 'string',
],
],
],
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for returning the endpoint results.
*
* @return callable
*/
protected function get_suggestions_callback(): callable {
return function ( Request $request ) {
$country_codes = $request->get_param( 'country_codes' );
$rates_output = [];
foreach ( $country_codes as $country_code ) {
$suggestions = $this->shipping_suggestion->get_suggestions( $country_code );
// Prepare the output.
$suggestions = array_map(
function ( $suggestion ) use ( $request ) {
$response = $this->prepare_item_for_response( $suggestion, $request );
return $this->prepare_response_for_collection( $response );
},
$suggestions
);
// Merge the suggestions for all countries into one array.
$rates_output = array_merge( $rates_output, $suggestions );
}
return $rates_output;
};
}
/**
* @return array
*/
protected function get_schema_properties(): array {
$schema = $this->get_shipping_rate_schema();
// Suggested shipping rates don't have an id.
unset( $schema['id'] );
// All properties are read-only.
return array_map(
function ( $property ) {
$property['readonly'] = true;
$property['context'] = [ 'view' ];
return $property;
},
$schema
);
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'shipping_rates_suggestions';
}
}
ShippingTimeBatchController.php 0000644 00000005132 15155646463 0012703 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BatchSchemaTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class ShippingTimeBatchController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ShippingTimeBatchController extends ShippingTimeController {
use BatchSchemaTrait;
use BatchShippingTrait;
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
"{$this->route_base}/batch",
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_batch_create_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_item_schema(),
],
[
'methods' => TransportMethods::DELETABLE,
'callback' => $this->get_batch_delete_shipping_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_item_delete_schema(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback for creating items via batch.
*
* @return callable
*/
protected function get_batch_create_callback(): callable {
return function ( Request $request ) {
$country_codes = $request->get_param( 'country_codes' );
$time = $request->get_param( 'time' );
$max_time = $request->get_param( 'max_time' );
$responses = [];
$errors = [];
foreach ( $country_codes as $country_code ) {
$new_request = new Request( 'POST', "/{$this->get_namespace()}/{$this->route_base}" );
$new_request->set_body_params(
[
'country_code' => $country_code,
'time' => $time,
'max_time' => $max_time,
]
);
$response = $this->server->dispatch_request( $new_request );
if ( 201 !== $response->get_status() ) {
$errors[] = $response->get_data();
} else {
$responses[] = $response->get_data();
}
}
return new Response(
[
'errors' => $errors,
'success' => $responses,
],
201
);
};
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'batch_shipping_times';
}
}
ShippingTimeController.php 0000644 00000023537 15155646463 0011752 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\CountryCodeTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\ShippingTimeQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ISO3166AwareInterface;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
use WP_Error;
defined( 'ABSPATH' ) || exit;
/**
* Class ShippingTimeController
*
* ContainerAware used for:
* - ShippingTimeQuery
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class ShippingTimeController extends BaseController implements ContainerAwareInterface, ISO3166AwareInterface {
use ContainerAwareTrait;
use CountryCodeTrait;
/**
* The base for routes in this controller.
*
* @var string
*/
protected $route_base = 'mc/shipping/times';
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
$this->route_base,
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_read_times_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_create_time_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_args_schema(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
"{$this->route_base}/(?P<country_code>\\w{2})",
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_read_time_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
[
'methods' => TransportMethods::DELETABLE,
'callback' => $this->get_delete_time_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for reading times.
*
* @return callable
*/
protected function get_read_times_callback(): callable {
return function ( Request $request ) {
$times = $this->get_all_shipping_times();
$items = [];
foreach ( $times as $time ) {
$data = $this->prepare_item_for_response(
[
'country_code' => $time['country'],
'time' => $time['time'],
'max_time' => $time['max_time'],
],
$request
);
$items[ $time['country'] ] = $this->prepare_response_for_collection( $data );
}
return $items;
};
}
/**
* Get the callback function for reading a single time.
*
* @return callable
*/
protected function get_read_time_callback(): callable {
return function ( Request $request ) {
$country = $request->get_param( 'country_code' );
$time = $this->get_shipping_time_for_country( $country );
if ( empty( $time ) ) {
return new Response(
[
'message' => __( 'No time available.', 'google-listings-and-ads' ),
'country' => $country,
],
404
);
}
return $this->prepare_item_for_response(
[
'country_code' => $time[0]['country'],
'time' => $time[0]['time'],
'max_time' => $time[0]['max_time'],
],
$request
);
};
}
/**
* Get the callback to crate a new time.
*
* @return callable
*/
protected function get_create_time_callback(): callable {
return function ( Request $request ) {
$query = $this->get_query_object();
$country_code = $request->get_param( 'country_code' );
$existing = ! empty( $query->where( 'country', $country_code )->get_results() );
try {
$data = [
'country' => $country_code,
'time' => $request->get_param( 'time' ),
'max_time' => $request->get_param( 'max_time' ),
];
if ( $existing ) {
$query->update(
$data,
[
'id' => $query->get_results()[0]['id'],
]
);
} else {
$query->insert( $data );
}
return new Response(
[
'status' => 'success',
'message' => sprintf(
/* translators: %s is the country code in ISO 3166-1 alpha-2 format. */
__( 'Successfully added time for country: "%s".', 'google-listings-and-ads' ),
$country_code
),
],
201
);
} catch ( InvalidQuery $e ) {
return $this->error_from_exception(
$e,
'gla_error_creating_shipping_time',
[
'code' => 400,
'message' => $e->getMessage(),
]
);
}
};
}
/**
* Get the callback function for deleting a time.
*
* @return callable
*/
protected function get_delete_time_callback(): callable {
return function ( Request $request ) {
try {
$country_code = $request->get_param( 'country_code' );
$this->get_query_object()->delete( 'country', $country_code );
return [
'status' => 'success',
'message' => sprintf(
/* translators: %s is the country code in ISO 3166-1 alpha-2 format. */
__( 'Successfully deleted the time for country: "%s".', 'google-listings-and-ads' ),
$country_code
),
];
} catch ( InvalidQuery $e ) {
return $this->error_from_exception(
$e,
'gla_error_deleting_shipping_time',
[
'code' => 400,
'message' => $e->getMessage(),
]
);
}
};
}
/**
* @return array
*/
protected function get_all_shipping_times(): array {
return $this->get_query_object()->set_limit( 100 )->get_results();
}
/**
* @param string $country
*
* @return array
*/
protected function get_shipping_time_for_country( string $country ): array {
return $this->get_query_object()->where( 'country', $country )->get_results();
}
/**
* Get the shipping time query object.
*
* @return ShippingTimeQuery
*/
protected function get_query_object(): ShippingTimeQuery {
return $this->container->get( ShippingTimeQuery::class );
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'country_code' => [
'type' => 'string',
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'sanitize_callback' => $this->get_country_code_sanitize_callback(),
'validate_callback' => $this->get_country_code_validate_callback(),
'required' => true,
],
'time' => [
'type' => 'integer',
'description' => __( 'The minimum shipping time in days.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => [ $this, 'validate_shipping_times' ],
],
'max_time' => [
'type' => 'integer',
'description' => __( 'The maximum shipping time in days.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => [ $this, 'validate_shipping_times' ],
],
];
}
/**
* Get the args schema for the controller.
*
* @return array
*/
protected function get_args_schema(): array {
$schema = $this->get_schema_properties();
$schema['time']['required'] = true;
$schema['max_time']['required'] = true;
return $schema;
}
/**
* Validate the shipping times.
*
* @param mixed $value
* @param Request $request
* @param string $param
*
* @return WP_Error|true
*/
public function validate_shipping_times( $value, $request, $param ) {
$time = $request->get_param( 'time' );
$max_time = $request->get_param( 'max_time' );
if ( rest_is_integer( $value ) === false ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
sprintf( __( '%1$s is not of type %2$s.', 'google-listings-and-ads' ), $param, 'integer' ),
[ 'param' => $param ]
);
}
if ( $value < 0 ) {
return new WP_Error( 'invalid_shipping_times', __( 'Shipping times cannot be negative.', 'google-listings-and-ads' ), [ 'param' => $param ] );
}
if ( $time > $max_time ) {
return new WP_Error( 'invalid_shipping_times', __( 'The minimum shipping time cannot be greater than the maximum shipping time.', 'google-listings-and-ads' ), [ 'param' => $param ] );
}
return true;
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'shipping_times';
}
/**
* Retrieves all of the registered additional fields for a given object-type.
*
* @param string $object_type Optional. The object type.
*
* @return array Registered additional fields (if any), empty array if none or if the object type could
* not be inferred.
*/
protected function get_additional_fields( $object_type = null ): array {
$fields = parent::get_additional_fields( $object_type );
$fields['country'] = [
'schema' => [
'type' => 'string',
'description' => __( 'Country in which the shipping time applies.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'get_callback' => function ( $fields ) {
return $this->iso3166_data_provider->alpha2( $fields['country_code'] )['name'];
},
];
return $fields;
}
}
SupportedCountriesController.php 0000644 00000010101 15155646463 0013212 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\CountryCodeTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\EmptySchemaPropertiesTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use WP_REST_Request as Request;
defined( 'ABSPATH' ) || exit;
/**
* Class SupportedCountriesController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers
*/
class SupportedCountriesController extends BaseController {
use CountryCodeTrait;
use EmptySchemaPropertiesTrait;
/**
* The WC proxy object.
*
* @var WC
*/
protected $wc;
/**
* @var GoogleHelper
*/
protected $google_helper;
/**
* SupportedCountriesController constructor.
*
* @param RESTServer $server
* @param WC $wc
* @param GoogleHelper $google_helper
*/
public function __construct( RESTServer $server, WC $wc, GoogleHelper $google_helper ) {
parent::__construct( $server );
$this->wc = $wc;
$this->google_helper = $google_helper;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/countries',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_countries_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_query_args(),
],
]
);
}
/**
* Get the callback function for returning supported countries.
*
* @return callable
*/
protected function get_countries_callback(): callable {
return function ( Request $request ) {
$return = [
'countries' => $this->get_supported_countries( $request ),
];
if ( $request->get_param( 'continents' ) ) {
$return['continents'] = $this->get_supported_continents();
}
return $return;
};
}
/**
* Get the array of supported countries.
*
* @return array
*/
protected function get_supported_countries(): array {
$all_countries = $this->wc->get_countries();
$mc_countries = $this->google_helper->get_mc_supported_countries_currencies();
$supported = [];
foreach ( $mc_countries as $country => $currency ) {
if ( ! array_key_exists( $country, $all_countries ) ) {
continue;
}
$supported[ $country ] = [
'name' => $all_countries[ $country ],
'currency' => $currency,
];
}
uasort(
$supported,
function ( $a, $b ) {
return $a['name'] <=> $b['name'];
}
);
return $supported;
}
/**
* Get the array of supported continents.
*
* @return array
*/
protected function get_supported_continents(): array {
$all_continents = $this->wc->get_continents();
foreach ( $all_continents as $continent_code => $continent ) {
$supported_countries_of_continent = $this->google_helper->get_supported_countries_from_continent( $continent_code );
if ( empty( $supported_countries_of_continent ) ) {
unset( $all_continents[ $continent_code ] );
} else {
$all_continents[ $continent_code ]['countries'] = array_values( $supported_countries_of_continent );
}
}
return $all_continents;
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'supported_countries';
}
/**
* Get the arguments for the query endpoint.
*
* @return array
*/
protected function get_query_args(): array {
return [
'continents' => [
'description' => __( 'Include continents data if set to true.', 'google-listings-and-ads' ),
'type' => 'boolean',
'validate_callback' => 'rest_validate_request_arg',
],
];
}
}
SyncableProductsCountController.php 0000644 00000007025 15155646463 0013641 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateSyncableProductsCount;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class SyncableProductsCountController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class SyncableProductsCountController extends BaseOptionsController {
/**
* @var JobRepository
*/
protected $job_repository;
/**
* SyncableProductsCountController constructor.
*
* @param RESTServer $server
* @param JobRepository $job_repository
*/
public function __construct( RESTServer $server, JobRepository $job_repository ) {
parent::__construct( $server );
$this->job_repository = $job_repository;
}
/**
* Registers the routes for the objects of the controller.
*/
public function register_routes() {
$this->register_route(
'mc/syncable-products-count',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_syncable_products_count_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->update_syncable_products_count_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
}
/**
* Get the callback function for marking setup complete.
*
* @return callable
*/
protected function get_syncable_products_count_callback(): callable {
return function ( Request $request ) {
$response = [
'count' => null,
];
$count = $this->options->get( OptionsInterface::SYNCABLE_PRODUCTS_COUNT );
if ( isset( $count ) ) {
$response['count'] = (int) $count;
}
return $this->prepare_item_for_response( $response, $request );
};
}
/**
* Get the callback for syncing shipping.
*
* @return callable
*/
protected function update_syncable_products_count_callback(): callable {
return function ( Request $request ) {
$this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT );
$this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA );
$job = $this->job_repository->get( UpdateSyncableProductsCount::class );
$job->schedule();
return new Response(
[
'status' => 'success',
'message' => __( 'Successfully scheduled a job to update the number of syncable products.', 'google-listings-and-ads' ),
],
201
);
};
}
/**
* Get the item schema properties for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'count' => [
'type' => 'number',
'description' => __( 'The number of products that are ready to be synced to Google.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'syncable_products_count';
}
}
TargetAudienceController.php 0000644 00000020277 15155646463 0012234 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\CountryCodeTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ISO3166AwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\ShippingZone;
use Locale;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
use function wp_get_available_translations;
defined( 'ABSPATH' ) || exit;
/**
* Class TargetAudienceController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter
*/
class TargetAudienceController extends BaseOptionsController implements ISO3166AwareInterface {
use CountryCodeTrait;
/**
* The WP proxy object.
*
* @var WP
*/
protected $wp;
/**
* @var ShippingZone
*/
protected $shipping_zone;
/**
* @var WC
*/
protected $wc;
/**
* @var GoogleHelper
*/
protected $google_helper;
/**
* TargetAudienceController constructor.
*
* @param RESTServer $server
* @param WP $wp
* @param WC $wc
* @param ShippingZone $shipping_zone
* @param GoogleHelper $google_helper
*/
public function __construct( RESTServer $server, WP $wp, WC $wc, ShippingZone $shipping_zone, GoogleHelper $google_helper ) {
parent::__construct( $server );
$this->wp = $wp;
$this->wc = $wc;
$this->shipping_zone = $shipping_zone;
$this->google_helper = $google_helper;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'mc/target_audience',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_read_audience_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_update_audience_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'mc/target_audience/suggestions',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_suggest_audience_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for reading the target audience data.
*
* @return callable
*/
protected function get_read_audience_callback(): callable {
return function ( Request $request ) {
return $this->prepare_item_for_response( $this->get_target_audience_option(), $request );
};
}
/**
* Get the callback function for suggesting the target audience data.
*
* @return callable
*
* @since 1.9.0
*/
protected function get_suggest_audience_callback(): callable {
return function ( Request $request ) {
return $this->prepare_item_for_response( $this->get_target_audience_suggestion(), $request );
};
}
/**
* Get the callback function for updating the target audience data.
*
* @return callable
*/
protected function get_update_audience_callback(): callable {
return function ( Request $request ) {
$data = $this->prepare_item_for_database( $request );
$this->update_target_audience_option( $data );
$this->prepare_item_for_response( $data, $request );
return new Response(
[
'status' => 'success',
'message' => __( 'Successfully updated the Target Audience settings.', 'google-listings-and-ads' ),
],
201
);
};
}
/**
* Retrieves all of the registered additional fields for a given object-type.
*
* @param string $object_type Optional. The object type.
*
* @return array Registered additional fields (if any), empty array if none or if the object type could
* not be inferred.
*/
protected function get_additional_fields( $object_type = null ): array {
$fields = parent::get_additional_fields( $object_type );
// Fields are expected to be an array with a 'get_callback' callable that returns the field value.
$fields['locale'] = [
'schema' => [
'type' => 'string',
'description' => __( 'The locale for the site.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'get_callback' => function () {
return $this->wp->get_locale();
},
];
$fields['language'] = [
'schema' => [
'type' => 'string',
'description' => __( 'The language to use for product listings.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'get_callback' => $this->get_language_callback(),
];
return $fields;
}
/**
* Get the option data for the target audience.
*
* @return array
*/
protected function get_target_audience_option(): array {
return $this->options->get( OptionsInterface::TARGET_AUDIENCE, [] );
}
/**
* Get the suggested values for the target audience option.
*
* @return string[]
*
* @since 1.9.0
*/
protected function get_target_audience_suggestion(): array {
$countries = $this->shipping_zone->get_shipping_countries();
$base_country = $this->wc->get_base_country();
// Add WooCommerce store country if it's supported and not already in the list.
if ( ! in_array( $base_country, $countries, true ) && $this->google_helper->is_country_supported( $base_country ) ) {
$countries[] = $base_country;
}
return [
'location' => 'selected',
'countries' => $countries,
];
}
/**
* Update the option data for the target audience.
*
* @param array $data
*
* @return bool
*/
protected function update_target_audience_option( array $data ): bool {
return $this->options->update( OptionsInterface::TARGET_AUDIENCE, $data );
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'location' => [
'type' => 'string',
'description' => __( 'Location where products will be shown.', 'google-listings-and-ads' ),
'context' => [ 'edit', 'view' ],
'validate_callback' => 'rest_validate_request_arg',
'required' => true,
'enum' => [
'all',
'selected',
],
],
'countries' => [
'type' => 'array',
'description' => __(
'Array of country codes in ISO 3166-1 alpha-2 format.',
'google-listings-and-ads'
),
'context' => [ 'edit', 'view' ],
'sanitize_callback' => $this->get_country_code_sanitize_callback(),
'validate_callback' => $this->get_country_code_validate_callback(),
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'target_audience';
}
/**
* Get the callback to provide the language in use for the site.
*
* @return callable
*/
protected function get_language_callback(): callable {
$locale = $this->wp->get_locale();
// Default to using the Locale class if it is available.
if ( class_exists( Locale::class ) ) {
return function () use ( $locale ): string {
return Locale::getDisplayLanguage( $locale, $locale );
};
}
return function () use ( $locale ): string {
// en_US isn't provided by the translations API.
if ( 'en_US' === $locale ) {
return 'English';
}
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
return wp_get_available_translations()[ $locale ]['native_name'] ?? $locale;
};
}
}