From b2ff0ba1a2b8012b5664ffea8f8d73b3114c236b Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Fri, 13 Jan 2023 16:54:48 +0000 Subject: [PATCH] Multichannel Marketing - API (#36222) * Rename `get_errors_no` to `get_errors_count` * Remove the validation for marketing channel slugs Do not check if the marketing channel's slug exists in the list returned by WooCommerce.com Recommendation API. This essentially allows any third-party extension to register as a marketing channel. * Revert InstalledExtensions The InstalledExtensions class will be used by the previous generation of Marketing dashboard (if the user has not enabled the new "Marketing" feature); therefore, it's best to restore it to the original code. * Fix code style * Add channel property to MarketingCampaign * Add methods to filter the recommended marketing channels and extensions * Add `marketing/recommendations` API * Add unit tests for `marketing/recommendations` API * Add `marketing/channels` API * Add unit tests for `marketing/channels` API * Add `marketing/campaigns` API * Add unit tests for `marketing/campaigns` API * Translate Exception message * Remove doc references to predetermined list of marketing channels * Add `unregister_all` method To allow unregistering all marketing channels. * Unregister all channels on test tear down * Change API access denied authorization code * Change API access permission * Add MarketingCampaignType class This allows defining campaign types for each marketing channel. * Add campaign type property to campaign class * Add `marketing/campaign-types` API This API returns the aggregated list of supported marketing campaign types for all registered marketing channels. * Add unit tests for `marketing/campaign-types` API * Remove unused jsonSerialize method * Fix unit tests Co-authored-by: Nima --- plugins/woocommerce/src/Admin/API/Init.php | 4 + .../src/Admin/API/MarketingCampaignTypes.php | 211 ++++++++++++++++ .../src/Admin/API/MarketingCampaigns.php | 237 ++++++++++++++++++ .../src/Admin/API/MarketingChannels.php | 192 ++++++++++++++ .../Admin/API/MarketingRecommendations.php | 235 +++++++++++++++++ .../src/Admin/Marketing/MarketingCampaign.php | 46 ++-- .../Admin/Marketing/MarketingCampaignType.php | 130 ++++++++++ .../Marketing/MarketingChannelInterface.php | 7 + .../src/Admin/Marketing/MarketingChannels.php | 9 + .../woocommerce/src/Admin/Marketing/Price.php | 16 +- .../Admin/Marketing/MarketingSpecs.php | 72 ++++++ .../Admin/API/MarketingCampaignTypesTest.php | 101 ++++++++ .../src/Admin/API/MarketingCampaignsTest.php | 155 ++++++++++++ .../src/Admin/API/MarketingChannelsTest.php | 77 ++++++ .../API/MarketingRecommendationsTest.php | 138 ++++++++++ .../Admin/Marketing/MarketingCampaignTest.php | 34 +-- 16 files changed, 1602 insertions(+), 62 deletions(-) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaigns.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingChannels.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingRecommendations.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 42b2bac82e7..625374b2683 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -64,6 +64,10 @@ class Init { 'Automattic\WooCommerce\Admin\API\Experiments', 'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\MarketingOverview', + 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', + 'Automattic\WooCommerce\Admin\API\MarketingChannels', + 'Automattic\WooCommerce\Admin\API\MarketingCampaigns', + 'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php new file mode 100644 index 00000000000..f01dba7a649 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php @@ -0,0 +1,211 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the supported campaign types from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_supported_campaign_types() as $campaign_type ) { + $response = $this->prepare_item_for_response( $campaign_type, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaignType $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'channel' => [ + 'slug' => $item->get_channel()->get_slug(), + 'name' => $item->get_channel()->get_name(), + ], + 'create_url' => $item->get_create_url(), + 'icon_url' => $item->get_icon_url(), + ]; + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign_type', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'description' => __( 'The unique identifier of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'The name of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + 'create_url' => [ + 'description' => __( 'URL to the create campaign page for this campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon_url' => [ + 'description' => __( 'URL to an image/icon for the campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php new file mode 100644 index 00000000000..d8f9ac79378 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -0,0 +1,237 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the campaigns from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_campaigns() as $campaign ) { + $response = $this->prepare_item_for_response( $campaign, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + // Pagination. + $page = $request['page']; + $items_per_page = $request['per_page']; + $offset = ( $page - 1 ) * $items_per_page; + $paginated_results = array_slice( $responses, $offset, $items_per_page ); + + $response = rest_ensure_response( $paginated_results ); + + $total_campaigns = count( $responses ); + $max_pages = ceil( $total_campaigns / $items_per_page ); + $response->header( 'X-WP-Total', $total_campaigns ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + // Add previous and next page links to response header. + $request_params = $request->get_query_params(); + $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaign $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'channel' => $item->get_type()->get_channel()->get_slug(), + 'title' => $item->get_title(), + 'manage_url' => $item->get_manage_url(), + ]; + + if ( $item->get_cost() instanceof Price ) { + $data['cost'] = [ + 'value' => wc_format_decimal( $item->get_cost()->get_value() ), + 'currency' => $item->get_cost()->get_currency(), + ]; + } + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The unique identifier for the marketing channel that this campaign belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'title' => [ + 'description' => __( 'Title of the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'manage_url' => [ + 'description' => __( 'URL to the campaign management page.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'cost' => [ + 'description' => __( 'Cost of the marketing campaign.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + 'type' => 'object', + 'properties' => [ + 'value' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'currency' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php new file mode 100644 index 00000000000..bb22cd01c74 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -0,0 +1,192 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing channels. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Return installed marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + $channels = $marketing_channels_service->get_registered_channels(); + + $responses = []; + foreach ( $channels as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingChannelInterface $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'slug' => $item->get_slug(), + 'is_setup_completed' => $item->is_setup_completed(), + 'settings_url' => $item->get_setup_url(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'product_listings_status' => $item->get_product_listings_status(), + 'errors_count' => $item->get_errors_count(), + 'icon' => $item->get_icon_url(), + ]; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_channel', + 'type' => 'object', + 'properties' => [ + 'slug' => [ + 'description' => __( 'Unique identifier string for the marketing channel extension, also known as the plugin slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'description' => __( 'Path to the channel icon.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'is_setup_completed' => [ + 'type' => 'boolean', + 'description' => __( 'Whether or not the marketing channel is set up.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'settings_url' => [ + 'description' => __( 'URL to the settings page, or the link to complete the setup/onboarding if the channel has not been set up yet.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product_listings_status' => [ + 'description' => __( 'Status of the marketing channel\'s product listings.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'errors_count' => [ + 'description' => __( 'Number of channel issues/errors (e.g. account-related errors, product synchronization issues, etc.).', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php new file mode 100644 index 00000000000..7e9cbf398b8 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php @@ -0,0 +1,235 @@ +namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'permission_callback' => [ $this, 'get_items_permissions_check' ], + 'args' => [ + 'category' => [ + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_title_with_dashes', + 'enum' => [ 'channels', 'extensions' ], + 'required' => true, + ], + ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + } + + /** + * Check whether a given request has permission to view marketing recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Retrieves a collection of recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + + $category = $request->get_param( 'category' ); + if ( 'channels' === $category ) { + $items = $marketing_specs->get_recommended_marketing_channels(); + } elseif ( 'extensions' === $category ) { + $items = $marketing_specs->get_recommended_marketing_extensions_excluding_channels(); + } else { + return new WP_Error( 'woocommerce_rest_invalid_category', __( 'The specified category for recommendations is invalid. Allowed values: "channels", "extensions".', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $responses = []; + foreach ( $items as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param array $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_recommendation', + 'type' => 'object', + 'properties' => [ + 'title' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'url' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'direct_install' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'plugin' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'categories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'string', + ], + ], + 'subcategories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + 'tags' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php index 7b3f99a4b3a..c85ced83f21 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -7,14 +7,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * MarketingCampaign class * * @since x.x.x */ -class MarketingCampaign implements JsonSerializable { +class MarketingCampaign { /** * The unique identifier. * @@ -22,6 +20,13 @@ class MarketingCampaign implements JsonSerializable { */ protected $id; + /** + * The marketing campaign type. + * + * @var MarketingCampaignType + */ + protected $type; + /** * Title of the marketing campaign. * @@ -46,13 +51,15 @@ class MarketingCampaign implements JsonSerializable { /** * MarketingCampaign constructor. * - * @param string $id The marketing campaign's unique identifier. - * @param string $title The title of the marketing campaign. - * @param string $manage_url The URL to the channel's campaign management page. - * @param Price|null $cost The cost of the marketing campaign with the currency. + * @param string $id The marketing campaign's unique identifier. + * @param MarketingCampaignType $type The marketing campaign type. + * @param string $title The title of the marketing campaign. + * @param string $manage_url The URL to the channel's campaign management page. + * @param Price|null $cost The cost of the marketing campaign with the currency. */ - public function __construct( string $id, string $title, string $manage_url, Price $cost = null ) { + public function __construct( string $id, MarketingCampaignType $type, string $title, string $manage_url, Price $cost = null ) { $this->id = $id; + $this->type = $type; $this->title = $title; $this->manage_url = $manage_url; $this->cost = $cost; @@ -67,6 +74,15 @@ class MarketingCampaign implements JsonSerializable { return $this->id; } + /** + * Returns the marketing campaign type. + * + * @return MarketingCampaignType + */ + public function get_type(): MarketingCampaignType { + return $this->type; + } + /** * Returns the title of the marketing campaign. * @@ -93,18 +109,4 @@ class MarketingCampaign implements JsonSerializable { public function get_cost(): ?Price { return $this->cost; } - - /** - * Serialize the marketing campaign data. - * - * @return array - */ - public function jsonSerialize() { - return [ - 'id' => $this->get_id(), - 'title' => $this->get_title(), - 'manage_url' => $this->get_manage_url(), - 'cost' => $this->get_cost(), - ]; - } } diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php new file mode 100644 index 00000000000..032dc6f249d --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php @@ -0,0 +1,130 @@ +id = $id; + $this->channel = $channel; + $this->name = $name; + $this->description = $description; + $this->create_url = $create_url; + $this->icon_url = $icon_url; + } + + /** + * Returns the marketing campaign's unique identifier. + * + * @return string + */ + public function get_id(): string { + return $this->id; + } + + /** + * Returns the marketing channel that this campaign type belongs to. + * + * @return MarketingChannelInterface + */ + public function get_channel(): MarketingChannelInterface { + return $this->channel; + } + + /** + * Returns the name of the marketing campaign type. + * + * @return string + */ + public function get_name(): string { + return $this->name; + } + + /** + * Returns the description of the marketing campaign type. + * + * @return string + */ + public function get_description(): string { + return $this->description; + } + + /** + * Returns the URL to the create campaign page. + * + * @return string + */ + public function get_create_url(): string { + return $this->create_url; + } + + /** + * Returns the URL to an image/icon for the campaign type. + * + * @return string + */ + public function get_icon_url(): string { + return $this->icon_url; + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index b2dbc3819a2..6794c4b4259 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -73,6 +73,13 @@ interface MarketingChannelInterface { */ public function get_errors_count(): int; + /** + * Returns an array of marketing campaign types that the channel supports. + * + * @return MarketingCampaignType[] Array of marketing campaign type objects. + */ + public function get_supported_campaign_types(): array; + /** * Returns an array of the channel's marketing campaigns. * diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 786aeb33b5f..61cce159181 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -37,6 +37,15 @@ class MarketingChannels { $this->registered_channels[ $channel->get_slug() ] = $channel; } + /** + * Unregisters all marketing channels. + * + * @return void + */ + public function unregister_all(): void { + unset( $this->registered_channels ); + } + /** * Returns an array of all registered marketing channels. * diff --git a/plugins/woocommerce/src/Admin/Marketing/Price.php b/plugins/woocommerce/src/Admin/Marketing/Price.php index 9dbb00837ae..961228d5730 100644 --- a/plugins/woocommerce/src/Admin/Marketing/Price.php +++ b/plugins/woocommerce/src/Admin/Marketing/Price.php @@ -5,14 +5,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * Price class * * @since x.x.x */ -class Price implements JsonSerializable { +class Price { /** * The price. * @@ -55,16 +53,4 @@ class Price implements JsonSerializable { public function get_currency(): string { return $this->currency; } - - /** - * Serialize the price data. - * - * @return array - */ - public function jsonSerialize() { - return [ - 'value' => $this->get_value(), - 'currency' => $this->get_currency(), - ]; - } } diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php index ee36ba1375c..e6ed4e875d9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php @@ -28,6 +28,20 @@ class MarketingSpecs { */ const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; + /** + * Slug of the category specifying marketing extensions on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing'; + + /** + * Slug of the subcategory specifying marketing channels on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels'; + /** * Load recommended plugins from WooCommerce.com * @@ -61,6 +75,64 @@ class MarketingSpecs { return array_values( $plugins ); } + /** + * Return only the recommended marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_channels(): array { + return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] ); + } + + /** + * Return all recommended marketing extensions EXCEPT the marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_extensions_excluding_channels(): array { + return array_filter( + $this->get_recommended_plugins(), + function ( array $plugin_data ) { + return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data ); + } + ); + } + + /** + * Returns whether a plugin is a marketing extension. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_plugin( array $plugin_data ): bool { + $categories = $plugin_data['categories'] ?? []; + + return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true ); + } + + /** + * Returns whether a plugin is a marketing channel. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_channel_plugin( array $plugin_data ): bool { + if ( ! $this->is_marketing_plugin( $plugin_data ) ) { + return false; + } + + $subcategories = $plugin_data['subcategories'] ?? []; + foreach ( $subcategories as $subcategory ) { + if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) { + return true; + } + } + + return false; + } + /** * Load knowledge base posts from WooCommerce.com * diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php new file mode 100644 index 00000000000..f38b49e1bb5 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php @@ -0,0 +1,101 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-1' ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Return the sample campaign type by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-2' ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-type-1', + 'test-campaign-type-2', + ], + array_column( $data, 'id' ) + ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php new file mode 100644 index 00000000000..341799b6127 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php @@ -0,0 +1,155 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Create a mock marketing campaign. + $test_campaign_1 = $this->createMock( MarketingCampaign::class ); + $test_campaign_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-1' ); + $test_campaign_1->expects( $this->any() )->method( 'get_type' )->willReturn( $test_campaign_type_1 ); + // Return the sample campaign by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Create a mock marketing campaign for the second marketing channel. + $test_campaign_2 = $this->createMock( MarketingCampaign::class ); + $test_campaign_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-2' ); + $test_campaign_2->expects( $this->any() )->method( 'get_type' )->willReturn( $test_campaign_type_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-1', + 'test-campaign-2', + ], + array_column( $data, 'id' ) + ); + $this->assertEquals( + [ + 'test-channel-1', + 'test-channel-2', + ], + array_column( $data, 'channel' ) + ); + } + + /** + * Tests that the marketing campaigns are paginated and then returned by the endpoint. + */ + public function test_paginates_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Return mock campaigns by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( + [ + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + ] + ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( + [ + 'page' => '1', + 'per_page' => '2', + ] + ); + $response = $this->server->dispatch( $request ); + $headers = $response->get_headers(); + + $this->assertCount( 2, $response->get_data() ); + + $this->assertArrayHasKey( 'Link', $headers ); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + $this->assertEquals( 5, $headers['X-WP-Total'] ); + $this->assertEquals( 3, $headers['X-WP-TotalPages'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php new file mode 100644 index 00000000000..9159b977ba4 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php @@ -0,0 +1,77 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the registered marketing channels are returned by the endpoint. + */ + public function test_returns_registered_marketing_channels() { + // Register marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + $test_channel_1->expects( $this->any() )->method( 'get_name' )->willReturn( 'Test Channel One' ); + $this->marketing_channels_service->register( $test_channel_1 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'test-channel-1', $data[0]['slug'] ); + $this->assertEquals( 'Test Channel One', $data[0]['name'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php new file mode 100644 index 00000000000..46335da52e3 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php @@ -0,0 +1,138 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + set_transient( + MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, + [ + [ + 'title' => 'Example Marketing Channel', + 'description' => 'List your products and create ads, etc.', + 'url' => 'https://woocommerce.com/products/example-channel', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example.svg', + 'product' => 'example-channel', + 'plugin' => 'example-channel/example-channel.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => MarketingSpecs::MARKETING_CHANNEL_SUBCATEGORY_SLUG, + 'name' => 'Sales channels', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example Marketing Extension', + 'description' => 'Automate your customer communications, etc.', + 'url' => 'https://woocommerce.com/products/example-marketing-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-marketing-extension.svg', + 'product' => 'example-marketing-extension', + 'plugin' => 'example-marketing-extension/example-marketing-extension.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => 'email', + 'name' => 'Email', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example NON Marketing Extension', + 'description' => 'Handle coupons, etc.', + 'url' => 'https://woocommerce.com/products/example-random-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-random-extension.svg', + 'product' => 'example-random-extension', + 'plugin' => 'example-random-extension/example-random-extension.php', + 'categories' => [ 'coupons' ], + 'subcategories' => [], + 'tags' => [], + ], + ] + ); + } + + /** + * Tests that the marketing channel recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_channels() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'channels' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Channel', $data[0]['title'] ); + $this->assertEquals( 'example-channel', $data[0]['product'] ); + } + + /** + * Tests that the marketing extension recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_extensions() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'extensions' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Extension', $data[0]['title'] ); + $this->assertEquals( 'example-marketing-extension', $data[0]['product'] ); + } + + /** + * Tests that the endpoint returns an error if the provided category is invalid. + */ + public function test_returns_error_if_invalid_category_provided() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'test-non-existing-invalid-category' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php index 401e1630294..4277cff9437 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign; +use Automattic\WooCommerce\Admin\Marketing\MarketingCampaignType; use Automattic\WooCommerce\Admin\Marketing\Price; use WC_Unit_Test_Case; @@ -12,12 +13,15 @@ use WC_Unit_Test_Case; class MarketingCampaignTest extends WC_Unit_Test_Case { /** - * @testdox `get_id`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. + * @testdox `get_id`, `get_type`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. */ public function test_get_methods_return_properties() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $this->assertEquals( '1234', $marketing_campaign->get_id() ); + $this->assertEquals( $test_campaign_type_1, $marketing_campaign->get_type() ); $this->assertEquals( 'Ad #1234', $marketing_campaign->get_title() ); $this->assertEquals( 'https://example.com/manage-campaigns', $marketing_campaign->get_manage_url() ); $this->assertNotNull( $marketing_campaign->get_cost() ); @@ -29,30 +33,10 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox `cost` property can be null. */ public function test_cost_can_be_null() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns' ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns' ); $this->assertNull( $marketing_campaign->get_cost() ); } - - /** - * @testdox It can be serialized to JSON including all its properties. - */ - public function test_can_be_serialized_to_json() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); - - $json = wp_json_encode( $marketing_campaign ); - $this->assertNotEmpty( $json ); - $this->assertEqualSets( - [ - 'id' => $marketing_campaign->get_id(), - 'title' => $marketing_campaign->get_title(), - 'manage_url' => $marketing_campaign->get_manage_url(), - 'cost' => [ - 'value' => $marketing_campaign->get_cost()->get_value(), - 'currency' => $marketing_campaign->get_cost()->get_currency(), - ], - ], - json_decode( $json, true ) - ); - } }