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

namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsConversionAction;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\BillingSetupStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Connection;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Middleware;
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\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 Exception;

defined( 'ABSPATH' ) || exit;

/**
 * Class AccountService
 *
 * Container used to access:
 * - Ads
 * - AdsConversionAction
 * - Connection
 * - Merchant
 * - MerchantAccountState
 * - Middleware
 * - TransientsInterface
 *
 * @since 1.11.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
 */
class AccountService implements ContainerAwareInterface, OptionsAwareInterface, Service {

	use ContainerAwareTrait;
	use OptionsAwareTrait;

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

	/**
	 * AccountService constructor.
	 *
	 * @param AdsAccountState $state
	 */
	public function __construct( AdsAccountState $state ) {
		$this->state = $state;
	}

	/**
	 * Get Ads accounts associated with the connected Google account.
	 *
	 * @return array
	 * @throws Exception When an API error occurs.
	 */
	public function get_accounts(): array {
		return $this->container->get( Ads::class )->get_ads_accounts();
	}

	/**
	 * Get the connected ads account.
	 *
	 * @return array
	 */
	public function get_connected_account(): array {
		$id = $this->options->get_ads_id();

		$status = [
			'id'       => $id,
			'currency' => $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ),
			'symbol'   => html_entity_decode( get_woocommerce_currency_symbol( $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ) ), ENT_QUOTES ),
			'status'   => $id ? 'connected' : 'disconnected',
		];

		$incomplete = $this->state->last_incomplete_step();
		if ( ! empty( $incomplete ) ) {
			$status['status'] = 'incomplete';
			$status['step']   = $incomplete;
		}

		$status += $this->state->get_step_data( 'set_id' );

		return $status;
	}

	/**
	 * Use an existing Ads account. Mark the 'set_id' step as done and sets the Ads ID.
	 *
	 * @param int $account_id The Ads account ID to use.
	 *
	 * @throws Exception If there is already an Ads account ID.
	 */
	public function use_existing_account( int $account_id ) {
		$ads_id = $this->options->get_ads_id();
		if ( $ads_id && $ads_id !== $account_id ) {
			throw new Exception(
				/* translators: 1: is a numeric account ID */
				sprintf( __( 'Ads account %1$d already connected.', 'google-listings-and-ads' ), $ads_id )
			);
		}

		$state = $this->state->get();

		// Don't do anything if this step was already finished.
		if ( AdsAccountState::STEP_DONE === $state['set_id']['status'] ) {
			return;
		}

		$this->container->get( Middleware::class )->link_ads_account( $account_id );

		// Skip billing setup flow when using an existing account.
		$state['set_id']['status']  = AdsAccountState::STEP_DONE;
		$state['billing']['status'] = AdsAccountState::STEP_DONE;
		$this->state->update( $state );
	}

	/**
	 * Performs the steps necessary to setup an ads account.
	 * Should always resume up at the last pending or unfinished step.
	 * If the Ads account has already been created, the ID is simply returned.
	 *
	 * @return array The newly created (or pre-existing) Ads ID.
	 * @throws Exception If an error occurs during any step.
	 */
	public function setup_account(): array {
		$state   = $this->state->get();
		$ads_id  = $this->options->get_ads_id();
		$account = [ 'id' => $ads_id ];

		foreach ( $state as $name => &$step ) {
			if ( AdsAccountState::STEP_DONE === $step['status'] ) {
				continue;
			}

			try {
				switch ( $name ) {
					case 'set_id':
						// Just in case, don't create another Ads ID.
						if ( ! empty( $ads_id ) ) {
							break;
						}
						$account = $this->container->get( Middleware::class )->create_ads_account();

						$step['data']['sub_account']       = true;
						$step['data']['created_timestamp'] = time();
						break;

					case 'billing':
						$this->check_billing_status( $account );
						break;

					case 'conversion_action':
						$this->create_conversion_action();
						break;

					case 'link_merchant':
						// Continue to next step if the MC account is not connected yet.
						if ( ! $this->options->get_merchant_id() ) {
							// Save step as pending and continue the foreach loop with `continue 2`.
							$state[ $name ]['status'] = AdsAccountState::STEP_PENDING;
							$this->state->update( $state );
							continue 2;
						}

						$this->link_merchant_account();
						break;

					case 'account_access':
						$this->check_ads_account_has_access();
						break;

					default:
						throw new Exception(
							/* translators: 1: is a string representing an unknown step name */
							sprintf( __( 'Unknown ads account creation step %1$s', 'google-listings-and-ads' ), $name )
						);
				}
				$step['status']  = AdsAccountState::STEP_DONE;
				$step['message'] = '';
				$this->state->update( $state );
			} catch ( Exception $e ) {
				$step['status']  = AdsAccountState::STEP_ERROR;
				$step['message'] = $e->getMessage();
				$this->state->update( $state );
				throw $e;
			}
		}

		return $account;
	}

	/**
	 * Gets the billing setup status and returns a setup URL if available.
	 *
	 * @return array
	 */
	public function get_billing_status(): array {
		$status = $this->container->get( Ads::class )->get_billing_status();

		if ( BillingSetupStatus::APPROVED === $status ) {
			$this->state->complete_step( 'billing' );
			return [ 'status' => $status ];
		}

		$billing_url = $this->options->get( OptionsInterface::ADS_BILLING_URL );

		// Check if user has provided the access and ocid is present.
		$connection_status = $this->container->get( Connection::class )->get_status();
		$email             = $connection_status['email'] ?? '';
		$has_access        = $this->container->get( Ads::class )->has_access( $email );
		$ocid              = $this->options->get( OptionsInterface::ADS_ACCOUNT_OCID, null );

		// Link directly to the payment page if the customer already has access.
		if ( $has_access ) {
			$billing_url = add_query_arg(
				[
					'ocid' => $ocid ?: 0,
				],
				'https://ads.google.com/aw/signup/payment'
			);
		}

		return [
			'status'      => $status,
			'billing_url' => $billing_url,
		];
	}

	/**
	 * Check if the Ads account has access.
	 *
	 * @throws ExceptionWithResponseData If the account doesn't have access.
	 */
	private function check_ads_account_has_access() {
		$access_status = $this->get_ads_account_has_access();

		if ( ! $access_status['has_access'] ) {
			throw new ExceptionWithResponseData(
				__( 'Account must be accepted before completing setup.', 'google-listings-and-ads' ),
				428,
				null,
				$access_status
			);
		}
	}

	/**
	 * Gets the Ads account access status.
	 *
	 * @return array {
	 *     Returns the access status, last completed account setup step,
	 *     and invite link if available.
	 *
	 *     @type bool   $has_access  Whether the customer has access to the account.
	 *     @type string $step        The last completed setup step for the Ads account.
	 *     @type string $invite_link The URL to the invite link.
	 * }
	 */
	public function get_ads_account_has_access() {
		$has_access = false;

		// Check if an Ads ID is present.
		if ( $this->options->get_ads_id() ) {
			$connection_status = $this->container->get( Connection::class )->get_status();
			$email             = $connection_status['email'] ?? '';
		}

		// If no email, means google account is not connected.
		if ( ! empty( $email ) ) {
			$has_access = $this->container->get( Ads::class )->has_access( $email );
		}

		// If we have access, complete the step so that it won't be called next time.
		if ( $has_access ) {
			$this->state->complete_step( 'account_access' );
		}

		return [
			'has_access'  => $has_access,
			'step'        => $this->state->last_incomplete_step(),
			'invite_link' => $this->options->get( OptionsInterface::ADS_BILLING_URL, '' ),
		];
	}

	/**
	 * Disconnect Ads account
	 */
	public function disconnect() {
		$this->options->delete( OptionsInterface::ADS_ACCOUNT_CURRENCY );
		$this->options->delete( OptionsInterface::ADS_ACCOUNT_OCID );
		$this->options->delete( OptionsInterface::ADS_ACCOUNT_STATE );
		$this->options->delete( OptionsInterface::ADS_BILLING_URL );
		$this->options->delete( OptionsInterface::ADS_CONVERSION_ACTION );
		$this->options->delete( OptionsInterface::ADS_ID );
		$this->options->delete( OptionsInterface::ADS_SETUP_COMPLETED_AT );
		$this->options->delete( OptionsInterface::CAMPAIGN_CONVERT_STATUS );
		$this->container->get( TransientsInterface::class )->delete( TransientsInterface::ADS_CAMPAIGN_COUNT );
	}

	/**
	 * Confirm the billing flow has been completed.
	 *
	 * @param array $account Account details.
	 *
	 * @throws ExceptionWithResponseData If this step hasn't been completed yet.
	 */
	private function check_billing_status( array $account ) {
		$status = BillingSetupStatus::UNKNOWN;

		// Only check billing status if we haven't just created the account.
		if ( empty( $account['billing_url'] ) ) {
			$status = $this->container->get( Ads::class )->get_billing_status();
		}

		if ( BillingSetupStatus::APPROVED !== $status ) {
			throw new ExceptionWithResponseData(
				__( 'Billing setup must be completed.', 'google-listings-and-ads' ),
				428,
				null,
				[
					'billing_url'    => $this->options->get( OptionsInterface::ADS_BILLING_URL ),
					'billing_status' => $status,
				]
			);
		}
	}

	/**
	 * Get the callback function for linking a merchant account.
	 *
	 * @throws Exception When the ads account hasn't been set yet.
	 */
	private function link_merchant_account() {
		if ( ! $this->options->get_ads_id() ) {
			throw new Exception( 'An Ads account must be connected' );
		}

		$mc_state = $this->container->get( MerchantAccountState::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() );
		}

		$mc_state->complete_step( 'link_ads' );
	}

	/**
	 * Create the generic GLA conversion action and store the details as an option.
	 *
	 * @throws Exception If the conversion action can't be created.
	 */
	private function create_conversion_action(): void {
		$action = $this->container->get( AdsConversionAction::class )->create_conversion_action();
		$this->options->update( OptionsInterface::ADS_CONVERSION_ACTION, $action );
	}
}
AdsAwareInterface.php000064400000000563151542565630010605 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;

