From dcfc8ea17d279e8a1fd9092e211e281eee683d40 Mon Sep 17 00:00:00 2001 From: Allen Snook Date: Thu, 25 Oct 2018 15:06:54 -0700 Subject: [PATCH 1/6] Start adding subscription notes to the merchant inbox --- .../class-wc-admin-notes-new-sales-record.php | 4 +- ...wc-admin-notes-woo-subscriptions-notes.php | 233 ++++++++++++++++++ plugins/woocommerce-admin/wc-admin.php | 1 + 3 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php index 5360ce1085c..924af710a3f 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php @@ -63,9 +63,9 @@ class WC_Admin_Notes_New_Sales_Record { update_option( self::RECORD_AMOUNT_OPTION_KEY, $total ); $formatted_yesterday = date( 'F jS', strtotime( $yesterday ) ); - $formatted_total = html_entity_decode( strip_tags( wc_price( $total ) ) ); + $formatted_total = html_entity_decode( wp_strip_all_tags( wc_price( $total ) ) ); $formatted_record_date = date( 'F jS', strtotime( $record_date ) ); - $formatted_record_amt = html_entity_decode( strip_tags( wc_price( $record_amt ) ) ); + $formatted_record_amt = html_entity_decode( wp_strip_all_tags( wc_price( $record_amt ) ) ); $content = sprintf( /* translators: 1 and 4: Date (e.g. October 16th), 2 and 3: Amount (e.g. $160.00) */ diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php new file mode 100644 index 00000000000..cad7d4104d0 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php @@ -0,0 +1,233 @@ +remove_notes(); + $this->add_no_connection_note(); + return; + } + + // The site just connected. + if ( empty( $old_token ) && ! empty( $new_token ) ) { + $this->remove_notes(); + $this->refresh_subscription_notes(); + return; + } + + // Something else changed. Refresh all the things. + $this->prune_inactive_subscription_notes(); + $this->refresh_subscription_notes(); + } + + /** + * Checks the connection. Adds a note (as necessary) if there is no connection. + */ + public function check_connection() { + if ( ! $this->is_connected() ) { + $data_store = WC_Data_Store::load( 'admin-note' ); + $note_ids = $data_store->get_notes_with_name( self::CONNECTION_NOTE_NAME ); + if ( ! empty( $note_ids ) ) { + return; + } + + $this->remove_notes(); + $this->add_no_connection_note(); + } + } + + /** + * Whether or not we think the site is currently connected to WooCommerce.com. + * + * @return bool + */ + public function is_connected() { + $auth = WC_Helper_Options::get( 'auth' ); + return ( ! empty( $auth['access_token'] ) ); + } + + /** + * Returns the WooCommerce.com provided site ID for this site. + * + * @return int|false + */ + public function get_connected_site_id() { + if ( ! $this->is_connected() ) { + return false; + } + + $auth = WC_Helper_Options::get( 'auth' ); + return absint( $auth['site_id'] ); + } + + /** + * Returns an array of product_ids whose subscriptions are active on this site. + * + * @return array + */ + public function get_subscription_active_product_ids() { + $site_id = $this->get_connected_site_id(); + if ( ! $site_id ) { + return array(); + } + + $product_ids = array(); + + if ( $this->is_connected() ) { + $subscriptions = WC_Helper::get_subscriptions(); + + foreach ( (array) $subscriptions as $subscription ) { + if ( in_array( $site_id, $subscription['connections'], true ) ) { + $product_ids[] = $subscription['product_id']; + } + } + } + + return $product_ids; + } + + /** + * Clears all connection or subscription notes. + */ + public function remove_notes() { + WC_Admin_Notes::delete_notes_with_name( self::CONNECTION_NOTE_NAME ); + WC_Admin_Notes::delete_notes_with_name( self::SUBSCRIPTION_NOTE_NAME ); + } + + /** + * Adds a note prompting to connect to WooCommerce.com. + */ + public function add_no_connection_note() { + $note = new WC_Admin_Note(); + $note->set_title( __( 'Connect to WooCommerce.com', 'wc-admin' ) ); + $note->set_content( __( 'Connect to get important product notifications and updates.', 'wc-admin' ) ); + $note->set_content_data( (object) array() ); + $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note->set_icon( 'info' ); + $note->set_name( self::CONNECTION_NOTE_NAME ); + $note->set_source( 'wc-admin' ); + $note->add_action( + 'connect', + __( 'Connect', 'wc-admin' ), + '?page=wc-addons§ion=helper' + ); + $note->save(); + } + + /** + * Gets the product_id (if any) associated with a note. + * + * @param WC_Admin_Note $note The note object to interrogate. + * @return string + */ + public function get_product_id_from_subscription_note( &$note ) { + $content_data = $note->get_content_data(); + + if ( array_key_exists( 'product_id', $content_data ) ) { + return $content_data['product_id']; + } + + return ''; + } + + /** + * Removes notes for product_ids no longer active on this site. + */ + public function prune_inactive_subscription_notes() { + error_log( 'in prune_inactive_subscription_notes' ); + $active_product_ids = $this->get_subscription_active_product_ids(); + + $data_store = WC_Data_Store::load( 'admin-note' ); + $note_ids = $data_store->get_notes_with_name( self::SUBSCRIPTION_NOTE_NAME ); + + foreach ( (array) $note_ids as $note_id ) { + error_log( "in prune_inactive_subscription_notes - evaluating note ID $note_id" ); + $note = WC_Admin_Notes::get_note( $note_id ); + $product_id = $this->get_product_id_from_subscription_note( $note ); + if ( ! empty( $product_id ) ) { + if ( ! in_array( $product_id, $active_product_ids, true ) ) { + error_log( "product $product_id is no longer active on this site. deleting note $note_id" ); + $note->delete(); + } + } + } + } + + /** + * For each active subscription on this site, checks the expiration date and creates/updates notes. + */ + public function refresh_subscription_notes() { + if ( ! $this->is_connected() ) { + return; + } + + error_log( 'in refresh_subscription_notes' ); + $subscriptions = WC_Helper::get_subscriptions(); + $active_product_ids = $this->get_subscription_active_product_ids(); + + foreach ( (array) $active_product_ids as $active_product_id ) { + // Find that products subscription. + // Check the expiration date. + // Is it expiring soon? + // Do we have a note already for it? + // Yes? Update it to show the number of days remaining. + // No? Create one. + // Has it expired? + // Do we have a note already for it? + // Yes? Does the note already indicate the extension has expired? + // Yes? Do nothing. + // No? Update the date to indicate it has expired. + // No note? Create one, indicate the extension has expired. + // Has it neither expired nor is expiring soon? + // Do we have a note already for it? + // Yes? Delete any note for that extension. + } + } +} + +new WC_Admin_Notes_Woo_Subscriptions_Notes(); diff --git a/plugins/woocommerce-admin/wc-admin.php b/plugins/woocommerce-admin/wc-admin.php index a46eb430790..71a65a743ee 100755 --- a/plugins/woocommerce-admin/wc-admin.php +++ b/plugins/woocommerce-admin/wc-admin.php @@ -133,6 +133,7 @@ function wc_admin_plugins_loaded() { // Admin note providers. require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-new-sales-record.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-settings-notes.php'; + require_once dirname( __FILE__ ) . '/includes/class-wc-admin-notes-woo-subscriptions-notes.php'; } add_action( 'plugins_loaded', 'wc_admin_plugins_loaded' ); From 3acd858d372c6b535a20fb929a6570ec8ade0c4c Mon Sep 17 00:00:00 2001 From: Allen Snook Date: Thu, 25 Oct 2018 16:52:01 -0700 Subject: [PATCH 2/6] Hook up the expiring, expired subscription notes --- ...wc-admin-notes-woo-subscriptions-notes.php | 216 +++++++++++++++--- 1 file changed, 188 insertions(+), 28 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php index cad7d4104d0..a14d3929895 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php @@ -15,13 +15,16 @@ defined( 'ABSPATH' ) || exit; class WC_Admin_Notes_Woo_Subscriptions_Notes { const CONNECTION_NOTE_NAME = 'wc-admin-wc-helper-connection'; const SUBSCRIPTION_NOTE_NAME = 'wc-admin-wc-helper-subscription'; + const NOTIFY_WHEN_DAYS_LEFT = 460; // TODO: Put this back to 60. /** * Hook all the things. */ public function __construct() { + add_action( 'admin_init', array( $this, 'remove_notes' ) ); // TODO For testing only. Do not commit this line. add_action( 'admin_init', array( $this, 'check_connection' ) ); - add_action( 'admin_init', array( $this, 'refresh_subscription_notes' ) ); // TODO Do not commit this line. + add_action( 'admin_init', array( $this, 'prune_inactive_subscription_notes' ) ); // TODO For testing only. Do not commit this line. + add_action( 'admin_init', array( $this, 'refresh_subscription_notes' ) ); // TODO For testing only. Do not commit this line. add_action( 'update_option_woocommerce_helper_data', array( $this, 'update_option_woocommerce_helper_data' ), 10, 2 ); // TODO : prune_inactive_subscription_notes daily. // TODO : refresh_subscription_notes daily. @@ -60,9 +63,7 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { return; } - // Something else changed. Refresh all the things. - $this->prune_inactive_subscription_notes(); - $this->refresh_subscription_notes(); + // TODO - refresh our notes if the user changes a subscription to active. } /** @@ -163,41 +164,188 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { * Gets the product_id (if any) associated with a note. * * @param WC_Admin_Note $note The note object to interrogate. - * @return string + * @return int|false */ public function get_product_id_from_subscription_note( &$note ) { $content_data = $note->get_content_data(); - if ( array_key_exists( 'product_id', $content_data ) ) { - return $content_data['product_id']; + if ( property_exists( $content_data, 'product_id' ) ) { + return intval( $content_data->product_id ); } - return ''; + return false; } /** * Removes notes for product_ids no longer active on this site. */ public function prune_inactive_subscription_notes() { - error_log( 'in prune_inactive_subscription_notes' ); $active_product_ids = $this->get_subscription_active_product_ids(); $data_store = WC_Data_Store::load( 'admin-note' ); $note_ids = $data_store->get_notes_with_name( self::SUBSCRIPTION_NOTE_NAME ); foreach ( (array) $note_ids as $note_id ) { - error_log( "in prune_inactive_subscription_notes - evaluating note ID $note_id" ); $note = WC_Admin_Notes::get_note( $note_id ); $product_id = $this->get_product_id_from_subscription_note( $note ); if ( ! empty( $product_id ) ) { if ( ! in_array( $product_id, $active_product_ids, true ) ) { - error_log( "product $product_id is no longer active on this site. deleting note $note_id" ); $note->delete(); } } } } + /** + * Finds a note for a given product ID, if the note exists at all. + * + * @param int $product_id The product ID to search for. + * @return WC_Admin_Note|false + */ + public function find_note_for_product_id( $product_id ) { + $product_id = intval( $product_id ); + + $data_store = WC_Data_Store::load( 'admin-note' ); + $note_ids = $data_store->get_notes_with_name( self::SUBSCRIPTION_NOTE_NAME ); + foreach ( (array) $note_ids as $note_id ) { + $note = WC_Admin_Notes::get_note( $note_id ); + $found_product_id = $this->get_product_id_from_subscription_note( $note ); + + if ( $product_id === $found_product_id ) { + return $note; + } + } + + return false; + } + + /** + * Deletes a note for a given product ID, if the note exists at all. + * + * @param int $product_id The product ID to search for. + */ + public function delete_any_note_for_product_id( $product_id ) { + $product_id = intval( $product_id ); + + $note = $this->find_note_for_product_id( $product_id ); + if ( $note ) { + $note->delete(); + } + } + + /** + * Adds or updates a note for an expiring subscription. + * + * @param array $subscription The subscription to work with. + */ + public function add_or_update_subscription_expiring( $subscription ) { + $product_id = $subscription['product_id']; + $product_name = $subscription['product_name']; + $expires = intval( $subscription['expires'] ); + $time_now_gmt = current_time( 'timestamp', 0 ); + $days_until_expiration = ceil( ( $expires - $time_now_gmt ) / DAY_IN_SECONDS ); + + $note = $this->find_note_for_product_id( $product_id ); + + $note_title = sprintf( + /* translators: name of the extension subscription expiring soon */ + __( '%s subscription expiring soon', 'wc-admin' ), + $product_name + ); + + $note_content = sprintf( + /* translators: number of days until the subscription expires */ + __( 'Your subscription expires in %d days. Enable autorenew to avoid losing updates and access to support.', 'wc-admin' ), + $days_until_expiration + ); + + $note_content_data = (object) array( + 'product_id' => $product_id, + 'product_name' => $product_name, + 'expired' => false, + 'days_until_expiration' => $days_until_expiration, + ); + + if ( ! $note ) { + $note = new WC_Admin_Note(); + $note->set_title( $note_title ); + $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_WARNING ); + $note->set_icon( 'notice' ); + $note->set_name( self::SUBSCRIPTION_NOTE_NAME ); + $note->set_source( 'wc-admin' ); + $note->add_action( + 'enable-autorenew', + __( 'Enable Autorenew', 'wc-admin' ), + 'https://woocommerce.com/my-account/my-subscriptions/' + ); + } + + $note->set_content( $note_content ); + $note->set_content_data( $note_content_data ); + $note->save(); + } + + /** + * Adds a note for an expired subscription, or updates an expiring note to expired. + * + * @param array $subscription The subscription to work with. + */ + public function add_or_update_subscription_expired( $subscription ) { + $product_id = $subscription['product_id']; + $product_name = $subscription['product_name']; + $product_page = $subscription['product_url']; + $expires = intval( $subscription['expires'] ); + $expires_date = date( 'F jS', $expires ); + + $note = $this->find_note_for_product_id( $product_id ); + if ( $note ) { + $note_content_data = $note->get_content_data(); + if ( $note_content_data->expired ) { + // We've already got a full fledged expired note for this. Bail. + // These notes' content doesn't change with time. + return; + } + } + + $note_title = sprintf( + /* translators: name of the extension subscription that expired */ + __( '%s subscription expired', 'wc-admin' ), + $product_name + ); + + $note_content = sprintf( + /* translators: date the subscription expired, e.g. Jun 7th 2018 */ + __( 'Your subscription expired on %s. Get a new subscription to continue receiving updates and access to support.', 'wc-admin' ), + $expires_date + ); + + $note_content_data = (object) array( + 'product_id' => $product_id, + 'product_name' => $product_name, + 'expired' => true, + 'expires' => $expires, + 'expires_date' => $expires_date, + ); + + if ( ! $note ) { + $note = new WC_Admin_Note(); + } + + $note->set_title( $note_title ); + $note->set_content( $note_content ); + $note->set_content_data( $note_content_data ); + $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_WARNING ); + $note->set_icon( 'notice' ); + $note->set_name( self::SUBSCRIPTION_NOTE_NAME ); + $note->set_source( 'wc-admin' ); + $note->add_action( + 'renew-subscription', + __( 'Renew Subscription', 'wc-admin' ), + $product_page + ); + $note->save(); + } + /** * For each active subscription on this site, checks the expiration date and creates/updates notes. */ @@ -206,26 +354,38 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { return; } - error_log( 'in refresh_subscription_notes' ); $subscriptions = WC_Helper::get_subscriptions(); $active_product_ids = $this->get_subscription_active_product_ids(); - foreach ( (array) $active_product_ids as $active_product_id ) { - // Find that products subscription. - // Check the expiration date. - // Is it expiring soon? - // Do we have a note already for it? - // Yes? Update it to show the number of days remaining. - // No? Create one. - // Has it expired? - // Do we have a note already for it? - // Yes? Does the note already indicate the extension has expired? - // Yes? Do nothing. - // No? Update the date to indicate it has expired. - // No note? Create one, indicate the extension has expired. - // Has it neither expired nor is expiring soon? - // Do we have a note already for it? - // Yes? Delete any note for that extension. + foreach ( (array) $subscriptions as $subscription ) { + // Only concern ourselves with active products. + $product_id = $subscription['product_id']; + if ( ! in_array( $product_id, $active_product_ids, true ) ) { + continue; + } + + // If the subscription will auto-renew, clean up and exit. + if ( $subscription['autorenew'] ) { + $this->delete_any_note_for_product_id( $product_id ); + continue; + } + + // If the subscription is not expiring soon, clean up and exit. + $expires = intval( $subscription['expires'] ); + $time_now_gmt = current_time( 'timestamp', 0 ); + if ( $expires > $time_now_gmt + self::NOTIFY_WHEN_DAYS_LEFT * DAY_IN_SECONDS ) { + $this->delete_any_note_for_product_id( $product_id ); + continue; + } + + // Otherwise, if the subscription can still have auto-renew enabled, let them know that now. + if ( $expires > $time_now_gmt ) { + $this->add_or_update_subscription_expiring( $subscription ); + continue; + } + + // If we got this far, the subscription has completely expired, let them know. + $this->add_or_update_subscription_expired( $subscription ); } } } From 8851a8cfe70ae88f9aa606bbf7f646b464846319 Mon Sep 17 00:00:00 2001 From: Allen Snook Date: Fri, 26 Oct 2018 14:15:51 -0700 Subject: [PATCH 3/6] Refresh subscription notes daily --- ...wc-admin-notes-woo-subscriptions-notes.php | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php index a14d3929895..b0ac20d937d 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php @@ -13,21 +13,17 @@ defined( 'ABSPATH' ) || exit; * WC_Admin_Notes_Woo_Subscriptions_Notes */ class WC_Admin_Notes_Woo_Subscriptions_Notes { - const CONNECTION_NOTE_NAME = 'wc-admin-wc-helper-connection'; - const SUBSCRIPTION_NOTE_NAME = 'wc-admin-wc-helper-subscription'; - const NOTIFY_WHEN_DAYS_LEFT = 460; // TODO: Put this back to 60. + const LAST_REFRESH_OPTION_KEY = 'wc-admin-wc-helper-last-refresh'; + const CONNECTION_NOTE_NAME = 'wc-admin-wc-helper-connection'; + const SUBSCRIPTION_NOTE_NAME = 'wc-admin-wc-helper-subscription'; + const NOTIFY_WHEN_DAYS_LEFT = 60; /** * Hook all the things. */ public function __construct() { - add_action( 'admin_init', array( $this, 'remove_notes' ) ); // TODO For testing only. Do not commit this line. - add_action( 'admin_init', array( $this, 'check_connection' ) ); - add_action( 'admin_init', array( $this, 'prune_inactive_subscription_notes' ) ); // TODO For testing only. Do not commit this line. - add_action( 'admin_init', array( $this, 'refresh_subscription_notes' ) ); // TODO For testing only. Do not commit this line. + add_action( 'admin_init', array( $this, 'admin_init' ) ); add_action( 'update_option_woocommerce_helper_data', array( $this, 'update_option_woocommerce_helper_data' ), 10, 2 ); - // TODO : prune_inactive_subscription_notes daily. - // TODO : refresh_subscription_notes daily. } /** @@ -62,8 +58,35 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $this->refresh_subscription_notes(); return; } + } - // TODO - refresh our notes if the user changes a subscription to active. + /** + * Things to do on admin_init. + */ + public function admin_init() { + $this->check_connection(); + + if ( $this->is_connected() ) { + $refresh_notes = false; + + // Did the user just do something on the helper page?. + if ( isset( $_GET['wc-helper-status'] ) ) { + $refresh_notes = true; + } + + // Has it been more than a day since we last checked? + // Note: We do it this way and not wp_scheduled_task since WC_Helper_Options is not loaded for cron. + $time_now_gmt = current_time( 'timestamp', 0 ); + $last_refresh = intval( get_option( self::LAST_REFRESH_OPTION_KEY, 0 ) ); + if ( $last_refresh + DAY_IN_SECONDS <= $time_now_gmt ) { + update_option( self::LAST_REFRESH_OPTION_KEY, $time_now_gmt ); + $refresh_notes = true; + } + + if ( $refresh_notes ) { + $this->refresh_subscription_notes(); + } + } } /** @@ -74,6 +97,7 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $data_store = WC_Data_Store::load( 'admin-note' ); $note_ids = $data_store->get_notes_with_name( self::CONNECTION_NOTE_NAME ); if ( ! empty( $note_ids ) ) { + // We already have a connection note. Exit early. return; } @@ -243,10 +267,21 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $product_name = $subscription['product_name']; $expires = intval( $subscription['expires'] ); $time_now_gmt = current_time( 'timestamp', 0 ); - $days_until_expiration = ceil( ( $expires - $time_now_gmt ) / DAY_IN_SECONDS ); + $days_until_expiration = intval( ceil( ( $expires - $time_now_gmt ) / DAY_IN_SECONDS ) ); $note = $this->find_note_for_product_id( $product_id ); + if ( $note ) { + $content_data = $note->get_content_data(); + if ( property_exists( $content_data, 'days_until_expiration' ) ) { + $note_days_until_expiration = intval( $content_data->days_until_expiration ); + if ( $days_until_expiration === $note_days_until_expiration ) { + // Note is already up to date. Bail. + return; + } + } + } + $note_title = sprintf( /* translators: name of the extension subscription expiring soon */ __( '%s subscription expiring soon', 'wc-admin' ), @@ -347,13 +382,15 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { } /** - * For each active subscription on this site, checks the expiration date and creates/updates notes. + * For each active subscription on this site, checks the expiration date and creates/updates/deletes notes. */ public function refresh_subscription_notes() { if ( ! $this->is_connected() ) { return; } + $this->prune_inactive_subscription_notes(); + $subscriptions = WC_Helper::get_subscriptions(); $active_product_ids = $this->get_subscription_active_product_ids(); From 24c80427fc906b9b236a6668a59464f39dd90085 Mon Sep 17 00:00:00 2001 From: Allen Snook Date: Fri, 26 Oct 2018 15:07:59 -0700 Subject: [PATCH 4/6] Fix a bug in the notes updater; better handle test case where note expiration/expired date changes at woocommerce.com causing an expired note to become an expiring note (corner case) --- .../includes/class-wc-admin-note.php | 7 ++++++ ...wc-admin-notes-woo-subscriptions-notes.php | 25 +++++++++++-------- .../class-wc-admin-notes-data-store.php | 16 ++++++++++-- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-note.php b/plugins/woocommerce-admin/includes/class-wc-admin-note.php index 00cc427e515..422b70e197b 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-note.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-note.php @@ -437,6 +437,13 @@ class WC_Admin_Note extends WC_Data { $this->set_date_prop( 'date_reminder', $date ); } + /** + * Clear actions from a note. + */ + public function clear_actions() { + $this->set_prop( 'actions', array() ); + } + /** * Add an action to the note * diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php index b0ac20d937d..8d9bb58f919 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php @@ -303,18 +303,20 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { if ( ! $note ) { $note = new WC_Admin_Note(); - $note->set_title( $note_title ); - $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_WARNING ); - $note->set_icon( 'notice' ); - $note->set_name( self::SUBSCRIPTION_NOTE_NAME ); - $note->set_source( 'wc-admin' ); - $note->add_action( - 'enable-autorenew', - __( 'Enable Autorenew', 'wc-admin' ), - 'https://woocommerce.com/my-account/my-subscriptions/' - ); } + // Reset everything in case we are repurposing an expired note as an expiring note. + $note->set_title( $note_title ); + $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_WARNING ); + $note->set_icon( 'notice' ); + $note->set_name( self::SUBSCRIPTION_NOTE_NAME ); + $note->set_source( 'wc-admin' ); + $note->clear_actions(); + $note->add_action( + 'enable-autorenew', + __( 'Enable Autorenew', 'wc-admin' ), + 'https://woocommerce.com/my-account/my-subscriptions/' + ); $note->set_content( $note_content ); $note->set_content_data( $note_content_data ); $note->save(); @@ -337,7 +339,7 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $note_content_data = $note->get_content_data(); if ( $note_content_data->expired ) { // We've already got a full fledged expired note for this. Bail. - // These notes' content doesn't change with time. + // Expired notes' content don't change with time. return; } } @@ -373,6 +375,7 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $note->set_icon( 'notice' ); $note->set_name( self::SUBSCRIPTION_NOTE_NAME ); $note->set_source( 'wc-admin' ); + $note->clear_actions(); $note->add_action( 'renew-subscription', __( 'Renew Subscription', 'wc-admin' ), diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php index 3e3779717ea..5995534ea01 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php @@ -104,6 +104,18 @@ class WC_Admin_Notes_Data_Store extends WC_Data_Store_WP implements WC_Object_Da global $wpdb; if ( $note->get_id() ) { + $date_created = $note->get_date_created(); + $date_created_timestamp = $date_created->getTimestamp(); + $date_created_to_db = gmdate( 'Y-m-d H:i:s', $date_created_timestamp ); + + $date_reminder = $note->get_date_reminder(); + if ( is_null( $date_reminder ) ) { + $date_reminder_to_db = null; + } else { + $date_reminder_timestamp = $date_reminder->getTimestamp(); + $date_reminder_to_db = gmdate( 'Y-m-d H:i:s', $date_reminder_timestamp ); + } + $wpdb->update( $wpdb->prefix . 'woocommerce_admin_notes', array( @@ -116,8 +128,8 @@ class WC_Admin_Notes_Data_Store extends WC_Data_Store_WP implements WC_Object_Da 'content_data' => wp_json_encode( $note->get_content_data() ), 'status' => $note->get_status(), 'source' => $note->get_source(), - 'date_created' => $note->get_date_created(), - 'date_reminder' => $note->get_date_reminder(), + 'date_created' => $date_created_to_db, + 'date_reminder' => $date_reminder_to_db, ), array( 'note_id' => $note->get_id() ) ); From 962bb73288d319fa8eabefb1d1aecd9cfcddb178 Mon Sep 17 00:00:00 2001 From: Allen Snook Date: Fri, 26 Oct 2018 15:16:39 -0700 Subject: [PATCH 5/6] Remove temporary comments on reports data store --- .../class-wc-admin-reports-data-store.php | 584 ------------------ 1 file changed, 584 deletions(-) delete mode 100644 plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php deleted file mode 100644 index 4d2eaf91140..00000000000 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php +++ /dev/null @@ -1,584 +0,0 @@ -order_by || '' === $this->order ) { - return 0; - // TODO: should return WP_Error here perhaps? - } - if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) { - return 0; - } elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) { - return strtolower( $this->order ) === 'desc' ? -1 : 1; - } elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) { - return strtolower( $this->order ) === 'desc' ? 1 : -1; - } - } - - /** - * Sorts intervals according to user's request. - * - * They are pre-sorted in SQL, but after adding gaps, they need to be sorted including the added ones. - * - * @param stdClass $data Data object, must contain an array under $data->intervals. - * @param string $sort_by Ordering property. - * @param string $direction DESC/ASC. - */ - protected function sort_intervals( &$data, $sort_by, $direction ) { - $this->order_by = $this->normalize_order_by( $sort_by ); - $this->order = $direction; - usort( $data->intervals, array( $this, 'interval_cmp' ) ); - } - - /** - * Fills in interval gaps from DB with 0-filled objects. - * - * @param array $db_intervals Array of all intervals present in the db. - * @param DateTime $datetime_start Start date. - * @param DateTime $datetime_end End date. - * @param string $time_interval Time interval, e.g. day, week, month. - * @param stdClass $data Data with SQL extracted intervals. - * @return stdClass - */ - protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) { - // TODO: this is ugly and messy. - // At this point, we don't know when we can stop iterating, as the ordering can be based on any value. - $end_datetime = new DateTime( $datetime_end ); - $time_ids = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) ); - $db_intervals = array_flip( $db_intervals ); - $datetime = new DateTime( $datetime_start ); - // Totals object used to get all needed properties. - $totals_arr = get_object_vars( $data->totals ); - foreach ( $totals_arr as $key => $val ) { - $totals_arr[ $key ] = 0; - } - while ( $datetime <= $end_datetime ) { - $next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); - $time_id = WC_Admin_Reports_Interval::time_interval_id( $time_interval, $datetime ); - // Either create fill-zero interval or use data from db. - if ( $next_start > $end_datetime ) { - $interval_end = $end_datetime->format( 'Y-m-d H:i:s' ); - } else { - $prev_end_timestamp = (int) $next_start->format( 'U' ) - 1; - $prev_end = new DateTime(); - $prev_end->setTimestamp( $prev_end_timestamp ); - $interval_end = $prev_end->format( 'Y-m-d H:i:s' ); - } - if ( array_key_exists( $time_id, $time_ids ) ) { - // For interval present in the db for this time frame, just fill in dates. - $record = &$data->intervals[ $time_ids[ $time_id ] ]; - $record['date_start'] = $datetime->format( 'Y-m-d H:i:s' ); - $record['date_end'] = $interval_end; - } elseif ( ! array_key_exists( $time_id, $db_intervals ) ) { - // For intervals present in the db outside of this time frame, do nothing. - // For intervals not present in the db, fabricate it. - $record_arr = array(); - $record_arr['time_interval'] = $time_id; - $record_arr['date_start'] = $datetime->format( 'Y-m-d H:i:s' ); - $record_arr['date_end'] = $interval_end; - $data->intervals[] = array_merge( $record_arr, $totals_arr ); - } - $datetime = $next_start; - } - return $data; - } - - /** - * Removes extra records from intervals so that only requested number of records get returned. - * - * @param stdClass $data Data from whose intervals the records get removed. - * @param int $page_no Offset requested by the user. - * @param int $items_per_page Number of records requested by the user. - * @param int $db_interval_count Database interval count. - * @param int $expected_interval_count Expected interval count on the output. - * @param string $order_by Order by field. - */ - protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by ) { - if ( 'date' === strtolower( $order_by ) ) { - $offset = 0; - } else { - $offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count; - $offset = $offset < 0 ? 0 : $offset; - } - $count = $expected_interval_count - ( $page_no - 1 ) * $items_per_page; - if ( $count < 0 ) { - $count = 0; - } elseif ( $count > $items_per_page ) { - $count = $items_per_page; - } - $data->intervals = array_slice( $data->intervals, $offset, $count ); - } - - /** - * Updates the LIMIT query part for Intervals query of the report. - * - * If there are less records in the database than time intervals, then we need to remap offset in SQL query - * to fetch correct records. - * - * @param array $intervals_query Array with clauses for the Intervals SQL query. - * @param array $query_args Query arguements. - * @param int $db_interval_count Database interval count. - * @param int $expected_interval_count Expected interval count on the output. - */ - protected function update_intervals_sql_params( &$intervals_query, &$query_args, $db_interval_count, $expected_interval_count ) { - if ( $db_interval_count === $expected_interval_count ) { - return; - } - if ( 'date' === strtolower( $query_args['orderby'] ) ) { - // page X in request translates to slightly different dates in the db, in case some - // records are missing from the db. - $start_iteration = 0; - $end_iteration = 0; - if ( 'asc' === strtolower( $query_args['order'] ) ) { - // ORDER BY date ASC. - $new_start_date = new DateTime( $query_args['after'] ); - $intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page']; - $latest_end_date = new DateTime( $query_args['before'] ); - for ( $i = 0; $i < $intervals_to_skip; $i++ ) { - if ( $new_start_date > $latest_end_date ) { - $new_start_date = $latest_end_date; - $start_iteration = 0; - break; - } - $new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'] ); - $start_iteration ++; - } - - $new_end_date = clone $new_start_date; - for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) { - if ( $new_end_date > $latest_end_date ) { - $new_end_date = $latest_end_date; - $end_iteration = 0; - break; - } - $new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'] ); - $end_iteration ++; - } - if ( $end_iteration ) { - $new_end_date_timestamp = (int) $new_end_date->format( 'U' ) - 1; - $new_end_date->setTimestamp( $new_end_date_timestamp ); - } - } else { - // ORDER BY date DESC. - $new_end_date = new DateTime( $query_args['before'] ); - $intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page']; - $earliest_start_date = new DateTime( $query_args['after'] ); - for ( $i = 0; $i < $intervals_to_skip; $i++ ) { - if ( $new_end_date < $earliest_start_date ) { - $new_end_date = $earliest_start_date; - $end_iteration = 0; - break; - } - $new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'], true ); - $end_iteration ++; - } - - $new_start_date = clone $new_end_date; - for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) { - if ( $new_start_date < $earliest_start_date ) { - $new_start_date = $earliest_start_date; - $start_iteration = 0; - break; - } - $new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'], true ); - $start_iteration ++; - } - if ( $start_iteration ) { - // TODO: is this correct? should it only be added if iterate runs? other two iterate instances, too? - $new_start_date_timestamp = (int) $new_start_date->format( 'U' ) + 1; - $new_start_date->setTimestamp( $new_start_date_timestamp ); - } - } - $query_args['adj_after'] = $new_start_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); - $query_args['adj_before'] = $new_end_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); - $intervals_query['where_clause'] = ''; - $intervals_query['where_clause'] .= " AND date_created <= '{$query_args['adj_before']}'"; - $intervals_query['where_clause'] .= " AND date_created >= '{$query_args['adj_after']}'"; - $intervals_query['limit'] = 'LIMIT 0,' . $intervals_query['per_page']; - } else { - if ( 'asc' === $query_args['order'] ) { - $offset = ( ( $query_args['page'] - 1 ) * $intervals_query['per_page'] ) - ( $expected_interval_count - $db_interval_count ); - $offset = $offset < 0 ? 0 : $offset; - $count = $query_args['page'] * $intervals_query['per_page'] - ( $expected_interval_count - $db_interval_count ); - if ( $count < 0 ) { - $count = 0; - } elseif ( $count > $intervals_query['per_page'] ) { - $count = $intervals_query['per_page']; - } - $intervals_query['limit'] = 'LIMIT ' . $offset . ',' . $count; - } - // Otherwise no change in limit clause. - $query_args['adj_after'] = $query_args['after']; - $query_args['adj_before'] = $query_args['before']; - } - } - - /** - * Returns string to be used as cache key for the data. - * - * @param array $params Query parameters. - * @return string - */ - protected function get_cache_key( $params ) { - // TODO: this is not working in PHP 5.2 (but revenue class has static methods, so it cannot use object property). - return 'woocommerce_' . $this::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); // phpcs:ignore PHPCompatibility.Syntax.NewDynamicAccessToStatic - } - - /** - * Casts strings returned from the database to appropriate data types for output. - * - * @param array $array Associative array of values extracted from the database. - * @return array|WP_Error - */ - protected function cast_numbers( $array ) { - $retyped_array = array(); - $column_types = apply_filters( 'woocommerce_rest_reports_column_types', $this->column_types, $array ); - foreach ( $array as $column_name => $value ) { - if ( isset( $column_types[ $column_name ] ) ) { - $retyped_array[ $column_name ] = $column_types[ $column_name ]( $value ); - } else { - $retyped_array[ $column_name ] = $value; - } - } - return $retyped_array; - } - - /** - * Returns a list of columns selected by the query_args formatted as a comma separated string. - * - * @param array $query_args User-supplied options. - * @return string - */ - protected function selected_columns( $query_args ) { - $selections = $this->report_columns; - - if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { - $keep = array(); - foreach ( $query_args['fields'] as $field ) { - if ( isset( $selections[ $field ] ) ) { - $keep[ $field ] = $selections[ $field ]; - } - } - $selections = implode( ', ', $keep ); - } else { - $selections = implode( ', ', $selections ); - } - return $selections; - } - - /** - * Get the order statuses used when calculating reports. - * - * @return array - */ - protected static function get_report_order_statuses() { - return apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ); - } - - /** - * Maps order status provided by the user to the one used in the database. - * - * @param string $status Order status. - * @return string - */ - protected function normalize_order_status( $status ) { - $status = trim( $status ); - return 'wc-' . $status; - } - - /** - * Normalizes order_by clause to match to SQL query. - * - * @param string $order_by Order by option requeste by user. - * @return string - */ - protected function normalize_order_by( $order_by ) { - if ( 'date' === $order_by ) { - return 'time_interval'; - } - - return $order_by; - } - - /** - * Updates start and end dates for intervals so that they represent intervals' borders, not times when data in db were recorded. - * - * E.g. if there are db records for only Tuesday and Thursday this week, the actual week interval is [Mon, Sun], not [Tue, Thu]. - * - * @param DateTime $datetime_start Start date. - * @param DateTime $datetime_end End date. - * @param string $time_interval Time interval, e.g. day, week, month. - * @param array $intervals Array of intervals extracted from SQL db. - */ - protected function update_interval_boundary_dates( $datetime_start, $datetime_end, $time_interval, &$intervals ) { - foreach ( $intervals as $key => $interval ) { - $datetime = new DateTime( $interval['datetime_anchor'] ); - - $prev_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval, true ); - // TODO: not sure if the +1/-1 here are correct, especially as they are applied before the ?: below. - $prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1; - $prev_start->setTimestamp( $prev_start_timestamp ); - if ( $datetime_start ) { - $start_datetime = new DateTime( $datetime_start ); - $date_start = $prev_start < $start_datetime ? $start_datetime : $prev_start; - $intervals[ $key ]['date_start'] = $date_start->format( 'Y-m-d H:i:s' ); - } else { - $intervals[ $key ]['date_start'] = $prev_start->format( 'Y-m-d H:i:s' ); - } - - $next_end = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); - $next_end_timestamp = (int) $next_end->format( 'U' ) - 1; - $next_end->setTimestamp( $next_end_timestamp ); - if ( $datetime_end ) { - $end_datetime = new DateTime( $datetime_end ); - $date_end = $next_end > $end_datetime ? $end_datetime : $next_end; - $intervals[ $key ]['date_end'] = $date_end->format( 'Y-m-d H:i:s' ); - } else { - $intervals[ $key ]['date_end'] = $next_end->format( 'Y-m-d H:i:s' ); - } - - $intervals[ $key ]['interval'] = $time_interval; - } - } - - /** - * Change structure of intervals to form a correct response. - * - * @param array $intervals Time interval, e.g. day, week, month. - */ - protected function create_interval_subtotals( &$intervals ) { - foreach ( $intervals as $key => $interval ) { - // Move intervals result to subtotals object. - $intervals[ $key ] = array( - 'interval' => $interval['time_interval'], - 'date_start' => $interval['date_start'], - 'date_start_gmt' => $interval['date_start'], - 'date_end' => $interval['date_end'], - 'date_end_gmt' => $interval['date_end'], - ); - - unset( $interval['interval'] ); - unset( $interval['date_start'] ); - unset( $interval['date_end'] ); - unset( $interval['datetime_anchor'] ); - unset( $interval['time_interval'] ); - $intervals[ $key ]['subtotals'] = (object) $this->cast_numbers( $interval ); - } - } - - /** - * Fills WHERE clause of SQL request for 'Totals' section of data response based on user supplied parameters. - * - * @param array $query_args Parameters supplied by the user. - * @return array - */ - protected function get_time_period_sql_params( $query_args ) { - $sql_query = array( - 'from_clause' => '', - 'where_clause' => '', - ); - - if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) { - $datetime = new DateTime( $query_args['before'] ); - $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); - $sql_query['where_clause'] .= " AND date_created <= '$datetime_str'"; - - } - - if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) { - $datetime = new DateTime( $query_args['after'] ); - $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); - $sql_query['where_clause'] .= " AND date_created >= '$datetime_str'"; - } - - return $sql_query; - } - - /** - * Fills LIMIT clause of SQL request based on user supplied parameters. - * - * @param array $query_args Parameters supplied by the user. - * @return array - */ - protected function get_limit_sql_params( $query_args ) { - $sql_query['per_page'] = get_option( 'posts_per_page' ); - if ( isset( $query_args['per_page'] ) && is_numeric( $query_args['per_page'] ) ) { - $sql_query['per_page'] = (int) $query_args['per_page']; - } - - $sql_query['offset'] = 0; - if ( isset( $query_args['page'] ) ) { - $sql_query['offset'] = ( (int) $query_args['page'] - 1 ) * $sql_query['per_page']; - } - - $sql_query['limit'] = "LIMIT {$sql_query['offset']}, {$sql_query['per_page']}"; - return $sql_query; - } - - /** - * Fills ORDER BY clause of SQL request based on user supplied parameters. - * - * @param array $query_args Parameters supplied by the user. - * @return array - */ - protected function get_order_by_sql_params( $query_args ) { - $sql_query['order_by_clause'] = ''; - if ( isset( $query_args['orderby'] ) ) { - $sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] ); - } - - if ( isset( $query_args['order'] ) ) { - $sql_query['order_by_clause'] .= ' ' . $query_args['order']; - } else { - $sql_query['order_by_clause'] .= ' DESC'; - } - - return $sql_query; - } - - /** - * Fills FROM and WHERE clauses of SQL request for 'Intervals' section of data response based on user supplied parameters. - * - * @param array $query_args Parameters supplied by the user. - * @return array - */ - protected function get_intervals_sql_params( $query_args ) { - $intervals_query = array( - 'from_clause' => '', - 'where_clause' => '', - ); - - $intervals_query = array_merge( $intervals_query, $this->get_time_period_sql_params( $query_args ) ); - - if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) { - $interval = $query_args['interval']; - $intervals_query['select_clause'] = WC_Admin_Reports_Interval::db_datetime_format( $interval ); - } - - $intervals_query = array_merge( $intervals_query, $this->get_limit_sql_params( $query_args ) ); - - $intervals_query = array_merge( $intervals_query, $this->get_order_by_sql_params( $query_args ) ); - - return $intervals_query; - } - - /** - * Returns an array of products belonging to given categories. - * - * @param array $categories List of categories IDs. - * @return array|stdClass - */ - protected function get_products_by_cat_ids( $categories ) { - $product_categories = get_categories( - array( - 'hide_empty' => 0, - 'taxonomy' => 'product_cat', - ) - ); - $cat_slugs = array(); - $categories = array_flip( $categories ); - foreach ( $product_categories as $product_cat ) { - if ( key_exists( $product_cat->cat_ID, $categories ) ) { - $cat_slugs[] = $product_cat->slug; - } - } - $args = array( - 'category' => $cat_slugs, - 'limit' => -1, - ); - return wc_get_products( $args ); - } - - /** - * Returns ids of allowed products, based on query arguments from the user. - * - * @param array $query_args Parameters supplied by the user. - * @return array - */ - protected function get_allowed_products( $query_args ) { - $allowed_products = array(); - if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) { - $allowed_products = $this->get_products_by_cat_ids( $query_args['categories'] ); - $allowed_products = wc_list_pluck( $allowed_products, 'get_id' ); - } - - if ( isset( $query_args['products'] ) && is_array( $query_args['products'] ) && count( $query_args['products'] ) > 0 ) { - if ( count( $allowed_products ) > 0 ) { - $allowed_products = array_intersect( $allowed_products, $query_args['products'] ); - } else { - $allowed_products = $query_args['products']; - } - } - return $allowed_products; - } - -} From 9246ab6ef47050676ba3cb2e233edd41834241ec Mon Sep 17 00:00:00 2001 From: Allen Snook Date: Wed, 7 Nov 2018 07:25:17 -0500 Subject: [PATCH 6/6] Correct merge of class-wc-admin-reports-data-store --- .../class-wc-admin-reports-data-store.php | 584 ++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php new file mode 100644 index 00000000000..4d2eaf91140 --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php @@ -0,0 +1,584 @@ +order_by || '' === $this->order ) { + return 0; + // TODO: should return WP_Error here perhaps? + } + if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) { + return 0; + } elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) { + return strtolower( $this->order ) === 'desc' ? -1 : 1; + } elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) { + return strtolower( $this->order ) === 'desc' ? 1 : -1; + } + } + + /** + * Sorts intervals according to user's request. + * + * They are pre-sorted in SQL, but after adding gaps, they need to be sorted including the added ones. + * + * @param stdClass $data Data object, must contain an array under $data->intervals. + * @param string $sort_by Ordering property. + * @param string $direction DESC/ASC. + */ + protected function sort_intervals( &$data, $sort_by, $direction ) { + $this->order_by = $this->normalize_order_by( $sort_by ); + $this->order = $direction; + usort( $data->intervals, array( $this, 'interval_cmp' ) ); + } + + /** + * Fills in interval gaps from DB with 0-filled objects. + * + * @param array $db_intervals Array of all intervals present in the db. + * @param DateTime $datetime_start Start date. + * @param DateTime $datetime_end End date. + * @param string $time_interval Time interval, e.g. day, week, month. + * @param stdClass $data Data with SQL extracted intervals. + * @return stdClass + */ + protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) { + // TODO: this is ugly and messy. + // At this point, we don't know when we can stop iterating, as the ordering can be based on any value. + $end_datetime = new DateTime( $datetime_end ); + $time_ids = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) ); + $db_intervals = array_flip( $db_intervals ); + $datetime = new DateTime( $datetime_start ); + // Totals object used to get all needed properties. + $totals_arr = get_object_vars( $data->totals ); + foreach ( $totals_arr as $key => $val ) { + $totals_arr[ $key ] = 0; + } + while ( $datetime <= $end_datetime ) { + $next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); + $time_id = WC_Admin_Reports_Interval::time_interval_id( $time_interval, $datetime ); + // Either create fill-zero interval or use data from db. + if ( $next_start > $end_datetime ) { + $interval_end = $end_datetime->format( 'Y-m-d H:i:s' ); + } else { + $prev_end_timestamp = (int) $next_start->format( 'U' ) - 1; + $prev_end = new DateTime(); + $prev_end->setTimestamp( $prev_end_timestamp ); + $interval_end = $prev_end->format( 'Y-m-d H:i:s' ); + } + if ( array_key_exists( $time_id, $time_ids ) ) { + // For interval present in the db for this time frame, just fill in dates. + $record = &$data->intervals[ $time_ids[ $time_id ] ]; + $record['date_start'] = $datetime->format( 'Y-m-d H:i:s' ); + $record['date_end'] = $interval_end; + } elseif ( ! array_key_exists( $time_id, $db_intervals ) ) { + // For intervals present in the db outside of this time frame, do nothing. + // For intervals not present in the db, fabricate it. + $record_arr = array(); + $record_arr['time_interval'] = $time_id; + $record_arr['date_start'] = $datetime->format( 'Y-m-d H:i:s' ); + $record_arr['date_end'] = $interval_end; + $data->intervals[] = array_merge( $record_arr, $totals_arr ); + } + $datetime = $next_start; + } + return $data; + } + + /** + * Removes extra records from intervals so that only requested number of records get returned. + * + * @param stdClass $data Data from whose intervals the records get removed. + * @param int $page_no Offset requested by the user. + * @param int $items_per_page Number of records requested by the user. + * @param int $db_interval_count Database interval count. + * @param int $expected_interval_count Expected interval count on the output. + * @param string $order_by Order by field. + */ + protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by ) { + if ( 'date' === strtolower( $order_by ) ) { + $offset = 0; + } else { + $offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count; + $offset = $offset < 0 ? 0 : $offset; + } + $count = $expected_interval_count - ( $page_no - 1 ) * $items_per_page; + if ( $count < 0 ) { + $count = 0; + } elseif ( $count > $items_per_page ) { + $count = $items_per_page; + } + $data->intervals = array_slice( $data->intervals, $offset, $count ); + } + + /** + * Updates the LIMIT query part for Intervals query of the report. + * + * If there are less records in the database than time intervals, then we need to remap offset in SQL query + * to fetch correct records. + * + * @param array $intervals_query Array with clauses for the Intervals SQL query. + * @param array $query_args Query arguements. + * @param int $db_interval_count Database interval count. + * @param int $expected_interval_count Expected interval count on the output. + */ + protected function update_intervals_sql_params( &$intervals_query, &$query_args, $db_interval_count, $expected_interval_count ) { + if ( $db_interval_count === $expected_interval_count ) { + return; + } + if ( 'date' === strtolower( $query_args['orderby'] ) ) { + // page X in request translates to slightly different dates in the db, in case some + // records are missing from the db. + $start_iteration = 0; + $end_iteration = 0; + if ( 'asc' === strtolower( $query_args['order'] ) ) { + // ORDER BY date ASC. + $new_start_date = new DateTime( $query_args['after'] ); + $intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page']; + $latest_end_date = new DateTime( $query_args['before'] ); + for ( $i = 0; $i < $intervals_to_skip; $i++ ) { + if ( $new_start_date > $latest_end_date ) { + $new_start_date = $latest_end_date; + $start_iteration = 0; + break; + } + $new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'] ); + $start_iteration ++; + } + + $new_end_date = clone $new_start_date; + for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) { + if ( $new_end_date > $latest_end_date ) { + $new_end_date = $latest_end_date; + $end_iteration = 0; + break; + } + $new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'] ); + $end_iteration ++; + } + if ( $end_iteration ) { + $new_end_date_timestamp = (int) $new_end_date->format( 'U' ) - 1; + $new_end_date->setTimestamp( $new_end_date_timestamp ); + } + } else { + // ORDER BY date DESC. + $new_end_date = new DateTime( $query_args['before'] ); + $intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page']; + $earliest_start_date = new DateTime( $query_args['after'] ); + for ( $i = 0; $i < $intervals_to_skip; $i++ ) { + if ( $new_end_date < $earliest_start_date ) { + $new_end_date = $earliest_start_date; + $end_iteration = 0; + break; + } + $new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'], true ); + $end_iteration ++; + } + + $new_start_date = clone $new_end_date; + for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) { + if ( $new_start_date < $earliest_start_date ) { + $new_start_date = $earliest_start_date; + $start_iteration = 0; + break; + } + $new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'], true ); + $start_iteration ++; + } + if ( $start_iteration ) { + // TODO: is this correct? should it only be added if iterate runs? other two iterate instances, too? + $new_start_date_timestamp = (int) $new_start_date->format( 'U' ) + 1; + $new_start_date->setTimestamp( $new_start_date_timestamp ); + } + } + $query_args['adj_after'] = $new_start_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); + $query_args['adj_before'] = $new_end_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); + $intervals_query['where_clause'] = ''; + $intervals_query['where_clause'] .= " AND date_created <= '{$query_args['adj_before']}'"; + $intervals_query['where_clause'] .= " AND date_created >= '{$query_args['adj_after']}'"; + $intervals_query['limit'] = 'LIMIT 0,' . $intervals_query['per_page']; + } else { + if ( 'asc' === $query_args['order'] ) { + $offset = ( ( $query_args['page'] - 1 ) * $intervals_query['per_page'] ) - ( $expected_interval_count - $db_interval_count ); + $offset = $offset < 0 ? 0 : $offset; + $count = $query_args['page'] * $intervals_query['per_page'] - ( $expected_interval_count - $db_interval_count ); + if ( $count < 0 ) { + $count = 0; + } elseif ( $count > $intervals_query['per_page'] ) { + $count = $intervals_query['per_page']; + } + $intervals_query['limit'] = 'LIMIT ' . $offset . ',' . $count; + } + // Otherwise no change in limit clause. + $query_args['adj_after'] = $query_args['after']; + $query_args['adj_before'] = $query_args['before']; + } + } + + /** + * Returns string to be used as cache key for the data. + * + * @param array $params Query parameters. + * @return string + */ + protected function get_cache_key( $params ) { + // TODO: this is not working in PHP 5.2 (but revenue class has static methods, so it cannot use object property). + return 'woocommerce_' . $this::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); // phpcs:ignore PHPCompatibility.Syntax.NewDynamicAccessToStatic + } + + /** + * Casts strings returned from the database to appropriate data types for output. + * + * @param array $array Associative array of values extracted from the database. + * @return array|WP_Error + */ + protected function cast_numbers( $array ) { + $retyped_array = array(); + $column_types = apply_filters( 'woocommerce_rest_reports_column_types', $this->column_types, $array ); + foreach ( $array as $column_name => $value ) { + if ( isset( $column_types[ $column_name ] ) ) { + $retyped_array[ $column_name ] = $column_types[ $column_name ]( $value ); + } else { + $retyped_array[ $column_name ] = $value; + } + } + return $retyped_array; + } + + /** + * Returns a list of columns selected by the query_args formatted as a comma separated string. + * + * @param array $query_args User-supplied options. + * @return string + */ + protected function selected_columns( $query_args ) { + $selections = $this->report_columns; + + if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { + $keep = array(); + foreach ( $query_args['fields'] as $field ) { + if ( isset( $selections[ $field ] ) ) { + $keep[ $field ] = $selections[ $field ]; + } + } + $selections = implode( ', ', $keep ); + } else { + $selections = implode( ', ', $selections ); + } + return $selections; + } + + /** + * Get the order statuses used when calculating reports. + * + * @return array + */ + protected static function get_report_order_statuses() { + return apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ); + } + + /** + * Maps order status provided by the user to the one used in the database. + * + * @param string $status Order status. + * @return string + */ + protected function normalize_order_status( $status ) { + $status = trim( $status ); + return 'wc-' . $status; + } + + /** + * Normalizes order_by clause to match to SQL query. + * + * @param string $order_by Order by option requeste by user. + * @return string + */ + protected function normalize_order_by( $order_by ) { + if ( 'date' === $order_by ) { + return 'time_interval'; + } + + return $order_by; + } + + /** + * Updates start and end dates for intervals so that they represent intervals' borders, not times when data in db were recorded. + * + * E.g. if there are db records for only Tuesday and Thursday this week, the actual week interval is [Mon, Sun], not [Tue, Thu]. + * + * @param DateTime $datetime_start Start date. + * @param DateTime $datetime_end End date. + * @param string $time_interval Time interval, e.g. day, week, month. + * @param array $intervals Array of intervals extracted from SQL db. + */ + protected function update_interval_boundary_dates( $datetime_start, $datetime_end, $time_interval, &$intervals ) { + foreach ( $intervals as $key => $interval ) { + $datetime = new DateTime( $interval['datetime_anchor'] ); + + $prev_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval, true ); + // TODO: not sure if the +1/-1 here are correct, especially as they are applied before the ?: below. + $prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1; + $prev_start->setTimestamp( $prev_start_timestamp ); + if ( $datetime_start ) { + $start_datetime = new DateTime( $datetime_start ); + $date_start = $prev_start < $start_datetime ? $start_datetime : $prev_start; + $intervals[ $key ]['date_start'] = $date_start->format( 'Y-m-d H:i:s' ); + } else { + $intervals[ $key ]['date_start'] = $prev_start->format( 'Y-m-d H:i:s' ); + } + + $next_end = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); + $next_end_timestamp = (int) $next_end->format( 'U' ) - 1; + $next_end->setTimestamp( $next_end_timestamp ); + if ( $datetime_end ) { + $end_datetime = new DateTime( $datetime_end ); + $date_end = $next_end > $end_datetime ? $end_datetime : $next_end; + $intervals[ $key ]['date_end'] = $date_end->format( 'Y-m-d H:i:s' ); + } else { + $intervals[ $key ]['date_end'] = $next_end->format( 'Y-m-d H:i:s' ); + } + + $intervals[ $key ]['interval'] = $time_interval; + } + } + + /** + * Change structure of intervals to form a correct response. + * + * @param array $intervals Time interval, e.g. day, week, month. + */ + protected function create_interval_subtotals( &$intervals ) { + foreach ( $intervals as $key => $interval ) { + // Move intervals result to subtotals object. + $intervals[ $key ] = array( + 'interval' => $interval['time_interval'], + 'date_start' => $interval['date_start'], + 'date_start_gmt' => $interval['date_start'], + 'date_end' => $interval['date_end'], + 'date_end_gmt' => $interval['date_end'], + ); + + unset( $interval['interval'] ); + unset( $interval['date_start'] ); + unset( $interval['date_end'] ); + unset( $interval['datetime_anchor'] ); + unset( $interval['time_interval'] ); + $intervals[ $key ]['subtotals'] = (object) $this->cast_numbers( $interval ); + } + } + + /** + * Fills WHERE clause of SQL request for 'Totals' section of data response based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_time_period_sql_params( $query_args ) { + $sql_query = array( + 'from_clause' => '', + 'where_clause' => '', + ); + + if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) { + $datetime = new DateTime( $query_args['before'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_clause'] .= " AND date_created <= '$datetime_str'"; + + } + + if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) { + $datetime = new DateTime( $query_args['after'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_clause'] .= " AND date_created >= '$datetime_str'"; + } + + return $sql_query; + } + + /** + * Fills LIMIT clause of SQL request based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_limit_sql_params( $query_args ) { + $sql_query['per_page'] = get_option( 'posts_per_page' ); + if ( isset( $query_args['per_page'] ) && is_numeric( $query_args['per_page'] ) ) { + $sql_query['per_page'] = (int) $query_args['per_page']; + } + + $sql_query['offset'] = 0; + if ( isset( $query_args['page'] ) ) { + $sql_query['offset'] = ( (int) $query_args['page'] - 1 ) * $sql_query['per_page']; + } + + $sql_query['limit'] = "LIMIT {$sql_query['offset']}, {$sql_query['per_page']}"; + return $sql_query; + } + + /** + * Fills ORDER BY clause of SQL request based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_order_by_sql_params( $query_args ) { + $sql_query['order_by_clause'] = ''; + if ( isset( $query_args['orderby'] ) ) { + $sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] ); + } + + if ( isset( $query_args['order'] ) ) { + $sql_query['order_by_clause'] .= ' ' . $query_args['order']; + } else { + $sql_query['order_by_clause'] .= ' DESC'; + } + + return $sql_query; + } + + /** + * Fills FROM and WHERE clauses of SQL request for 'Intervals' section of data response based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_intervals_sql_params( $query_args ) { + $intervals_query = array( + 'from_clause' => '', + 'where_clause' => '', + ); + + $intervals_query = array_merge( $intervals_query, $this->get_time_period_sql_params( $query_args ) ); + + if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) { + $interval = $query_args['interval']; + $intervals_query['select_clause'] = WC_Admin_Reports_Interval::db_datetime_format( $interval ); + } + + $intervals_query = array_merge( $intervals_query, $this->get_limit_sql_params( $query_args ) ); + + $intervals_query = array_merge( $intervals_query, $this->get_order_by_sql_params( $query_args ) ); + + return $intervals_query; + } + + /** + * Returns an array of products belonging to given categories. + * + * @param array $categories List of categories IDs. + * @return array|stdClass + */ + protected function get_products_by_cat_ids( $categories ) { + $product_categories = get_categories( + array( + 'hide_empty' => 0, + 'taxonomy' => 'product_cat', + ) + ); + $cat_slugs = array(); + $categories = array_flip( $categories ); + foreach ( $product_categories as $product_cat ) { + if ( key_exists( $product_cat->cat_ID, $categories ) ) { + $cat_slugs[] = $product_cat->slug; + } + } + $args = array( + 'category' => $cat_slugs, + 'limit' => -1, + ); + return wc_get_products( $args ); + } + + /** + * Returns ids of allowed products, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_allowed_products( $query_args ) { + $allowed_products = array(); + if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) { + $allowed_products = $this->get_products_by_cat_ids( $query_args['categories'] ); + $allowed_products = wc_list_pluck( $allowed_products, 'get_id' ); + } + + if ( isset( $query_args['products'] ) && is_array( $query_args['products'] ) && count( $query_args['products'] ) > 0 ) { + if ( count( $allowed_products ) > 0 ) { + $allowed_products = array_intersect( $allowed_products, $query_args['products'] ); + } else { + $allowed_products = $query_args['products']; + } + } + return $allowed_products; + } + +}