diff --git a/assets/css/admin/facebook-for-woocommerce-product-sync.css b/assets/css/admin/facebook-for-woocommerce-product-sync.css new file mode 100644 index 000000000..6c0466ba5 --- /dev/null +++ b/assets/css/admin/facebook-for-woocommerce-product-sync.css @@ -0,0 +1,4 @@ +/** Product Sync Settings Description */ +.wc-facebook-settings table.form-table .description { + width: 400px; +} diff --git a/assets/css/admin/facebook-for-woocommerce-products-admin.css b/assets/css/admin/facebook-for-woocommerce-products-admin.css index 4a61c95e4..61718862a 100644 --- a/assets/css/admin/facebook-for-woocommerce-products-admin.css +++ b/assets/css/admin/facebook-for-woocommerce-products-admin.css @@ -39,20 +39,6 @@ clear: both; } -#woocommerce-product-data .wc_facebook_commerce_enabled_field label, -#woocommerce-product-data .wc_facebook_commerce_fields label { - padding-right: 37px; - position: relative; - width: 113px; -} -#woocommerce-product-data .wc_facebook_commerce_enabled_field label .woocommerce-help-tip, -#woocommerce-product-data .wc_facebook_commerce_fields label .woocommerce-help-tip { - margin: 0; - position: absolute; - top: 4px; - right: 12px; -} - #woocommerce-product-data #wc-facebook-google-product-category-fields { width: 50%; } @@ -76,3 +62,7 @@ .column-facebook_sync { width: 11% !important; } + +.wc-facebook-settings .desc { + width: 400px; +} diff --git a/assets/js/admin/facebook-for-woocommerce-products-admin.js b/assets/js/admin/facebook-for-woocommerce-products-admin.js index 15ddda871..384c4e9d2 100644 --- a/assets/js/admin/facebook-for-woocommerce-products-admin.js +++ b/assets/js/admin/facebook-for-woocommerce-products-admin.js @@ -143,64 +143,6 @@ jQuery( document ).ready( function( $ ) { } - /** - * Disables and changes the checked status of the Sell on Instagram setting field. - * - * Additionally, shows/hides messages explaining that the product is not ready for Commerce. - * - * @since 2.1.0 - * - * @param {boolean} enabled whether the setting field should be enabled or not - * @param {jQuery} $container a common ancestor of all the elements that need to modified - */ - function toggleFacebookSellOnInstagramSetting( enabled, $container ) { - - let $field = $container.find( '#wc_facebook_commerce_enabled' ); - let checked = $field.prop( 'original' ); - - $field.prop( 'checked', enabled ? checked : false ).prop( 'disabled', ! enabled ); - - // trigger change to hide fields based on the new state - $field.trigger( 'change' ); - - // restore previously stored value so that we can later restore the field to the status it had before we disabled it here - $field.prop( 'original', checked ); - - $container.find( '#product-not-ready-notice, #variable-product-not-ready-notice' ).hide(); - - if ( isVariableProduct() && ! isSyncEnabledForVariableProduct() ) { - $container.find( '#variable-product-not-ready-notice' ).show(); - } else if ( ! enabled ) { - $container.find( '#product-not-ready-notice' ).show(); - } - } - - - /** - * Determines whether product properties are configured using appropriate values for Commerce. - * - * @since 2.1.0 - * - * @return {boolean} - */ - function isProductReadyForCommerce() { - - if ( ! isSyncEnabledForProduct() ) { - return false; - } - - if ( ! isPriceDefinedForProduct() ) { - return false; - } - - if ( ! isStockManagementEnabledForProduct() ) { - return false; - } - - return true; - } - - /** * Determines whether the product or one of its variations has Facebook Sync enabled. * @@ -335,14 +277,6 @@ jQuery( document ).ready( function( $ ) { */ function shouldShowMissingGoogleProductCategoryAlert() { - if ( ! $( '#wc_facebook_commerce_enabled' ).prop( 'checked' ) ) { - return false; - } - - if ( ! isProductReadyForCommerce() ) { - return false; - } - let selectedCategories = $( '.wc_facebook_commerce_fields .wc-facebook-google-product-category-select' ).map( ( i, element ) => { return $( element ).val() ? $( element ).val() : null; } ); @@ -614,8 +548,6 @@ jQuery( document ).ready( function( $ ) { const jsSyncModeToggle = $( this ).closest( '.wc-metabox-content' ).find( '.js-variable-fb-sync-toggle' ); toggleSyncAndShowOption( ! $( this ).prop( 'checked' ), jsSyncModeToggle ); } ); - - toggleFacebookSellOnInstagramSetting( isProductReadyForCommerce(), $( '#facebook_options' ) ); } ); // show/hide Custom Image URL setting @@ -653,9 +585,6 @@ jQuery( document ).ready( function( $ ) { } ); } ); - // toggle Sell on Instagram checkbox on page load - toggleFacebookSellOnInstagramSetting( isProductReadyForCommerce(), facebookSettingsPanel ); - let submitProductSave = false; $( 'form#post input[type="submit"]' ).on( 'click', function( e ) { diff --git a/assets/js/admin/facebook-for-woocommerce-products-admin.min.js b/assets/js/admin/facebook-for-woocommerce-products-admin.min.js index 97d9e042d..81661de92 100644 --- a/assets/js/admin/facebook-for-woocommerce-products-admin.min.js +++ b/assets/js/admin/facebook-for-woocommerce-products-admin.min.js @@ -1,2 +1,2 @@ -"use strict";jQuery(document).ready(function(r){var t,n,e,c,i,o,s,a,_,d,l,u,p,f,m,g,b,v,h,w,k,y,x,j,M,D,E=window.pagenow.length?window.pagenow:"";window.typenow.length&&window.typenow;"edit-product"===E&&(t=!1,r("input#doaction, input#doaction2").on("click",function(o){if(t)return!0;o.preventDefault();var e,c=r(this),o=c.prev("select").val();"facebook_include"===o?(e=[],r.each(r('input[name="post[]"]:checked'),function(){e.push(parseInt(r(this).val(),10))}),r.post(facebook_for_woocommerce_products_admin.ajax_url,{action:"facebook_for_woocommerce_set_product_sync_bulk_action_prompt",security:facebook_for_woocommerce_products_admin.set_product_sync_bulk_action_prompt_nonce,toggle:o,products:e},function(o){o&&!o.success?(closeExistingModal(),new r.WCBackboneModal.View({target:"facebook-for-woocommerce-modal",string:o.data})):(t=!0,c.trigger("click"))})):(t=!0,c.trigger("click"))})),"product"===E&&(n=function(o,e){e.find(".enable-if-sync-enabled").prop("disabled",!o)},e=function(o,e){o?(e.find("option[value='sync_and_show']").show(),e.prop("original")&&e.val(e.prop("original"))):(e.find("option[value='sync_and_show']").hide(),"sync_and_show"===e.val()&&e.val("sync_and_hide"))},c=function(o,e){var c=e.find("#wc_facebook_commerce_enabled"),t=c.prop("original");c.prop("checked",!!o&&t).prop("disabled",!o),c.trigger("change"),c.prop("original",t),e.find("#product-not-ready-notice, #variable-product-not-ready-notice").hide(),d()&&!s()?e.find("#variable-product-not-ready-notice").show():o||e.find("#product-not-ready-notice").show()},i=function(){return!!o()&&(!!_()&&!!u())},o=function(){return(d()?s:a)()},s=function(){var o=r(".js-variable-fb-sync-toggle");return 0===o.length?!!facebook_for_woocommerce_products_admin.is_sync_enabled_for_product:!!o.map(function(o,e){return"sync_disabled"!==r(e).val()?e:null}).length},a=function(){return"sync_disabled"!==x.val()},_=function(){return!!d()||l()},d=function(){var o=r("select#product-type").val();return!(!o||!o.match(/variable/))},l=function(){return!(!r("#_regular_price").val()&&!r("#fb_product_price").val())},u=function(){return p()},p=function(){return!(!r("#_manage_stock").prop("checked")||!r("#_stock").val())},f=function(o){o.attr("data-original-value",o.val())},m=function(o){return-1 { return facebook_for_woocommerce_product_categories; } else if(typeof(facebook_for_woocommerce_settings_sync) !== 'undefined'){ return facebook_for_woocommerce_settings_sync; + } else if(typeof(facebook_for_woocommerce_settings_commerce) !== 'undefined'){ + return facebook_for_woocommerce_settings_commerce; } else { return facebook_for_woocommerce_products_admin; } diff --git a/assets/js/admin/google-product-category-fields.min.js b/assets/js/admin/google-product-category-fields.min.js index fb60b3b81..7a9b5ecb7 100644 --- a/assets/js/admin/google-product-category-fields.min.js +++ b/assets/js/admin/google-product-category-fields.min.js @@ -1,2 +1,2 @@ -"use strict";var _createClass=function(){function a(e,t){for(var o=0;o').insertBefore(t).on("change","select.wc-facebook-google-product-category-select",function(e){o.onChange(n(e.target))}),this.addInitialSelects(t.val());t=this.globalsHolder().enhanced_attribute_optional_selector;void 0!==t&&n("#"+t).on("change",function(){n(".wc-facebook-enhanced-catalog-attribute-optional-row").toggleClass("hidden",!n(this).prop("checked"))})}window.WC_Facebook_Google_Product_Category_Fields=(_createClass(a,[{key:"globalsHolder",value:function(){return"undefined"!=typeof facebook_for_woocommerce_product_categories?facebook_for_woocommerce_product_categories:"undefined"!=typeof facebook_for_woocommerce_settings_sync?facebook_for_woocommerce_settings_sync:facebook_for_woocommerce_products_admin}},{key:"getPageType",value:function(){return"undefined"!=typeof facebook_for_woocommerce_product_categories?0===n("input[name=tag_ID]").length?this.globalsHolder().enhanced_attribute_page_type_add_category:this.globalsHolder().enhanced_attribute_page_type_edit_category:this.globalsHolder().enhanced_attribute_page_type_edit_product}},{key:"addInitialSelects",value:function(e){var t=this;e?(this.getSelectedCategoryIds(e).forEach(function(e){t.addSelect(t.getOptions(e[1]),e[0])}),e=this.getOptions(e),Object.keys(e).length&&this.addSelect(e)):(this.addSelect(this.getOptions()),this.addSelect({}))}},{key:"requestAttributesIfValid",value:function(){var e,t,o;"true"===n("#wc_facebook_can_show_enhanced_catalog_attributes_id").val()&&(n(".wc-facebook-enhanced-catalog-attribute-row").remove(),this.isValid()&&(e="#"+this.input_id,t=n(e).parents("div.form-field"),o=this.globalsHolder().enhanced_attribute_optional_selector,this.getPageType()===this.globalsHolder().enhanced_attribute_page_type_edit_category?t=n(e).parents("tr.form-field"):this.getPageType()===this.globalsHolder().enhanced_attribute_page_type_edit_product&&(t=n(e).parents("p.form-field")),n.get(this.globalsHolder().ajax_url,{action:"wc_facebook_enhanced_catalog_attributes",security:"",selected_category:n(e).val(),tag_id:parseInt(n("input[name=tag_ID]").val(),10),taxonomy:n("input[name=taxonomy]").val(),item_id:parseInt(n("input[name=post_ID]").val(),10),page_type:this.getPageType()},function(e){e=n(e);n("#"+o,e).on("change",function(){n(".wc-facebook-enhanced-catalog-attribute-optional-row").toggleClass("hidden",!n(this).prop("checked"))}),e.insertAfter(t),n(document.body).trigger("init_tooltips")})))}},{key:"onChange",value:function(e){e.hasClass("locked")&&e.closest(".wc-facebook-google-product-category-field").nextAll().remove();var t,o=e.val();o?(t=this.getOptions(o),Object.keys(t).length&&this.addSelect(t)):(o=e.closest("#wc-facebook-google-product-category-fields").find(".wc-facebook-google-product-category-select").not(e).last().val())||this.addSelect({}),n("#"+this.input_id).val(o),this.requestAttributesIfValid()}},{key:"isValid",value:function(){return 2<=n(".wc-facebook-google-product-category-select").filter(function(e,t){return""!==n(t).val()}).length}},{key:"addSelect",value:function(t,e){var o=n("#wc-facebook-google-product-category-fields"),a=o.find(".wc-facebook-google-product-category-select"),c=n('');a.addClass("locked"),o.append(n('').insertBefore(t).on("change","select.wc-facebook-google-product-category-select",function(e){o.onChange(r(e.target))}),this.addInitialSelects(t.val());t=this.globalsHolder().enhanced_attribute_optional_selector;void 0!==t&&r("#"+t).on("change",function(){r(".wc-facebook-enhanced-catalog-attribute-optional-row").toggleClass("hidden",!r(this).prop("checked"))})}window.WC_Facebook_Google_Product_Category_Fields=(_createClass(a,[{key:"globalsHolder",value:function(){return"undefined"!=typeof facebook_for_woocommerce_product_categories?facebook_for_woocommerce_product_categories:"undefined"!=typeof facebook_for_woocommerce_settings_sync?facebook_for_woocommerce_settings_sync:"undefined"!=typeof facebook_for_woocommerce_settings_commerce?facebook_for_woocommerce_settings_commerce:facebook_for_woocommerce_products_admin}},{key:"getPageType",value:function(){return"undefined"!=typeof facebook_for_woocommerce_product_categories?0===r("input[name=tag_ID]").length?this.globalsHolder().enhanced_attribute_page_type_add_category:this.globalsHolder().enhanced_attribute_page_type_edit_category:this.globalsHolder().enhanced_attribute_page_type_edit_product}},{key:"addInitialSelects",value:function(e){var t=this;e?(this.getSelectedCategoryIds(e).forEach(function(e){t.addSelect(t.getOptions(e[1]),e[0])}),e=this.getOptions(e),Object.keys(e).length&&this.addSelect(e)):(this.addSelect(this.getOptions()),this.addSelect({}))}},{key:"requestAttributesIfValid",value:function(){var e,t,o;"true"===r("#wc_facebook_can_show_enhanced_catalog_attributes_id").val()&&(r(".wc-facebook-enhanced-catalog-attribute-row").remove(),this.isValid()&&(e="#"+this.input_id,t=r(e).parents("div.form-field"),o=this.globalsHolder().enhanced_attribute_optional_selector,this.getPageType()===this.globalsHolder().enhanced_attribute_page_type_edit_category?t=r(e).parents("tr.form-field"):this.getPageType()===this.globalsHolder().enhanced_attribute_page_type_edit_product&&(t=r(e).parents("p.form-field")),r.get(this.globalsHolder().ajax_url,{action:"wc_facebook_enhanced_catalog_attributes",security:"",selected_category:r(e).val(),tag_id:parseInt(r("input[name=tag_ID]").val(),10),taxonomy:r("input[name=taxonomy]").val(),item_id:parseInt(r("input[name=post_ID]").val(),10),page_type:this.getPageType()},function(e){e=r(e);r("#"+o,e).on("change",function(){r(".wc-facebook-enhanced-catalog-attribute-optional-row").toggleClass("hidden",!r(this).prop("checked"))}),e.insertAfter(t),r(document.body).trigger("init_tooltips")})))}},{key:"onChange",value:function(e){e.hasClass("locked")&&e.closest(".wc-facebook-google-product-category-field").nextAll().remove();var t,o=e.val();o?(t=this.getOptions(o),Object.keys(t).length&&this.addSelect(t)):(o=e.closest("#wc-facebook-google-product-category-fields").find(".wc-facebook-google-product-category-select").not(e).last().val())||this.addSelect({}),r("#"+this.input_id).val(o),this.requestAttributesIfValid()}},{key:"isValid",value:function(){return 2<=r(".wc-facebook-google-product-category-select").filter(function(e,t){return""!==r(t).val()}).length}},{key:"addSelect",value:function(t,e){var o=r("#wc-facebook-google-product-category-fields"),a=o.find(".wc-facebook-google-product-category-select"),c=r('');a.addClass("locked"),o.append(r(''; + + return ob_get_clean(); + } + + + /** + * Gets the markup for the buttons used in the Complete modal. + * + * @since 2.1.0 + * + * @return string + */ + private function get_complete_modal_buttons() { + + return $this->get_modal_buttons( __( 'Submit order', 'facebook-for-woocommerce' ) ); + } + + + /** + * Gets the markup for the message used in the Refund modal. + * + * @since 2.1.0 + * + * @return string + */ + private function get_refund_modal_message() { + + ob_start(); + + ?> +