defined( 'ABSPATH' ) || exit;

/**
 * Interface AdsAwareInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
 */
interface AdsAwareInterface {

	/**
	 * @param AdsService $ads_service
	 */
	public function set_ads_object( AdsService $ads_service ): void;
}
AdsAwareTrait.php000064400000000743151542565630007770 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;

defined( 'ABSPATH' ) || exit;

/**
 * Trait AdsAwareTrait
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
 */
trait AdsAwareTrait {

	/**
	 * The AdsService object.
	 *
	 * @var AdsService
	 */
	protected $ads_service;

	/**
	 * @param AdsService $ads_service
	 */
	public function set_ads_object( AdsService $ads_service ): void {
		$this->ads_service = $ads_service;
	}
}
AdsService.php000064400000003740151542565630007325 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\AdsAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Class AdsService
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
 */
class AdsService implements OptionsAwareInterface, Service {

	use OptionsAwareTrait;

	/** @var AdsAccountState */
	protected $account_state;

	/**
	 * AdsService constructor.
	 *
	 * @since 1.11.0
	 *
	 * @param AdsAccountState $account_state
	 */
	public function __construct( AdsAccountState $account_state ) {
		$this->account_state = $account_state;
	}

	/**
	 * Determine whether Ads setup has been started.
	 *
	 * @since 1.11.0
	 * @return bool
	 */
	public function is_setup_started(): bool {
		return $this->account_state->last_incomplete_step() !== '' && ! $this->is_setup_complete();
	}

	/**
	 * Determine whether Ads setup has completed.
	 *
	 * @return bool
	 */
	public function is_setup_complete(): bool {
		return boolval( $this->options->get( OptionsInterface::ADS_SETUP_COMPLETED_AT, false ) );
	}

	/**
	 * Determine whether Ads has connected.
	 *
	 * @return bool
	 */
	public function is_connected(): bool {
		$google_connected = boolval( $this->options->get( OptionsInterface::GOOGLE_CONNECTED, false ) );
		return $google_connected && $this->is_setup_complete();
	}

	/**
	 * Determine whether the Ads account is connected, even when pending billing.
	 *
	 * @return bool
	 */
	public function connected_account(): bool {
		$id        = $this->options->get_ads_id();
		$last_step = $this->account_state->last_incomplete_step();
		return $id && ( $last_step === '' || $last_step === 'billing' );
	}
}
AssetSuggestionsService.php000064400000057466151542565630012146 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\ArrayUtil;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\ImageUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\DimensionUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsAssetGroupAsset;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType;
use Exception;
use WP_Query;
use wpdb;
use DOMDocument;

/**
 * Class AssetSuggestionsService
 *
 * Suggest assets and possible final URLs.
 *
 * @since 2.4.0
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
 */
class AssetSuggestionsService implements Service {

	/**
	 * WP Proxy
	 *
	 * @var WP
	 */
	protected WP $wp;

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

	/**
	 * Image utilities.
	 *
	 * @var ImageUtility
	 */
	protected ImageUtility $image_utility;

	/**
	 * The AdsAssetGroupAsset class.
	 *
	 * @var AdsAssetGroupAsset
	 */
	protected $asset_group_asset;

	/**
	 * WordPress database access abstraction class.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Image requirements.
	 */
	protected const IMAGE_REQUIREMENTS = [
		self::MARKETING_IMAGE_KEY          => [
			'minimum'     => [ 600, 314 ],
			'recommended' => [ 1200, 628 ],
			'max_qty'     => 8,
		],
		self::SQUARE_MARKETING_IMAGE_KEY   => [
			'minimum'     => [ 300, 300 ],
			'recommended' => [ 1200, 1200 ],
			'max_qty'     => 8,
		],
		self::PORTRAIT_MARKETING_IMAGE_KEY => [
			'minimum'     => [ 480, 600 ],
			'recommended' => [ 960, 1200 ],
			'max_qty'     => 4,
		],
		self::LOGO_IMAGE_KEY               => [
			'minimum'     => [ 128, 128 ],
			'recommended' => [ 1200, 1200 ],
			'max_qty'     => 20,
		],
	];

	/**
	 * Default maximum marketing images.
	 */
	protected const DEFAULT_MAXIMUM_MARKETING_IMAGES = 20;

	/**
	 * The subsize key for the square marketing image.
	 */
	protected const SQUARE_MARKETING_IMAGE_KEY = 'gla_square_marketing_asset';
	/**
	 * The subsize key for the marketing image.
	 */
	protected const MARKETING_IMAGE_KEY = 'gla_marketing_asset';

	/**
	 * The subsize key for the portrait marketing image.
	 */
	protected const PORTRAIT_MARKETING_IMAGE_KEY = 'gla_portrait_marketing_asset';
	/**
	 * The subsize key for the logo image.
	 */
	protected const LOGO_IMAGE_KEY = 'gla_logo_asset';

	/**
	 * The homepage key ID.
	 */
	protected const HOMEPAGE_KEY_ID = 0;

	/**
	 * AssetSuggestionsService constructor.
	 *
	 * @param WP                 $wp WP Proxy.
	 * @param WC                 $wc WC Proxy.
	 * @param ImageUtility       $image_utility Image utility.
	 * @param wpdb               $wpdb WordPress database access abstraction class.
	 * @param AdsAssetGroupAsset $asset_group_asset The AdsAssetGroupAsset class.
	 */
	public function __construct( WP $wp, WC $wc, ImageUtility $image_utility, wpdb $wpdb, AdsAssetGroupAsset $asset_group_asset ) {
		$this->wp                = $wp;
		$this->wc                = $wc;
		$this->wpdb              = $wpdb;
		$this->image_utility     = $image_utility;
		$this->asset_group_asset = $asset_group_asset;
	}

	/**
	 * Get WP and other campaigns' assets from the specific post or term.
	 *
	 * @param int    $id Post ID, Term ID or self::HOMEPAGE_KEY_ID if it's the homepage.
	 * @param string $type Only possible values are post, term and homepage.
	 */
	public function get_assets_suggestions( int $id, string $type ): array {
		$asset_group_assets = $this->get_asset_group_asset_suggestions( $id, $type );

		if ( ! empty( $asset_group_assets ) ) {
			return $asset_group_assets;
		}

		return $this->get_wp_assets( $id, $type );
	}

	/**
	 * Get URL for a specific post or term.
	 *
	 * @param int    $id Post ID, Term ID or self::HOMEPAGE_KEY_ID
	 * @param string $type Only possible values are post, term and homepage.
	 *
	 * @return string The URL.
	 * @throws Exception If the ID is invalid.
	 */
	protected function get_url( int $id, string $type ): string {
		if ( $type === 'post' ) {
			$url = get_permalink( $id );
		} elseif ( $type === 'term' ) {
			$url = get_term_link( $id );
		} else {
			$url = get_bloginfo( 'url' );
		}

		if ( is_wp_error( $url ) || empty( $url ) ) {
			throw new Exception(
				/* translators: 1: is an integer representing an unknown Term ID */
				sprintf( __( 'Invalid Term ID or Post ID or site url %1$d', 'google-listings-and-ads' ), $id )
			);
		}

		return $url;
	}


	/**
	 * Get other campaigns' assets from the specific url.
	 *
	 * @param int    $id Post or Term ID.
	 * @param string $type Only possible values are post or term.
	 */
	protected function get_asset_group_asset_suggestions( int $id, string $type ): array {
		$final_url = $this->get_url( $id, $type );

		// Suggest the assets from the first asset group if exists.
		$asset_group_assets = $this->asset_group_asset->get_assets_by_final_url( $final_url, true );

		if ( empty( $asset_group_assets ) ) {
			return [];
		}

		return array_merge( $this->get_suggestions_common_fields( [] ), [ 'final_url' => $final_url ], $asset_group_assets );
	}

	/**
	 * Get assets from specific post or term.
	 *
	 * @param int    $id Post or Term ID, or self::HOMEPAGE_KEY_ID.
	 * @param string $type Only possible values are post or term.
	 *
	 * @return array All assets available for specific term, post or homepage.
	 * @throws Exception If the ID is invalid.
	 */
	protected function get_wp_assets( int $id, string $type ): array {
		if ( $type === 'post' ) {
			return $this->get_post_assets( $id );
		} elseif ( $type === 'term' ) {
			return $this->get_term_assets( $id );
		} else {
			return $this->get_homepage_assets();
		}
	}

	/**
	 * Get assets from the homepage.
	 *
	 * @return array Assets available for the homepage.
	 * @throws Exception If the homepage id is invalid.
	 */
	protected function get_homepage_assets(): array {
		$home_page = $this->wp->get_static_homepage();

		// Static homepage.
		if ( $home_page ) {
			return $this->get_post_assets( $home_page->ID );
		}

		// Get images from the latest posts.
		$posts               = $this->wp->get_posts( [] );
		$inserted_images_ids = array_map( [ $this, 'get_html_inserted_images' ], array_column( $posts, 'post_content' ) );
		$ids                 = array_merge( $this->get_post_image_attachments( [ 'post_parent__in' => array_column( $posts, 'ID' ) ] ), ...$inserted_images_ids );
		$marketing_images    = $this->get_url_attachments_by_ids( $ids );

		// Non static homepage.
		return array_merge(
			[
				AssetFieldType::HEADLINE      => [ __( 'Homepage', 'google-listings-and-ads' ) ],
				AssetFieldType::LONG_HEADLINE => [ get_bloginfo( 'name' ) . ': ' . __( 'Homepage', 'google-listings-and-ads' ) ],
				AssetFieldType::DESCRIPTION   => ArrayUtil::remove_empty_values( [ __( 'Homepage', 'google-listings-and-ads' ), get_bloginfo( 'description' ) ] ),
				'display_url_path'            => [],
				'final_url'                   => get_bloginfo( 'url' ),
			],
			$this->get_suggestions_common_fields( $marketing_images )
		);
	}

