File "AccountService.php"
Full Path: /home/warrior1/public_html/plugins/google-listings-and-ads/src/MerchantCenter/AccountService.php
File size: 16.08 KB
MIME-type: text/x-php
Charset: utf-8
<?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
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\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\Jobs\CleanupSyncedProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
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 Exception;
use Psr\Container\ContainerInterface;
defined( 'ABSPATH' ) || exit;
/**
* Class AccountService
*
* Container used to access:
* - CleanupSyncedProducts
* - Merchant
* - MerchantAccountState
* - MerchantCenterService
* - MerchantIssueTable
* - MerchantStatuses
* - Middleware
* - SiteVerification
* - ShippingRateTable
* - ShippingTimeTable
*
* @since 1.12.0
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
class AccountService implements OptionsAwareInterface, Service {
use OptionsAwareTrait;
use PluginHelper;
/**
* @var ContainerInterface
*/
protected $container;
/**
* @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 ContainerInterface $container
*/
public function __construct( ContainerInterface $container ) {
$this->state = $container->get( MerchantAccountState::class );
$this->container = $container;
}
/**
* 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 {
$id = $this->options->get_merchant_id();
$status = [
'id' => $id,
'status' => $id ? 'connected' : 'disconnected',
];
$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( 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;
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', [] );
}
}
/**
* 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 );
}
}