+ render_refund_reason_field( 'wc_facebook_refund_reason_modal', false ); + + return ob_get_clean(); + } + + + /** + * Gets the markup for the buttons used in the Refund modal. + * + * @since 2.1.0 + * + * @return string + */ + private function get_refund_modal_buttons() { + + return $this->get_modal_buttons( __( 'Submit refund', 'facebook-for-woocommerce' ) ); + } + + + /** + * Gets the markup for the message used in the Cancel modal. + * + * @since 2.1.0 + * + * @return string + */ + private function get_cancel_modal_message() { + + ob_start(); + + ?> +

+ 'wc_facebook_cancel_reason', + 'label' => '', + 'options' => facebook_for_woocommerce()->get_commerce_handler()->get_orders_handler()->get_cancellation_reasons(), + ] ); + + return ob_get_clean(); + } + + + /** + * Gets the markup for the buttons used in the Cancel modal. + * + * @since 2.1.0 + * + * @return string + */ + private function get_cancel_modal_buttons() { + + return $this->get_modal_buttons( __( 'Submit cancellation', 'facebook-for-woocommerce' ) ); + } + + + /** + * Renders the refund reason field. + * + * @internal + * + * @since 2.1.0 + */ + public function render_refund_reason_field( $select_id = '', $hidden = true ) { + + if ( ! $this->is_edit_order_screen() ) { + return; + } + + ?> + + get_parent_id() ); + + if ( ! $order instanceof \WC_Order || ! Commerce\Orders::is_commerce_order( $order ) ) { + return; + } + + $reason_code = wc_clean( Framework\SV_WC_Helper::get_posted_value( 'wc_facebook_refund_reason' ) ); + + facebook_for_woocommerce()->get_commerce_handler()->get_orders_handler()->add_order_refund( $order_refund, $reason_code ); + } + + + /** + * Sets a transient to display a notice regarding bulk updates for Commerce orders' statuses. + * + * @internal + * + * @since 2.1.0 + * + * @param string $redirect_url redirect URL carrying results + * @param string $action bulk action + * @param int[] $order_ids IDs of orders affected by the bulk action + * @return string + */ + public function handle_bulk_update( $redirect_url, $action, $order_ids ) { + + // listen for order status change actions + if ( empty( $action ) || empty( $order_ids ) || ! Framework\SV_WC_Helper::str_starts_with( $action, 'mark_' ) ) { + return $redirect_url; + } + + $commerce_orders = []; + + foreach ( $order_ids as $index => $order_id ) { + + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + continue; + } + + if ( Commerce\Orders::is_commerce_order( $order ) ) { + + unset( $order_ids[ $index ] ); + + $commerce_orders[] = $order->get_order_number(); + } + } + + if ( ! empty( $commerce_orders ) ) { + + // set the orders that are not going to be updated in the transient for reference + set_transient( $this->bulk_order_update_transient, $commerce_orders, MINUTE_IN_SECONDS ); + + // this will prevent WooCommerce to keep processing the orders we don't want to be changed in bulk + add_filter( 'woocommerce_bulk_action_ids', static function( $ids ) use ( $order_ids ) { + return $order_ids; + } ); + + // finally, parse the URL (main filter callback param) + if ( empty( $order_ids ) ) { + + $redirect_url = admin_url( 'edit.php?post_type=shop_order' ); + + } else { + + $redirect_url = add_query_arg( + [ + 'post_type' => 'shop_order', + 'bulk_action' => $action, + 'changed' => count( $order_ids ), + 'ids' => implode( ',', $order_ids ), + ], + $redirect_url + ); + } + } + + return $redirect_url; + } + + + /** + * Removes the status actions from the order list table rows. + * + * @internal + * + * @since 2.1.0 + * + * @param array $actions existing actions + * @param \WC_Order $order order object + * @return array + */ + public function remove_list_table_actions( $actions, $order ) { + + if ( $order instanceof \WC_Order && Commerce\Orders::is_commerce_order( $order ) ) { + unset( $actions['processing'], $actions['complete'] ); + } + + return $actions; + } + + + /** + * Removes the status actions from the list table order preview modal. + * + * @internal + * + * @since 2.1.0 + * + * @param array $actions existing actions + * @param \WC_Order $order order object + * @return array + */ + public function remove_order_preview_actions( $actions, $order ) { + + if ( $order instanceof \WC_Order && Commerce\Orders::is_commerce_order( $order ) ) { + unset( $actions['status'] ); + } + + return $actions; + } + + + /** + * Prevents sending emails for Commerce orders. + * + * @internal + * + * @since 2.1.0 + * + * @param bool $is_enabled whether the email is enabled in the first place + * @param \WC_Order $order order object + * @return bool + */ + public function maybe_stop_order_email( $is_enabled, $order ) { + + // will decide whether to allow $is_enabled to be filtered + $is_previously_enabled = $is_enabled; + + // checks whether or not the order is a Commerce order + $is_commerce_order = $order instanceof \WC_Order && \SkyVerge\WooCommerce\Facebook\Commerce\Orders::is_commerce_order( $order ); + + // decides whether to disable or to keep emails enabled + $is_enabled = $is_enabled && ! $is_commerce_order; + + if ( $is_previously_enabled && $is_commerce_order ) { + + /** + * Filters the flag used to determine whether the email is enabled. + * + * @param bool $is_enabled whether the email is enabled + * @param \WC_Order $order order object + * @param Orders $this admin orders instance + * @since 2.1.0 + * + */ + $is_enabled = (bool) apply_filters( 'wc_facebook_commerce_send_woocommerce_emails', $is_enabled, $order, $this ); + } + + return $is_enabled; + } + + + /** + * Determines whether or not the order is editable. + * + * @internal + * + * @since 2.1.0 + * + * @param bool $maybe_editable whether the order is editable in the first place + * @param \WC_Order $order order object + * @return bool + */ + public function is_order_editable( $maybe_editable, $order ) { + + // if the order is a WC_Order, determines whether it is pending or not + $is_order_pending = $order instanceof \WC_Order && Commerce\Orders::is_order_pending( $order ); + + return $maybe_editable && ! $is_order_pending; + } + + + /** + * Determines whether or not the current screen is an orders screen. + * + * @internal + * + * @since 2.1.0 + * + * @return bool + */ + public function is_orders_screen() { + + return Framework\SV_WC_Helper::is_current_screen( 'edit-shop_order' ) || + Framework\SV_WC_Helper::is_current_screen( 'shop_order' ); + } + + + /** + * Determines whether or not the current screen is an order edit screen. + * + * @internal + * + * @since 2.1.0 + * + * @return bool + */ + public function is_edit_order_screen() { + + return Framework\SV_WC_Helper::is_current_screen( 'shop_order' ); + } + + +} diff --git a/includes/Admin/Products.php b/includes/Admin/Products.php index 034d9a14e..d2475fa65 100644 --- a/includes/Admin/Products.php +++ b/includes/Admin/Products.php @@ -24,9 +24,6 @@ class Products { - /** @var string Commerce enabled field */ - const FIELD_COMMERCE_ENABLED = 'wc_facebook_commerce_enabled'; - /** @var string Google Product category ID field */ const FIELD_GOOGLE_PRODUCT_CATEGORY_ID = 'wc_facebook_google_product_category_id'; @@ -199,51 +196,19 @@ function( $attribute ) use ( $product ) { /** * Renders the Commerce settings fields. * - * @internal + * TODO remove this deprecated method by version 2.4.0 or by March 2021 {DK 2020-12-23} * + * @internal + * @deprecated since 2.3.0 * @since 2.1.0 * * @param \WC_Product $product product object */ public static function render_commerce_fields( \WC_Product $product ) { - ?> -

- - -

- - - - - - new Settings_Screens\Product_Sets(), Settings_Screens\Messenger::ID => new Settings_Screens\Messenger(), Settings_Screens\Advertise::ID => new Settings_Screens\Advertise(), + Settings_Screens\Commerce::ID => new Settings_Screens\Commerce(), ); add_action( 'admin_menu', array( $this, 'add_menu_item' ) ); diff --git a/includes/Admin/Settings_Screens/Commerce.php b/includes/Admin/Settings_Screens/Commerce.php new file mode 100644 index 000000000..8b4d7a40b --- /dev/null +++ b/includes/Admin/Settings_Screens/Commerce.php @@ -0,0 +1,481 @@ +id = self::ID; + $this->label = __( 'Commerce', 'facebook-for-woocommerce' ); + $this->title = __( 'Commerce', 'facebook-for-woocommerce' ); + + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); + + add_action( 'woocommerce_admin_field_commerce_google_product_categories', [ $this, 'render_google_product_category_field' ] ); + } + + + /** + * Enqueues the assets. + * + * @internal + * + * @since 2.1.0-dev.1 + */ + public function enqueue_assets() { + + if ( Admin\Settings::PAGE_ID !== Framework\SV_WC_Helper::get_requested_value( 'page' ) || ( self::ID !== Framework\SV_WC_Helper::get_requested_value( 'tab' ) ) ) { + return; + } + + wp_enqueue_script( 'facebook-for-woocommerce-settings-commerce', facebook_for_woocommerce()->get_plugin_url() . '/assets/js/admin/settings-commerce.min.js', [ 'facebook-for-woocommerce-modal', 'jquery-tiptip' ], \WC_Facebookcommerce::PLUGIN_VERSION ); + + wp_localize_script( 'facebook-for-woocommerce-settings-commerce', 'facebook_for_woocommerce_settings_commerce', [ + 'default_google_product_category_modal_message' => $this->get_default_google_product_category_modal_message(), + 'default_google_product_category_modal_message_empty' => $this->get_default_google_product_category_modal_message_empty(), + 'default_google_product_category_modal_buttons' => $this->get_default_google_product_category_modal_buttons(), + ] ); + + } + + + /** + * Gets the message for Default Google Product Category modal. + * + * @since 2.1.0-dev.1 + * + * @return string + */ + private function get_default_google_product_category_modal_message() { + + return wp_kses_post( __( 'Products and categories that inherit this global setting (i.e. they do not have a specific Google product category set) will use the new default immediately. Are you sure you want to proceed?', 'facebook-for-woocommerce' ) ); + } + + + /** + * Gets the message for Default Google Product Category modal when the selection is empty. + * + * @since 2.1.0-dev.1 + * + * @return string + */ + private function get_default_google_product_category_modal_message_empty() { + + return sprintf( + /* translators: Placeholders: %1$s - tag, %2$s - tag */ + esc_html__( 'Products and categories that inherit this global setting (they do not have a specific Google product category set) will use the new default immediately. %1$sIf you have cleared the Google Product Category%2$s, items inheriting the default will not be available for Instagram or Facebook checkout. Are you sure you want to proceed?', 'facebook-for-woocommerce' ), + '', '' + ); + } + + + /** + * Gets the markup for the buttons used in the Default Google Product Category modal. + * + * @since 2.1.0-dev.1 + * + * @return string + */ + private function get_default_google_product_category_modal_buttons() { + + ob_start(); + + ?> + + + get_connection_handler(); + $is_debug_mode = $connection_handler->get_plugin()->get_integration()->is_debug_mode_enabled(); + + // if not connected, fall back to standard display + if ( ! $connection_handler->is_connected() ) { + parent::render(); + return; + } + + $commerce_handler = facebook_for_woocommerce()->get_commerce_handler(); + + if ( ! $commerce_handler->is_available() ) { + $this->render_us_only_limitation_notice(); + return; + } + + /** + * Build the basic static elements. + * + * Display useful Commerce related info: + * + * + Commerce Account ID: just the ID + * + CTA: Checkout Method + * + Facebook Channel + * + IG Channel + */ + + $cms_id = $connection_handler->get_commerce_merchant_settings_id(); + $static_items = [ + 'commerce_manager' => [ + 'label' => __( 'Commerce Manager account', 'facebook-for-woocommerce' ), + 'value' => $cms_id, + 'url' => "https://business.facebook.com/commerce_manager/{$cms_id}" + ], + 'checkout_method' => [ + 'label' => __( 'Checkout Method', 'facebook-for-woocommerce' ), + ], + 'fb_channel' => [ + 'label' => __( 'Facebook Channel', 'facebook-for-woocommerce' ), + ], + 'ig_channel' => [ + 'label' => __( 'Instagram Channel', 'facebook-for-woocommerce' ), + ], + 'cta' => [ + 'label' => __( 'Call to Action', 'facebook-for-woocommerce' ), + 'debug' => true, + ], + 'shop_setup' => [ + 'label' => __( 'Shop Setup', 'facebook-for-woocommerce' ), + 'debug' => true, + ], + 'payment_setup' => [ + 'label' => __( 'Payment Setup', 'facebook-for-woocommerce' ), + 'debug' => true, + ], + 'review_status' => [ + 'label' => __( 'Review Status', 'facebook-for-woocommerce' ), + 'debug' => true, + ] + ]; + + $commerce_connect_url = $connection_handler->get_commerce_connect_url(); + $commerce_connect_message = __( 'Your Checkout setup is not complete.', 'facebook-for-woocommerce' ); + $commerce_connect_caption = __( 'Finish Setup', 'facebook-for-woocommerce' ); + + // If the Commerce Merchant Settings ID is set, update the Commerce Account details + if ( $cms_id ) { + + try { + + $response = facebook_for_woocommerce()->get_api()->get_commerce_merchant_settings( $cms_id ); + + if ( $display_name = $response->get_display_name() ) { + $static_items['commerce_manager']['value'] = $display_name; + } + + // Get Commerce Attributes + $cta = $response->get_cta(); + $setup_status = $response->get_setup_status(); + $review_status = null; + + // Set Debug Info + $static_items['cta']['value'] = $cta; + + if ( $setup_status ) { + $static_items['shop_setup']['value'] = $setup_status->shop_setup; + $static_items['payment_setup']['value'] = $setup_status->payment_setup; + + if ( $setup_status->review_status ) { + $review_status = $setup_status->review_status->status; + $static_items['review_status']['value'] = $review_status; + } + } + + // CTA may be offsite if setup is not complete, + // For test Commerce accounts has_onsite_intent is false, however, CTA will be onsite + if ( $response->has_onsite_intent() || $cta === 'ONSITE_CHECKOUT' ) { + $static_items['checkout_method']['value'] = 'Checkout on Instagram or Facebook'; + + if ( $setup_status ) { + if ( $review_status === 'REJECTED' ) { + $this->render_admin_message( + self::LEVEL_ERROR, + "Your shop does not follow Facebook's Merchant Agreement and is not visible to potential customers. Please go to Facebook Commerce Manager to learn more or to request a review." + ); + } + + if ( $setup_status->payment_setup === 'VERIFICATION_NEEDED' ) { + $this->render_admin_message( + self::LEVEL_WARNING, + "For your security, Facebook requires additional information to confirm your business identity. Please go to Facebook Commerce Manager to complete verification and prevent your shop from closing." + ); + } + + if ( + $cta === 'ONSITE_CHECKOUT' && + $setup_status->shop_setup === 'SETUP' && + $setup_status->payment_setup !== 'NOT_SETUP' + ) { + $commerce_connect_url = $connection_handler->get_commerce_connect_url( $cms_id ); + $commerce_connect_message = __( 'Your store is not connected to Checkout on Instagram or Facebook.', 'facebook-for-woocommerce' ); + $commerce_connect_caption = __( 'Connect', 'facebook-for-woocommerce' ); + } + } + } else { + $static_items['checkout_method']['value'] = 'Checkout on Another Website'; + } + + if ( $fb_channel = $response->get_facebook_channel() ) { + $static_items['fb_channel']['value'] = $fb_channel->id ? 'Enabled' : ''; + } + + if ( $ig_channel = $response->get_instagram_channel() ) { + $static_items['ig_channel']['value'] = $ig_channel->id ? 'Enabled' : ''; + } + + } catch ( Framework\SV_WC_API_Exception $exception ) { + facebook_for_woocommerce()->log( 'Error retrieving Commerce Merchant Settings: ' . $exception->getMessage() ); + } + } + + // if the user has authorized the pages_read_engagement scope, they can go directly to the Commerce onboarding + if ( 'yes' === get_option( 'wc_facebook_has_authorized_pages_read_engagement' ) ) { + + if ( $commerce_connected = $commerce_handler->is_connected() ) { + $connect_url = $connection_handler->get_commerce_manage_url(); + $commerce_connect_message = __( 'Your store is connected to Checkout.', 'facebook-for-woocommerce' ); + $commerce_connect_caption = __( 'Manage', 'facebook-for-woocommerce' ); + } else { + $connect_url = $commerce_connect_url; + } + + // otherwise, they've connected FBE before that scope was requested so they need to re-auth and then go to the Commerce onboarding + } else { + + $connect_url = $connection_handler->get_connect_url( true ); + } + + ?> + + + + + + + + +
+

+ + + + + + +

+

+ +

+
+ + + + + $item ) : + + $item = wp_parse_args( $item, [ + 'type' => '', + 'label' => '', + 'value' => '', + 'url' => '', + 'debug' => '', + ] ); + + ?> + + + + + + + + + + + + + + + + + +
+ + is_connected() ) { + parent::render(); + } + } + + + /** + * Renders the notice about the US-only limitation for Instagram Checkout. + * + * @since 2.1.0-dev.1 + */ + private function render_us_only_limitation_notice() { + + ?> + +

+ + + + + + + + render( $field['id'] ); ?> + + + + get_connection_handler(); + $commerce_handler = facebook_for_woocommerce()->get_commerce_handler(); + + if ( ! $connection_handler->is_connected() || ! $commerce_handler->is_available() ) { + return [ [] ]; + } + + return [ + [ + 'type' => 'title', + 'title' => 'Product Settings' + ], + [ + 'id' => \SkyVerge\WooCommerce\Facebook\Commerce::OPTION_GOOGLE_PRODUCT_CATEGORY_ID, + 'type' => 'commerce_google_product_categories', + 'title' => __( 'Default Google product category', 'facebook-for-woocommerce' ), + 'desc_tip' => __( 'Choose a default Google product category for your products. Defaults can also be set for product categories. Products need at least two category levels defined to sell via Instagram or Facebook.', 'facebook-for-woocommerce' ), + ], + [ + 'type' => 'sectionend', + ] + ]; + } + + + /** + * Gets the "disconnected" message. + * + * @since 2.1.0-dev.1 + * + * @return string + */ + public function get_disconnected_message() { + + return sprintf( + /* translators: Placeholders: %1$s - tag, %2$s - tag */ + __( 'Please %1$sconnect to Facebook%2$s to enable Checkout on Instagram or Facebook.', 'facebook-for-woocommerce' ), + '', '' + ); + } + + +} diff --git a/includes/Admin/Settings_Screens/Connection.php b/includes/Admin/Settings_Screens/Connection.php index cf3ea6e5c..ca038bad3 100644 --- a/includes/Admin/Settings_Screens/Connection.php +++ b/includes/Admin/Settings_Screens/Connection.php @@ -150,6 +150,24 @@ public function render() { ], ]; + // If the Page ID is set, update the URL and get its name for display + if ( $page_id = $static_items['page']['value'] ) { + + try { + + $response = facebook_for_woocommerce()->get_api()->get_page( $page_id ); + + if ( $url = $response->get_url() ) { + $static_items['page']['url'] = $url; + } + + if ( $name = $response->get_name() ) { + $static_items['page']['value'] = $name; + } + + } catch ( Framework\SV_WC_API_Exception $exception ) {} + } + // if the catalog ID is set, update the URL and try to get its name for display if ( $catalog_id = $static_items['catalog']['value'] ) { diff --git a/includes/Admin/Settings_Screens/Product_Sync.php b/includes/Admin/Settings_Screens/Product_Sync.php index 49d4c5cad..f5cf1c5cc 100644 --- a/includes/Admin/Settings_Screens/Product_Sync.php +++ b/includes/Admin/Settings_Screens/Product_Sync.php @@ -39,8 +39,8 @@ class Product_Sync extends Admin\Abstract_Settings_Screen { public function __construct() { $this->id = self::ID; - $this->label = __( 'Product sync', 'facebook-for-woocommerce' ); - $this->title = __( 'Product sync', 'facebook-for-woocommerce' ); + $this->label = __( 'Product Sync', 'facebook-for-woocommerce' ); + $this->title = __( 'Product Sync', 'facebook-for-woocommerce' ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); @@ -63,6 +63,8 @@ public function enqueue_assets() { return; } + wp_enqueue_style( 'wc-facebook-admin-product-sync', facebook_for_woocommerce()->get_plugin_url() . '/assets/css/admin/facebook-for-woocommerce-product-sync.css', [], \WC_Facebookcommerce::VERSION ); + wp_enqueue_script( 'wc-backbone-modal', null, [ 'backbone' ] ); wp_enqueue_script( 'facebook-for-woocommerce-modal', plugins_url( '/facebook-for-woocommerce/assets/js/facebook-for-woocommerce-modal.min.js' ), [ 'jquery', 'wc-backbone-modal', 'jquery-blockui' ], \WC_Facebookcommerce::PLUGIN_VERSION ); wp_enqueue_script( 'facebook-for-woocommerce-settings-sync', plugins_url( '/facebook-for-woocommerce/assets/js/admin/facebook-for-woocommerce-settings-sync.min.js' ), [ 'jquery', 'wc-backbone-modal', 'jquery-blockui', 'jquery-tiptip', 'facebook-for-woocommerce-modal', 'wc-enhanced-select' ], \WC_Facebookcommerce::PLUGIN_VERSION ); @@ -118,7 +120,7 @@ private function get_default_google_product_category_modal_message_empty() { return sprintf( /* translators: Placeholders: %1$s - tag, %2$s - tag */ - esc_html__( 'Products and categories that inherit this global setting (they do not have a specific Google product category set) will use the new default immediately. %1$sIf you have cleared the Google Product Category%2$s, items inheriting the default will not be available for Instagram checkout. Are you sure you want to proceed?', 'facebook-for-woocommerce' ), + esc_html__( 'Products and categories that inherit this global setting (they do not have a specific Google product category set) will use the new default immediately. %1$sIf you have cleared the Google Product Category%2$s, items inheriting the default will not be available for Instagram or Facebook checkout. Are you sure you want to proceed?', 'facebook-for-woocommerce' ), '', '' ); } @@ -164,7 +166,7 @@ public function render_title( $field ) {