	/**
	 * Get assets from specific post.
	 *
	 * @param int $id Post ID.
	 *
	 * @return array All assets for specific post.
	 * @throws Exception If the Post ID is invalid.
	 */
	protected function get_post_assets( int $id ): array {
		$post = get_post( $id );

		if ( ! $post || $post->post_status === 'trash' ) {
			throw new Exception(
				/* translators: 1: is an integer representing an unknown Post ID */
				sprintf( __( 'Invalid Post ID %1$d', 'google-listings-and-ads' ), $id )
			);
		}

		$attachments_ids = $this->get_post_image_attachments(
			[
				'post_parent' => $id,
			]
		);

		if ( $id === wc_get_page_id( 'shop' ) ) {
			$attachments_ids = [ ...$attachments_ids, ...$this->get_shop_attachments() ];
		}

		if ( $post->post_type === 'product' || $post->post_type === 'product_variation' ) {
			$product         = $this->wc->maybe_get_product( $id );
			$attachments_ids = [ ...$attachments_ids, ...$product->get_gallery_image_ids() ];
		}

		$attachments_ids  = [ ...$attachments_ids, ...$this->get_gallery_images_ids( $id ), ...$this->get_html_inserted_images( $post->post_content ), get_post_thumbnail_id( $id ) ];
		$marketing_images = $this->get_url_attachments_by_ids( $attachments_ids );
		$long_headline    = get_bloginfo( 'name' ) . ': ' . $post->post_title;

		return array_merge(
			[
				AssetFieldType::HEADLINE      => [ $post->post_title ],
				AssetFieldType::LONG_HEADLINE => [ $long_headline ],
				AssetFieldType::DESCRIPTION   => ArrayUtil::remove_empty_values( [ $post->post_excerpt, get_bloginfo( 'description' ) ] ),
				'display_url_path'            => [ $post->post_name ],
				'final_url'                   => get_permalink( $id ),
			],
			$this->get_suggestions_common_fields( $marketing_images )
		);
	}

	/**
	 * Get assets from specific term.
	 *
	 * @param int $id Term ID.
	 *
	 * @return array All assets for specific term.
	 * @throws Exception If the Term ID is invalid.
	 */
	protected function get_term_assets( int $id ): array {
		$term = get_term( $id );

		if ( ! $term ) {
			throw new Exception(
				/* translators: 1: is an integer representing an unknown Term ID */
				sprintf( __( 'Invalid Term ID %1$d', 'google-listings-and-ads' ), $id )
			);
		}

		$posts_assigned_to_term     = $this->get_posts_assigned_to_a_term( $term->term_id, $term->taxonomy );
		$posts_ids_assigned_to_term = [];
		$attachments_ids            = [];

		foreach ( $posts_assigned_to_term as $post ) {
			$attachments_ids[]            = get_post_thumbnail_id( $post->ID );
			$posts_ids_assigned_to_term[] = $post->ID;
		}

		if ( count( $posts_assigned_to_term ) ) {
			$attachments_ids = [ ...$this->get_post_image_attachments( [ 'post_parent__in' => $posts_ids_assigned_to_term ] ), ...$attachments_ids ];
		}

		$marketing_images = $this->get_url_attachments_by_ids( $attachments_ids );

		return array_merge(
			[
				AssetFieldType::HEADLINE      => [ $term->name ],
				AssetFieldType::LONG_HEADLINE => [ get_bloginfo( 'name' ) . ': ' . $term->name ],
				AssetFieldType::DESCRIPTION   => ArrayUtil::remove_empty_values( [ wp_strip_all_tags( $term->description ), get_bloginfo( 'description' ) ] ),
				'display_url_path'            => [ $term->slug ],
				'final_url'                   => get_term_link( $term->term_id ),
			],
			$this->get_suggestions_common_fields( $marketing_images )
		);
	}

	/**
	 * Get inserted images from HTML.
	 *
	 * @param string $html HTML string.
	 *
	 * @return array Array of image IDs.
	 */
	protected function get_html_inserted_images( string $html ): array {
		if ( empty( $html ) ) {
			return [];
		}

		// Malformed HTML can cause DOMDocument to throw warnings. With the below line, we can suppress them and work only with the HTML that has been parsed.
		libxml_use_internal_errors( true );

		$dom = new DOMDocument();
		if ( $dom->loadHTML( $html ) ) {
			$images     = $dom->getElementsByTagName( 'img' );
			$images_ids = [];
			$pattern    = '/-\d+x\d+\.(jpg|jpeg|png)$/i';
			foreach ( $images as $image ) {
				$url_unscaled = preg_replace(
					$pattern,
					'.${1}',
					$image->getAttribute( 'src' ),
				);

				$image_id = attachment_url_to_postid( $url_unscaled );

				// Look for scaled image if the original image is not found.
				if ( $image_id === 0 ) {
					$url_scaled = preg_replace(
						$pattern,
						'-scaled.${1}',
						$image->getAttribute( 'src' ),
					);
					$image_id   = attachment_url_to_postid( $url_scaled );
				}

				if ( $image_id > 0 ) {
					$images_ids[] = $image_id;
				}
			}
		}

		return $images_ids;
	}

	/**
	 * Get logo images urls.
	 *
	 * @return array Logo images urls.
	 */
	protected function get_logo_images(): array {
		$logo_images = $this->get_url_attachments_by_ids( [ get_theme_mod( 'custom_logo' ) ], [ self::LOGO_IMAGE_KEY ] );
		return $logo_images[ self::LOGO_IMAGE_KEY ] ?? [];
	}

	/**
	 * Get posts linked to a specific term.
	 *
	 * @param int    $term_id Term ID.
	 * @param string $taxonomy_name Taxonomy name.
	 *
	 * @return array List of posts assigned to the term.
	 */
	protected function get_posts_assigned_to_a_term( int $term_id, string $taxonomy_name ): array {
		$args = [
			'post_type'   => 'any',
			'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES,
			'tax_query'   => [
				[
					'taxonomy'         => $taxonomy_name,
					'terms'            => $term_id,
					'field'            => 'term_id',
					'include_children' => false,
				],
			],
		];

		return $this->wp->get_posts( $args );
	}

	/**
	 * Get attachments related to the shop page.
	 *
	 * @return array Shop attachments.
	 */
	protected function get_shop_attachments(): array {
		return $this->get_post_image_attachments(
			[
				'post_parent__in' => $this->get_shop_products(),
			]
		);
	}

	/**
	 *
	 * Get products that will be use to offer image assets.
	 *
	 * @param array $args See WP_Query::parse_query() for all available arguments.
	 * @return array Shop products.
	 */
	protected function get_shop_products( array $args = [] ): array {
		$defaults = [
			'post_type'   => 'product',
			'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES,
			'fields'      => 'ids',
		];

		$args = wp_parse_args( $args, $defaults );

		return $this->wp->get_posts( $args );
	}

	/**
	 * Get gallery images ids.
	 *
	 * @param int $post_id Post ID that contains the gallery.
	 *
	 * @return array List of gallery images ids.
	 */
	protected function get_gallery_images_ids( int $post_id ): array {
		$gallery = get_post_gallery( $post_id, false );

		if ( ! $gallery || ! isset( $gallery['ids'] ) ) {
			return [];
		}

		return explode( ',', $gallery['ids'] );
	}

	/**
	 * Get unique attachments ids converted to int values.
	 *
	 * @param array $ids Attachments ids.
	 * @param int   $maximum_images Maximum number of images to return.
	 *
	 * @return array List of unique attachments ids converted to int values.
	 */
	protected function prepare_image_ids( array $ids, int $maximum_images = self::DEFAULT_MAXIMUM_MARKETING_IMAGES ): array {
		$ids = array_unique( ArrayUtil::remove_empty_values( $ids ) );
		$ids = array_map( 'intval', $ids );
		return array_slice( $ids, 0, $maximum_images );
	}

	/**
	 * Get URL for each attachment using an array of attachment ids and a list of subsizes.
	 *
	 * @param array $ids Attachments ids.
	 * @param array $size_keys Image subsize keys.
	 * @param int   $maximum_images Maximum number of images to return.
	 *
	 * @return array A list of attachments urls.
	 */
	protected function get_url_attachments_by_ids( array $ids, array $size_keys = [ self::SQUARE_MARKETING_IMAGE_KEY, self::MARKETING_IMAGE_KEY, self::PORTRAIT_MARKETING_IMAGE_KEY ], $maximum_images = self::DEFAULT_MAXIMUM_MARKETING_IMAGES ): array {
		$ids = $this->prepare_image_ids( $ids, $maximum_images );

		$marketing_images = [];

		foreach ( $ids as $id ) {

			$metadata = wp_get_attachment_metadata( $id );

			if ( ! $metadata ) {
				continue;
			}

			foreach ( $size_keys as $size_key ) {
				if ( count( $marketing_images[ $size_key ] ?? [] ) >= self::IMAGE_REQUIREMENTS[ $size_key ]['max_qty'] ) {
					continue;
				}

				$minimum_size     = new DimensionUtility( ...self::IMAGE_REQUIREMENTS[ $size_key ]['minimum'] );
				$recommended_size = new DimensionUtility( ...self::IMAGE_REQUIREMENTS[ $size_key ]['recommended'] );
				$image_size       = new DimensionUtility( $metadata['width'], $metadata['height'] );
				$suggested_size   = $this->image_utility->recommend_size( $image_size, $recommended_size, $minimum_size );

				// If the original size matches the suggested size with a precision of +-1px.
				if ( $suggested_size && $suggested_size->equals( $image_size ) ) {
					$marketing_images[ $size_key ][] = wp_get_attachment_url( $id );
				} elseif ( isset( $metadata['sizes'][ $size_key ] ) ) {
					// use the sub size.
					$marketing_images[ $size_key ][] = wp_get_attachment_image_url( $id, $size_key );
				} elseif ( $suggested_size && $this->image_utility->maybe_add_subsize_image( $id, $size_key, $suggested_size ) ) {
					// use the resized image.
					$marketing_images[ $size_key ][] = wp_get_attachment_image_url( $id, $size_key );
				}
			}
		}

		return $marketing_images;
	}


