<?php declare( strict_types=1 ); namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; use Automattic\WooCommerce\GoogleListingsAndAds\Exception\MerchantApiException; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface; use Google\Exception as GoogleException; use Google\Service\Exception as GoogleServiceException; use Google\Service\ShoppingContent; use Google\Service\ShoppingContent\Account; use Google\Service\ShoppingContent\AccountAdsLink; use Google\Service\ShoppingContent\AccountStatus; use Google\Service\ShoppingContent\ProductstatusesCustomBatchResponse; use Google\Service\ShoppingContent\ProductstatusesCustomBatchRequest; use Google\Service\ShoppingContent\Product; use Exception; use Google\Service\ShoppingContent\RequestPhoneVerificationRequest; use Google\Service\ShoppingContent\VerifyPhoneNumberRequest; defined( 'ABSPATH' ) || exit; /** * Class Merchant * * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Google */ class Merchant implements OptionsAwareInterface { use OptionsAwareTrait; /** * The shopping service. * * @var ShoppingContent */ protected $service; /** * Merchant constructor. * * @param ShoppingContent $service */ public function __construct( ShoppingContent $service ) { $this->service = $service; } /** * @return Product[] */ public function get_products(): array { $products = $this->service->products->listProducts( $this->options->get_merchant_id() ); $return = []; while ( ! empty( $products->getResources() ) ) { foreach ( $products->getResources() as $product ) { $return[] = $product; } if ( empty( $products->getNextPageToken() ) ) { break; } $products = $this->service->products->listProducts( $this->options->get_merchant_id(), [ 'pageToken' => $products->getNextPageToken() ] ); } return $return; } /** * Claim a website for the user's Merchant Center account. * * @param bool $overwrite Whether to include the overwrite directive. * @return bool * @throws Exception If the website claim fails. */ public function claimwebsite( bool $overwrite = false ): bool { try { $id = $this->options->get_merchant_id(); $params = $overwrite ? [ 'overwrite' => true ] : []; $this->service->accounts->claimwebsite( $id, $id, $params ); do_action( 'woocommerce_gla_site_claim_success', [ 'details' => 'google_proxy' ] ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); do_action( 'woocommerce_gla_site_claim_failure', [ 'details' => 'google_proxy' ] ); $error_message = __( 'Unable to claim website.', 'google-listings-and-ads' ); if ( 403 === $e->getCode() ) { $error_message = __( 'Website already claimed, use overwrite to complete the process.', 'google-listings-and-ads' ); } throw new Exception( $error_message, $e->getCode() ); } return true; } /** * 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 string $phone_number Phone number to be verified. * @param string $verification_method Verification method to receive verification code. * @param string $language_code Language code IETF BCP 47 syntax (for example, en-US). Language code is used * to provide localized SMS and PHONE_CALL. Default language used is en-US if * not provided. * * @return string The verification ID to use in subsequent calls to * `Merchant::verify_phone_number`. * * @throws GoogleServiceException If there are any Google API errors. * * @see https://tools.ietf.org/html/bcp47 IETF BCP 47 language codes. * @see https://wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements ISO 3166-1 alpha-2 * officially assigned codes. * * @since 1.5.0 */ public function request_phone_verification( string $region_code, string $phone_number, string $verification_method, string $language_code = 'en-US' ): string { $merchant_id = $this->options->get_merchant_id(); $request = new RequestPhoneVerificationRequest( [ 'phoneRegionCode' => $region_code, 'phoneNumber' => $phone_number, 'phoneVerificationMethod' => $verification_method, 'languageCode' => $language_code, ] ); try { return $this->service->accounts->requestphoneverification( $merchant_id, $merchant_id, $request )->getVerificationId(); } catch ( GoogleServiceException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw $e; } } /** * Validates verification code to verify phone number for the account. * * @param string $verification_id The verification ID returned by * `Merchant::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 string Verified phone number if verification is successful. * * @throws GoogleServiceException If there are any Google API errors. * * @since 1.5.0 */ public function verify_phone_number( string $verification_id, string $verification_code, string $verification_method ): string { $merchant_id = $this->options->get_merchant_id(); $request = new VerifyPhoneNumberRequest( [ 'verificationId' => $verification_id, 'verificationCode' => $verification_code, 'phoneVerificationMethod' => $verification_method, ] ); try { return $this->service->accounts->verifyphonenumber( $merchant_id, $merchant_id, $request )->getVerifiedPhoneNumber(); } catch ( GoogleServiceException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw $e; } } /** * Retrieve the user's Merchant Center account information. * * @param int $id Optional - the Merchant Center account to retrieve * * @return Account The user's Merchant Center account. * @throws MerchantApiException If the account can't be retrieved. */ public function get_account( int $id = 0 ): Account { $id = $id ?: $this->options->get_merchant_id(); try { $mc_account = $this->service->accounts->get( $id, $id ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw MerchantApiException::account_retrieve_failed( $e->getCode() ); } return $mc_account; } /** * Get hash of the site URL we used during onboarding. * If not available in a local option, it's fetched from the Merchant Center account. * * @since 1.13.0 * @return string|null */ public function get_claimed_url_hash(): ?string { $claimed_url_hash = $this->options->get( OptionsInterface::CLAIMED_URL_HASH ); if ( empty( $claimed_url_hash ) && $this->options->get_merchant_id() ) { try { $account_url = $this->get_account()->getWebsiteUrl(); if ( empty( $account_url ) || ! $this->get_accountstatus()->getWebsiteClaimed() ) { return null; } $claimed_url_hash = md5( untrailingslashit( $account_url ) ); $this->options->update( OptionsInterface::CLAIMED_URL_HASH, $claimed_url_hash ); } catch ( Exception $e ) { return null; } } return $claimed_url_hash; } /** * Retrieve the user's Merchant Center account information. * * @param int $id Optional - the Merchant Center account to retrieve * @return AccountStatus The user's Merchant Center account status. * @throws Exception If the account can't be retrieved. */ public function get_accountstatus( int $id = 0 ): AccountStatus { $id = $id ?: $this->options->get_merchant_id(); try { $mc_account_status = $this->service->accountstatuses->get( $id, $id ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw new Exception( __( 'Unable to retrieve Merchant Center account status.', 'google-listings-and-ads' ), $e->getCode() ); } return $mc_account_status; } /** * Retrieve a batch of Merchant Center Product Statuses using the provided Merchant Center product IDs. * * @since 1.1.0 * * @param string[] $mc_product_ids * * @return ProductstatusesCustomBatchResponse; */ public function get_productstatuses_batch( array $mc_product_ids ): ProductstatusesCustomBatchResponse { $merchant_id = $this->options->get_merchant_id(); $entries = []; foreach ( $mc_product_ids as $index => $id ) { $entries[] = [ 'batchId' => $index + 1, 'productId' => $id, 'method' => 'GET', 'merchantId' => $merchant_id, ]; } // Retrieve batch. $request = new ProductstatusesCustomBatchRequest(); $request->setEntries( $entries ); return $this->service->productstatuses->custombatch( $request ); } /** * Update the provided Merchant Center account information. * * @param Account $account The Account data to update. * * @return Account The user's Merchant Center account. * @throws MerchantApiException If the account can't be retrieved. */ public function update_account( Account $account ): Account { try { $account = $this->service->accounts->update( $account->getId(), $account->getId(), $account ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw MerchantApiException::account_update_failed( $e->getCode() ); } return $account; } /** * Link a Google Ads ID to this Merchant account. * * @param int $ads_id Google Ads ID to link. * * @return bool * @throws MerchantApiException When unable to retrieve or update account data. */ public function link_ads_id( int $ads_id ): bool { $account = $this->get_account(); $ads_links = $account->getAdsLinks(); // Stop early if we already have a link setup. foreach ( $ads_links as $link ) { if ( $ads_id === absint( $link->getAdsId() ) ) { return false; } } $link = new AccountAdsLink(); $link->setAdsId( $ads_id ); $link->setStatus( 'active' ); $account->setAdsLinks( array_merge( $ads_links, [ $link ] ) ); $this->update_account( $account ); return true; } /** * Check if we have access to the merchant account. * * @param string $email Email address of the connected account. * * @return bool */ public function has_access( string $email ): bool { $id = $this->options->get_merchant_id(); try { $account = $this->service->accounts->get( $id, $id ); foreach ( $account->getUsers() as $user ) { if ( $email === $user->getEmailAddress() && $user->getAdmin() ) { return true; } } } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); } return false; } /** * Update the Merchant Center ID to use for requests. * * @param int $id Merchant ID number. * * @return bool */ public function update_merchant_id( int $id ): bool { return $this->options->update( OptionsInterface::MERCHANT_ID, $id ); } }