- + get_connection_handler()->is_connected() ) : ?> 'yes', ], + [ + 'id' => \WC_Facebookcommerce_Integration::SETTING_FB_RETAILER_ID_TYPE, + 'title' => __( 'Product Identifier', 'facebook-for-woocommerce' ), + 'type' => 'select', + 'class' => 'product-sync-field', + 'desc_tip' => __( 'WooCommerce product attribute mapped to retailer_id on Facebook.', 'facebook-for-woocommerce' ), + 'desc' => __( 'WARNING: use with caution, as changing this option after syncing products to Facebook + may result in having duplicate products in your Facebook catalog.', 'facebook-for-woocommerce' ), + 'default' => \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_SKU_PRODUCT_ID, + 'options' => [ + \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_SKU_PRODUCT_ID => __( 'SKU + Product ID', 'facebook-for-woocommerce' ), + \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_SKU => __( 'SKU', 'facebook-for-woocommerce' ), + \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_PRODUCT_ID => __( 'Product ID', 'facebook-for-woocommerce' ), + ], + ], + [ 'id' => \WC_Facebookcommerce_Integration::SETTING_EXCLUDED_PRODUCT_CATEGORY_IDS, 'title' => __( 'Exclude categories from sync', 'facebook-for-woocommerce' ), diff --git a/includes/Commerce.php b/includes/Commerce.php index 07b62fb2e..bc753b783 100644 --- a/includes/Commerce.php +++ b/includes/Commerce.php @@ -23,7 +23,6 @@ class Commerce { /** @var string option that stores the plugin-level fallback Google product category ID */ const OPTION_GOOGLE_PRODUCT_CATEGORY_ID = 'wc_facebook_google_product_category_id'; - /** @var Commerce\Orders the orders handler */ protected $orders; @@ -114,7 +113,10 @@ public function is_connected() { $connection_handler = facebook_for_woocommerce()->get_connection_handler(); - $connected = (bool) strlen( $connection_handler->get_page_access_token() ) && ! empty( $connection_handler->get_commerce_manager_id() ); + $category_id = get_option( self::OPTION_GOOGLE_PRODUCT_CATEGORY_ID, '' ); + + $connected = (bool) strlen( $connection_handler->get_page_access_token() ) && ! empty( $connection_handler->get_commerce_merchant_settings_id() ) && + $connection_handler->is_onsite_checkout_connected(); /** * Filters whether the site is connected. diff --git a/includes/Commerce/Orders.php b/includes/Commerce/Orders.php index d76bc973c..fd9808085 100644 --- a/includes/Commerce/Orders.php +++ b/includes/Commerce/Orders.php @@ -170,11 +170,7 @@ public function update_local_order( Order $remote_order, \WC_Order $local_order // add/update items foreach ( $remote_order->get_items() as $item ) { - $product = Products::get_product_by_fb_product_id( $item['product_id'] ); - - if ( empty( $product ) ) { - $product = Products::get_product_by_fb_retailer_id( $item['retailer_id'] ); - } + $product = Products::get_product_by_fb_retailer_id( $item['retailer_id'] ); if ( ! $product instanceof \WC_Product ) { @@ -521,7 +517,13 @@ public function get_order_update_interval() { * @since 2.1.0 */ public function schedule_local_orders_update() { - if ( facebook_for_woocommerce()->get_commerce_handler()->is_connected() && false === as_next_scheduled_action( self::ACTION_FETCH_ORDERS, [], \WC_Facebookcommerce::PLUGIN_ID ) ) { + // only schedule if connected + if ( ! facebook_for_woocommerce()->get_commerce_handler()->is_connected() ) { + as_unschedule_all_actions( self::ACTION_FETCH_ORDERS ); + return; + } + + if ( ! as_next_scheduled_action( self::ACTION_FETCH_ORDERS, [], \WC_Facebookcommerce::PLUGIN_ID ) ) { $interval = $this->get_order_update_interval(); diff --git a/includes/Handlers/Connection.php b/includes/Handlers/Connection.php index 210a4a705..480b1581c 100644 --- a/includes/Handlers/Connection.php +++ b/includes/Handlers/Connection.php @@ -71,8 +71,11 @@ class Connection { /** @var string the page access token option name */ const OPTION_PAGE_ACCESS_TOKEN = 'wc_facebook_page_access_token'; - /** @var string the Commerce manager ID option name */ - const OPTION_COMMERCE_MANAGER_ID = 'wc_facebook_commerce_manager_id'; + /** @var bool option that stores the Commerce setup status */ + const OPTION_COMMERCE_SETUP_COMPLETE = 'wc_facebook_commerce_setup_complete'; + + /** @var bool option that stores the onsite checkout connected state */ + const OPTION_ONSITE_CHECKOUT_CONNECTED = 'wc_facebook_onsite_checkout_connected'; /** @var string webhook event subscribed object */ const WEBHOOK_SUBSCRIBED_OBJECT = 'user'; @@ -110,6 +113,8 @@ public function __construct( \WC_Facebookcommerce $plugin ) { add_action( 'admin_action_' . self::ACTION_DISCONNECT, [ $this, 'handle_disconnect' ] ); + add_action( 'woocommerce_api_' . self::ACTION_CONNECT_COMMERCE, [ $this, 'handle_connect_commerce' ] ); + add_action( 'woocommerce_api_' . self::ACTION_FBE_REDIRECT, [ $this, 'handle_fbe_redirect' ] ); add_action( 'fbe_webhook', array( $this, 'fbe_install_webhook' ) ); @@ -216,6 +221,8 @@ private function update_installation_data() { $page_id = sanitize_text_field( $response->get_page_id() ); + $cms_id = sanitize_text_field( $response->get_commerce_merchant_settings_id() ); + if ( $page_id ) { update_option( \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, $page_id ); @@ -224,6 +231,14 @@ private function update_installation_data() { $page_access_token = $this->retrieve_page_access_token( $page_id ); $this->update_page_access_token( $page_access_token ); + + if ( !$cms_id ) { + // attempt to fetch commerce merchant settings id for the configured page, if none is set for FBE + $page_response = facebook_for_woocommerce()->get_api()->get_page( $page_id ); + if ( $cms = $page_response->get_commerce_merchant_settings() ) { + $cms_id = sanitize_text_field( $cms->id ); + } + } } if ( $response->get_pixel_id() ) { @@ -246,8 +261,38 @@ private function update_installation_data() { $this->update_instagram_business_id( sanitize_text_field( $response->get_instagram_business_id() ) ); } - if ( $response->get_commerce_merchant_settings_id() ) { - $this->update_commerce_merchant_settings_id( sanitize_text_field( $response->get_commerce_merchant_settings_id() ) ); + if ( $cms_id ) { + $this->update_commerce_installation_data( $cms_id ); + } + } + + + /** + * Retrieves and stores the connected Commerce account installation data. + * Use following guidelines to determine eligibility for Order Management: + * https://developers.facebook.com/docs/commerce-platform/platforms/onboarding/troubleshooting#shop_setup_status + * @since 2.3.0 + * + * @param string $cms_id Commerce Merchant Settings ID + * @throws SV_WC_API_Exception + */ + public function update_commerce_installation_data( $cms_id ) { + $response = facebook_for_woocommerce()->get_api()->get_commerce_merchant_settings( $cms_id ); + $this->update_commerce_merchant_settings_id( $cms_id ); + + $onsite_intent = $response->has_onsite_intent(); + $setup_status = $response->get_setup_status(); + + $complete = (( $response->has_onsite_intent() || $response->get_cta() === 'ONSITE_CHECKOUT' ) && + $setup_status && + $setup_status->shop_setup === 'SETUP' && + $setup_status->payment_setup !== 'NOT_SETUP' + ); + $this->update_commerce_setup_complete( $complete ); + + if ( $complete ) { + $oma_response = facebook_for_woocommerce()->get_api()->get_order_management_apps( $cms_id ); + $this->update_onsite_checkout_connected( in_array( $this->get_client_id(), $oma_response->get_apps() ) ); } } @@ -370,6 +415,8 @@ private function disconnect() { $this->update_system_user_id( '' ); $this->update_business_manager_id( '' ); $this->update_ad_account_id( '' ); + $this->update_commerce_setup_complete( false ); + $this->update_onsite_checkout_connected( false ); $this->update_instagram_business_id( '' ); $this->update_commerce_merchant_settings_id( '' ); @@ -381,6 +428,71 @@ private function disconnect() { } + /** + * Handles the connection redirect from Commerce onboarding. + * + * @internal + * + * @since 2.1.0-dev.1 + */ + public function handle_connect_commerce() { + + // don't handle anything unless the user can manage WooCommerce settings + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + try { + + facebook_for_woocommerce()->log( 'Processing Facebook commerce connection.' ); + + if ( empty( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], self::ACTION_CONNECT_COMMERCE ) ) { + throw new SV_WC_API_Exception( 'Invalid nonce' ); + } + + $cms_id = ! empty( $_GET['commerce_manager_id'] ) ? sanitize_text_field( $_GET['commerce_manager_id'] ) : ''; + + if ( ! $cms_id ) { + throw new SV_WC_API_Exception( 'Commerce Merchant Settings ID is missing' ); + } + + // store the commerce manager ID if queried successfully + $this->update_commerce_installation_data( $cms_id ); + + // if setup is complete but not already connected, need to enable order management + if ( $this->is_commerce_setup_complete() && ! $this->is_onsite_checkout_connected() ) { + // get a current access token for the configured page + $page_access_token = $this->retrieve_page_access_token( facebook_for_woocommerce()->get_integration()->get_facebook_page_id() ); + + $this->update_page_access_token( $page_access_token ); + + // allow the commerce manager to manage orders for the page shop + $this->enable_order_management( $cms_id, $page_access_token ); + $this->update_onsite_checkout_connected( true ); + } + + if ( $this->is_onsite_checkout_connected() ) { + facebook_for_woocommerce()->get_message_handler()->add_message( __( 'Connection complete! Thanks for using Facebook for WooCommerce.', 'facebook-for-woocommerce' ) ); + } + + } catch ( SV_WC_API_Exception $exception ) { + + $message = sprintf( + /* translators: Placeholders: %s - connection error message */ + __( 'Connection failed: %s', 'facebook-for-woocommerce' ), + $exception->getMessage() + ); + + facebook_for_woocommerce()->log( $message ); + + facebook_for_woocommerce()->get_message_handler()->add_error( $message ); + } + + wp_safe_redirect( add_query_arg( 'tab', 'commerce', facebook_for_woocommerce()->get_settings_url() ) ); + exit; + } + + /** * Retrieves the configured page access token remotely. * @@ -428,6 +540,37 @@ private function retrieve_page_access_token( $page_id ) { } + /** + * Enables order management for the given Commerce Account. + * + * @since 2.4.0 + * + * @param string $cms_id Commerce Merchant Settings ID + * @param string $page_access_token page access token + * @throws SV_WC_API_Exception + */ + private function enable_order_management( $cms_id, $page_access_token ) { + + facebook_for_woocommerce()->log( 'Enabling order management' ); + + $response = wp_remote_post( "https://graph.facebook.com/{$cms_id}/order_management_apps?access_token={$page_access_token}" ); + + $body = wp_remote_retrieve_body( $response ); + $body = json_decode( $body, true ); + + if ( ! is_array( $body ) || empty( $body['success'] ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + + facebook_for_woocommerce()->log( print_r( $body, true ) ); + + throw new SV_WC_API_Exception( sprintf( + /* translators: Placeholders: %s - API error message */ + __( 'Could not enable order management. %s', 'facebook for woocommerce' ), + wp_remote_retrieve_response_message( $response ) + ) ); + } + } + + /** * Gets the API access token. * @@ -531,7 +674,7 @@ public function get_connect_url( $connect_commerce = false ) { * * @return string */ - public function get_commerce_connect_url() { + public function get_commerce_connect_url( $cms_id = null ) { // build the site URL to which the user will ultimately return $site_url = add_query_arg( [ @@ -539,14 +682,20 @@ public function get_commerce_connect_url() { 'nonce' => wp_create_nonce( self::ACTION_CONNECT_COMMERCE ), ], home_url( '/' ) ); - // build the proxy app URL where the user will land after onboarding, to be redirected to the site URL - $redirect_url = add_query_arg( 'site_url', urlencode( $site_url ), 'https://connect.woocommerce.com/auth/facebookcommerce/' ); + if ( $cms_id ) { + $connect_url = add_query_arg( 'commerce_manager_id', $cms_id, $site_url ); + } else { + // build the proxy app URL where the user will land after onboarding, to be redirected to the site URL + $redirect_url = add_query_arg( 'site_url', urlencode( $site_url ), 'https://connect.woocommerce.com/auth/facebookcommerce/' ); - // build the final connect URL, direct to Facebook - $connect_url = add_query_arg( [ - 'app_id' => $this->get_client_id(), // this endpoint calls the client ID "app ID" - 'redirect_url' => urlencode( $redirect_url ), - ], 'https://www.facebook.com/commerce_manager/onboarding/' ); + // build the final connect URL, direct to Facebook + $connect_url = add_query_arg( [ + 'app_id' => $this->get_client_id(), // this endpoint calls the client ID "app ID" + 'app_redirect_uri' => urlencode( $redirect_url ), + 'external_business_id' => $this->get_external_business_id(), + 'tab' => 'Commerce', + ], 'https://www.facebook.com/facebook_business_extension' ); + } /** * Filters the URL used to connect to Facebook Commerce. @@ -559,6 +708,22 @@ public function get_commerce_connect_url() { } + /** + * Gets the URL to manage the Commerce connection. + * + * @since 2.4.0 + * + * @return string + */ + public function get_commerce_manage_url() { + + $app_id = $this->get_client_id(); + $business_id = $this->get_external_business_id(); + + return "https://www.facebook.com/facebook_business_extension?app_id={$app_id}&external_business_id={$business_id}&tab=Commerce"; + } + + /** * Gets the URL to manage the connection. * @@ -604,6 +769,7 @@ public function get_scopes() { 'catalog_management', 'ads_management', 'ads_read', + 'instagram_basic', 'pages_read_engagement', // this scope is needed to enable order management if using the Commerce feature 'instagram_basic', ]; @@ -741,19 +907,6 @@ public function get_system_user_id() { } - /** - * Gets the Commerce manager ID value. - * - * @since 2.1.0 - * - * @return string - */ - public function get_commerce_manager_id() { - - return get_option( self::OPTION_COMMERCE_MANAGER_ID, '' ); - } - - /** * Gets Instagram Business ID value. * @@ -900,8 +1053,8 @@ private function get_connect_parameters_extras() { 'timezone' => $this->get_timezone_string(), 'currency' => get_woocommerce_currency(), 'business_vertical' => 'ECOMMERCE', - 'domain' => home_url(), - 'channel' => 'COMMERCE_OFFSITE', + 'channel' => 'COMMERCE', + 'domain' => (string) apply_filters( 'wc_facebook_connection_domain_url', home_url( ',') ), ], 'business_config' => [ 'business' => [ @@ -992,15 +1145,15 @@ public function update_system_user_id( $value ) { /** - * Stores the given Commerce manager ID. + * Stores the given onsite checkout connected state. * - * @since 2.1.0 + * @since 2.3.0 * - * @param string $id the ID + * @param bool $connected The onsite checkout state */ - public function update_commerce_manager_id( $id ) { + public function update_onsite_checkout_connected( $connected ) { - update_option( self::OPTION_COMMERCE_MANAGER_ID, $id ); + update_option( self::OPTION_ONSITE_CHECKOUT_CONNECTED, $connected ); } @@ -1017,6 +1170,19 @@ public function update_instagram_business_id( $id ) { } + /** + * Stores the Commerce setup completion state. + * + * @since 2.3.0 + * + * @param bool $connected The Commerce setup completion state. + */ + public function update_commerce_setup_complete( $complete ) { + + update_option( self::OPTION_COMMERCE_SETUP_COMPLETE, $complete ); + } + + /** * Stores the given Commerce merchant settings ID. * @@ -1084,6 +1250,32 @@ public function is_connected() { } + /** + * Determines whether onsite checkout is connected. + * + * @since 2.3.0 + * + * @return bool + */ + public function is_onsite_checkout_connected() { + + return get_option( self::OPTION_ONSITE_CHECKOUT_CONNECTED, false ); + } + + + /** + * Determines whether Commerce setup is complete. + * + * @since 2.3.0 + * + * @return bool + */ + public function is_commerce_setup_complete() { + + return get_option( self::OPTION_COMMERCE_SETUP_COMPLETE, false ); + } + + /** * Determines whether the site has previously connected to FBE 2. * @@ -1261,7 +1453,7 @@ function( $change ) { } if ( ! empty( $values->commerce_merchant_settings_id ) ) { - $this->update_commerce_merchant_settings_id( sanitize_text_field( $values->commerce_merchant_settings_id ) ); + $this->update_commerce_installation_data( sanitize_text_field( $values->commerce_merchant_settings_id ) ); $log_data[ self::OPTION_COMMERCE_MERCHANT_SETTINGS_ID ] = sanitize_text_field( $values->commerce_merchant_settings_id ); } diff --git a/includes/Lifecycle.php b/includes/Lifecycle.php index cde4448b4..6b1af2885 100644 --- a/includes/Lifecycle.php +++ b/includes/Lifecycle.php @@ -43,6 +43,7 @@ public function __construct( $plugin ) { '2.0.3', '2.0.4', '2.3.6', + '3.0.0', ]; } @@ -347,4 +348,19 @@ protected function upgrade_to_2_3_6() { delete_transient( 'wc_facebook_google_product_categories' ); } + /** + * Upgrades to version 3.0.0 ft. Instagram Checkout + * + * @since 2.4.0 + */ + protected function upgrade_to_3_0_0() { + + // Migrate previous Commerce Manager ID to Commerce Merchant Settings ID if one does not exist + $old_cms_id = get_option( 'wc_facebook_commerce_manager_id' ); + if ( $old_cms_id && false === get_option( 'wc_facebook_commerce_merchant_settings_id' ) ) { + update_option( 'wc_facebook_commerce_merchant_settings_id', $old_cms_id ); + } + } + + } diff --git a/includes/Products.php b/includes/Products.php index d7ed89727..d1689a01e 100644 --- a/includes/Products.php +++ b/includes/Products.php @@ -43,9 +43,6 @@ class Products { /** @var string product image source option to use the parent product image in Facebook */ const PRODUCT_IMAGE_SOURCE_CUSTOM = 'custom'; - /** @var string the meta key used to flag if Commerce is enabled for the product */ - const COMMERCE_ENABLED_META_KEY = '_wc_facebook_commerce_enabled'; - /** @var string the meta key used to store the Google product category ID for the product */ const GOOGLE_PRODUCT_CATEGORY_META_KEY = '_wc_facebook_google_product_category'; @@ -457,55 +454,6 @@ public static function get_product_price( \WC_Product $product ) { } - /** - * Determines whether the product meets all of the criteria needed for Commerce. - * - * @since 2.1.0 - * - * @param \WC_Product $product the product object - */ - public static function is_product_ready_for_commerce( \WC_Product $product ) { - - return $product->managing_stock() - && self::get_product_price( $product ) - && self::is_commerce_enabled_for_product( $product ) - && self::product_should_be_synced( $product ); - } - - - /** - * Determines whether Commerce is enabled for the product. - * - * @since 2.1.0 - * - * @param \WC_Product $product the product object - * @return bool - */ - public static function is_commerce_enabled_for_product( \WC_Product $product ) { - - if ( $product->is_type( 'variation' ) ) { - $product = wc_get_product( $product->get_parent_id() ); - } - - return $product instanceof \WC_Product && wc_string_to_bool( $product->get_meta( self::COMMERCE_ENABLED_META_KEY ) ); - } - - - /** - * Enables or disables Commerce for a product. - * - * @since 2.1.0 - * - * @param \WC_Product $product the product object - * @param bool $is_enabled whether or not Commerce is to be enabled - */ - public static function update_commerce_enabled_for_product( \WC_Product $product, $is_enabled ) { - - $product->update_meta_data( self::COMMERCE_ENABLED_META_KEY, wc_bool_to_string( $is_enabled ) ); - $product->save_meta_data(); - } - - /** * Gets the Google product category ID stored for the product. * @@ -1257,50 +1205,6 @@ public static function get_distinct_product_attributes( \WC_Product $product ) { } - /** - * Gets a product by its Facebook product ID, from the `fb_product_item_id` or `fb_product_group_id`. - * - * @since 2.1.0 - * - * @param string $fb_product_id Facebook product ID - * @return \WC_Product|null - */ - public static function get_product_by_fb_product_id( $fb_product_id ) { - - $product = null; - - // try to by the `fb_product_item_id` meta - $products = wc_get_products( - array( - 'limit' => 1, - 'meta_key' => \WC_Facebookcommerce_Integration::FB_PRODUCT_ITEM_ID, - 'meta_value' => $fb_product_id, - ) - ); - - if ( ! empty( $products ) ) { - $product = current( $products ); - } - - if ( empty( $product ) ) { - // try to by the `fb_product_group_id` meta - $products = wc_get_products( - array( - 'limit' => 1, - 'meta_key' => \WC_Facebookcommerce_Integration::FB_PRODUCT_GROUP_ID, - 'meta_value' => $fb_product_id, - ) - ); - - if ( ! empty( $products ) ) { - $product = current( $products ); - } - } - - return ! empty( $product ) ? $product : null; - } - - /** * Gets a product by its Facebook retailer ID. * @@ -1316,7 +1220,17 @@ public static function get_product_by_fb_retailer_id( $fb_retailer_id ) { if ( strpos( $fb_retailer_id, \WC_Facebookcommerce_Utils::FB_RETAILER_ID_PREFIX ) !== false ) { $product_id = str_replace( \WC_Facebookcommerce_Utils::FB_RETAILER_ID_PREFIX, '', $fb_retailer_id ); } else { - $product_id = substr( $fb_retailer_id, strrpos( $fb_retailer_id, '_' ) + 1 ); + switch ( get_option( \WC_Facebookcommerce_Integration::SETTING_FB_RETAILER_ID_TYPE )) { + case \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_SKU: + $product_id = wc_get_product_id_by_sku( $fb_retailer_id ); + break; + case \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_PRODUCT_ID: + $product_id = $fb_retailer_id; + break; + default: + $product_id = substr( $fb_retailer_id, strrpos( $fb_retailer_id, '_' ) + 1 ); + break; + } } $product = wc_get_product( $product_id ); diff --git a/includes/fbgraph.php b/includes/fbgraph.php index dea1aa713..247c61f84 100644 --- a/includes/fbgraph.php +++ b/includes/fbgraph.php @@ -363,38 +363,6 @@ public function is_product_catalog_valid( $product_catalog_id ) { } - // POST https://graph.facebook.com/vX.X/{product-catalog-id}/product_groups - public function create_product_group( $product_catalog_id, $data ) { - $url = $this->build_url( $product_catalog_id, '/product_groups' ); - return self::_post( $url, $data ); - } - - // POST https://graph.facebook.com/vX.X/{product-group-id}/products - public function create_product_item( $product_group_id, $data ) { - $url = $this->build_url( $product_group_id, '/products' ); - return self::_post( $url, $data ); - } - - public function update_product_group( $product_catalog_id, $data ) { - $url = $this->build_url( $product_catalog_id ); - return self::_post( $url, $data ); - } - - public function update_product_item( $product_id, $data ) { - $url = $this->build_url( $product_id ); - return self::_post( $url, $data ); - } - - public function delete_product_item( $product_item_id ) { - $product_item_url = $this->build_url( $product_item_id ); - return self::_delete( $product_item_url ); - } - - public function delete_product_group( $product_group_id ) { - $product_group_url = $this->build_url( $product_group_id ); - return self::_delete( $product_group_url ); - } - // POST https://graph.facebook.com/vX.X/{product-catalog-id}/product_sets public function create_product_set_item( $product_catalog_id, $data ) { $url = $this->build_url( $product_catalog_id, '/product_sets' ); @@ -586,11 +554,6 @@ public function get_asset_ids( $external_business_id ) { } - public function set_default_variant( $product_group_id, $data ) { - $url = $this->build_url( $product_group_id ); - return self::_post( $url, $data ); - } - private function build_url( $field_id, $param = '', $api_version = '' ) { $api_url = self::GRAPH_API_URL; if ( ! empty( $api_version ) ) { diff --git a/includes/fbproduct.php b/includes/fbproduct.php index a6ce2f233..a60471345 100644 --- a/includes/fbproduct.php +++ b/includes/fbproduct.php @@ -603,8 +603,8 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel $product_data = $this->apply_enhanced_catalog_fields_from_attributes( $product_data, $google_product_category ); } - // add the Commerce values (only inventory for the moment) - if ( Products::is_product_ready_for_commerce( $this->woo_product ) ) { + // Add inventory if managing stock + if ( $this->woo_product->managing_stock() ) { $product_data['inventory'] = (int) max( 0, $this->woo_product->get_stock_quantity() ); } diff --git a/includes/fbutils.php b/includes/fbutils.php index c25ed501f..b749d0950 100644 --- a/includes/fbutils.php +++ b/includes/fbutils.php @@ -90,12 +90,25 @@ public static function make_url( $url ) { * @return string */ public static function get_fb_retailer_id( $woo_product ) { - $woo_id = $woo_product->get_id(); - // Call $woo_product->get_id() instead of ->id to account for Variable // products, which have their own variant_ids. - return $woo_product->get_sku() ? $woo_product->get_sku() . '_' . - $woo_id : self::FB_RETAILER_ID_PREFIX . $woo_id; + $woo_id = $woo_product->get_id(); + $sku = $woo_product->get_sku(); + $retailer_id = $sku ? $sku . '_' . $woo_id : self::FB_RETAILER_ID_PREFIX . $woo_id; + + switch ( get_option( \WC_Facebookcommerce_Integration::SETTING_FB_RETAILER_ID_TYPE )) { + case \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_SKU: + $retailer_id = $sku; + break; + case \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_PRODUCT_ID: + $retailer_id = strval($woo_id); + break; + default: + // \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_SKU_PRODUCT_ID + break; + } + + return $retailer_id; } /** diff --git a/readme.txt b/readme.txt index 267f23880..170c46810 100644 --- a/readme.txt +++ b/readme.txt @@ -39,6 +39,12 @@ When opening a bug on GitHub, please give us as many details as possible. == Changelog == += 2021.nn.nn - version 3.0.0-dev.1 = + * Feature - Integrate with Instagram Checkout so customers can purchase your products directly on Facebook platforms + * Feature - Manage Facebook & Instagram generated orders in WooCommerce + * Feature - Sync product inventory counts between your Facebook catalog and WooCommerce shop + * Feature - Define custom product attributes such as gender, size, color, and pattern + = 2021.03.31 - version 2.3.5 = * Fix - critical issue for pre 5.0.0 WC sites diff --git a/tests/acceptance/Admin/OrderRefundsCest.php b/tests/acceptance/Admin/OrderRefundsCest.php new file mode 100644 index 000000000..b0f62bd02 --- /dev/null +++ b/tests/acceptance/Admin/OrderRefundsCest.php @@ -0,0 +1,45 @@ +haveOptionInDatabase( Connection::OPTION_ACCESS_TOKEN, '1234' ); + $I->haveOptionInDatabase( WC_Facebookcommerce_Integration::OPTION_PRODUCT_CATALOG_ID, '1234' ); + + $I->haveOptionInDatabase( Connection::OPTION_PAGE_ACCESS_TOKEN, '1234' ); + $I->haveOptionInDatabase( Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, '1234' ); + + // always log in + $I->loginAsAdmin(); + } + + + /** + * Tests that on the edit screen page of a pending order created via Commerce, the order actions metabox is hidden. + * + * @param \AcceptanceTester $I + * @throws \Exception + */ + public function try_maybe_remove_order_metaboxes( AcceptanceTester $I ) { + + $order = $I->haveOrderInDatabase(); + $order->set_status( 'pending' ); + $order->set_created_via( 'instagram' ); + $order->save(); + + $I->amOnOrderPage( $order->get_id() ); + $I->dontSeeElement( [ 'css' => '#woocommerce-order-actions' ] ); + } + + +} diff --git a/tests/acceptance/Admin/Orders/CommerceCest.php b/tests/acceptance/Admin/Orders/CommerceCest.php new file mode 100644 index 000000000..e728ed8ff --- /dev/null +++ b/tests/acceptance/Admin/Orders/CommerceCest.php @@ -0,0 +1,435 @@ +haveOptionInDatabase( Connection::OPTION_ACCESS_TOKEN, '1234' ); + $I->haveOptionInDatabase( WC_Facebookcommerce_Integration::OPTION_PRODUCT_CATALOG_ID, '1234' ); + + $I->haveOptionInDatabase( Connection::OPTION_PAGE_ACCESS_TOKEN, '1234' ); + $I->haveOptionInDatabase( Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, '1234' ); + + // always log in + $I->loginAsAdmin(); + } + + + /** + * Test that the order list table action buttons are not displayed for Commerce orders. + * + * @param AcceptanceTester $I tester instance + * @throws \WC_Data_Exception + */ + public function try_order_list_table_actions_removed( AcceptanceTester $I ) { + + $commerce_order = $this->get_commerce_order( '1234' ); + + $woo_order = new \WC_Order(); + $woo_order->set_status( 'pending' ); + $woo_order->save(); + + $I->amOnOrdersPage(); + + // ensure the Actions column isn't hidden + $I->click( '#show-settings-link' ); + $I->checkOption( '#wc_actions-hide' ); + + // not present for commerce orders + $I->dontSeeElement( "tr#post-{$commerce_order->get_order_number()} .wc-action-button-processing" ); + $I->dontSeeElement( "tr#post-{$commerce_order->get_order_number()} .wc-action-button-complete" ); + + // present for non-commerce orders + $I->seeElement( "tr#post-{$woo_order->get_order_number()} .wc-action-button-processing" ); + $I->seeElement( "tr#post-{$woo_order->get_order_number()} .wc-action-button-complete" ); + } + + + /** + * Test that the order preview modal status action buttons are not displayed for Commerce orders. + * + * @param AcceptanceTester $I tester instance + * @throws \WC_Data_Exception + */ + public function try_order_preview_actions_removed( AcceptanceTester $I ) { + + $commerce_order = $this->get_commerce_order( '1234' ); + + $woo_order = new \WC_Order(); + $woo_order->set_status( 'pending' ); + $woo_order->save(); + + $I->amOnOrdersPage(); + + $I->click( "tr#post-{$commerce_order->get_order_number()} .order-preview" ); + $I->waitForElementVisible( '.wc-order-preview' ); + + $I->dontSeeElement( '.wc-order-preview .wc-action-button-group' ); + + $I->click( '.modal-close' ); + + $I->click( "tr#post-{$woo_order->get_order_number()} .order-preview" ); + $I->waitForElementVisible( '.wc-order-preview' ); + + $I->seeElement( '.wc-order-preview .wc-action-button-group' ); + } + + + /** + * Test that only supported statuses are available on the Edit Order screen. + * + * @param AcceptanceTester $I tester instance + * @throws \WC_Data_Exception + */ + public function try_edit_order_statuses( AcceptanceTester $I ) { + + $commerce_order = $this->get_commerce_order( '1234' ); + + $I->amOnOrderPage( $commerce_order->get_id() ); + + $I->canSeeOptionIsSelected( '#order_status', 'Pending payment' ); + + $I->cantSeeElement( '#order_status option[value="wc-processing"]' ); + + $commerce_order->set_status( 'processing' ); + $commerce_order->save(); + + $I->amOnOrderPage( $commerce_order->get_id() ); + + $I->canSeeOptionIsSelected( '#order_status', 'Processing' ); + + $I->canSeeElement( '#order_status option[value="wc-completed"]' ); + $I->canSeeElement( '#order_status option[value="wc-cancelled"]' ); + $I->cantSeeElement( '#order_status option[value="wc-pending"]' ); + $I->cantSeeElement( '#order_status option[value="wc-refunded"]' ); + + $commerce_order->set_status( 'completed' ); + $commerce_order->save(); + + $I->amOnOrderPage( $commerce_order->get_id() ); + + $I->canSeeOptionIsSelected( '#order_status', 'Completed' ); + + $I->canSeeElement( '#order_status option[value="wc-refunded"]' ); + $I->cantSeeElement( '#order_status option[value="wc-pending"]' ); + $I->cantSeeElement( '#order_status option[value="wc-processing"]' ); + + $commerce_order->set_status( 'cancelled' ); + $commerce_order->save(); + + $I->amOnOrderPage( $commerce_order->get_id() ); + + $I->canSeeOptionIsSelected( '#order_status', 'Cancelled' ); + + $I->cantSeeElement( '#order_status option[value="wc-pending"]' ); + $I->cantSeeElement( '#order_status option[value="wc-processing"]' ); + $I->cantSeeElement( '#order_status option[value="wc-completed"]' ); + $I->cantSeeElement( '#order_status option[value="wc-refunded"]' ); + + $commerce_order->set_status( 'refunded' ); + $commerce_order->save(); + + $I->amOnOrderPage( $commerce_order->get_id() ); + + $I->canSeeOptionIsSelected( '#order_status', 'Refunded' ); + + $I->cantSeeElement( '#order_status option[value="wc-pending"]' ); + $I->cantSeeElement( '#order_status option[value="wc-processing"]' ); + $I->cantSeeElement( '#order_status option[value="wc-completed"]' ); + $I->cantSeeElement( '#order_status option[value="wc-cancelled"]' ); + } + + + /** + * Test that an order can be cancelled using the Cancel Order modal. + * + * @param AcceptanceTester $I tester instance + */ + public function try_successfully_cancelling_an_order( AcceptanceTester $I ) { + + $remote_id = '1234'; + + $order = $this->get_order_to_cancel( $I, $remote_id ); + + $this->prepare_request_response( $I, 'POST', "/{$remote_id}/cancellations", [ + 'response_body' => json_encode( [ 'id' => '7890' ] ), + ] ); + + $I->amEditingPostWithId( $order->get_id() ); + + $I->wantTo( 'test that an order can be successfully canceled using the Cancel Order modal' ); + + $I->amGoingTo( 'set the order status to Cancelled and update the order' ); + + $I->executeJS( "jQuery( '#order_status' ).val( 'wc-cancelled' ).trigger( 'change' )" ); + $I->click( 'button[name="save"]' ); + + $I->see( 'Select a reason for cancelling this order:', '.facebook-for-woocommerce-modal' ); + + $I->amGoingTo( 'set the cancel reason and confirm the modal action' ); + + $I->selectOption( '#wc_facebook_cancel_reason', 'CUSTOMER_REQUESTED' ); + $I->click( '.facebook-for-woocommerce-modal #btn-ok' ); + + $I->expect( 'the order to be updated' ); + + $I->waitForText( 'Order updated.', 15 ); + $I->assertEquals( 'wc-cancelled', $I->executeJS( "return jQuery( '#order_status' ).val()" ) ); + } + + + /** + * Gets a new order object to be used in the Cancel Order modal tests. + * + * @param AcceptanceTester $I tester instance + * @param string $remote_id Facebook order ID + * @return \WC_Order + */ + private function get_order_to_cancel( AcceptanceTester $I, string $remote_id ) { + + $item = new \WC_Order_Item_Product(); + $item->set_name( 'Test' ); + $item->set_quantity( 2 ); + $item->set_total( 1.00 ); + $item->set_product( $I->haveProductInDatabase() ); + $item->save(); + + $order = new \WC_Order(); + $order->set_billing_first_name( 'John' ); + $order->set_billing_last_name( 'Doe' ); + $order->set_status( 'processing' ); + $order->add_item( $item ); + $order->update_meta_data( Orders::REMOTE_ID_META_KEY, $remote_id ); + $order->set_created_via( 'facebook' ); + $order->save(); + + return $order; + } + + + /** + * Creates a must use plugin that overwrites the response for the given HTTP request. + * + * @param AcceptanceTester $I tester instance + * @param string $method request method + * @param string $path partial path to match against HTTP request URLs + * @param array $args response parameters + */ + private function prepare_request_response( AcceptanceTester $I, $method, $path, $args = [] ) { + + $args = wp_parse_args( $args, [ + 'request_method' => $method, + 'request_path' => $path, + 'response_headers' => [], + 'response_cookies' => [], + 'response_body' => [], + 'response_code' => 200, + 'response_message' => 'Ok', + ] ); + + $args['response_headers'] = json_encode( $args['response_headers'] ); + $args['response_cookies'] = json_encode( $args['response_cookies'] ); + $args['response_body'] = json_encode( $args['response_body'] ); + + $code = << json_decode( '{$args['response_headers']}' ), + 'cookies' => json_decode( '{$args['response_cookies']}' ), + 'body' => json_decode( '{$args['response_body']}' ), + 'response' => [ + 'code' => '{$args['response_code']}', + 'message' => '{$args['response_message']}', + ], + 'http_response' => null, + ]; + } + + return \$response; + +}, 10, 3 ); +PHP; + + $I->haveMuPlugin( sprintf( 'pre-http-request-%s.php', sanitize_file_name( $args['request_path'] ) ), $code ); + } + + + /** + * Test that merchants can decide not to cancel the order through the Cancel Order modal. + * + * @param AcceptanceTester $I tester instance + */ + public function try_deciding_not_to_cancel_the_order( AcceptanceTester $I ) { + + $remote_id = '1234'; + + $order = $this->get_order_to_cancel( $I, $remote_id ); + + $I->amEditingPostWithId( $order->get_id() ); + + $I->wantTo( 'test that I can close the Cancel Order modal without cancelling the order' ); + + $I->amGoingTo( 'set the order status to Cancelled and update the order' ); + + $I->executeJS( "jQuery( '#order_status' ).val( 'wc-cancelled' ).trigger( 'change' )" ); + $I->click( 'button[name="save"]' ); + + $I->see( 'Select a reason for cancelling this order:', '.facebook-for-woocommerce-modal' ); + + $I->amGoingTo( 'click the Cancel button' ); + + $I->click( '.wc-facebook-modal-cancel-button' ); + + $I->expect( 'the modal to disappear' ); + + $I->dontSeeElement( '.facebook-for-woocommerce-modal' ); + } + + + /** + * Test that the Cancel Order modal shows API errors. + * + * @param AcceptanceTester $I tester instance + */ + public function try_cancelling_an_order_with_api_error( AcceptanceTester $I ) { + + $remote_id = '1234'; + + $order = $this->get_order_to_cancel( $I, $remote_id ); + + $this->prepare_request_response( $I, 'POST', "/{$remote_id}/cancellations", [ + 'response_body' => json_encode( [ + 'error' => [ + 'type' => 'Error', + 'message' => 'Facebook API error.', + 'error_user_msg' => 'Super detailed error message.', + ], + ] ), + ] ); + + $I->amEditingPostWithId( $order->get_id() ); + + $I->wantTo( 'test that an error is shown if the cancel order API request returns an error' ); + + $I->amGoingTo( 'set the order status to Cancelled and update the order' ); + + $I->executeJS( "jQuery( '#order_status' ).val( 'wc-cancelled' ).trigger( 'change' )" ); + $I->click( 'button[name="save"]' ); + + $I->see( 'Select a reason for cancelling this order:', '.facebook-for-woocommerce-modal' ); + + $I->amGoingTo( 'set the cancel reason and confirm the modal action' ); + + $I->selectOption( '#wc_facebook_cancel_reason', 'CUSTOMER_REQUESTED' ); + $I->click( '.facebook-for-woocommerce-modal #btn-ok' ); + + $I->expect( 'an error to be shown' ); + + $I->waitForText( 'Error: Super detailed error message.', 15, '.facebook-for-woocommerce-modal' ); + } + + + /** + * Test that the Cancel Order modal does not show up for already cancelled orders. + * + * @param AcceptanceTester $I tester instance + */ + public function try_saving_an_already_cancelled_order( AcceptanceTester $I ) { + + $remote_id = '1234'; + + $order = $this->get_order_to_cancel( $I, $remote_id ); + + $order->set_status( 'cancelled' ); + $order->save(); + + $I->amEditingPostWithId( $order->get_id() ); + + $I->wantTo( 'test that the Cancel Order modal does not show up for already cancelled orders' ); + + $I->expect( 'the order status is already set to cancelled' ); + + $I->assertEquals( 'wc-cancelled', $I->executeJS( "return jQuery( '#order_status' ).val()" ) ); + + $I->amGoingTo( 'update the order' ); + $I->click( 'button[name="save"]' ); + + $I->expect( 'the order to be updated' ); + + $I->waitForText( 'Order updated.', 15 ); + $I->assertEquals( 'wc-cancelled', $I->executeJS( "return jQuery( '#order_status' ).val()" ) ); + } + + + /** + * Tests that the refund reason fields are shown. + * + * @param AcceptanceTester $I + */ + public function try_refund_fields_are_present( AcceptanceTester $I ) { + + $remote_id = '1234'; + + $order = $this->get_order_to_cancel( $I, $remote_id ); + + $order->set_status( 'completed' ); + $order->save(); + + $I->amEditingPostWithId( $order->get_id() ); + + $I->wantTo( 'test that the Commerce refund reason fields are shown' ); + + $I->amGoingTo( 'click the Refund button' ); + + $I->click( 'button.refund-items' ); + + $I->expect( 'that the Facebook refund reason field is present' ); + + $I->waitForElementVisible( '#wc_facebook_refund_reason', 15 ); + $I->see( 'Refund reason:', 'label' ); + + $I->expect( 'that the refund description field is present' ); + + $I->seeElement( '#refund_reason' ); + $I->see( 'Refund description (optional):', 'label' ); + + $I->amGoingTo( 'select a Commerce refund reason and enter a refund description' ); + + $I->selectOption( '#wc_facebook_refund_reason', 'NOT_AS_DESCRIBED' ); + $I->fillField( '#refund_reason', 'I have my reasons' ); + } + + + /** + * Gets an order with the minimum commerce data. + * + * @param string $remote_id desired remote ID + * @return \WC_Order $order + * @throws \WC_Data_Exception + */ + private function get_commerce_order( string $remote_id ) { + + $order = new \WC_Order(); + $order->set_created_via( 'instagram' ); + $order->update_meta_data( Orders::REMOTE_ID_META_KEY, $remote_id ); + $order->set_status( 'pending' ); + $order->save(); + + return $order; + } + + +} diff --git a/tests/acceptance/Admin/ProductCommerceSettingsCest.php b/tests/acceptance/Admin/ProductCommerceSettingsCest.php index 97a36b75f..121f4e500 100644 --- a/tests/acceptance/Admin/ProductCommerceSettingsCest.php +++ b/tests/acceptance/Admin/ProductCommerceSettingsCest.php @@ -26,13 +26,13 @@ public function _before( AcceptanceTester $I ) { */ $I->haveOptionInDatabase( Connection::OPTION_ACCESS_TOKEN, '1234' ); $I->haveOptionInDatabase( Connection::OPTION_PAGE_ACCESS_TOKEN, '1234' ); - $I->haveOptionInDatabase( Connection::OPTION_COMMERCE_MANAGER_ID, '1234' ); + $I->haveOptionInDatabase( Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, '1234' ); $I->haveOptionInDatabase( \WC_Facebookcommerce_Integration::OPTION_PRODUCT_CATALOG_ID, '1234' ); $I->haveOptionInDatabase( \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, '1234' ); // set these in the database so that the Commerce fields are rendered $I->haveOptionInDatabase( Connection::OPTION_PAGE_ACCESS_TOKEN, '1234' ); - $I->haveOptionInDatabase( Connection::OPTION_COMMERCE_MANAGER_ID, '1234' ); + $I->haveOptionInDatabase( Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, '1234' ); // save two generic products $this->sync_enabled_product = $I->haveProductInDatabase(); @@ -47,217 +47,6 @@ public function _before( AcceptanceTester $I ) { } - /** - * Test that the Commerce fields are present. - * - * @param AcceptanceTester $I tester instance - */ - public function try_fields_are_visible( AcceptanceTester $I ) { - - $I->amEditingPostWithId( $this->sync_enabled_product->get_id() ); - - $I->wantTo( 'Test that the Commerce fields are visible' ); - - $I->click( '.fb_commerce_tab_options' ); - - $I->see( 'Sell on Instagram', '.form-field' ); - } - - - /** - * Test that the Commerce fields are not visible for products with Facebook sync disabled. - * - * @param AcceptanceTester $I tester instance - */ - public function try_fields_are_not_visible( AcceptanceTester $I ) { - - $I->amEditingPostWithId( $this->sync_disabled_product->get_id() ); - - $I->wantTo( 'Test that the Commerce fields are not visible' ); - - $I->click( '.fb_commerce_tab_options' ); - - $I->dontSee( 'Sell on Instagram', '.form-field' ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - public function try_fields_are_hidden_when_facebook_sync_is_disabled( AcceptanceTester $I ) { - - $I->amEditingPostWithId( $this->sync_enabled_product->get_id() ); - - $I->wantTo( 'Test that the Commerce fields are hidden when Facebook sync is disabled' ); - - $I->click( '.fb_commerce_tab_options' ); - - $I->selectOption( '#wc_facebook_sync_mode', 'Do not sync' ); - - $I->dontSee( 'Sell on Instagram', '.form-field' ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - public function try_commerce_enabled_field_is_enabled( AcceptanceTester $I ) { - - $this->sync_enabled_product->set_regular_price( 10 ); - $this->sync_enabled_product->set_manage_stock( true ); - $this->sync_enabled_product->set_stock_quantity( 3 ); - $this->sync_enabled_product->save(); - - $I->amEditingPostWithId( $this->sync_enabled_product->get_id() ); - - $I->wantTo( 'Test that the Commerce Enabled field is enabled' ); - - $this->see_commerce_enabled_field_is_enabled( $I ); - $this->dont_see_product_not_ready_notice( $I ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - private function see_commerce_enabled_field_is_enabled( AcceptanceTester $I ) { - - $I->expect( 'Commerce Enabled field is enabled (but not necessarily checked)' ); - - $I->scrollTo( '.fb_commerce_tab_options', null, -200 ); - $I->click( '.fb_commerce_tab_options' ); - $I->assertFalse( (bool) $I->executeJS( "return jQuery( '#wc_facebook_commerce_enabled' ).prop( 'disabled' )" ) ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - private function dont_see_product_not_ready_notice( AcceptanceTester $I ) { - - $I->expect( 'The product not ready notice is not shown' ); - - $I->dontSeeElement( '#product-not-ready-notice' ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - public function try_commerce_enabled_field_is_disabled_if_price_is_not_set( AcceptanceTester $I ) { - - $this->sync_enabled_product->set_regular_price( null ); - $this->sync_enabled_product->set_manage_stock( true ); - $this->sync_enabled_product->set_stock_quantity( 3 ); - $this->sync_enabled_product->save(); - - $I->amEditingPostWithId( $this->sync_enabled_product->get_id() ); - - $I->wantTo( 'Test that the Commerce Enabled field is disabled when no regular price is set' ); - - $this->see_commerce_enabled_field_is_disabled( $I ); - - $I->expect( 'The product not ready notice is shown' ); - - $I->see( 'This product does not meet the requirements to sell on Instagram.', '#product-not-ready-notice' ); - - $I->amGoingTo( 'Set the regular price to $10' ); - - $I->click( '.general_options' ); - $I->fillField( '#_regular_price', 10 ); - - $this->see_commerce_enabled_field_is_enabled( $I ); - $this->dont_see_product_not_ready_notice( $I ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - private function see_commerce_enabled_field_is_disabled( AcceptanceTester $I ) { - - $I->expect( 'Commerce Enabled field is not checked and is disabled' ); - - $I->click( '.fb_commerce_tab_options' ); - $I->dontSeeCheckboxIsChecked( '#wc_facebook_commerce_enabled' ); - $I->assertTrue( (bool) $I->executeJS( "return jQuery( '#wc_facebook_commerce_enabled' ).prop( 'disabled' )" ) ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - public function try_commerce_enabled_field_is_disabled_if_stock_management_is_disabled( AcceptanceTester $I ) { - - $this->sync_enabled_product->set_regular_price( 10 ); - $this->sync_enabled_product->set_manage_stock( false ); - $this->sync_enabled_product->set_stock_quantity( null ); - $this->sync_enabled_product->save(); - - $I->amEditingPostWithId( $this->sync_enabled_product->get_id() ); - - $I->wantTo( 'Test that the Commerce Enabled field is disabled when Stock Management is disabled' ); - - $this->see_commerce_enabled_field_is_disabled( $I ); - - $I->expect( 'The product not ready notice is shown' ); - - $I->see( 'This product does not meet the requirements to sell on Instagram.', '#product-not-ready-notice' ); - - $I->amGoingTo( 'Enable Stock Management' ); - - $I->click( '.inventory_options' ); - $I->checkOption( '#_manage_stock' ); - - $this->see_commerce_enabled_field_is_enabled( $I ); - $this->dont_see_product_not_ready_notice( $I ); - } - - - /** - * @param AcceptanceTester $I tester instance - */ - public function try_commerce_enabled_field_is_disabled_if_no_variations_have_sync_enabled( AcceptanceTester $I ) { - - $product_objects = $I->haveVariableProductInDatabase(); - - /** @var \WC_Product_Variable */ - $variable_product = $product_objects['product']; - - /** @var \WC_Product_Variation */ - $product_variation = $product_objects['variations']['product_variation']; - - $variable_product->set_manage_stock( true ); - $variable_product->save(); - - \SkyVerge\WooCommerce\Facebook\Products::disable_sync_for_products( [ $variable_product ] ); - - $I->amEditingPostWithId( $variable_product->get_id() ); - - $I->wantTo( 'Test that the Commerce Enabled field is disabled if no variations have sync enabled' ); - - $this->see_commerce_enabled_field_is_disabled( $I ); - - $I->expect( 'The product not ready notice is shown' ); - - $I->see( 'To sell this product on Instagram, at least one variation must be synced to Facebook.', '#variable-product-not-ready-notice' ); - - $I->amGoingTo( 'Enable Facebook sync for a variation' ); - - $I->click( '.variations_tab' ); - $index = $I->openVariationMetabox( $product_variation ); - - $I->waitForElementVisible( "#variable_facebook_sync_mode{$index}" ); - $I->selectOption( "#variable_facebook_sync_mode{$index}", 'sync_and_show' ); - - $this->see_commerce_enabled_field_is_enabled( $I ); - - $I->expect( 'The product not ready notice is not shown' ); - - $I->dontSeeElement( '#variable-product-not-ready-notice' ); - } - - /** * @dataProvider provider_missing_google_product_category_alert_is_shown */ diff --git a/tests/acceptance/Admin/Settings/CommerceCest.php b/tests/acceptance/Admin/Settings/CommerceCest.php index 92014da2b..7f16950c9 100644 --- a/tests/acceptance/Admin/Settings/CommerceCest.php +++ b/tests/acceptance/Admin/Settings/CommerceCest.php @@ -6,15 +6,13 @@ class SettingsCommerceCest { /** * Runs before each test. * - * @skip - * * @param AcceptanceTester $I tester instance */ public function _before( AcceptanceTester $I ) { $I->haveOptionInDatabase( \SkyVerge\WooCommerce\Facebook\Handlers\Connection::OPTION_ACCESS_TOKEN, '1235' ); $I->haveOptionInDatabase( \SkyVerge\WooCommerce\Facebook\Handlers\Connection::OPTION_PAGE_ACCESS_TOKEN, '1235' ); - $I->haveOptionInDatabase( \SkyVerge\WooCommerce\Facebook\Handlers\Connection::OPTION_COMMERCE_MANAGER_ID, '1235' ); + $I->haveOptionInDatabase( \SkyVerge\WooCommerce\Facebook\Handlers\Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, '1235' ); $I->loginAsAdmin(); } @@ -23,8 +21,6 @@ public function _before( AcceptanceTester $I ) { /** * Test that the Instagram Checkout connection message is shown. * - * @skip - * * @param AcceptanceTester $I tester instance */ public function try_store_connect_to_instagram_message( AcceptanceTester $I ) { @@ -39,8 +35,6 @@ public function try_store_connect_to_instagram_message( AcceptanceTester $I ) { /** * Test that the Instagram Checkout connection message is shown if the store is not connected. * - * @skip - * * @param AcceptanceTester $I tester instance */ public function try_store_not_connected_to_instagram_message( AcceptanceTester $I ) { @@ -58,8 +52,6 @@ public function try_store_not_connected_to_instagram_message( AcceptanceTester $ /** * Test that the Connect button is shown if the store is not connected. * - * @skip - * * @param AcceptanceTester $I tester instance */ public function try_connect_button_is_present( AcceptanceTester $I ) { @@ -79,8 +71,6 @@ public function try_connect_button_is_present( AcceptanceTester $I ) { /** * Test that the Facebook connection message is shown if the plugin is not connected. * - * @skip - * * @param AcceptanceTester $I tester instance */ public function try_connect_message( AcceptanceTester $I ) { @@ -96,8 +86,6 @@ public function try_connect_message( AcceptanceTester $I ) { /** * Test that the US-only limitation message is shown if the default country is not US. * - * @skip - * * @param AcceptanceTester $I tester instance */ public function try_us_only_limitation_message( AcceptanceTester $I ) { diff --git a/tests/acceptance/ConnectionCest.php b/tests/acceptance/ConnectionCest.php index 9f68293b2..4b6e04da2 100644 --- a/tests/acceptance/ConnectionCest.php +++ b/tests/acceptance/ConnectionCest.php @@ -61,6 +61,85 @@ public function try_handle_connect( AcceptanceTester $I ) { } + /** @see Connection::handle_connect_commerce() */ + public function try_handle_connect_commerce_as_guest( AcceptanceTester $I ) { + + $I->wantTo( 'Ensure nothing is stored by the callback for guests' ); + + $I->amOnUrl( add_query_arg( [ + 'wc-api' => Connection::ACTION_CONNECT_COMMERCE, + 'nonce' => wp_create_nonce( Connection::ACTION_CONNECT_COMMERCE ), + 'commerce_manager_id' => 'xyz', + ], home_url( '/' ) ) ); + + $I->dontSeeOptionInDatabase( [ + 'option_name' => Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, + ] ); + + $I->see( '-1' ); + } + + + /** @see Connection::handle_connect_commerce() */ + public function try_handle_connect_commerce_with_bad_nonce( AcceptanceTester $I ) { + + $I->wantTo( 'Ensure nothing is stored with an invalid nonce' ); + + $I->loginAsAdmin(); + + $I->amOnUrl( add_query_arg( [ + 'wc-api' => Connection::ACTION_CONNECT_COMMERCE, + 'nonce' => 'bad-nonce', + 'commerce_manager_id' => 'xyz', + ], home_url( '/' ) ) ); + + $I->dontSeeOptionInDatabase( [ + 'option_name' => Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, + ] ); + } + + + /** @see Connection::handle_connect_commerce() */ + public function try_handle_connect_commerce_with_no_commerce_manager_id( AcceptanceTester $I ) { + + $I->wantTo( 'Ensure nothing is stored if the commerce manager ID is missing' ); + + $I->loginAsAdmin(); + + $I->amOnUrl( add_query_arg( [ + 'wc-api' => Connection::ACTION_CONNECT_COMMERCE, + 'nonce' => wp_create_nonce( Connection::ACTION_CONNECT_COMMERCE ), + ], home_url( '/' ) ) ); + + $I->dontSeeOptionInDatabase( [ + 'option_name' => Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, + ] ); + } + + + /** @see Connection::handle_connect_commerce() */ + public function try_handle_connect_commerce( AcceptanceTester $I ) { + + $I->wantTo( 'Ensure the callback stores the commerce manager ID if everything passes security checks' ); + + $this->add_get_pages_success_response( $I ); + $this->add_enable_order_management_success_response( $I ); + + $I->haveOptionInDatabase( \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, 'PAGE_ID' ); + + $I->loginAsAdmin(); + + $I->amOnUrl( add_query_arg( [ + 'wc-api' => Connection::ACTION_CONNECT_COMMERCE, + 'nonce' => wp_create_nonce( Connection::ACTION_CONNECT_COMMERCE ), + 'commerce_manager_id' => 'xyz', + ], home_url( '/' ) ) ); + + $I->haveOptionInDatabase( Connection::OPTION_PAGE_ACCESS_TOKEN, 'PAGE_ACCESS_TOKEN' ); + $I->haveOptionInDatabase( Connection::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, 'xyz' ); + } + + /** * Adds a MU plugin to filter the Facebook API response and simulate a successful GET of the page accounts. * @@ -108,4 +187,42 @@ private function add_get_pages_success_response( AcceptanceTester $I ) { } + /** + * Adds a MU plugin to filter the Facebook API response and simulate a successful GET of the page accounts. + * + * @param AcceptanceTester $I + */ + private function add_enable_order_management_success_response( AcceptanceTester $I ) { + + // simulate a successful configuration response + $code = << [], + 'body' => json_encode( + [ + 'success' => true, + ] + ), + 'response' => [ + 'code' => 200, + 'message' => 'Ok', + ], + 'cookies' => [], + 'http_response' => null, + ]; + } + + return \$response; + +}, 10, 3 ); +PHP; + + $I->haveMuPlugin( 'get-order-management-response-filter.php', $code ); + } + + } diff --git a/tests/integration/API/Catalog/Product_Group/Products/Read/RequestTest.php b/tests/integration/API/Catalog/Product_Group/Products/Read/RequestTest.php index 3b6b727a4..9e341208d 100644 --- a/tests/integration/API/Catalog/Product_Group/Products/Read/RequestTest.php +++ b/tests/integration/API/Catalog/Product_Group/Products/Read/RequestTest.php @@ -21,11 +21,13 @@ protected function _before() { parent::_before(); - // the API cannot be instantiated if an access token is not defined - facebook_for_woocommerce()->get_connection_handler()->update_access_token( 'access_token' ); + if ( ! class_exists( \SkyVerge\WooCommerce\Facebook\API\Request::class ) ) { + // the API cannot be instantiated if an access token is not defined + facebook_for_woocommerce()->get_connection_handler()->update_access_token( 'access_token' ); - // create an instance of the API and load all the request and response classes - facebook_for_woocommerce()->get_api(); + // create an instance of the API and load all the request and response classes + facebook_for_woocommerce()->get_api(); + } } diff --git a/tests/integration/API/Catalog/RequestTest.php b/tests/integration/API/Catalog/RequestTest.php index b57d87000..2c69f49b0 100644 --- a/tests/integration/API/Catalog/RequestTest.php +++ b/tests/integration/API/Catalog/RequestTest.php @@ -19,11 +19,11 @@ public function _before() { parent::_before(); if ( ! class_exists( \SkyVerge\WooCommerce\Facebook\API\Request::class ) ) { - require_once 'includes/API/Request.php'; - } + // the API cannot be instantiated if an access token is not defined + facebook_for_woocommerce()->get_connection_handler()->update_access_token( 'access_token' ); - if ( ! class_exists( Request::class ) ) { - require_once 'includes/API/Catalog/Request.php'; + // create an instance of the API and load all the request and response classes + facebook_for_woocommerce()->get_api(); } } diff --git a/tests/integration/API/Pages/Read/RequestTest.php b/tests/integration/API/Pages/Read/RequestTest.php index 3a460fc7e..a53745a80 100644 --- a/tests/integration/API/Pages/Read/RequestTest.php +++ b/tests/integration/API/Pages/Read/RequestTest.php @@ -44,7 +44,7 @@ public function test_get_params() { $request = new Request( '1234' ); - $this->assertEquals( [ 'fields' => 'name,link' ], $request->get_params() ); + $this->assertEquals( [ 'fields' => 'name,link,commerce_merchant_settings' ], $request->get_params() ); } diff --git a/tests/integration/API/User/Permissions/Delete/RequestTest.php b/tests/integration/API/User/Permissions/Delete/RequestTest.php index 8fe17e130..f4e559cd4 100644 --- a/tests/integration/API/User/Permissions/Delete/RequestTest.php +++ b/tests/integration/API/User/Permissions/Delete/RequestTest.php @@ -19,11 +19,11 @@ public function _before() { parent::_before(); if ( ! class_exists( \SkyVerge\WooCommerce\Facebook\API\Request::class ) ) { - require_once 'includes/API/Request.php'; - } + // the API cannot be instantiated if an access token is not defined + facebook_for_woocommerce()->get_connection_handler()->update_access_token( 'access_token' ); - if ( ! class_exists( Request::class ) ) { - require_once 'includes/API/User/Permissions/Delete/Request.php'; + // create an instance of the API and load all the request and response classes + facebook_for_woocommerce()->get_api(); } } diff --git a/tests/integration/API/User/RequestTest.php b/tests/integration/API/User/RequestTest.php index 75dbfa06f..7ff59722c 100644 --- a/tests/integration/API/User/RequestTest.php +++ b/tests/integration/API/User/RequestTest.php @@ -19,11 +19,11 @@ public function _before() { parent::_before(); if ( ! class_exists( \SkyVerge\WooCommerce\Facebook\API\Request::class ) ) { - require_once 'includes/API/Request.php'; - } + // the API cannot be instantiated if an access token is not defined + facebook_for_woocommerce()->get_connection_handler()->update_access_token( 'access_token' ); - if ( ! class_exists( Request::class ) ) { - require_once 'includes/API/User/Request.php'; + // create an instance of the API and load all the request and response classes + facebook_for_woocommerce()->get_api(); } } diff --git a/tests/integration/APITest.php b/tests/integration/APITest.php index 4a750e75e..12b03e0a3 100644 --- a/tests/integration/APITest.php +++ b/tests/integration/APITest.php @@ -68,6 +68,26 @@ public function test_set_access_token() { } + /** @see API::set_request_authorization_header() */ + public function test_set_request_authorization_header() { + + $api = new API( 'access_token' ); + + $property = new ReflectionProperty( $api, 'request_headers' ); + $property->setAccessible( true ); + + $method = new ReflectionMethod( $api, 'set_request_authorization_header' ); + $method->setAccessible( true ); + $method->invokeArgs( $api, [ 'new_access_token' ] ); + + $request_headers = $property->getValue( $api ); + + $this->assertIsArray( $request_headers ); + $this->assertArrayHasKey( 'Authorization', $request_headers ); + $this->assertEquals( 'Bearer new_access_token', $request_headers['Authorization'] ); + } + + /** @see API::perform_request() */ public function test_do_post_parse_response_validation_retry() { @@ -99,44 +119,6 @@ public function __construct() { } - /** - * @see API::do_post_parse_response_validation() - * - * @param int $code error code - * @param string $exception expected exception class name - * - * @dataProvider provider_do_post_parse_response_validation - */ - public function test_do_post_parse_response_validation( $code, $exception ) { - - $message = sprintf( '(#%d) Message describing the error', $code ); - - $this->expectException( $exception ); - $this->expectExceptionCode( $code ); - $this->expectExceptionMessageRegExp( '/' . preg_quote( $message, '/' ) . '/' ); - - // mock the response for the HTTP request - $args = [ - 'request_path' => '1234/product_groups', - 'response_body' => [ - 'error' => [ - 'message' => $message, - 'type' => 'OAuthException', - 'code' => $code, - ] - ], - 'response_code' => 400, - 'response_message' => 'Bad Request', - ]; - - $this->prepare_request_response( $args ); - - $api = new API( 'access_token' ); - - $api->create_product_group( '1234', [] ); - } - - /** * Intercepts HTTP requests and returns a prepared response. * @@ -179,45 +161,6 @@ private function prepare_request_response( $args ) { } - /** @see API::test_do_post_parse_response_validation() */ - public function provider_do_post_parse_response_validation() { - - return [ - [ 4, API\Exceptions\Request_Limit_Reached::class ], - [ 17, API\Exceptions\Request_Limit_Reached::class ], - [ 32, API\Exceptions\Request_Limit_Reached::class ], - [ 613, API\Exceptions\Request_Limit_Reached::class ], - [ 80004, API\Exceptions\Request_Limit_Reached::class ], - - [ null, Framework\SV_WC_API_Exception::class ], - [ 102, Framework\SV_WC_API_Exception::class ], - [ 190, Framework\SV_WC_API_Exception::class ], - ]; - } - - - /** @see API::do_post_parse_response_validation() */ - public function test_do_post_parse_response_validation_with_a_valid_response() { - - $product_group_id = '111001234947059'; - - $args = [ - 'request_path' => '1234/product_groups', - 'response_body' => [ - 'id' => $product_group_id, - ], - ]; - - $this->prepare_request_response( $args ); - - $api = new API( 'access_token' ); - - $response = $api->create_product_group( '1234', [] ); - - $this->assertEquals( $product_group_id, $response->get_id() ); - } - - /** @see API::get_catalog() */ public function test_get_catalog() { @@ -296,7 +239,7 @@ public function test_get_page() { $this->assertInstanceOf( API\Pages\Read\Request::class, $api->get_request() ); $this->assertEquals( 'GET', $api->get_request()->get_method() ); $this->assertEquals( "/{$page_id}", $api->get_request()->get_path() ); - $this->assertEquals( [ 'fields' => 'name,link' ], $api->get_request()->get_params() ); + $this->assertEquals( [ 'fields' => 'name,link,commerce_merchant_settings' ], $api->get_request()->get_params() ); $this->assertEquals( [], $api->get_request()->get_data() ); $this->assertInstanceOf( API\Pages\Read\Response::class, $api->get_response() ); @@ -361,74 +304,6 @@ public function test_send_item_updates() { } - /** @see API::create_product_group() */ - public function test_create_product_group() { - - $catalog_id = '123456'; - $product_group_data = [ 'test' => 'test' ]; - - // test will fail if do_remote_request() is not called once - $api = $this->make( API::class, [ - 'do_remote_request' => \Codeception\Stub\Expected::once(), - ] ); - - $api->create_product_group( '123456', $product_group_data ); - - $this->assertInstanceOf( Request::class, $api->get_request() ); - $this->assertEquals( 'POST', $api->get_request()->get_method() ); - $this->assertEquals( "/{$catalog_id}/product_groups", $api->get_request()->get_path() ); - $this->assertEquals( [], $api->get_request()->get_params() ); - $this->assertEquals( $product_group_data, $api->get_request()->get_data() ); - - $this->assertInstanceOf( Response::class, $api->get_response() ); - } - - - /** @see API::update_product_group() */ - public function test_update_product_group() { - - $product_group_id = '1234'; - $product_group_data = [ 'test' => 'test' ]; - - // test will fail if do_remote_request() is not called once - $api = $this->make( API::class, [ - 'do_remote_request' => \Codeception\Stub\Expected::once(), - ] ); - - $api->update_product_group( $product_group_id, $product_group_data ); - - $this->assertInstanceOf( Request::class, $api->get_request() ); - $this->assertEquals( 'POST', $api->get_request()->get_method() ); - $this->assertEquals( "/{$product_group_id}", $api->get_request()->get_path() ); - $this->assertEquals( [], $api->get_request()->get_params() ); - $this->assertEquals( $product_group_data, $api->get_request()->get_data() ); - - $this->assertInstanceOf( Response::class, $api->get_response() ); - } - - - /** @see API::delete_product_group() */ - public function test_delete_product_group() { - - $product_group_id = '1234'; - - // test will fail if do_remote_request() is not called once - $api = $this->make( API::class, [ - 'do_remote_request' => \Codeception\Stub\Expected::once(), - ] ); - - $api->delete_product_group( $product_group_id ); - - $this->assertInstanceOf( Request::class, $api->get_request() ); - $this->assertEquals( 'DELETE', $api->get_request()->get_method() ); - $this->assertEquals( "/{$product_group_id}", $api->get_request()->get_path() ); - $this->assertEquals( [], $api->get_request()->get_params() ); - $this->assertEquals( [], $api->get_request()->get_data() ); - - $this->assertInstanceOf( Response::class, $api->get_response() ); - } - - /** @see API::get_product_group_products() */ public function test_get_product_group_products() { @@ -480,74 +355,6 @@ public function test_find_product_item() { } - /** @see API::create_product_item() */ - public function test_create_product_item() { - - $product_group_id = '123456'; - $product_data = [ 'test' => 'test' ]; - - // test will fail if do_remote_request() is not called once - $api = $this->make( API::class, [ - 'do_remote_request' => \Codeception\Stub\Expected::once(), - ] ); - - $api->create_product_item( $product_group_id, $product_data ); - - $this->assertInstanceOf( Request::class, $api->get_request() ); - $this->assertEquals( 'POST', $api->get_request()->get_method() ); - $this->assertEquals( "/{$product_group_id}/products", $api->get_request()->get_path() ); - $this->assertEquals( [], $api->get_request()->get_params() ); - $this->assertEquals( $product_data, $api->get_request()->get_data() ); - - $this->assertInstanceOf( Response::class, $api->get_response() ); - } - - - /** @see API::update_product_item() */ - public function test_update_product_item() { - - $product_item_id = '123456'; - $product_data = [ 'test' => 'test' ]; - - // test will fail if do_remote_request() is not called once - $api = $this->make( API::class, [ - 'do_remote_request' => \Codeception\Stub\Expected::once(), - ] ); - - $api->update_product_item( $product_item_id, $product_data ); - - $this->assertInstanceOf( Request::class, $api->get_request() ); - $this->assertEquals( 'POST', $api->get_request()->get_method() ); - $this->assertEquals( "/{$product_item_id}", $api->get_request()->get_path() ); - $this->assertEquals( [], $api->get_request()->get_params() ); - $this->assertEquals( $product_data, $api->get_request()->get_data() ); - - $this->assertInstanceOf( Response::class, $api->get_response() ); - } - - - /** @see API::delete_product_item() */ - public function test_delete_product_item() { - - $product_item_id = '123456'; - - // test will fail if do_remote_request() is not called once - $api = $this->make( API::class, [ - 'do_remote_request' => \Codeception\Stub\Expected::once(), - ] ); - - $api->delete_product_item( $product_item_id ); - - $this->assertInstanceOf( Request::class, $api->get_request() ); - $this->assertEquals( 'DELETE', $api->get_request()->get_method() ); - $this->assertEquals( "/{$product_item_id}", $api->get_request()->get_path() ); - $this->assertEquals( [], $api->get_request()->get_params() ); - $this->assertEquals( [], $api->get_request()->get_data() ); - - $this->assertInstanceOf( Response::class, $api->get_response() ); - } - - /** @see API::send_pixel_events() */ public function test_send_pixel_events() { diff --git a/tests/integration/Admin/OrdersTest.php b/tests/integration/Admin/OrdersTest.php new file mode 100644 index 000000000..c2f563d6e --- /dev/null +++ b/tests/integration/Admin/OrdersTest.php @@ -0,0 +1,156 @@ +get_connection_handler()->update_access_token( 'access_token' ); + + $order = new \WC_Order(); + $order->set_created_via( 'facebook' ); + $order->save(); + + $refund = new \WC_Order_Refund(); + $refund->set_parent_id( $order->get_id() ); + $refund->save(); + + $this->expectException( SV_WC_Plugin_Exception::class ); + $this->expectExceptionMessage( 'Remote ID for parent order not found' ); + + $this->get_orders_handler()->handle_refund( $refund->get_id() ); + } + + + /** @see Admin\Orders::handle_refund() */ + public function test_handle_refund_for_non_commerce_orders() { + + $order = new \WC_Order(); + $order->set_created_via( 'checkout' ); + $order->save(); + + $refund = new \WC_Order_Refund(); + $refund->set_parent_id( $order->get_id() ); + $refund->save(); + + $commerce_orders_handler = facebook_for_woocommerce()->get_commerce_handler()->get_orders_handler(); + + $this->tester->setPropertyValue( facebook_for_woocommerce()->get_commerce_handler(), 'orders', $this->make( Orders::class, [ + 'add_order_refund' => \Codeception\Stub\Expected::never(), + ] ) ); + + $this->get_orders_handler()->handle_refund( $refund->get_id() ); + + $this->tester->setPropertyValue( facebook_for_woocommerce()->get_commerce_handler(), 'orders', $commerce_orders_handler ); + } + + + // TODO: add test for handle_bulk_update() + + + /** + * @see Admin\Orders::is_order_editable() + * + * @param bool $maybe_editable + * @param string $created_via + * @param string $status + * @param bool $expected + * + * @dataProvider provider_is_order_editable + * + * @throws WC_Data_Exception + */ + public function test_is_order_editable( $maybe_editable, $created_via, $status, $expected ) { + + $order = new \WC_Order(); + $order->set_created_via( $created_via ); + $order->set_status( $status ); + $order->save(); + + $this->assertEquals( $expected, $this->get_orders_handler()->is_order_editable( $maybe_editable, $order ) ); + } + + + /** @see test_is_order_editable */ + public function provider_is_order_editable() { + + return [ + [ false, 'checkout', 'pending', false ], + [ true, 'checkout', 'pending', true ], + [ true, 'instagram', 'pending', false ], + [ true, 'instagram', 'processing', true ], + [ true, 'facebook', 'pending', false ], + [ true, 'facebook', 'processing', true ], + ]; + } + + + /** Utility methods ***********************************************************************************************/ + + + /** + * Gets an orders handler instance. + * + * @since 2.1.0-dev.1 + * + * @return Admin\Orders + */ + private function get_orders_handler() { + + return new Admin\Orders(); + } + + +} diff --git a/tests/integration/Admin/ProductsTest.php b/tests/integration/Admin/ProductsTest.php index 38cd0171b..bf4efa9fb 100644 --- a/tests/integration/Admin/ProductsTest.php +++ b/tests/integration/Admin/ProductsTest.php @@ -39,9 +39,6 @@ protected function _after() { // TODO: add test for render_google_product_category_fields() - // TODO: add test for render_commerce_fields() - - /** @see Products::save_commerce_fields() */ public function test_save_commerce_fields() { global $post; @@ -49,7 +46,6 @@ public function test_save_commerce_fields() { $product = $this->tester->get_product( [ 'attributes' => $this->tester->create_product_attributes() ] ); $post = get_post( $product->get_id() ); - $_POST[ Admin\Products::FIELD_COMMERCE_ENABLED ] = 'yes'; $_POST[ Admin\Products::FIELD_GOOGLE_PRODUCT_CATEGORY_ID ] = '1234'; $enhanced_catalog_prefix = Admin\Enhanced_Catalog_Attribute_Fields::FIELD_ENHANCED_CATALOG_ATTRIBUTE_PREFIX; @@ -58,7 +54,6 @@ public function test_save_commerce_fields() { $this->get_products_handler()->save_commerce_fields( $product ); $gender = \SkyVerge\WooCommerce\Facebook\Products::get_enhanced_catalog_attribute( 'gender', $product ); - $this->assertEquals( true, \SkyVerge\WooCommerce\Facebook\Products::is_commerce_enabled_for_product( $product ) ); $this->assertEquals( '1234', \SkyVerge\WooCommerce\Facebook\Products::get_google_product_category_id( $product ) ); $this->assertEquals( 'male', $gender ); } diff --git a/tests/integration/Admin/SettingsTest.php b/tests/integration/Admin/SettingsTest.php index 671454483..867166617 100644 --- a/tests/integration/Admin/SettingsTest.php +++ b/tests/integration/Admin/SettingsTest.php @@ -19,9 +19,12 @@ protected function _before() { require_once 'includes/Admin/Settings.php'; require_once 'includes/Admin/Abstract_Settings_Screen.php'; + require_once 'includes/Admin/Settings_Screens/Advertise.php'; require_once 'includes/Admin/Settings_Screens/Connection.php'; + require_once 'includes/Admin/Settings_Screens/Product_Sets.php'; require_once 'includes/Admin/Settings_Screens/Product_Sync.php'; require_once 'includes/Admin/Settings_Screens/Messenger.php'; + require_once 'includes/Admin/Settings_Screens/Commerce.php'; require_once 'includes/Admin/Settings_Screens/Advertise.php'; require_once 'includes/Admin/Settings_Screens/Product_Sets.php'; } @@ -72,6 +75,8 @@ public function test_get_screens() { $this->assertArrayHasKey( 'messenger', $screens ); $this->assertInstanceOf( Admin\Settings_Screens\Messenger::class, $screens['messenger'] ); + $this->assertArrayHasKey( 'commerce', $screens ); + $this->assertInstanceOf( Admin\Settings_Screens\Commerce::class, $screens['commerce'] ); } @@ -117,8 +122,10 @@ public function test_get_tabs() { $tabs = $this->get_setting_handler()->get_tabs(); + $this->assertArrayHasKey( 'advertise', $tabs ); $this->assertArrayHasKey( 'product_sync', $tabs ); $this->assertArrayHasKey( 'messenger', $tabs ); + $this->assertArrayHasKey( 'commerce', $tabs ); $this->assertArrayHasKey( 'advertise', $tabs ); } diff --git a/tests/integration/Commerce/OrdersTest.php b/tests/integration/Commerce/OrdersTest.php index 495fd7242..c40b5a68d 100644 --- a/tests/integration/Commerce/OrdersTest.php +++ b/tests/integration/Commerce/OrdersTest.php @@ -26,11 +26,6 @@ public function _before() { // the API cannot be instantiated if an access token is not defined facebook_for_woocommerce()->get_connection_handler()->update_access_token( 'access_token' ); facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( 'fake_page_access_token' ); - facebook_for_woocommerce()->get_connection_handler()->update_commerce_manager_id( 'fake_commerce_manager_id' ); - // This would usually covered as a hook on the init action but if the acceptance - // test setup doesn't already have a page acess token or commerece manager id - // then it'll have been skipped. - $this->get_commerce_orders_handler()->schedule_local_orders_update(); // create an instance of the API and load all the request and response classes facebook_for_woocommerce()->get_api(); @@ -372,7 +367,8 @@ public function test_update_local_orders_create() { // ensure Commerce is connected facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( '1234' ); - facebook_for_woocommerce()->get_connection_handler()->update_commerce_manager_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_commerce_merchant_settings_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_onsite_checkout_connected( true ); $product = $this->tester->get_product(); @@ -402,7 +398,8 @@ public function test_update_local_orders_update() { // ensure Commerce is connected facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( '1234' ); - facebook_for_woocommerce()->get_connection_handler()->update_commerce_manager_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_commerce_merchant_settings_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_onsite_checkout_connected( true ); $product = $this->tester->get_product(); @@ -440,7 +437,8 @@ public function test_update_local_orders_acknowledge() { // ensure Commerce is connected facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( '1234' ); - facebook_for_woocommerce()->get_connection_handler()->update_commerce_manager_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_commerce_merchant_settings_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_onsite_checkout_connected( true ); $product = $this->tester->get_product(); @@ -466,7 +464,7 @@ public function test_update_cancelled_orders() { // ensure Commerce is connected facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( '1234' ); - facebook_for_woocommerce()->get_connection_handler()->update_commerce_manager_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_commerce_merchant_settings_id( '1234' ); $product = $this->tester->get_product( [ 'manage_stock' => true, @@ -556,7 +554,8 @@ public function test_schedule_local_orders_update() { // ensure Commerce is connected facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( '1234' ); - facebook_for_woocommerce()->get_connection_handler()->update_commerce_manager_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_commerce_merchant_settings_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_onsite_checkout_connected( true ); facebook_for_woocommerce()->get_commerce_handler()->get_orders_handler()->schedule_local_orders_update(); @@ -564,6 +563,20 @@ public function test_schedule_local_orders_update() { } + /** @see Orders::schedule_local_orders_update() */ + public function test_schedule_local_orders_update_not_connected() { + + // ensure Commerce is not connected + facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_commerce_merchant_settings_id( '1234' ); + facebook_for_woocommerce()->get_connection_handler()->update_onsite_checkout_connected( false ); + + facebook_for_woocommerce()->get_commerce_handler()->get_orders_handler()->schedule_local_orders_update(); + + $this->assertFalse( as_next_scheduled_action( Orders::ACTION_FETCH_ORDERS, [], \WC_Facebookcommerce::PLUGIN_ID ) ); + } + + /** @see Orders::fulfill_order() */ public function test_fulfill_order_no_remote_id() { diff --git a/tests/integration/CommerceTest.php b/tests/integration/CommerceTest.php index ea21a0b83..382608a26 100644 --- a/tests/integration/CommerceTest.php +++ b/tests/integration/CommerceTest.php @@ -128,7 +128,8 @@ public function provider_is_available_filter() { public function test_is_connected( $access_token, $manager_id, $is_connected ) { facebook_for_woocommerce()->get_connection_handler()->update_page_access_token( $access_token ); - facebook_for_woocommerce()->get_connection_handler()->update_commerce_manager_id( $manager_id ); + facebook_for_woocommerce()->get_connection_handler()->update_commerce_merchant_settings_id( $manager_id ); + facebook_for_woocommerce()->get_connection_handler()->update_onsite_checkout_connected( true ); $this->assertSame( $is_connected, $this->get_commerce_handler()->is_connected() ); } diff --git a/tests/integration/Handlers/ConnectionTest.php b/tests/integration/Handlers/ConnectionTest.php index edc393be0..0f016dc04 100644 --- a/tests/integration/Handlers/ConnectionTest.php +++ b/tests/integration/Handlers/ConnectionTest.php @@ -25,9 +25,9 @@ public function _before() { /** @see Connection::__construct() */ public function test_constructor() { - $connection = $this->get_connection(); + $connection = $this->get_connection(); - $this->assertInstanceOf( Connection::class, $connection ); + $this->assertInstanceOf( Connection::class, $connection ); } @@ -116,8 +116,8 @@ public function test_get_commerce_connect_url() { $connect_url = $this->get_connection()->get_commerce_connect_url(); - $this->assertStringContainsString( 'https://www.facebook.com/commerce_manager/onboarding/?app_id=', $connect_url ); - $this->assertStringContainsString( 'redirect_url=https%3A%2F%2Fconnect.woocommerce.com%2Fauth%2Ffacebookcommerce%2F%3Fsite_url%3D', $connect_url ); + $this->assertStringContainsString( 'https://www.facebook.com/facebook_business_extension', $connect_url ); + $this->assertStringContainsString( 'redirect_uri=https%3A%2F%2Fconnect.woocommerce.com%2Fauth%2Ffacebookcommerce%2F%3Fsite_url%3D', $connect_url ); $this->assertStringContainsString( 'wc-api%253D' . Connection::ACTION_CONNECT_COMMERCE . '%2526nonce%253D', $connect_url ); } @@ -300,13 +300,23 @@ public function test_get_system_user_id() { } - /** @see Connection::get_commerce_manager_id() */ - public function test_get_commerce_manager_id() { + /** @see Connection::is_onsite_checkout_connected() */ + public function test_is_onsite_checkout_connected() { + + $connected = true; + update_option( Connection::OPTION_ONSITE_CHECKOUT_CONNECTED, $connected ); + + $this->assertSame( $connected, $this->get_connection()->is_onsite_checkout_connected() ); + } + + + /** @see Connection::is_commerce_setup_complete() */ + public function test_is_commerce_setup_complete() { - $commerce_manager_id = 'commerce manager id'; - update_option( Connection::OPTION_COMMERCE_MANAGER_ID, $commerce_manager_id ); + $complete = true; + update_option( Connection::OPTION_COMMERCE_SETUP_COMPLETE, $complete ); - $this->assertSame( $commerce_manager_id, $this->get_connection()->get_commerce_manager_id() ); + $this->assertSame( $complete, $this->get_connection()->is_commerce_setup_complete() ); } @@ -612,13 +622,23 @@ public function test_update_system_user_id() { } - /** @see Connection::update_commerce_manager_id() */ - public function test_update_commerce_manager_id() { + /** @see Connection::update_onsite_checkout_connected() */ + public function test_update_onsite_checkout_connected() { + + $connected = true; + $this->get_connection()->update_onsite_checkout_connected( $connected ); + + $this->assertSame( $connected, get_option( Connection::OPTION_ONSITE_CHECKOUT_CONNECTED ) ); + } + + + /** @see Connection::update_commerce_setup_complete() */ + public function test_update_commerce_setup_complete() { - $commerce_manager_id = 'commerce manager id'; - $this->get_connection()->update_commerce_manager_id( $commerce_manager_id ); + $complete = true; + $this->get_connection()->update_commerce_setup_complete( $complete ); - $this->assertSame( $commerce_manager_id, get_option( Connection::OPTION_COMMERCE_MANAGER_ID ) ); + $this->assertSame( $complete, get_option( Connection::OPTION_COMMERCE_SETUP_COMPLETE ) ); } @@ -691,7 +711,6 @@ public function test_is_connected() { $connection = $this->get_connection(); $connection->update_access_token( 'access token' ); - $connection->update_commerce_manager_id( 'manager id' ); $this->assertTrue( $connection->is_connected() ); } diff --git a/tests/integration/Products_Test.php b/tests/integration/Products_Test.php index 429620299..c20e212be 100644 --- a/tests/integration/Products_Test.php +++ b/tests/integration/Products_Test.php @@ -324,149 +324,6 @@ public function test_get_product_price_filter() { } - /** - * @see \SkyVerge\WooCommerce\Facebook\Products::is_product_ready_for_commerce() - * - * @param bool $manage_stock_option WC general option to manage stock - * @param bool $manage_stock_prop product property to manage stock - * @param string $product_price product price - * @param bool $commerce_enabled commerce enabled for product - * @param bool $sync_enabled sync enabled for product - * @param bool $expected_result the expected result - * - * @dataProvider provider_is_product_ready_for_commerce - */ - public function test_is_product_ready_for_commerce( $manage_stock_option, $manage_stock_prop, $product_price, $commerce_enabled, $sync_enabled, $expected_result ) { - - $product = $this->get_product(); - - update_option( 'woocommerce_manage_stock', $manage_stock_option ? 'yes' : 'no' ); - $product->set_manage_stock( $manage_stock_prop ); - $product->set_regular_price( $product_price ); - Products::update_commerce_enabled_for_product( $product, $commerce_enabled ); - if ($sync_enabled) { - Products::enable_sync_for_products( [$product]); - } else { - Products::disable_sync_for_products( [$product]); - } - - $this->assertEquals( $expected_result, Facebook\Products::is_product_ready_for_commerce( $product ) ); - } - - - /** @see test_is_product_ready_for_commerce */ - public function provider_is_product_ready_for_commerce() { - - return [ - [ true, true, '10.00', true, true, true ], - [ false, true, '10.00', true, true, false ], - [ true, false, '10.00', true, true, false ], - [ true, true, '0', true, true, false ], - [ true, true, '10.00', false, true, false ], - [ true, true, '10.00', true, false, false ], - ]; - } - - - /** - * @see \SkyVerge\WooCommerce\Facebook\Products::is_commerce_enabled_for_product() - * - * @param string $meta_value meta value - * @param bool $expected_result the expected result - * - * @dataProvider provider_is_commerce_enabled_for_product - */ - public function test_is_commerce_enabled_for_product( $meta_value, $expected_result ) { - - $product = $this->get_product(); - - if ( ! empty( $meta_value ) ) { - $product->update_meta_data( Products::COMMERCE_ENABLED_META_KEY, $meta_value, true ); - } else { - $product->delete_meta_data( Products::COMMERCE_ENABLED_META_KEY ); - } - - $this->assertEquals( $expected_result, Facebook\Products::is_commerce_enabled_for_product( $product ) ); - } - - - /** @see test_is_commerce_enabled_for_product */ - public function provider_is_commerce_enabled_for_product() { - - return [ - [ 'yes', true ], - [ true, true ], - [ 'no', false ], - [ false, false ], - [ null, false ], // if a product does not have this meta set, Commerce is not enabled for it - ]; - } - - - /** @see \SkyVerge\WooCommerce\Facebook\Products::is_commerce_enabled_for_product() */ - public function test_is_commerce_enabled_for_variation() { - - $product = $this->get_variable_product(); - - Products::update_commerce_enabled_for_product( $product, true ); - - foreach ( $product->get_children() as $child_id ) { - - $variation = wc_get_product( $child_id ); - - $this->assertTrue( Facebook\Products::is_commerce_enabled_for_product( $variation ) ); - } - } - - - /** @see \SkyVerge\WooCommerce\Facebook\Products::is_commerce_enabled_for_product() */ - public function test_is_commerce_disabled_for_variation() { - - $product = $this->get_variable_product(); - - foreach ( $product->get_children() as $child_id ) { - - $variation = wc_get_product( $child_id ); - - $this->assertFalse( Facebook\Products::is_commerce_enabled_for_product( $variation ) ); - } - } - - - /** - * @see \SkyVerge\WooCommerce\Facebook\Products::update_commerce_enabled_for_product() - * - * @param bool $param_value param value - * @param string $expected_meta_value the expected meta value - * - * @dataProvider provider_update_commerce_enabled_for_product - */ - public function test_update_commerce_enabled_for_product( $param_value, $expected_meta_value ) { - - $product = $this->get_product(); - - Products::update_commerce_enabled_for_product( $product, $param_value ); - - // get a fresh product object to ensure the status is stored - $product = wc_get_product( $product->get_id() ); - - $this->assertEquals( $expected_meta_value, $product->get_meta( Products::COMMERCE_ENABLED_META_KEY ) ); - } - - - /** @see test_update_commerce_enabled_for_product */ - public function provider_update_commerce_enabled_for_product() { - - return [ - [ true, 'yes' ], - [ 'yes', 'yes' ], - [ false, 'no' ], - [ 'no', 'no' ], - [ '', 'no' ], - ]; - } - - /** @see Products::get_google_product_category_id() */ public function test_get_google_product_category_id_simple_product() { @@ -1359,43 +1216,37 @@ public function provider_product_has_attribute() { } - /** @see Products::get_product_by_fb_product_id() */ - public function test_get_product_by_fb_product_id_item_id() { + /** @see Products::get_product_by_fb_retailer_id() */ + public function test_get_product_by_fb_retailer_id_with_sku() { $product = $this->get_product(); + $product->set_sku( '123456_a' ); + $product->save(); - $product->update_meta_data( \WC_Facebookcommerce_Integration::FB_PRODUCT_ITEM_ID, '444444', true ); - $product->save_meta_data(); + $retailer_id = \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ); - $product_found = Facebook\Products::get_product_by_fb_product_id( '444444' ); + $product_found = Facebook\Products::get_product_by_fb_retailer_id( $retailer_id ); $this->assertInstanceOf( \WC_Product::class, $product_found ); $this->assertEquals( $product->get_id(), $product_found->get_id() ); } - /** @see Products::get_product_by_fb_product_id() */ - public function test_get_product_by_fb_product_id_group_id() { - - $product = $this->get_product(); + /** @see Products::get_product_by_fb_retailer_id() */ + public function test_get_product_by_fb_retailer_id_without_sku() { - $product->update_meta_data( \WC_Facebookcommerce_Integration::FB_PRODUCT_GROUP_ID, '123456', true ); - $product->save_meta_data(); + $product = $this->get_product(); + $retailer_id = \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ); - $product_found = Facebook\Products::get_product_by_fb_product_id( '123456' ); + $product_found = Facebook\Products::get_product_by_fb_retailer_id( $retailer_id ); $this->assertInstanceOf( \WC_Product::class, $product_found ); $this->assertEquals( $product->get_id(), $product_found->get_id() ); } - /** @see Products::get_product_by_fb_product_id() */ - public function test_get_product_by_fb_product_id_not_found() { - - $this->assertEquals( null, Facebook\Products::get_product_by_fb_product_id( '777777' ) ); - } - - /** @see Products::get_product_by_fb_retailer_id() */ - public function test_get_product_by_fb_retailer_id_with_sku() { + public function test_get_product_by_fb_retailer_id_type_sku() { + + update_option( \WC_Facebookcommerce_Integration::SETTING_FB_RETAILER_ID_TYPE, \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_SKU ); $product = $this->get_product(); $product->set_sku( '123456_a' ); @@ -1409,10 +1260,15 @@ public function test_get_product_by_fb_retailer_id_with_sku() { } - /** @see Products::get_product_by_fb_retailer_id() */ - public function test_get_product_by_fb_retailer_id_without_sku() { + /** @see Products::get_product_by_fb_retailer_id() */ + public function test_get_product_by_fb_retailer_id_type_product_id() { + + update_option( \WC_Facebookcommerce_Integration::SETTING_FB_RETAILER_ID_TYPE, \WC_Facebookcommerce_Integration::FB_RETAILER_ID_TYPE_PRODUCT_ID ); + + $product = $this->get_product(); + $product->set_sku( '123456_a' ); + $product->save(); - $product = $this->get_product(); $retailer_id = \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ); $product_found = Facebook\Products::get_product_by_fb_retailer_id( $retailer_id ); @@ -1420,7 +1276,6 @@ public function test_get_product_by_fb_retailer_id_without_sku() { $this->assertEquals( $product->get_id(), $product_found->get_id() ); } - /** Helper methods ************************************************************************************************/ @@ -1469,4 +1324,3 @@ private function add_excluded_category() { } - diff --git a/tests/integration/WC_Facebook_Product_Test.php b/tests/integration/WC_Facebook_Product_Test.php index 678efddb5..9f8b71a0b 100644 --- a/tests/integration/WC_Facebook_Product_Test.php +++ b/tests/integration/WC_Facebook_Product_Test.php @@ -104,7 +104,6 @@ public function test_prepare_product_ready_for_commerce_inventory( $woo_quantity ] ); Products::enable_sync_for_products( [ $product ] ); - Products::update_commerce_enabled_for_product( $product, true ); $data = ( new \WC_Facebook_Product( $product ) )->prepare_product(); @@ -469,7 +468,6 @@ private function get_product_ready_for_commerce( $attributes = [] ) { ] ); Products::enable_sync_for_products( [ $product ] ); - Products::update_commerce_enabled_for_product( $product, true ); return $product; } diff --git a/tests/integration/WC_Facebookcommerce_Integration_Test.php b/tests/integration/WC_Facebookcommerce_Integration_Test.php index f9c92dd82..20ca96266 100644 --- a/tests/integration/WC_Facebookcommerce_Integration_Test.php +++ b/tests/integration/WC_Facebookcommerce_Integration_Test.php @@ -774,77 +774,6 @@ public function test_is_product_sync_enabled_filter() { } - /** @see \WC_Facebookcommerce_Integration::on_product_delete() */ - public function test_on_product_delete_with_simple_product() { - - $product = $this->tester->get_product( [ 'status' => 'trash' ] ); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'delete_product_item' => Expected::once(), - // called from delete_product_item() and from delete_product_group() - 'get_product_fbid' => null, - ] ); - - $integration->on_product_delete( $product->get_id() ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_product_delete() */ - public function test_on_product_delete_with_variable_product() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'publish', - ] ); - - $product->set_name( 'Test Name' ); - $product->set_status( 'trash' ); - $product->save(); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'delete_product_item' => Expected::never(), - // get_product_fbid() is used to verify that delete_product_group() was called because we cannot set an expectation on private methods - 'get_product_fbid' => Expected::once(), - ] ); - - $integration->on_product_delete( $product->get_id() ); - - $this->tester->assertProductsAreScheduledForDelete( $product->get_children() ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_product_delete() */ - public function test_on_product_delete_with_product_variation() { - - $variation = $this->tester->get_product_variation( [ 'status' => 'trash' ] ); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'delete_product_item' => Expected::never(), - 'get_product_fbid' => Expected::never(), - ] ); - - $integration->on_product_delete( $variation->get_id() ); - - $this->tester->assertProductsAreScheduledForDelete( [ $variation->get_id() ] ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_product_delete() */ - public function test_on_product_delete_with_invalid_product_id() { - - $this->tester->setPropertyValue( facebook_for_woocommerce()->get_products_sync_handler(), 'requests', [] ); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'delete_product_item' => Expected::never(), - 'get_product_fbid' => Expected::never(), - ] ); - - $integration->on_product_delete( 0 ); - - $this->tester->assertProductsAreNotScheduledForDelete(); - } - - /** * @see \WC_Facebookcommerce_Integration::fb_change_product_published_status() * @@ -955,195 +884,13 @@ public function test_on_product_publish_with_unpublished_product() { } - /** @see \WC_Facebookcommerce_Integration::on_variable_product_publish() */ - public function test_on_variable_product_publish() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'publish', - ] ); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'get_product_fbid' => null, - 'create_product_group' => Expected::once(), - 'update_product_group' => Expected::never(), - 'delete_product_item' => Expected::never(), - ] ); - - $integration->on_variable_product_publish( $product->get_id() ); - - $this->tester->assertProductsAreScheduledForSync( $product->get_children() ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_variable_product_publish() */ - public function test_on_variable_product_publish_when_product_sync_is_disabled() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'publish', - ] ); - - add_filter( 'wc_facebook_is_product_sync_enabled', '__return_false' ); - - $this->check_on_variable_product_publish_does_not_sync_product_variations( $product->get_id(), $product ); - } - - - /** - * Tests that variable products and product variations are not synced. - * - * @param int $product_id product ID - * @param \WC_Product_Variable $product variable product object - * @param array $excluded_variation_ids IDs of variations that shouldn't be synced - */ - private function check_on_variable_product_publish_does_not_sync_product_variations( $product_id, \WC_Product_Variable $product = null, array $excluded_variation_ids = null ) { - - $this->tester->setPropertyValue( facebook_for_woocommerce()->get_products_sync_handler(), 'requests', [] ); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'get_product_fbid' => null, - // if $excluded_variation_ids is not empty, assume that at least one variation will be synced and the product group will be created - 'create_product_group' => empty( $excluded_variation_ids ) ? Expected::never() : Expected::once(), - 'update_product_group' => Expected::never(), - // may be called for outofstock parent products - 'delete_product_item' => null, - ] ); - - $integration->on_variable_product_publish( $product_id ); - - if ( null === $excluded_variation_ids ) { - $excluded_variation_ids = $product ? $product->get_children() : []; - } - - $this->tester->assertProductsAreNotScheduledForSync( $excluded_variation_ids ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_variable_product_publish() */ - public function test_on_variable_product_publish_with_invalid_product_id() { - - $this->check_on_variable_product_publish_does_not_sync_product_variations( 0 ); - } - - - /** - * @see \WC_Facebookcommerce_Integration::on_variable_product_publish() - * - * Despite its name, the method is called with products with status other than publish too. - */ - public function test_on_variable_product_publish_with_unpublished_parent_product() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'draft', - ] ); - - $this->check_on_variable_product_publish_does_not_sync_product_variations( $product->get_id(), $product ); - } - - - /** - * @see \WC_Facebookcommerce_Integration::on_variable_product_publish() - * - * Despite its name, the method is called with products with status other than publish too. - */ - public function test_on_variable_product_publish_with_unpublished_variations() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'publish', - ] ); - - $excluded_variation_ids = array_slice( $product->get_children(), -2 ); - - foreach ( $excluded_variation_ids as $variation_id ) { - - $variation = wc_get_product( $variation_id ); - - $variation->set_status( 'draft' ); - $variation->save(); - } - - $this->check_on_variable_product_publish_does_not_sync_product_variations( - $product->get_id(), - $product, - $excluded_variation_ids - ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_variable_product_publish() */ - public function test_on_variable_product_publish_with_out_of_stock_parent_product() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'publish', - 'stock_status' => 'outofstock', - ] ); - - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); - - $this->check_on_variable_product_publish_does_not_sync_product_variations( $product->get_id(), $product ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_variable_product_publish() */ - public function test_on_variable_product_publish_with_out_of_stock_product_variations() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'publish', - ] ); - - $excluded_variation_ids = array_slice( $product->get_children(), -2 ); - - foreach ( $excluded_variation_ids as $variation_id ) { - - $variation = wc_get_product( $variation_id ); - - $variation->set_stock_status( 'outofstock' ); - $variation->save(); - } - - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); - - $this->check_on_variable_product_publish_does_not_sync_product_variations( - $product->get_id(), - $product, - $excluded_variation_ids - ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_variable_product_publish() */ - public function test_on_variable_product_publish_with_do_not_sync_product_variations() { - - $product = $this->tester->get_variable_product( [ - 'children' => 3, - 'status' => 'publish', - ] ); - - $excluded_variation_ids = array_slice( $product->get_children(), -2 ); - - Products::disable_sync_for_products( array_map( 'wc_get_product', $excluded_variation_ids ) ); - - $this->check_on_variable_product_publish_does_not_sync_product_variations( - $product->get_id(), - $product, - $excluded_variation_ids - ); - } - - /** @see \WC_Facebookcommerce_Integration::on_simple_product_publish() */ public function test_on_simple_product_publish() { $product = $this->tester->get_product( [ 'status' => 'publish' ] ); $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'get_product_fbid' => Expected::once(), - 'create_product_simple' => Expected::once(), + 'create_or_update_product_item' => Expected::once(), ] ); $integration->on_simple_product_publish( $product->get_id() ); @@ -1152,67 +899,6 @@ public function test_on_simple_product_publish() { } - - /** @see \WC_Facebookcommerce_Integration::on_simple_product_publish() */ - public function test_on_simple_product_publish_when_product_sync_is_disabled() { - - $product = $this->tester->get_product( [ 'status' => 'publish' ] ); - - add_filter( 'wc_facebook_is_product_sync_enabled', '__return_false' ); - - $this->check_on_simple_product_publish_does_not_sync_product( $product->get_id(), $product ); - } - - - /** - * Tests that on_simple_product_publish() does not sync a product. - * - * @param int $product_id product ID - */ - private function check_on_simple_product_publish_does_not_sync_product( $product_id ) { - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'get_product_fbid' => Expected::never(), - 'create_product_simple' => Expected::never(), - 'update_product_item' => Expected::never(), - // may be called for outofstock products - 'delete_product_item' => null, - ] ); - - $integration->on_simple_product_publish( $product_id ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_simple_product_publish() */ - public function test_on_simple_product_publish_with_invalid_product_id() { - - $this->check_on_simple_product_publish_does_not_sync_product( 0 ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_simple_product_publish() */ - public function test_on_simple_product_publish_with_unpublished_product() { - - $product = $this->tester->get_product( [ 'status' => 'draft' ] ); - - $this->check_on_simple_product_publish_does_not_sync_product( $product->get_id() ); - } - - - /** @see \WC_Facebookcommerce_Integration::on_simple_product_publish() */ - public function test_on_simple_product_publish_with_out_of_stock_product() { - - $product = $this->tester->get_variable_product( [ - 'status' => 'publish', - 'stock_status' => 'outofstock', - ] ); - - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); - - $this->check_on_simple_product_publish_does_not_sync_product( $product->get_id() ); - } - - /** @see \WC_Facebookcommerce_Integration::product_should_be_synced() */ public function test_product_should_be_synced_when_product_sync_is_disabled() { @@ -1279,65 +965,6 @@ public function test_product_should_be_synced_with_invalid_product() { } - /** @see \WC_Facebookcommerce_Integration::update_fb_visibility() */ - public function test_update_fb_visibility_with_product_variation() { - - $product = $this->tester->get_product_variation( [ - 'status' => 'publish', - ] ); - - // update_fb_visibility() expects the Facebook Page ID to be configured - update_option( \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, '123456' ); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'get_product_fbid' => Expected::never(), - 'update_product_group' => Expected::never(), - ] ); - - $integration->update_fb_visibility( $product->get_id(), \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_VISIBLE ); - - $this->tester->assertProductsAreScheduledForSync( [ $product->get_id() ] ); - } - - - /** @see \WC_Facebookcommerce_Integration::update_fb_visibility() */ - public function test_update_fb_visibility_with_invalid_product_id() { - - $this->check_update_fb_visibility_does_not_sync_product_variation( 0 ); - } - - - /** - * Tests update_fb_visibility() does not schedule a product variation for sync. - * - * @see \WC_Facebookcommerce_Integration::update_fb_visibility() - * - * @param int $variation_id product variation ID - * @param \WC_Product_Variation $variation product variation object - */ - private function check_update_fb_visibility_does_not_sync_product_variation( $variation_id, \WC_Product_Variation $variation = null ) { - - // update_fb_visibility() expects the Facebook Page ID to be configured - update_option( \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, '123456' ); - - $integration = $this->make( \WC_Facebookcommerce_Integration::class, [ - 'get_product_fbid' => Expected::never(), - 'update_product_group' => Expected::never(), - ] ); - - $integration->update_fb_visibility( $variation_id, \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_VISIBLE ); - - $this->tester->assertProductsAreNotScheduledForSync( [ $variation_id ] ); - } - - - /** @see \WC_Facebookcommerce_Integration::update_fb_visibility() */ - public function test_update_fb_visibility_with_unpublished_product_variation() { - - $this->check_update_fb_visibility_does_not_sync_product_variation( 0 ); - } - - /** * @see \WC_Facebookcommerce_Integration::on_quick_and_bulk_edit_save() *