	/**
	 * Get Attachmets for specific posts.
	 *
	 * @param array $args See WP_Query::parse_query() for all available arguments.
	 *
	 * @return array List of attachments
	 */
	protected function get_post_image_attachments( array $args = [] ): array {
		$defaults = [
			'post_type'      => 'attachment',
			'post_mime_type' => [ 'image/jpeg', 'image/png', 'image/jpg' ],
			'fields'         => 'ids',
			'numberposts'    => self::DEFAULT_MAXIMUM_MARKETING_IMAGES,
		];

		$args = wp_parse_args( $args, $defaults );

		return $this->wp->get_posts( $args );
	}

	/**
	 * Get posts that can be used to suggest assets
	 *
	 * @param string $search The search query.
	 * @param int    $per_page Number of items per page.
	 * @param int    $offset Used in the get_posts query.
	 *
	 * @return array formatted post suggestions
	 */
	protected function get_post_suggestions( string $search, int $per_page, int $offset = 0 ): array {
		if ( $per_page <= 0 ) {
			return [];
		}

		$post_suggestions    = [];
		$excluded_post_types = [ 'attachment' ];

		$post_types = $this->wp->get_post_types(
			[
				'exclude_from_search' => false,
				'public'              => true,
			]
		);

		// Exclude attachment post_type
		$filtered_post_types = array_diff( $post_types, $excluded_post_types );

		$args = [
			'post_type'        => $filtered_post_types,
			'posts_per_page'   => $per_page,
			'post_status'      => 'publish',
			'search_title'     => $search,
			'offset'           => $offset,
			'suppress_filters' => false,
		];

		add_filter( 'posts_where', [ $this, 'title_filter' ], 10, 2 );

		$posts = $this->wp->get_posts( $args );

		remove_filter( 'posts_where', [ $this, 'title_filter' ] );

		foreach ( $posts as $post ) {
			$post_suggestions[] = $this->format_final_url_response( $post->ID, 'post', $post->post_title, get_permalink( $post->ID ) );
		}

		return $post_suggestions;
	}

	/**
	 * Filter for the posts_where hook, adds WHERE clause to search
	 * for the 'search_title' parameter in the post titles (when present).
	 *
	 * @param string   $where The WHERE clause of the query.
	 * @param WP_Query $wp_query The WP_Query instance (passed by reference).
	 *
	 * @return string The updated WHERE clause.
	 */
	public function title_filter( string $where, WP_Query $wp_query ): string {
		$search_title = $wp_query->get( 'search_title' );
		if ( $search_title ) {
			$title_search = '%' . $this->wpdb->esc_like( $search_title ) . '%';
			$where       .= $this->wpdb->prepare( " AND `{$this->wpdb->posts}`.`post_title` LIKE %s", $title_search ); // phpcs:ignore WordPress.DB.PreparedSQL
		}
		return $where;
	}

	/**
	 * Get terms that can be used to suggest assets
	 *
	 * @param string $search The search query
	 * @param int    $per_page Number of items per page
	 *
	 * @return array formatted terms suggestions
	 */
	protected function get_terms_suggestions( string $search, int $per_page ): array {
		$terms_suggestions = [];

		// get_terms  evaluates $per_page_terms = 0 as a falsy, therefore it will not add the LIMIT clausure returning all the results.
		// See: https://github.com/WordPress/WordPress/blob/abe134c2090e84080adc46187884201a4badd649/wp-includes/class-wp-term-query.php#L868
		if ( $per_page <= 0 ) {
			return [];
		}

		// Get all taxonomies that are public, show_in_menu = true helps to exclude taxonomies such as "product_shipping_class".
		$taxonomies = $this->wp->get_taxonomies(
			[
				'public'       => true,
				'show_in_menu' => true,
			],
		);

		$terms = $this->wp->get_terms(
			[
				'taxonomy'   => $taxonomies,
				'hide_empty' => false,
				'number'     => $per_page,
				'name__like' => $search,
			]
		);

		foreach ( $terms as $term ) {
			$terms_suggestions[] = $this->format_final_url_response( $term->term_id, 'term', $term->name, get_term_link( $term->term_id, $term->taxonomy ) );
		}

		return $terms_suggestions;
	}


	/**
	 * Return a list of final urls that can be used to suggest assets.
	 *
	 * @param string $search The search query
	 * @param int    $per_page Number of items per page
	 * @param string $order_by Order by: type, title, url
	 *
	 * @return array final urls with their title, id & type.
	 */
	public function get_final_url_suggestions( string $search = '', int $per_page = 30, string $order_by = 'title' ): array {
		if ( empty( $search ) ) {
			return $this->get_defaults_final_url_suggestions();
		}

		$homepage = [];

		// If the search query contains the word "homepage" add the homepage to the results.
		if ( strpos( 'homepage', strtolower( $search ) ) !== false ) {
			$homepage[] = $this->get_homepage_final_url();
			--$per_page;
		}

		// Split possible results between posts and terms.
		$per_page_posts = (int) ceil( $per_page / 2 );

		$posts = $this->get_post_suggestions( $search, $per_page_posts );

		// Try to get more results using the terms
		$per_page_terms = $per_page - count( $posts );

		$terms = $this->get_terms_suggestions( $search, $per_page_terms );

		$pending_results = $per_page - count( $posts ) - count( $terms );
		$more_results    = [];

		// Try to get more results using posts
		if ( $pending_results > 0 && count( $posts ) === $per_page_posts ) {
			$more_results = $this->get_post_suggestions( $search, $pending_results, $per_page_posts );
		}

		$result = array_merge( $homepage, $posts, $terms, $more_results );

		return $this->sort_results( $result, $order_by );
	}

	/**
	 * Get the final url for the homepage.
	 *
	 * @return array final url for the homepage.
	 */
	protected function get_homepage_final_url(): array {
		return $this->format_final_url_response( self::HOMEPAGE_KEY_ID, 'homepage', __( 'Homepage', 'google-listings-and-ads' ), get_bloginfo( 'url' ) );
	}

	/**
	 * Get defaults final urls suggestions.
	 *
	 * @return array default final urls.
	 */
	protected function get_defaults_final_url_suggestions(): array {
		$defaults  = [ $this->get_homepage_final_url() ];
		$shop_page = $this->wp->get_shop_page();

		if ( $shop_page ) {
			$defaults[] = $this->format_final_url_response( $shop_page->ID, 'post', $shop_page->post_title, get_permalink( $shop_page->ID ) );
		}

		return $defaults;
	}

	/**
	 *  Order suggestions alphabetically
	 *
	 *  @param array  $results Results as an associative array
	 *  @param string $field   Sort by a specific field
	 *
	 * @return array response sorted alphabetically
	 */
	protected function sort_results( array $results, string $field ): array {
		usort(
			$results,
			function ( $a, $b ) use ( $field ) {
				return strcmp( strtolower( (string) $a[ $field ] ), strtolower( (string) $b[ $field ] ) );
			}
		);

		return $results;
	}

	/**
	 * Return an assotiave array with the page suggestion response format.
	 *
	 * @param int    $id post id, term id or self::HOMEPAGE_KEY_ID.
	 * @param string $type post|term
	 * @param string $title page|term title
	 * @param string $url page|term url
	 *
	 * @return array response formated.
	 */
	protected function format_final_url_response( int $id, string $type, string $title, string $url ): array {
		return [
			'id'    => $id,
			'type'  => $type,
			'title' => $title,
			'url'   => $url,
		];
	}

	/**
	 * Get the suggested common fieds.
	 *
	 * @param array $marketing_images Marketing images.
	 *
	 * @return array Suggested common fields.
	 */
	protected function get_suggestions_common_fields( array $marketing_images ): array {
		return [
			AssetFieldType::LOGO                     => $this->get_logo_images(),
			AssetFieldType::BUSINESS_NAME            => get_bloginfo( 'name' ),
			AssetFieldType::SQUARE_MARKETING_IMAGE   => $marketing_images[ self::SQUARE_MARKETING_IMAGE_KEY ] ?? [],
			AssetFieldType::MARKETING_IMAGE          => $marketing_images [ self::MARKETING_IMAGE_KEY ] ?? [],
			AssetFieldType::PORTRAIT_MARKETING_IMAGE => $marketing_images [ self::PORTRAIT_MARKETING_IMAGE_KEY ] ?? [],
			AssetFieldType::CALL_TO_ACTION_SELECTION => null,
		];
	}
}
GoogleAdsClient.php000064400000002660151544577520010303 0ustar00<?php
declare( strict_types=1 );

/**
 * Overrides vendor/googleads/google-ads-php/src/Google/Ads/GoogleAds/Lib/V18/GoogleAdsClient.php
 *
 * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
 * phpcs:disable WordPress.NamingConventions.ValidVariableName
 */

namespace Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Auth\Credentials\InsecureCredentials;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Auth\HttpHandler\HttpHandlerFactory;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\GuzzleHttp\Client;

/**
 * A Google Ads API client for handling common configuration and OAuth2 settings.
 */
class GoogleAdsClient {
	use ServiceClientFactoryTrait;

	/** @var Client $httpClient */
	private $httpClient = null;

	/**
	 * GoogleAdsClient constructor
	 *
	 * @param string $endpoint Endpoint URL to send requests to.
	 */
	public function __construct( string $endpoint ) {
		$this->oAuth2Credential = new InsecureCredentials();
		$this->endpoint         = $endpoint;
	}

	/**
	 * Set a guzzle client to use for requests.
	 *
	 * @param Client $client Guzzle client.
	 */
	public function setHttpClient( Client $client ) {
		$this->httpClient = $client;
	}

	/**
	 * Build a HTTP Handler to handle the requests.
	 */
	protected function buildHttpHandler() {
		return [ HttpHandlerFactory::build( $this->httpClient ), 'async' ];
	}
}
ServiceClientFactoryTrait.php000064400000015464151544577520012401 0ustar00<?php
declare( strict_types=1 );

/**
 * Overrides vendor/googleads/google-ads-php/src/Google/Ads/GoogleAds/Lib/V18/ServiceClientFactoryTrait.php
 *
 * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
 * phpcs:disable WordPress.NamingConventions.ValidVariableName
 * phpcs:disable Squiz.Commenting.VariableComment
 */

namespace Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads;

use Google\Ads\GoogleAds\Constants;
use Google\Ads\GoogleAds\Lib\ConfigurationTrait;
use Google\Ads\GoogleAds\V18\Services\Client\AccountLinkServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupAdLabelServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupAdServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupCriterionServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AssetGroupListingGroupFilterServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AssetGroupServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\BillingSetupServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CampaignBudgetServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CampaignCriterionServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CampaignServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\ConversionActionServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CustomerServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CustomerUserAccessServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\GeoTargetConstantServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\GoogleAdsServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\ProductLinkInvitationServiceClient;

/**
 * Contains service client factory methods.
 */
trait ServiceClientFactoryTrait {
	use ConfigurationTrait;

	private static $CREDENTIALS_LOADER_KEY  = 'credentials';
	private static $DEVELOPER_TOKEN_KEY     = 'developer-token';
	private static $LOGIN_CUSTOMER_ID_KEY   = 'login-customer-id';
	private static $LINKED_CUSTOMER_ID_KEY  = 'linked-customer-id';
	private static $SERVICE_ADDRESS_KEY     = 'serviceAddress';
	private static $DEFAULT_SERVICE_ADDRESS = 'googleads.googleapis.com';
	private static $TRANSPORT_KEY           = 'transport';

	/**
	 * Gets the Google Ads client options for making API calls.
	 *
	 * @return array the client options
	 */
	public function getGoogleAdsClientOptions(): array {
		$clientOptions = [
			self::$CREDENTIALS_LOADER_KEY => $this->getOAuth2Credential(),
			self::$DEVELOPER_TOKEN_KEY    => '',
			self::$TRANSPORT_KEY          => 'rest',
			'libName'                     => Constants::LIBRARY_NAME,
			'libVersion'                  => Constants::LIBRARY_VERSION,
		];

		if ( ! empty( $this->getEndpoint() ) ) {
			$clientOptions += [ self::$SERVICE_ADDRESS_KEY => $this->getEndpoint() ];
		}

		if ( isset( $this->httpClient ) ) {
			$clientOptions['transportConfig'] = [
				'rest' => [
					'httpHandler' => $this->buildHttpHandler(),
				],
			];
		}

		return $clientOptions;
	}


	/**
	 * @return AccountLinkServiceClient
	 */
	public function getAccountLinkServiceClient(): AccountLinkServiceClient {
		return new AccountLinkServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return AdGroupAdLabelServiceClient
	 */
	public function getAdGroupAdLabelServiceClient(): AdGroupAdLabelServiceClient {
		return new AdGroupAdLabelServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return AdGroupAdServiceClient
	 */
	public function getAdGroupAdServiceClient(): AdGroupAdServiceClient {
		return new AdGroupAdServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return AdGroupCriterionServiceClient
	 */
	public function getAdGroupCriterionServiceClient(): AdGroupCriterionServiceClient {
		return new AdGroupCriterionServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return AdGroupServiceClient
	 */
	public function getAdGroupServiceClient(): AdGroupServiceClient {
		return new AdGroupServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return AdServiceClient
	 */
	public function getAdServiceClient(): AdServiceClient {
		return new AdServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return AssetGroupListingGroupFilterServiceClient
	 */
	public function getAssetGroupListingGroupFilterServiceClient(): AssetGroupListingGroupFilterServiceClient {
		return new AssetGroupListingGroupFilterServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return AssetGroupServiceClient
	 */
	public function getAssetGroupServiceClient(): AssetGroupServiceClient {
		return new AssetGroupServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return BillingSetupServiceClient
	 */
	public function getBillingSetupServiceClient(): BillingSetupServiceClient {
		return new BillingSetupServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return CampaignBudgetServiceClient
	 */
	public function getCampaignBudgetServiceClient(): CampaignBudgetServiceClient {
		return new CampaignBudgetServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return CampaignCriterionServiceClient
	 */
	public function getCampaignCriterionServiceClient(): CampaignCriterionServiceClient {
		return new CampaignCriterionServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return CampaignServiceClient
	 */
	public function getCampaignServiceClient(): CampaignServiceClient {
		return new CampaignServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return ConversionActionServiceClient
	 */
	public function getConversionActionServiceClient(): ConversionActionServiceClient {
		return new ConversionActionServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return CustomerServiceClient
	 */
	public function getCustomerServiceClient(): CustomerServiceClient {
		return new CustomerServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return CustomerUserAccessServiceClient
	 */
	public function getCustomerUserAccessServiceClient(): CustomerUserAccessServiceClient {
		return new CustomerUserAccessServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return GeoTargetConstantServiceClient
	 */
	public function getGeoTargetConstantServiceClient(): GeoTargetConstantServiceClient {
		return new GeoTargetConstantServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return GoogleAdsServiceClient
	 */
	public function getGoogleAdsServiceClient(): GoogleAdsServiceClient {
		return new GoogleAdsServiceClient( $this->getGoogleAdsClientOptions() );
	}

	/**
	 * @return ProductLinkInvitationServiceClient
	 */
	public function getProductLinkInvitationServiceClient(): ProductLinkInvitationServiceClient {
		return new ProductLinkInvitationServiceClient( $this->getGoogleAdsClientOptions() );
	}
}
AccountController.php000064400000013122151556764470010740 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AccountService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
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\Ads
 */
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(
			'ads/accounts',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_accounts_callback(),
					'permission_callback' => $this->get_permission_callback(),
				],
				[
					'methods'             => TransportMethods::CREATABLE,
					'callback'            => $this->create_or_link_account_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_schema_properties(),
				],
				'schema' => $this->get_api_response_schema_callback(),
			]
		);

		$this->register_route(
			'ads/connection',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_connected_ads_account_callback(),
					'permission_callback' => $this->get_permission_callback(),
				],
				[
					'methods'             => TransportMethods::DELETABLE,
					'callback'            => $this->disconnect_ads_account_callback(),
					'permission_callback' => $this->get_permission_callback(),
				],
			]
		);

		$this->register_route(
			'ads/billing-status',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_billing_status_callback(),
					'permission_callback' => $this->get_permission_callback(),
				],
			]
		);

		$this->register_route(
			'ads/account-status',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_ads_account_has_access(),
					'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 () {
			try {
				return new Response( $this->account->get_accounts() );
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the callback function for creating or linking an account.
	 *
	 * @return callable
	 */
	protected function create_or_link_account_callback(): callable {
		return function ( Request $request ) {
			try {
				$link_id = absint( $request['id'] );
				if ( $link_id ) {
					$this->account->use_existing_account( $link_id );
				}

				$account_data = $this->account->setup_account();
				return $this->prepare_item_for_response( $account_data, $request );
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the callback function for the connected ads account.
	 *
	 * @return callable
	 */
	protected function get_connected_ads_account_callback(): callable {
		return function () {
			return $this->account->get_connected_account();
		};
	}

	/**
	 * Get the callback function for disconnecting a merchant.
	 *
	 * @return callable
	 */
	protected function disconnect_ads_account_callback(): callable {
		return function () {
			$this->account->disconnect();

			return [
				'status'  => 'success',
				'message' => __( 'Successfully disconnected.', 'google-listings-and-ads' ),
			];
		};
	}

	/**
	 * Get the callback function for retrieving the billing setup status.
	 *
	 * @return callable
	 */
	protected function get_billing_status_callback(): callable {
		return function () {
			return $this->account->get_billing_status();
		};
	}

	/**
	 * Get the callback function for retrieving the account access status for ads.
	 *
	 * @return callable
	 */
	protected function get_ads_account_has_access(): callable {
		return function () {
			try {
				return $this->account->get_ads_account_has_access();
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the item schema for the controller.
	 *
	 * @return array
	 */
	protected function get_schema_properties(): array {
		return [
			'id'          => [
				'type'              => 'number',
				'description'       => __( 'Google Ads Account ID.', 'google-listings-and-ads' ),
				'context'           => [ 'view', 'edit' ],
				'validate_callback' => 'rest_validate_request_arg',
				'required'          => false,
			],
			'billing_url' => [
				'type'        => 'string',
				'description' => __( 'Billing Flow URL.', 'google-listings-and-ads' ),
				'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 'account';
	}
}
AssetGroupController.php000064400000022143151556764500011435 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsAssetGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType;
use WP_REST_Request as Request;
use Exception;

defined( 'ABSPATH' ) || exit;

/**
 * Class for handling API requests related to the asset groups.
 * See https://developers.google.com/google-ads/api/reference/rpc/v18/AssetGroup
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
 */
class AssetGroupController extends BaseController {

	/**
	 * The AdsAssetGroup class.
	 *
	 * @var AdsAssetGroup $ads_asset_group
	 */
	protected $ads_asset_group;

	/**
	 * AssetGroupController constructor.
	 *
	 * @param RESTServer    $rest_server
	 * @param AdsAssetGroup $ads_asset_group
	 */
	public function __construct( RESTServer $rest_server, AdsAssetGroup $ads_asset_group ) {
		parent::__construct( $rest_server );
		$this->ads_asset_group = $ads_asset_group;
	}

	/**
	 * Register rest routes with WordPress.
	 */
	public function register_routes(): void {
		$this->register_route(
			'ads/campaigns/asset-groups/(?P<id>[\d]+)',
			[
				[
					'methods'             => TransportMethods::EDITABLE,
					'callback'            => $this->edit_asset_group_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->edit_asset_group_params(),
				],
			]
		);
		$this->register_route(
			'ads/campaigns/asset-groups',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_asset_groups_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_asset_group_params(),
				],
				[
					'methods'             => TransportMethods::CREATABLE,
					'callback'            => $this->create_asset_group_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_asset_group_params(),
				],
				'schema' => $this->get_api_response_schema_callback(),
			]
		);
	}

	/**
	 * Get the schema for the asset group.
	 *
	 * @return array The asset group schema.
	 */
	public function get_asset_group_fields(): array {
		return [
			'final_url' => [
				'type'        => 'string',
				'description' => __( 'Final URL.', 'google-listings-and-ads' ),
			],
			'path1'     => [
				'type'        => 'string',
				'description' => __( 'Asset Group path 1.', 'google-listings-and-ads' ),
			],
			'path2'     => [
				'type'        => 'string',
				'description' => __( 'Asset Group path 2.', 'google-listings-and-ads' ),
			],
		];
	}

	/**
	 * Get the edit asset group params params to update an asset group.
	 *
	 * @return array The edit asset group params.
	 */
	public function edit_asset_group_params(): array {
		return array_merge(
			[
				'id'     => [
					'description' => __( 'Asset Group ID.', 'google-listings-and-ads' ),
					'type'        => 'integer',
					'required'    => true,
				],
				'assets' => [
					'type'        => 'array',
					'description' => __( 'List of asset to be edited.', 'google-listings-and-ads' ),
					'items'       => $this->get_schema_asset(),
					'default'     => [],
				],
			],
			$this->get_asset_group_fields()
		);
	}

	/**
	 * Get the assets groups params.
	 *
	 * @return array
	 */
	public function get_asset_group_params(): array {
		return [
			'campaign_id' => [
				'description'       => __( 'Campaign ID.', 'google-listings-and-ads' ),
				'type'              => 'integer',
				'validate_callback' => 'rest_validate_request_arg',
				'required'          => true,
			],
		];
	}


	/**
	 * Get Asset Groups by Campaign ID.
	 *
	 * @return callable
	 */
	protected function get_asset_groups_callback(): callable {
		return function ( Request $request ) {
			try {
				$campaign_id = $request->get_param( 'campaign_id' );
				return array_map(
					function ( $item ) use ( $request ) {
						$data = $this->prepare_item_for_response( $item, $request );
						return $this->prepare_response_for_collection( $data );
					},
					$this->ads_asset_group->get_asset_groups_by_campaign_id( $campaign_id )
				);

			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Create asset group.
	 *
	 * @return callable
	 */
	public function create_asset_group_callback(): callable {
		return function ( Request $request ) {
			try {
				$asset_group_id = $this->ads_asset_group->create_asset_group( $request->get_param( 'campaign_id' ) );
				return [
					'status'  => 'success',
					'message' => __( 'Successfully created asset group.', 'google-listings-and-ads' ),
					'id'      => $asset_group_id,
				];
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Edit asset group.
	 *
	 * @return callable
	 */
	public function edit_asset_group_callback(): callable {
		return function ( Request $request ) {
			try {
				$asset_group_fields = array_intersect_key(
					$request->get_params(),
					$this->get_asset_group_fields()
				);

				if ( empty( $asset_group_fields ) && empty( $request->get_param( 'assets' ) ) ) {
					throw new Exception( __( 'No asset group fields to update.', 'google-listings-and-ads' ) );
				}

				$asset_group_id = $this->ads_asset_group->edit_asset_group( $request->get_param( 'id' ), $asset_group_fields, $request->get_param( 'assets' ) );
				return [
					'status'  => 'success',
					'message' => __( 'Successfully edited asset group.', 'google-listings-and-ads' ),
					'id'      => $asset_group_id,
				];
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the item schema for the controller.
	 *
	 * @return array
	 */
	protected function get_schema_properties(): array {
		return [
			'id'               => [
				'type'        => 'number',
				'description' => __( 'Asset Group ID', 'google-listings-and-ads' ),
				'context'     => [ 'view' ],
			],
			'final_url'        => [
				'type'        => 'string',
				'description' => __( 'Final URL', 'google-listings-and-ads' ),
				'context'     => [ 'view' ],

			],
			'display_url_path' => [
				'type'        => 'array',
				'description' => __( 'Text that may appear appended to the url displayed in the ad.', 'google-listings-and-ads' ),
				'context'     => [ 'view' ],
			],
			'assets'           => [
				'type'        => 'array',
				'description' => __( 'Asset is a part of an ad which can be shared across multiple ads. It can be an image, headlines, descriptions, etc.', 'google-listings-and-ads' ),
				'items'       => [
					'type'       => 'object',
					'properties' => [
						AssetFieldType::SQUARE_MARKETING_IMAGE => $this->get_schema_field_type_asset(),
						AssetFieldType::MARKETING_IMAGE => $this->get_schema_field_type_asset(),
						AssetFieldType::PORTRAIT_MARKETING_IMAGE => $this->get_schema_field_type_asset(),
						AssetFieldType::LOGO            => $this->get_schema_field_type_asset(),
						AssetFieldType::BUSINESS_NAME   => $this->get_schema_field_type_asset(),
						AssetFieldType::HEADLINE        => $this->get_schema_field_type_asset(),
						AssetFieldType::DESCRIPTION     => $this->get_schema_field_type_asset(),
						AssetFieldType::LONG_HEADLINE   => $this->get_schema_field_type_asset(),
						AssetFieldType::CALL_TO_ACTION_SELECTION => $this->get_schema_field_type_asset(),
					],
				],
			],

		];
	}

	/**
	 * Get the item schema for the field type asset.
	 *
	 * @return array the field type asset schema.
	 */
	protected function get_schema_field_type_asset(): array {
		return [
			'type'     => 'array',
			'items'    => $this->get_schema_asset(),
			'required' => false,
		];
	}

	/**
	 * Get the item schema for the asset.
	 *
	 * @return array
	 */
	protected function get_schema_asset() {
		return [
			'type'       => 'object',
			'properties' => [
				'id'         => [
					'type'        => [ 'integer', 'null' ],
					'description' => __( 'Asset ID', 'google-listings-and-ads' ),
				],
				'content'    => [
					'type'        => [ 'string', 'null' ],
					'description' => __( 'Asset content', 'google-listings-and-ads' ),
				],
				'field_type' => [
					'type'        => 'string',
					'description' => __( 'Asset field type', 'google-listings-and-ads' ),
					'required'    => true,
					'context'     => [ 'edit' ],
					'enum'        => [
						AssetFieldType::HEADLINE,
						AssetFieldType::LONG_HEADLINE,
						AssetFieldType::DESCRIPTION,
						AssetFieldType::BUSINESS_NAME,
						AssetFieldType::MARKETING_IMAGE,
						AssetFieldType::SQUARE_MARKETING_IMAGE,
						AssetFieldType::LOGO,
						AssetFieldType::CALL_TO_ACTION_SELECTION,
						AssetFieldType::PORTRAIT_MARKETING_IMAGE,
					],
				],
			],
		];
	}

	/**
	 * Get the item schema name for the controller.
	 *
	 * Used for building the API response schema.
	 *
	 * @return string
	 */
	protected function get_schema_title(): string {
		return 'asset-group';
	}
}
AssetSuggestionsController.php000064400000014135151556764500012655 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AssetSuggestionsService;
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 AssetSuggestionsController
 *
 * @since 2.4.0
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
 */
class AssetSuggestionsController extends BaseController {

	/**
	 * Service used to populate ads suggestions data.
	 *
	 * @var AssetSuggestionsService
	 */
	protected $asset_suggestions_service;

	/**
	 * AssetSuggestionsController constructor.
	 *
	 * @param RESTServer              $server
	 * @param AssetSuggestionsService $asset_suggestions
	 */
	public function __construct( RESTServer $server, AssetSuggestionsService $asset_suggestions ) {
		parent::__construct( $server );
		$this->asset_suggestions_service = $asset_suggestions;
	}

	/**
	 * Register rest routes with WordPress.
	 */
	public function register_routes(): void {
		$this->register_route(
			'assets/suggestions',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_assets_suggestions_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_assets_suggestions_params(),
				],
			]
		);
		$this->register_route(
			'assets/final-url/suggestions',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_final_url_suggestions_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_collection_params(),
				],
				'schema' => $this->get_api_response_schema_callback(),
			]
		);
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params(): array {
		return [
			'search'   => [
				'description'       => __( 'Search for post title or term name', 'google-listings-and-ads' ),
				'type'              => 'string',
				'default'           => '',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => 'rest_validate_request_arg',
			],
			'per_page' => [
				'description'       => __( 'The number of items to be return', 'google-listings-and-ads' ),
				'type'              => 'number',
				'default'           => 30,
				'sanitize_callback' => 'absint',
				'minimum'           => 1,
				'validate_callback' => 'rest_validate_request_arg',
			],
			'order_by' => [
				'description'       => __( 'Sort retrieved items by parameter', 'google-listings-and-ads' ),
				'type'              => 'string',
				'default'           => 'title',
				'sanitize_callback' => 'sanitize_text_field',
				'enum'              => [ 'type', 'title', 'url' ],
				'validate_callback' => 'rest_validate_request_arg',
			],
		];
	}

	/**
	 * Get the assets suggestions params.
	 *
	 * @return array
	 */
	public function get_assets_suggestions_params(): array {
		return [
			'id'   => [
				'description'       => __( 'Post ID or Term ID.', 'google-listings-and-ads' ),
				'type'              => 'number',
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
				'required'          => true,
			],
			'type' => [
				'description'       => __( 'Type linked to the id.', 'google-listings-and-ads' ),
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_text_field',
				'enum'              => [ 'post', 'term', 'homepage' ],
				'validate_callback' => 'rest_validate_request_arg',
				'required'          => true,
			],
		];
	}

	/**
	 * Get the callback function for the assets suggestions request.
	 *
	 * @return callable
	 */
	protected function get_assets_suggestions_callback(): callable {
		return function ( Request $request ) {
			try {
				$id   = $request->get_param( 'id' );
				$type = $request->get_param( 'type' );
				return $this->asset_suggestions_service->get_assets_suggestions( $id, $type );
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the callback function for the list of final-url suggestions request.
	 *
	 * @return callable
	 */
	protected function get_final_url_suggestions_callback(): callable {
		return function ( Request $request ) {
			$search   = $request->get_param( 'search' );
			$per_page = $request->get_param( 'per_page' );
			$order_by = $request->get_param( 'order_by' );
			return array_map(
				function ( $item ) use ( $request ) {
					$data = $this->prepare_item_for_response( $item, $request );
					return $this->prepare_response_for_collection( $data );
				},
				$this->asset_suggestions_service->get_final_url_suggestions( $search, $per_page, $order_by )
			);
		};
	}



	/**
	 * Get the item schema for the controller.
	 *
	 * @return array
	 */
	protected function get_schema_properties(): array {
		return [
			'id'    => [
				'type'        => 'number',
				'description' => __( 'Post ID or Term ID', 'google-listings-and-ads' ),
				'context'     => [ 'view' ],
			],
			'type'  => [
				'type'        => 'string',
				'description' => __( 'Post, term or homepage', 'google-listings-and-ads' ),
				'context'     => [ 'view' ],
				'enum'        => [ 'post', 'term', 'homepage' ],
				'readonly'    => true,
			],
			'title' => [
				'type'        => 'string',
				'description' => __( 'The post or term title', 'google-listings-and-ads' ),
				'context'     => [ 'view' ],
				'readonly'    => true,
			],
			'url'   => [
				'type'        => 'string',
				'description' => __( 'The URL linked to the post/term', '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 'asset_final_url_suggestions';
	}
}
BudgetRecommendationController.php000064400000012206151556764500013437 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads;
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\BudgetRecommendationQuery;
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 BudgetRecommendationController
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
 */
class BudgetRecommendationController extends BaseController implements ISO3166AwareInterface {

	use CountryCodeTrait;

	/**
	 * @var BudgetRecommendationQuery
	 */
	protected $budget_recommendation_query;

	/**
	 * @var Ads
	 */
	protected $ads;

	/**
	 * BudgetRecommendationController constructor.
	 *
	 * @param RESTServer                $rest_server
	 * @param BudgetRecommendationQuery $budget_recommendation_query
	 * @param Ads                       $ads
	 */
	public function __construct( RESTServer $rest_server, BudgetRecommendationQuery $budget_recommendation_query, Ads $ads ) {
		parent::__construct( $rest_server );
		$this->budget_recommendation_query = $budget_recommendation_query;
		$this->ads                         = $ads;
	}

	/**
	 * Register rest routes with WordPress.
	 */
	public function register_routes(): void {
		$this->register_route(
			'ads/campaigns/budget-recommendation',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_budget_recommendation_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_collection_params(),
				],
				'schema' => $this->get_api_response_schema_callback(),
			]
		);
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params(): array {
		return [
			'context'       => $this->get_context_param( [ 'default' => 'view' ] ),
			'country_codes' => [
				'type'              => 'array',
				'sanitize_callback' => $this->get_country_code_sanitize_callback(),
				'validate_callback' => $this->get_country_code_validate_callback(),
				'items'             => [
					'type' => 'string',
				],
				'required'          => true,
				'minItems'          => 1,
			],
		];
	}

	/**
	 * @return callable
	 */
	protected function get_budget_recommendation_callback(): callable {
		return function ( Request $request ) {
			$country_codes = $request->get_param( 'country_codes' );
			$currency      = $this->ads->get_ads_currency();

			if ( ! $currency ) {
				return new Response(
					[
						'message'       => __( 'No currency available for the Ads account.', 'google-listings-and-ads' ),
						'currency'      => $currency,
						'country_codes' => $country_codes,
					],
					400
				);
			}

			$recommendations = $this
				->budget_recommendation_query
				->where( 'country', $country_codes, 'IN' )
				->where( 'currency', $currency )
				->get_results();

			if ( ! $recommendations ) {
				return new Response(
					[
						'message'       => __( 'Cannot find any budget recommendations.', 'google-listings-and-ads' ),
						'currency'      => $currency,
						'country_codes' => $country_codes,
					],
					404
				);
			}

			$returned_recommendations = array_map(
				function ( $recommendation ) {
					return [
						'country'      => $recommendation['country'],
						'daily_budget' => (int) $recommendation['daily_budget'],
					];
				},
				$recommendations
			);

			return $this->prepare_item_for_response(
				[
					'currency'        => $currency,
					'recommendations' => $returned_recommendations,
				],
				$request
			);
		};
	}

	/**
	 * Get the item schema for the controller.
	 *
	 * @return array
	 */
	protected function get_schema_properties(): array {
		return [
			'currency'        => [
				'type'              => 'string',
				'description'       => __( 'The currency to use for the shipping rate.', 'google-listings-and-ads' ),
				'context'           => [ 'view' ],
				'validate_callback' => 'rest_validate_request_arg',
			],
			'recommendations' => [
				'type'  => 'array',
				'items' => [
					'type'       => 'object',
					'properties' => [
						'country'      => [
							'type'        => 'string',
							'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ),
							'context'     => [ 'view' ],
						],
						'daily_budget' => [
							'type'        => 'number',
							'description' => __( 'The recommended daily budget for a country.', 'google-listings-and-ads' ),
						],
					],
				],
			],
		];
	}

	/**
	 * Get the item schema name for the controller.
	 *
	 * Used for building the API response schema.
	 *
	 * @return string
	 */
	protected function get_schema_title(): string {
		return 'budget-recommendation';
	}
}
CampaignController.php000064400000030775151556764500011072 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsCampaign;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CampaignStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CampaignType;
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\Google\GoogleHelperAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ISO3166AwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use DateTime;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;

defined( 'ABSPATH' ) || exit;

/**
 * Class CampaignController
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
 */
class CampaignController extends BaseController implements GoogleHelperAwareInterface, ISO3166AwareInterface {

	use CountryCodeTrait;

	/**
	 * @var AdsCampaign
	 */
	protected $ads_campaign;

	/**
	 * CampaignController constructor.
	 *
	 * @param RESTServer  $server
	 * @param AdsCampaign $ads_campaign
	 */
	public function __construct( RESTServer $server, AdsCampaign $ads_campaign ) {
		parent::__construct( $server );
		$this->ads_campaign = $ads_campaign;
	}

	/**
	 * Register rest routes with WordPress.
	 */
	public function register_routes(): void {
		$this->register_route(
			'ads/campaigns',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_campaigns_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_collection_params(),
				],
				[
					'methods'             => TransportMethods::CREATABLE,
					'callback'            => $this->create_campaign_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_schema_properties(),
				],
				'schema' => $this->get_api_response_schema_callback(),
			]
		);

		$this->register_route(
			'ads/campaigns/(?P<id>[\d]+)',
			[
				[
					'methods'             => TransportMethods::READABLE,
					'callback'            => $this->get_campaign_callback(),
					'permission_callback' => $this->get_permission_callback(),
				],
				[
					'methods'             => TransportMethods::EDITABLE,
					'callback'            => $this->edit_campaign_callback(),
					'permission_callback' => $this->get_permission_callback(),
					'args'                => $this->get_edit_schema(),
				],
				[
					'methods'             => TransportMethods::DELETABLE,
					'callback'            => $this->delete_campaign_callback(),
					'permission_callback' => $this->get_permission_callback(),
				],
				'schema' => $this->get_api_response_schema_callback(),
			]
		);
	}

	/**
	 * Get the callback function for listing campaigns.
	 *
	 * @return callable
	 */
	protected function get_campaigns_callback(): callable {
		return function ( Request $request ) {
			try {
				$exclude_removed = $request->get_param( 'exclude_removed' );

				return array_map(
					function ( $campaign ) use ( $request ) {
						$data = $this->prepare_item_for_response( $campaign, $request );
						return $this->prepare_response_for_collection( $data );
					},
					$this->ads_campaign->get_campaigns( $exclude_removed, true, $request->get_params() )
				);
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the callback function for creating a campaign.
	 *
	 * @return callable
	 */
	protected function create_campaign_callback(): callable {
		return function ( Request $request ) {
			try {
				$fields = array_intersect_key( $request->get_json_params(), $this->get_schema_properties() );

				// Set the default value of campaign name.
				if ( empty( $fields['name'] ) ) {
					$current_date_time = ( new DateTime( 'now', wp_timezone() ) )->format( 'Y-m-d H:i:s' );
					$fields['name']    = sprintf(
					/* translators: %s: current date time. */
						__( 'Campaign %s', 'google-listings-and-ads' ),
						$current_date_time
					);
				}

				$campaign = $this->ads_campaign->create_campaign( $fields );

				/**
				 * When a campaign has been successfully created.
				 *
				 * @event gla_created_campaign
				 * @property int    id                 Campaign ID.
				 * @property string status             Campaign status, `enabled` or `paused`.
				 * @property string name               Campaign name, generated based on date.
				 * @property float  amount             Campaign budget.
				 * @property string country            Base target country code.
				 * @property string targeted_locations Additional target country codes.
				 * @property string source             The source of the campaign creation.
				 */
				do_action(
					'woocommerce_gla_track_event',
					'created_campaign',
					[
						'id'                 => $campaign['id'],
						'status'             => $campaign['status'],
						'name'               => $campaign['name'],
						'amount'             => $campaign['amount'],
						'country'            => $campaign['country'],
						'targeted_locations' => join( ',', $campaign['targeted_locations'] ),
						'source'             => $fields['label'] ?? '',
					]
				);

				return $this->prepare_item_for_response( $campaign, $request );
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the callback function for listing a single campaign.
	 *
	 * @return callable
	 */
	protected function get_campaign_callback(): callable {
		return function ( Request $request ) {
			try {
				$id       = absint( $request['id'] );
				$campaign = $this->ads_campaign->get_campaign( $id );

				if ( empty( $campaign ) ) {
					return new Response(
						[
							'message' => __( 'Campaign is not available.', 'google-listings-and-ads' ),
							'id'      => $id,
						],
						404
					);
				}

				return $this->prepare_item_for_response( $campaign, $request );
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the callback function for editing a campaign.
	 *
	 * @return callable
	 */
	protected function edit_campaign_callback(): callable {
		return function ( Request $request ) {
			try {
				$fields = array_intersect_key( $request->get_json_params(), $this->get_edit_schema() );
				if ( empty( $fields ) ) {
					return new Response(
						[
							'status'  => 'invalid_data',
							'message' => __( 'Invalid edit data.', 'google-listings-and-ads' ),
						],
						400
					);
				}

				$campaign_id = $this->ads_campaign->edit_campaign( absint( $request['id'] ), $fields );

				/**
				 * When a campaign has been successfully edited.
				 *
				 * @event gla_edited_campaign
				 * @property int    id     Campaign ID.
				 * @property string status Campaign status, `enabled` or `paused`.
				 * @property string name   Campaign name, generated based on date.
				 * @property float  amount Campaign budget.
				 */
				do_action(
					'woocommerce_gla_track_event',
					'edited_campaign',
					array_merge(
						[
							'id' => $campaign_id,
						],
						$fields,
					)
				);

				return [
					'status'  => 'success',
					'message' => __( 'Successfully edited campaign.', 'google-listings-and-ads' ),
					'id'      => $campaign_id,
				];
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the callback function for deleting a campaign.
	 *
	 * @return callable
	 */
	protected function delete_campaign_callback(): callable {
		return function ( Request $request ) {
			try {
				$deleted_id = $this->ads_campaign->delete_campaign( absint( $request['id'] ) );

				/**
				 * When a campaign has been successfully deleted.
				 *
				 * @event gla_deleted_campaign
				 * @property int id Campaign ID.
				 */
				do_action(
					'woocommerce_gla_track_event',
					'deleted_campaign',
					[
						'id' => $deleted_id,
					]
				);

				return [
					'status'  => 'success',
					'message' => __( 'Successfully deleted campaign.', 'google-listings-and-ads' ),
					'id'      => $deleted_id,
				];
			} catch ( Exception $e ) {
				return $this->response_from_exception( $e );
			}
		};
	}

	/**
	 * Get the schema for fields we are allowed to edit.
	 *
	 * @return array
	 */
	protected function get_edit_schema(): array {
		$allowed = [
			'name',
			'status',
			'amount',
		];

		$fields = array_intersect_key( $this->get_schema_properties(), array_flip( $allowed ) );

		// Unset required to allow editing individual fields.
		array_walk(
			$fields,
			function ( &$value ) {
				unset( $value['required'] );
			}
		);

		return $fields;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params(): array {
		return [
			'exclude_removed' => [
				'description'       => __( 'Exclude removed campaigns.', 'google-listings-and-ads' ),
				'type'              => 'boolean',
				'default'           => true,
				'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',
				'minimum'           => 1,
				'maximum'           => 10000,
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
			],
		];
	}

	/**
	 * Get the item schema for the controller.
	 *
	 * @return array
	 */
	protected function get_schema_properties(): array {
		return [
			'id'                 => [
				'type'        => 'integer',
				'description' => __( 'ID number.', 'google-listings-and-ads' ),
				'context'     => [ 'view' ],
				'readonly'    => true,
			],
			'name'               => [
				'type'              => 'string',
				'description'       => __( 'Descriptive campaign name.', 'google-listings-and-ads' ),
				'context'           => [ 'view', 'edit' ],
				'validate_callback' => 'rest_validate_request_arg',
				'required'          => false,
			],
			'status'             => [
				'type'              => 'string',
				'enum'              => CampaignStatus::labels(),
				'description'       => __( 'Campaign status.', 'google-listings-and-ads' ),
				'context'           => [ 'view', 'edit' ],
				'validate_callback' => 'rest_validate_request_arg',
			],
			'type'               => [
				'type'              => 'string',
				'enum'              => CampaignType::labels(),
				'description'       => __( 'Campaign type.', 'google-listings-and-ads' ),
				'context'           => [ 'view', 'edit' ],
				'validate_callback' => 'rest_validate_request_arg',
			],
			'amount'             => [
				'type'              => 'number',
				'description'       => __( 'Daily budget amount in the local currency.', 'google-listings-and-ads' ),
				'context'           => [ 'view', 'edit' ],
				'validate_callback' => 'rest_validate_request_arg',
				'required'          => true,
			],
			'country'            => [
				'type'              => 'string',
				'description'       => __( 'Country code of sale country 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_supported_country_code_validate_callback(),
				'readonly'          => true,
			],
			'targeted_locations' => [
				'type'              => 'array',
				'description'       => __( 'The locations that an Ads campaign is targeting 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_supported_country_code_validate_callback(),
				'required'          => true,
				'minItems'          => 1,
				'items'             => [
					'type' => 'string',
				],
			],
			'label'              => [
				'type'              => 'string',
				'description'       => __( 'The name of the label to assign to the campaign.', 'google-listings-and-ads' ),
				'context'           => [ 'edit' ],
				'validate_callback' => 'rest_validate_request_arg',
				'required'          => false,

			],
		];
	}

	/**
	 * Get the item schema name for the controller.
	 *
	 * Used for building the API response schema.
	 *
	 * @return string
	 */
	protected function get_schema_title(): string {
		return 'campaign';
	}
}
ReportsController.php000064400000015173151556764500011004 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsReport;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CampaignStatus;
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:
 * - AdsReport
 * - WP (in parent class)
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
 */
class ReportsController extends BaseReportsController {

	/**
	 * Register rest routes with WordPress.
	 */
	public function register_routes(): void {
		$this->register_route(
			'ads/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(
			'ads/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 AdsReport $ads */
				$ads  = $this->container->get( AdsReport::class );
				$data = $ads->get_report_data( 'campaigns', $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 AdsReport $ads */
				$ads  = $this->container->get( AdsReport::class );
				$data = $ads->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',
				'week',
				'month',
				'quarter',
				'year',
			],
			'validate_callback' => 'rest_validate_request_arg',
		];
		return $params;
	}

	/**
	 * Get the item schema for the controller.
	 *
	 * @return array
	 */
	protected function get_schema_properties(): array {
		return [
			'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(),
					],
				],
			],
			'campaigns' => [
				'type'  => 'array',
				'items' => [
					'type'       => 'object',
					'properties' => [
						'id'          => [
							'type'        => 'integer',
							'description' => __( 'ID number.', 'google-listings-and-ads' ),
							'context'     => [ 'view' ],
						],
						'name'        => [
							'type'        => 'string',
							'description' => __( 'Campaign name.', 'google-listings-and-ads' ),
							'context'     => [ 'view', 'edit' ],
						],
						'status'      => [
							'type'        => 'string',
							'enum'        => CampaignStatus::labels(),
							'description' => __( 'Campaign status.', 'google-listings-and-ads' ),
							'context'     => [ 'view' ],
						],
						'isConverted' => [
							'type'        => 'boolean',
							'description' => __( 'Whether the campaign has been converted', 'google-listings-and-ads' ),
							'context'     => [ 'view' ],
						],
						'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' ],
				],
				'sales'       => [
					'type'        => 'number',
					'description' => __( 'Sales amount.', 'google-listings-and-ads' ),
					'context'     => [ 'view' ],
				],
				'spend'       => [
					'type'        => 'number',
					'description' => __( 'Spend amount.', 'google-listings-and-ads' ),
					'context'     => [ 'view' ],
				],
				'conversions' => [
					'type'        => 'number',
					'description' => __( 'Conversions.', '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';
	}
}
SetupCompleteController.php000064400000005045151556764500012134 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\MerchantMetrics;
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\Proxies\RESTServer;
use WP_REST_Request as Request;
use WP_REST_Response as Response;

defined( 'ABSPATH' ) || exit;

/**
 * Class SetupCompleteController
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
 */
class SetupCompleteController extends BaseController {

	use EmptySchemaPropertiesTrait;

	/**
	 * Service used to access metrics from the Ads Account.
	 *
	 * @var MerchantMetrics
	 */
	protected $metrics;

	/**
	 * SetupCompleteController constructor.
	 *
	 * @param RESTServer      $server
	 * @param MerchantMetrics $metrics
	 */
	public function __construct( RESTServer $server, MerchantMetrics $metrics ) {
		parent::__construct( $server );
		$this->metrics = $metrics;
	}

	/**
	 * Registers the routes for the objects of the controller.
	 */
	public function register_routes() {
		$this->register_route(
			'ads/setup/complete',
			[
				[
					'methods'             => TransportMethods::CREATABLE,
					'callback'            => $this->get_setup_complete_callback(),
					'permission_callback' => $this->get_permission_callback(),
				],
			]
		);
	}

	/**
	 * Get the callback function for marking setup complete.
	 *
	 * @return callable
	 */
	protected function get_setup_complete_callback(): callable {
		return function ( Request $request ) {
			do_action( 'woocommerce_gla_ads_setup_completed' );

			/**
			 * Ads onboarding has been successfully completed.
			 *
			 * @event gla_ads_setup_completed
			 * @property int campaign_count Number of campaigns for the connected Ads account.
			 */
			do_action(
				'woocommerce_gla_track_event',
				'ads_setup_completed',
				[
					'campaign_count' => $this->metrics->get_campaign_count(),
				]
			);

			return new Response(
				[
					'status'  => 'success',
					'message' => __( 'Successfully marked Ads setup as completed.', 'google-listings-and-ads' ),
				]
			);
		};
	}

	/**
	 * Get the item schema name for the controller.
	 *
	 * Used for building the API response schema.
	 *
	 * @return string
	 */
	protected function get_schema_title(): string {
		return 'ads_setup_complete';
	}
}