Updates to 7.4.0

This commit is contained in:
WooCommerce
2025-04-15 10:17:59 +00:00
parent 60e548cc2d
commit c2175bfc37
32 changed files with 830 additions and 622 deletions

View File

@@ -1,5 +1,13 @@
*** WooCommerce Subscriptions Changelog *** *** WooCommerce Subscriptions Changelog ***
2025-04-14 - version 7.4.0
* Update: Increase the number of args accepted by wcs_get_subscriptions(), to bring about parity with wc_get_orders().
* Dev: Update wcs_maybe_prefix_key() and wcs_maybe_unprefix_key() to support an array of keys.
* Fix: Prevent sending renewal reminders for orders with a 0 total.
* Fix: Ensure the second parameter passed to the 'get_edit_post_link' filter is an integer.
* Fix: Prevent WooCommerce Subscriptions buttons from overflowing in the View subscription page
* Dev: Update subscriptions-core to 8.2.0
2025-03-31 - version 7.3.1 2025-03-31 - version 7.3.1
* Update: Display the subscription parent order icon in the WooCommerce Orders list table by default. * Update: Display the subscription parent order icon in the WooCommerce Orders list table by default.
* Dev: Update subscriptions-core to 8.1.1 * Dev: Update subscriptions-core to 8.1.1

View File

@@ -66,8 +66,8 @@ class WCS_Report_Dashboard {
WHERE wcorder.post_type IN ( 'shop_order' ) WHERE wcorder.post_type IN ( 'shop_order' )
AND wcsubs.post_type IN ( 'shop_subscription' ) AND wcsubs.post_type IN ( 'shop_subscription' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' ) AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s' AND wcorder.post_date >= %s
AND wcorder.post_date < '%s'", AND wcorder.post_date < %s",
date( 'Y-m-01', current_time( 'timestamp' ) ), date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
); );
@@ -76,6 +76,7 @@ class WCS_Report_Dashboard {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) );
$update_cache = true; $update_cache = true;
} }
@@ -95,8 +96,8 @@ class WCS_Report_Dashboard {
WHERE wcorder.post_type IN ( 'shop_order' ) WHERE wcorder.post_type IN ( 'shop_order' )
AND wcsubs.post_type IN ( 'shop_subscription' ) AND wcsubs.post_type IN ( 'shop_subscription' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' ) AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s' AND wcorder.post_date >= %s
AND wcorder.post_date < '%s' AND wcorder.post_date < %s
) AS orders ON orders.ID = order_total_meta.post_id ) AS orders ON orders.ID = order_total_meta.post_id
WHERE order_total_meta.meta_key = '_order_total'", WHERE order_total_meta.meta_key = '_order_total'",
date( 'Y-m-01', current_time( 'timestamp' ) ), date( 'Y-m-01', current_time( 'timestamp' ) ),
@@ -107,6 +108,7 @@ class WCS_Report_Dashboard {
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query ) ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query ) );
$update_cache = true; $update_cache = true;
} }
@@ -125,8 +127,8 @@ class WCS_Report_Dashboard {
) )
WHERE wcorder.post_type IN ( 'shop_order' ) WHERE wcorder.post_type IN ( 'shop_order' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' ) AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s' AND wcorder.post_date >= %s
AND wcorder.post_date < '%s'", AND wcorder.post_date < %s",
date( 'Y-m-01', current_time( 'timestamp' ) ), date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
); );
@@ -135,6 +137,7 @@ class WCS_Report_Dashboard {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) );
$update_cache = true; $update_cache = true;
} }
@@ -157,8 +160,8 @@ class WCS_Report_Dashboard {
) )
WHERE wcorder.post_type IN ( 'shop_order' ) WHERE wcorder.post_type IN ( 'shop_order' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' ) AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s' AND wcorder.post_date >= %s
AND wcorder.post_date < '%s' AND wcorder.post_date < %s
) AS orders ON orders.ID = order_total_meta.post_id ) AS orders ON orders.ID = order_total_meta.post_id
WHERE order_total_meta.meta_key = '_order_total'", WHERE order_total_meta.meta_key = '_order_total'",
date( 'Y-m-01', current_time( 'timestamp' ) ), date( 'Y-m-01', current_time( 'timestamp' ) ),
@@ -169,6 +172,7 @@ class WCS_Report_Dashboard {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query ) ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query ) );
$update_cache = true; $update_cache = true;
} }
@@ -183,7 +187,8 @@ class WCS_Report_Dashboard {
ON wcsubs.ID = wcsmeta_cancel.post_id ON wcsubs.ID = wcsmeta_cancel.post_id
AND wcsmeta_cancel.meta_key = '_schedule_cancelled' AND wcsmeta_cancel.meta_key = '_schedule_cancelled'
AND wcsubs.post_status NOT IN ( 'trash', 'auto-draft' ) AND wcsubs.post_status NOT IN ( 'trash', 'auto-draft' )
AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', '{$site_timezone}' ) BETWEEN '%s' AND '%s'", AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', %s ) BETWEEN %s AND %s",
$site_timezone,
date( 'Y-m-01', current_time( 'timestamp' ) ), date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
); );
@@ -192,6 +197,7 @@ class WCS_Report_Dashboard {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query ) ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query ) );
$update_cache = true; $update_cache = true;
} }

View File

@@ -74,7 +74,8 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
$oldest_subscription_age = floor( $oldest_subscription_age_in_days / $days_in_interval_period ); $oldest_subscription_age = floor( $oldest_subscription_age_in_days / $days_in_interval_period );
// Now get all subscriptions, not just those that have ended, and find out how long they have lived (or if they haven't ended yet, consider them as being alive for one period longer than the longest living subsription) // Now get all subscriptions, not just those that have ended, and find out how long they have lived (or if they haven't ended yet, consider them as being alive for one period longer than the longest living subsription)
$base_query = $wpdb->prepare( $subscription_ages = $wpdb->get_results(
$wpdb->prepare(
"SELECT "SELECT
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active, IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active,
COUNT(posts.ID) as count COUNT(posts.ID) as count
@@ -91,12 +92,13 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
GROUP BY periods_active GROUP BY periods_active
ORDER BY periods_active ASC", ORDER BY periods_active ASC",
$days_in_interval_period, $days_in_interval_period,
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subsription ( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subscription
wcs_get_date_meta_key( 'cancelled' ), // If a subscription has a cancelled date, use that to determine a more accurate lifetime wcs_get_date_meta_key( 'cancelled' ), // If a subscription has a cancelled date, use that to determine a more accurate lifetime
wcs_get_date_meta_key( 'end' ) // Otherwise, we want to use the end date for subscritions that have expired wcs_get_date_meta_key( 'end' ) // Otherwise, we want to use the end date for subscriptions that have expired
),
OBJECT_K
); );
$subscription_ages = $wpdb->get_results( $base_query, OBJECT_K );
$this->report_data->total_subscriptions = $this->report_data->unended_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) ); $this->report_data->total_subscriptions = $this->report_data->unended_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) );
$this->report_data->living_subscriptions = array(); $this->report_data->living_subscriptions = array();

View File

@@ -121,14 +121,23 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
$this->totals = self::get_data(); $this->totals = self::get_data();
$customer_query = apply_filters( 'wcs_reports_current_customer_query', $active_statuses = wcs_maybe_prefix_key( apply_filters( 'wcs_reports_active_statuses', [ 'active', 'pending-cancel' ] ), 'wc-' );
$paid_statuses = wcs_maybe_prefix_key( apply_filters( 'woocommerce_reports_paid_order_statuses', [ 'completed', 'processing' ] ), 'wc-' );
$active_statuses_placeholders = implode( ',', array_fill( 0, count( $active_statuses ), '%s' ) );
$paid_statuses_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
// Ignored for allowing interpolation in the IN statements.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
$query = apply_filters( 'wcs_reports_current_customer_query',
$wpdb->prepare(
"SELECT customer_ids.meta_value as customer_id, "SELECT customer_ids.meta_value as customer_id,
COUNT(subscription_posts.ID) as total_subscriptions, COUNT(subscription_posts.ID) as total_subscriptions,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total, COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
COUNT(DISTINCT parent_order.ID) as initial_order_count, COUNT(DISTINCT parent_order.ID) as initial_order_count,
SUM(CASE SUM(CASE
WHEN subscription_posts.post_status WHEN subscription_posts.post_status
IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1 IN ( {$active_statuses_placeholders} ) THEN 1
ELSE 0 ELSE 0
END) AS active_subscriptions END) AS active_subscriptions
FROM {$wpdb->posts} subscription_posts FROM {$wpdb->posts} subscription_posts
@@ -137,7 +146,7 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
AND customer_ids.meta_key = '_customer_user' AND customer_ids.meta_key = '_customer_user'
LEFT JOIN {$wpdb->posts} parent_order LEFT JOIN {$wpdb->posts} parent_order
ON parent_order.ID = subscription_posts.post_parent ON parent_order.ID = subscription_posts.post_parent
AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' ) AND parent_order.post_status IN ( {$paid_statuses_placeholders} )
LEFT JOIN {$wpdb->postmeta} parent_total LEFT JOIN {$wpdb->postmeta} parent_total
ON parent_total.post_id = parent_order.ID ON parent_total.post_id = parent_order.ID
AND parent_total.meta_key = '_order_total' AND parent_total.meta_key = '_order_total'
@@ -145,12 +154,25 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash') AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
GROUP BY customer_ids.meta_value GROUP BY customer_ids.meta_value
ORDER BY customer_id DESC ORDER BY customer_id DESC
LIMIT {$offset}, {$per_page}" ); LIMIT %d, %d",
array_merge( $active_statuses, $paid_statuses, [ $offset, $per_page ] )
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
$this->items = $wpdb->get_results( $customer_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$this->items = $wpdb->get_results( $query );
$customer_ids = wp_list_pluck( $this->items, 'customer_id' );
$customer_placeholders = implode( ',', array_fill( 0, count( $customer_ids ), '%s' ) );
$paid_statuses = wcs_maybe_prefix_key( apply_filters( 'woocommerce_reports_paid_order_statuses', [ 'completed', 'processing' ] ), 'wc-' );
$status_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
// Now get each customer's renewal and switch total // Now get each customer's renewal and switch total
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
$customer_renewal_switch_total_query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query', $customer_renewal_switch_total_query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query',
$wpdb->prepare(
"SELECT "SELECT
customer_ids.meta_value as customer_id, customer_ids.meta_value as customer_id,
COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total, COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
@@ -163,19 +185,23 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
INNER JOIN {$wpdb->postmeta} customer_ids INNER JOIN {$wpdb->postmeta} customer_ids
ON renewal_order_ids.meta_value = customer_ids.post_id ON renewal_order_ids.meta_value = customer_ids.post_id
AND customer_ids.meta_key = '_customer_user' AND customer_ids.meta_key = '_customer_user'
AND customer_ids.meta_value IN ('" . implode( "','", wp_list_pluck( $this->items, 'customer_id' ) ) . "' ) AND customer_ids.meta_value IN ( {$customer_placeholders} )
INNER JOIN {$wpdb->posts} renewal_order_posts INNER JOIN {$wpdb->posts} renewal_order_posts
ON renewal_order_ids.post_id = renewal_order_posts.ID ON renewal_order_ids.post_id = renewal_order_posts.ID
AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' ) AND renewal_order_posts.post_status IN ( {$status_placeholders} )
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
ON renewal_switch_totals.post_id = renewal_order_ids.post_id ON renewal_switch_totals.post_id = renewal_order_ids.post_id
AND renewal_switch_totals.meta_key = '_order_total' AND renewal_switch_totals.meta_key = '_order_total'
WHERE renewal_order_ids.meta_key = '_subscription_renewal' WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch' OR renewal_order_ids.meta_key = '_subscription_switch'
GROUP BY customer_id GROUP BY customer_id
ORDER BY customer_id" ORDER BY customer_id",
array_merge( $customer_ids, $paid_statuses )
)
); );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare.
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$customer_renewal_switch_totals = $wpdb->get_results( $customer_renewal_switch_total_query, OBJECT_K ); $customer_renewal_switch_totals = $wpdb->get_results( $customer_renewal_switch_total_query, OBJECT_K );
foreach ( $this->items as $index => $item ) { foreach ( $this->items as $index => $item ) {
@@ -212,14 +238,22 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
$args = apply_filters( 'wcs_reports_customer_total_args', $args ); $args = apply_filters( 'wcs_reports_customer_total_args', $args );
$args = wp_parse_args( $args, $default_args ); $args = wp_parse_args( $args, $default_args );
$active_statuses = wcs_maybe_prefix_key( apply_filters( 'wcs_reports_active_statuses', [ 'active', 'pending-cancel' ] ), 'wc-' );
$order_statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
$active_statuses_placeholders = implode( ',', array_fill( 0, count( $active_statuses ), '%s' ) );
$order_statuses_placeholders = implode( ',', array_fill( 0, count( $order_statuses ), '%s' ) );
$total_query = apply_filters( 'wcs_reports_customer_total_query', $total_query = apply_filters( 'wcs_reports_customer_total_query',
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
$wpdb->prepare(
"SELECT COUNT( DISTINCT customer_ids.meta_value) as total_customers, "SELECT COUNT( DISTINCT customer_ids.meta_value) as total_customers,
COUNT(subscription_posts.ID) as total_subscriptions, COUNT(subscription_posts.ID) as total_subscriptions,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total, COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
COUNT(DISTINCT parent_order.ID) as initial_order_count, COUNT(DISTINCT parent_order.ID) as initial_order_count,
COALESCE(SUM(CASE COALESCE(SUM(CASE
WHEN subscription_posts.post_status WHEN subscription_posts.post_status
IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1 IN ( {$active_statuses_placeholders} ) THEN 1
ELSE 0 ELSE 0
END), 0) AS active_subscriptions END), 0) AS active_subscriptions
FROM {$wpdb->posts} subscription_posts FROM {$wpdb->posts} subscription_posts
@@ -228,13 +262,16 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
AND customer_ids.meta_key = '_customer_user' AND customer_ids.meta_key = '_customer_user'
LEFT JOIN {$wpdb->posts} parent_order LEFT JOIN {$wpdb->posts} parent_order
ON parent_order.ID = subscription_posts.post_parent ON parent_order.ID = subscription_posts.post_parent
AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' ) AND parent_order.post_status IN ( {$order_statuses_placeholders} )
LEFT JOIN {$wpdb->postmeta} parent_total LEFT JOIN {$wpdb->postmeta} parent_total
ON parent_total.post_id = parent_order.ID ON parent_total.post_id = parent_order.ID
AND parent_total.meta_key = '_order_total' AND parent_total.meta_key = '_order_total'
WHERE subscription_posts.post_type = 'shop_subscription' WHERE subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash') AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
"); ",
array_merge( $active_statuses, $order_statuses )
) );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
$cached_results = get_transient( strtolower( __CLASS__ ) ); $cached_results = get_transient( strtolower( __CLASS__ ) );
$query_hash = md5( $total_query ); $query_hash = md5( $total_query );
@@ -247,13 +284,19 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
// Enable big selects for reports // Enable big selects for reports
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_data', $wpdb->get_row( $total_query ) ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_data', $wpdb->get_row( $total_query ) );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS ); set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
} }
$customer_totals = $cached_results[ $query_hash ]; $customer_totals = $cached_results[ $query_hash ];
$status_placeholders = implode( ',', array_fill( 0, count( $args['order_status'] ), '%s' ) );
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
$renewal_switch_total_query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query', $renewal_switch_total_query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query',
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
$wpdb->prepare(
"SELECT COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total, "SELECT COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
FROM {$wpdb->postmeta} renewal_order_ids FROM {$wpdb->postmeta} renewal_order_ids
@@ -263,19 +306,23 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash') AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
INNER JOIN {$wpdb->posts} renewal_order_posts INNER JOIN {$wpdb->posts} renewal_order_posts
ON renewal_order_ids.post_id = renewal_order_posts.ID ON renewal_order_ids.post_id = renewal_order_posts.ID
AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' ) AND renewal_order_posts.post_status IN ( {$status_placeholders} )
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
ON renewal_switch_totals.post_id = renewal_order_ids.post_id ON renewal_switch_totals.post_id = renewal_order_ids.post_id
AND renewal_switch_totals.meta_key = '_order_total' AND renewal_switch_totals.meta_key = '_order_total'
WHERE renewal_order_ids.meta_key = '_subscription_renewal' WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'" OR renewal_order_ids.meta_key = '_subscription_switch'",
$statuses
)
); );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
$query_hash = md5( $renewal_switch_total_query ); $query_hash = md5( $renewal_switch_total_query );
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
// Enable big selects for reports // Enable big selects for reports
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $wpdb->get_row( $renewal_switch_total_query ) ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $wpdb->get_row( $renewal_switch_total_query ) );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS ); set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
} }

View File

@@ -168,6 +168,7 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_data', $wpdb->get_results( $query, OBJECT_K ), $args ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_data', $wpdb->get_results( $query, OBJECT_K ), $args );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS ); set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
} }
@@ -201,26 +202,36 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
} }
} }
$placeholders = implode( ',', array_fill( 0, count( $args['order_status'] ), '%s' ) );
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
// Now let's get the total revenue for each product so we can provide an average lifetime value for that product // Now let's get the total revenue for each product so we can provide an average lifetime value for that product
$query = apply_filters( 'wcs_reports_product_lifetime_value_query', $query = apply_filters( 'wcs_reports_product_lifetime_value_query',
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
$wpdb->prepare(
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total "SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->posts} AS wcorders INNER JOIN {$wpdb->posts} AS wcorders
ON wcoitems.order_id = wcorders.ID ON wcoitems.order_id = wcorders.ID
AND wcorders.post_type = 'shop_order' AND wcorders.post_type = 'shop_order'
AND wcorders.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' ) AND wcorders.post_status IN ( {$placeholders} )
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2 INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' ) WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
AND wcoimeta2.meta_key = '_line_total' AND wcoimeta2.meta_key = '_line_total'
GROUP BY product_id" ); GROUP BY product_id",
$statuses
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$query_hash = md5( $query ); $query_hash = md5( $query );
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_lifetime_value_data', $wpdb->get_results( $query, OBJECT_K ), $args ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_lifetime_value_data', $wpdb->get_results( $query, OBJECT_K ), $args );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS ); set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
} }

View File

@@ -282,9 +282,15 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
} }
} }
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
$order_types = wc_get_order_types( 'order-count' );
$status_placeholders = implode( ', ', array_fill( 0, count( $args['order_status'] ), '%s' ) );
$order_types_placeholders = implode( ', ', array_fill( 0, count( $order_types ), '%s' ) );
/* /*
* New subscription orders * New subscription orders
*/ */
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
$query = $wpdb->prepare( $query = $wpdb->prepare(
"SELECT SUM(subscriptions.count) as count, "SELECT SUM(subscriptions.count) as count,
order_posts.post_date as post_date, order_posts.post_date as post_date,
@@ -304,23 +310,27 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
) AS subscriptions ON subscriptions.order_id = order_posts.ID ) AS subscriptions ON subscriptions.order_id = order_posts.ID
LEFT JOIN {$wpdb->postmeta} AS order_total_post_meta LEFT JOIN {$wpdb->postmeta} AS order_total_post_meta
ON order_posts.ID = order_total_post_meta.post_id ON order_posts.ID = order_total_post_meta.post_id
WHERE order_posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) WHERE order_posts.post_type IN ( {$order_types_placeholders} )
AND order_posts.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' ) AND order_posts.post_status IN ( {$status_placeholders} )
AND order_posts.post_date >= %s AND order_posts.post_date >= %s
AND order_posts.post_date < %s AND order_posts.post_date < %s
AND order_total_post_meta.meta_key = '_order_total' AND order_total_post_meta.meta_key = '_order_total'
GROUP BY YEAR(order_posts.post_date), MONTH(order_posts.post_date), DAY(order_posts.post_date) GROUP BY YEAR(order_posts.post_date), MONTH(order_posts.post_date), DAY(order_posts.post_date)
ORDER BY post_date ASC", ORDER BY post_date ASC",
date( 'Y-m-d', $this->start_date ), array_merge(
$query_end_date, [ date( 'Y-m-d', $this->start_date ), $query_end_date ],
date( 'Y-m-d', $this->start_date ), $order_types,
$query_end_date $statuses,
[ date( 'Y-m-d', $this->start_date ), $query_end_date ]
)
); );
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$query_hash = md5( $query ); $query_hash = md5( $query );
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_sign_up_data', (array) $wpdb->get_results( $query ), $args ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_sign_up_data', (array) $wpdb->get_results( $query ), $args );
$update_cache = true; $update_cache = true;
} }
@@ -364,7 +374,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
) searchdate, ) searchdate,
{$wpdb->posts} AS wcsubs, {$wpdb->posts} AS wcsubs,
{$wpdb->postmeta} AS wcsmeta {$wpdb->postmeta} AS wcsmeta
WHERE wcsubs.ID = wcsmeta.post_id AND wcsmeta.meta_key = '%s' WHERE wcsubs.ID = wcsmeta.post_id AND wcsmeta.meta_key = %s
AND DATE( wcsubs.post_date ) <= searchdate.Date AND DATE( wcsubs.post_date ) <= searchdate.Date
AND wcsubs.post_type IN ( 'shop_subscription' ) AND wcsubs.post_type IN ( 'shop_subscription' )
AND wcsubs.post_status NOT IN( 'auto-draft' ) AND wcsubs.post_status NOT IN( 'auto-draft' )
@@ -386,6 +396,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_subscriber_count_data', (array) $wpdb->get_results( $query ), $args ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_subscriber_count_data', (array) $wpdb->get_results( $query ), $args );
$update_cache = true; $update_cache = true;
} }
@@ -397,7 +408,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
* Subscription cancellations * Subscription cancellations
*/ */
$query = $wpdb->prepare( $query = $wpdb->prepare(
"SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', '{$site_timezone}' ) as cancel_date, GROUP_CONCAT( DISTINCT wcsubs.ID ) as subscription_ids "SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', %s ) as cancel_date, GROUP_CONCAT( DISTINCT wcsubs.ID ) as subscription_ids
FROM {$wpdb->posts} as wcsubs FROM {$wpdb->posts} as wcsubs
JOIN {$wpdb->postmeta} AS wcsmeta_cancel JOIN {$wpdb->postmeta} AS wcsmeta_cancel
ON wcsubs.ID = wcsmeta_cancel.post_id ON wcsubs.ID = wcsmeta_cancel.post_id
@@ -406,6 +417,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
GROUP BY YEAR( cancel_date ), MONTH( cancel_date ), DAY( cancel_date ) GROUP BY YEAR( cancel_date ), MONTH( cancel_date ), DAY( cancel_date )
HAVING cancel_date BETWEEN %s AND %s HAVING cancel_date BETWEEN %s AND %s
ORDER BY wcsmeta_cancel.meta_value ASC", ORDER BY wcsmeta_cancel.meta_value ASC",
$site_timezone,
wcs_get_date_meta_key( 'cancelled' ), wcs_get_date_meta_key( 'cancelled' ),
date( 'Y-m-d', $this->start_date ), date( 'Y-m-d', $this->start_date ),
$query_end_date $query_end_date
@@ -415,6 +427,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_cancel_count_data', (array) $wpdb->get_results( $query ), $args ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_cancel_count_data', (array) $wpdb->get_results( $query ), $args );
$update_cache = true; $update_cache = true;
} }
@@ -427,7 +440,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
* Subscriptions ended * Subscriptions ended
*/ */
$query = $wpdb->prepare( $query = $wpdb->prepare(
"SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_end.meta_value, '+00:00', '{$site_timezone}' ) as end_date, GROUP_CONCAT( DISTINCT wcsubs.ID ) as subscription_ids "SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_end.meta_value, '+00:00', %s ) as end_date, GROUP_CONCAT( DISTINCT wcsubs.ID ) as subscription_ids
FROM {$wpdb->posts} as wcsubs FROM {$wpdb->posts} as wcsubs
JOIN {$wpdb->postmeta} AS wcsmeta_end JOIN {$wpdb->postmeta} AS wcsmeta_end
ON wcsubs.ID = wcsmeta_end.post_id ON wcsubs.ID = wcsmeta_end.post_id
@@ -436,6 +449,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
GROUP BY YEAR( end_date ), MONTH( end_date ), DAY( end_date ) GROUP BY YEAR( end_date ), MONTH( end_date ), DAY( end_date )
HAVING end_date BETWEEN %s AND %s HAVING end_date BETWEEN %s AND %s
ORDER BY wcsmeta_end.meta_value ASC", ORDER BY wcsmeta_end.meta_value ASC",
$site_timezone,
wcs_get_date_meta_key( 'end' ), wcs_get_date_meta_key( 'end' ),
date( 'Y-m-d', $this->start_date ), date( 'Y-m-d', $this->start_date ),
$query_end_date $query_end_date
@@ -445,6 +459,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_ended_count_data', (array) $wpdb->get_results( $query ), $args ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_ended_count_data', (array) $wpdb->get_results( $query ), $args );
$update_cache = true; $update_cache = true;
} }

View File

@@ -56,9 +56,11 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
$query_end_date = get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) ); $query_end_date = get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) );
// Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment) // Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment)
$renewal_query = $wpdb->prepare( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
$this->report_data->renewal_data = $wpdb->get_results(
$wpdb->prepare(
" "
SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date, SUM(meta_order_total.meta_value) AS renewal_totals SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN(%s) AS retry_date, SUM(meta_order_total.meta_value) AS renewal_totals
FROM {$wpdb->posts} AS orders FROM {$wpdb->posts} AS orders
INNER JOIN {$wpdb->prefix}wcs_payment_retries AS retries ON ( orders.ID = retries.order_id ) INNER JOIN {$wpdb->prefix}wcs_payment_retries AS retries ON ( orders.ID = retries.order_id )
LEFT JOIN {$wpdb->postmeta} AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' ) LEFT JOIN {$wpdb->postmeta} AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' )
@@ -68,16 +70,19 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
GROUP BY {$this->group_by_query} GROUP BY {$this->group_by_query}
ORDER BY retry_date_gmt ASC ORDER BY retry_date_gmt ASC
", ",
$retry_date_in_local_time,
$query_start_date, $query_start_date,
$query_end_date $query_end_date
)
); );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->report_data->renewal_data = $wpdb->get_results( $renewal_query );
// Get the counts for all retries, grouped by day or month and status // Get the counts for all retries, grouped by day or month and status
$retry_query = $wpdb->prepare( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
$this->report_data->retry_data = $wpdb->get_results(
$wpdb->prepare(
" "
SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN(%s) AS retry_date
FROM {$wpdb->prefix}wcs_payment_retries AS retries FROM {$wpdb->prefix}wcs_payment_retries AS retries
WHERE retries.status IN ( 'complete', 'failed', 'pending' ) WHERE retries.status IN ( 'complete', 'failed', 'pending' )
AND retries.date_gmt >= %s AND retries.date_gmt >= %s
@@ -85,11 +90,12 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
GROUP BY {$this->group_by_query}, status GROUP BY {$this->group_by_query}, status
ORDER BY retry_date_gmt ASC ORDER BY retry_date_gmt ASC
", ",
$retry_date_in_local_time,
$query_start_date, $query_start_date,
$query_end_date $query_end_date
)
); );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->report_data->retry_data = $wpdb->get_results( $retry_query );
// Total up the query data // Total up the query data
$this->report_data->retry_failed_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'count' ) ) ); $this->report_data->retry_failed_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'count' ) ) );

View File

@@ -130,6 +130,7 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
$args = wp_parse_args( $args, $default_args ); $args = wp_parse_args( $args, $default_args );
// Query based on whole days, not minutes/hours so that we can cache the query for at least 24 hours // Query based on whole days, not minutes/hours so that we can cache the query for at least 24 hours
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
$base_query = $wpdb->prepare( $base_query = $wpdb->prepare(
"SELECT "SELECT
DATE(ms.meta_value) as scheduled_date, DATE(ms.meta_value) as scheduled_date,
@@ -155,7 +156,7 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
AND p.post_status = 'wc-active' AND p.post_status = 'wc-active'
AND mo.meta_key = '_order_total' AND mo.meta_key = '_order_total'
AND ms.meta_key = '_schedule_next_payment' AND ms.meta_key = '_schedule_next_payment'
AND ( ( ms.meta_value < '%s' AND me.meta_value = 0 ) OR ( me.meta_value > '%s' AND ms.meta_value < '%s' ) ) AND ( ( ms.meta_value < %s AND me.meta_value = 0 ) OR ( me.meta_value > %s AND ms.meta_value < %s ) )
AND mi.meta_key = '_billing_interval' AND mi.meta_key = '_billing_interval'
AND mp.meta_key = '_billing_period' AND mp.meta_key = '_billing_period'
AND me.meta_key = '_schedule_end ' AND me.meta_key = '_schedule_end '
@@ -165,6 +166,7 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
date( 'Y-m-d', $this->start_date ), date( 'Y-m-d', $this->start_date ),
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ) date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) )
); );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$cached_results = get_transient( strtolower( get_class( $this ) ) ); $cached_results = get_transient( strtolower( get_class( $this ) ) );
$query_hash = md5( $base_query ); $query_hash = md5( $base_query );
@@ -176,6 +178,7 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_upcoming_recurring_revenue_data', $wpdb->get_results( $base_query, OBJECT_K ), $args ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_upcoming_recurring_revenue_data', $wpdb->get_results( $base_query, OBJECT_K ), $args );
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS ); set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
} }

View File

@@ -148,7 +148,7 @@ class WCS_Early_Renewal_Modal_Handler {
if ( $renewal_order->needs_payment() ) { if ( $renewal_order->needs_payment() ) {
$renewal_order->delete( true ); $renewal_order->delete( true );
wc_add_notice( __( 'Payment for the renewal order was unsuccessful with your payment method on file, please try again.', 'woocommerce-subscriptions' ), 'error' ); wc_add_notice( __( 'Payment for the renewal order was unsuccessful with your payment method on file, please try again.', 'woocommerce-subscriptions' ), 'error' );
wp_redirect( wcs_get_early_renewal_url( $subscription ) ); wp_safe_redirect( wcs_get_early_renewal_url( $subscription ) );
exit(); exit();
} }
@@ -191,7 +191,7 @@ class WCS_Early_Renewal_Modal_Handler {
* @since 2.6.0 * @since 2.6.0
*/ */
private static function redirect() { private static function redirect() {
wp_redirect( remove_query_arg( array( 'process_early_renewal', 'subscription_id', 'wcs_nonce' ) ) ); wp_safe_redirect( remove_query_arg( array( 'process_early_renewal', 'subscription_id', 'wcs_nonce' ) ) );
exit(); exit();
} }

View File

@@ -94,7 +94,8 @@ class WCS_Retry_Database_Store extends WCS_Retry_Store {
$retry = null; $retry = null;
$raw_retry = $wpdb->get_row( $raw_retry = $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
"SELECT * FROM {$this->get_full_table_name()} WHERE retry_id = %d LIMIT 1", "SELECT * FROM %i WHERE retry_id = %d LIMIT 1",
$this->get_full_table_name(),
$retry_id $retry_id
) )
); );
@@ -183,8 +184,11 @@ class WCS_Retry_Database_Store extends WCS_Retry_Store {
$orderby = sprintf( ' ORDER BY %s', sanitize_sql_orderby( "{$args['orderby']} {$args['order']}" ) ); $orderby = sprintf( ' ORDER BY %s', sanitize_sql_orderby( "{$args['orderby']} {$args['order']}" ) );
$limit = ( $args['limit'] > 0 ) ? $wpdb->prepare( ' LIMIT %d', $args['limit'] ) : ''; $limit = ( $args['limit'] > 0 ) ? $wpdb->prepare( ' LIMIT %d', $args['limit'] ) : '';
$raw_retries = $wpdb->get_results( "SELECT * FROM {$this->get_full_table_name()} $where $orderby $limit" );
$retries = array(); $retries = array();
$raw_retries = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The subqueries are prepared above.
$wpdb->prepare( "SELECT * FROM %i $where $orderby $limit", $this->get_full_table_name() )
);
foreach ( $raw_retries as $raw_retry ) { foreach ( $raw_retries as $raw_retry ) {
if ( 'ids' === $return ) { if ( 'ids' === $return ) {

View File

@@ -183,12 +183,13 @@ class WC_Subscriptions_Switcher {
if ( isset( $_GET['switch-subscription'] ) && isset( $_GET['item'] ) ) { if ( isset( $_GET['switch-subscription'] ) && isset( $_GET['item'] ) ) {
$subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) ); $subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) );
$line_item = wcs_get_order_item( absint( $_GET['item'] ), $subscription ); $line_item = $subscription ? wcs_get_order_item( absint( $_GET['item'] ), $subscription ) : false;
$nonce = ! empty( $_GET['_wcsnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ) : false;
// Visiting a switch link for someone elses subscription or if the switch link doesn't contain a valid nonce // Visiting a switch link for someone elses subscription or if the switch link doesn't contain a valid nonce
if ( ! is_object( $subscription ) || empty( $_GET['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ), 'wcs_switch_request' ) || empty( $line_item ) || ! self::can_item_be_switched_by_user( $line_item, $subscription ) ) { if ( ! is_object( $subscription ) || empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wcs_switch_request' ) || empty( $line_item ) || ! self::can_item_be_switched_by_user( $line_item, $subscription ) ) {
wp_redirect( remove_query_arg( array( 'switch-subscription', 'auto-switch', 'item', '_wcsnonce' ) ) ); wp_safe_redirect( remove_query_arg( array( 'switch-subscription', 'auto-switch', 'item', '_wcsnonce' ) ) );
exit(); exit();
} else { } else {
@@ -238,7 +239,7 @@ class WC_Subscriptions_Switcher {
if ( $removed_item_count > 0 ) { if ( $removed_item_count > 0 ) {
wc_add_notice( _n( 'Your cart contained an invalid subscription switch request. It has been removed.', 'Your cart contained invalid subscription switch requests. They have been removed.', $removed_item_count, 'woocommerce-subscriptions' ), 'error' ); wc_add_notice( _n( 'Your cart contained an invalid subscription switch request. It has been removed.', 'Your cart contained invalid subscription switch requests. They have been removed.', $removed_item_count, 'woocommerce-subscriptions' ), 'error' );
wp_redirect( wc_get_cart_url() ); wp_safe_redirect( wc_get_cart_url() );
exit(); exit();
} }
} elseif ( is_product() && $product = wc_get_product( $post ) ) { // Automatically initiate the switch process for limited variable subscriptions } elseif ( is_product() && $product = wc_get_product( $post ) ) { // Automatically initiate the switch process for limited variable subscriptions
@@ -305,7 +306,7 @@ class WC_Subscriptions_Switcher {
} }
if ( apply_filters( 'wcs_initiate_auto_switch', self::can_item_be_switched_by_user( $item, $subscription ), $item, $subscription ) ) { if ( apply_filters( 'wcs_initiate_auto_switch', self::can_item_be_switched_by_user( $item, $subscription ), $item, $subscription ) ) {
wp_redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) ); wp_safe_redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) );
exit; exit;
} }
} }
@@ -1456,7 +1457,7 @@ class WC_Subscriptions_Switcher {
if ( ! current_user_can( 'switch_shop_subscription', $subscription->get_id() ) ) { if ( ! current_user_can( 'switch_shop_subscription', $subscription->get_id() ) ) {
wc_add_notice( __( 'You can not switch this subscription. It appears you do not own the subscription.', 'woocommerce-subscriptions' ), 'error' ); wc_add_notice( __( 'You can not switch this subscription. It appears you do not own the subscription.', 'woocommerce-subscriptions' ), 'error' );
WC()->cart->empty_cart( true ); WC()->cart->empty_cart( true );
wp_redirect( get_permalink( $product_id ) ); wp_safe_redirect( get_permalink( $product_id ) );
exit(); exit();
} }
@@ -1497,7 +1498,7 @@ class WC_Subscriptions_Switcher {
wc_add_notice( __( 'There was an error locating the switch details.', 'woocommerce-subscriptions' ), 'error' ); wc_add_notice( __( 'There was an error locating the switch details.', 'woocommerce-subscriptions' ), 'error' );
WC()->cart->empty_cart( true ); WC()->cart->empty_cart( true );
wp_redirect( get_permalink( wc_get_page_id( 'cart' ) ) ); wp_safe_redirect( get_permalink( wc_get_page_id( 'cart' ) ) );
exit(); exit();
} }
} }

File diff suppressed because it is too large Load Diff

2
vendor/autoload.php vendored
View File

@@ -22,4 +22,4 @@ if (PHP_VERSION_ID < 50600) {
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitdf568b8a1948145d8f675fb08a939b09::getLoader(); return ComposerAutoloaderInit99add140caccae6adfad678f070ccf26::getLoader();

View File

@@ -322,6 +322,7 @@ class InstalledVersions
} }
$installed = array(); $installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
@@ -330,9 +331,11 @@ class InstalledVersions
} elseif (is_file($vendorDir.'/composer/installed.php')) { } elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php'; $required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required; self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { $installed[] = $required;
self::$installed = $installed[count($installed) - 1]; if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $required;
$copiedLocalDir = true;
} }
} }
} }
@@ -350,7 +353,7 @@ class InstalledVersions
} }
} }
if (self::$installed !== array()) { if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed; $installed[] = self::$installed;
} }

View File

@@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer // autoload_real.php @generated by Composer
class ComposerAutoloaderInitdf568b8a1948145d8f675fb08a939b09 class ComposerAutoloaderInit99add140caccae6adfad678f070ccf26
{ {
private static $loader; private static $loader;
@@ -24,12 +24,12 @@ class ComposerAutoloaderInitdf568b8a1948145d8f675fb08a939b09
require __DIR__ . '/platform_check.php'; require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitdf568b8a1948145d8f675fb08a939b09', 'loadClassLoader'), true, true); spl_autoload_register(array('ComposerAutoloaderInit99add140caccae6adfad678f070ccf26', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitdf568b8a1948145d8f675fb08a939b09', 'loadClassLoader')); spl_autoload_unregister(array('ComposerAutoloaderInit99add140caccae6adfad678f070ccf26', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php'; require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitdf568b8a1948145d8f675fb08a939b09::getInitializer($loader)); call_user_func(\Composer\Autoload\ComposerStaticInit99add140caccae6adfad678f070ccf26::getInitializer($loader));
$loader->register(true); $loader->register(true);

View File

@@ -4,7 +4,7 @@
namespace Composer\Autoload; namespace Composer\Autoload;
class ComposerStaticInitdf568b8a1948145d8f675fb08a939b09 class ComposerStaticInit99add140caccae6adfad678f070ccf26
{ {
public static $prefixLengthsPsr4 = array ( public static $prefixLengthsPsr4 = array (
'C' => 'C' =>
@@ -126,9 +126,9 @@ class ComposerStaticInitdf568b8a1948145d8f675fb08a939b09
public static function getInitializer(ClassLoader $loader) public static function getInitializer(ClassLoader $loader)
{ {
return \Closure::bind(function () use ($loader) { return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitdf568b8a1948145d8f675fb08a939b09::$prefixLengthsPsr4; $loader->prefixLengthsPsr4 = ComposerStaticInit99add140caccae6adfad678f070ccf26::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitdf568b8a1948145d8f675fb08a939b09::$prefixDirsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit99add140caccae6adfad678f070ccf26::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitdf568b8a1948145d8f675fb08a939b09::$classMap; $loader->classMap = ComposerStaticInit99add140caccae6adfad678f070ccf26::$classMap;
}, null, ClassLoader::class); }, null, ClassLoader::class);
} }

View File

@@ -151,17 +151,17 @@
}, },
{ {
"name": "woocommerce/subscriptions-core", "name": "woocommerce/subscriptions-core",
"version": "8.1.1", "version": "8.2.0",
"version_normalized": "8.1.1.0", "version_normalized": "8.2.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Automattic/woocommerce-subscriptions-core.git", "url": "https://github.com/Automattic/woocommerce-subscriptions-core.git",
"reference": "fb74303f96c53800aaec99a27e376aa9ece968cd" "reference": "442d585955ab048673c765a8afca680b1ea38e07"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Automattic/woocommerce-subscriptions-core/zipball/fb74303f96c53800aaec99a27e376aa9ece968cd", "url": "https://api.github.com/repos/Automattic/woocommerce-subscriptions-core/zipball/442d585955ab048673c765a8afca680b1ea38e07",
"reference": "fb74303f96c53800aaec99a27e376aa9ece968cd", "reference": "442d585955ab048673c765a8afca680b1ea38e07",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -174,7 +174,7 @@
"woocommerce/woocommerce-sniffs": "0.1.0", "woocommerce/woocommerce-sniffs": "0.1.0",
"yoast/phpunit-polyfills": "1.1.0" "yoast/phpunit-polyfills": "1.1.0"
}, },
"time": "2025-03-31T00:33:21+00:00", "time": "2025-04-14T13:37:52+00:00",
"type": "wordpress-plugin", "type": "wordpress-plugin",
"extra": { "extra": {
"phpcodesniffer-search-depth": 2 "phpcodesniffer-search-depth": 2
@@ -204,7 +204,7 @@
"description": "Sell products and services with recurring payments in your WooCommerce Store.", "description": "Sell products and services with recurring payments in your WooCommerce Store.",
"homepage": "https://github.com/Automattic/woocommerce-subscriptions-core", "homepage": "https://github.com/Automattic/woocommerce-subscriptions-core",
"support": { "support": {
"source": "https://github.com/Automattic/woocommerce-subscriptions-core/tree/8.1.1", "source": "https://github.com/Automattic/woocommerce-subscriptions-core/tree/8.2.0",
"issues": "https://github.com/Automattic/woocommerce-subscriptions-core/issues" "issues": "https://github.com/Automattic/woocommerce-subscriptions-core/issues"
}, },
"install-path": "../woocommerce/subscriptions-core" "install-path": "../woocommerce/subscriptions-core"

View File

@@ -1,9 +1,9 @@
<?php return array( <?php return array(
'root' => array( 'root' => array(
'name' => 'woocommerce/woocommerce-subscriptions', 'name' => 'woocommerce/woocommerce-subscriptions',
'pretty_version' => 'dev-release/7.3.1', 'pretty_version' => 'dev-release/7.4.0',
'version' => 'dev-release/7.3.1', 'version' => 'dev-release/7.4.0',
'reference' => 'bbd68f5de02c5b7fe412d87e548a957d7bfb8abf', 'reference' => 'f038fe29017b9110f7f39251b9c042a8c076125a',
'type' => 'wordpress-plugin', 'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -20,18 +20,18 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'woocommerce/subscriptions-core' => array( 'woocommerce/subscriptions-core' => array(
'pretty_version' => '8.1.1', 'pretty_version' => '8.2.0',
'version' => '8.1.1.0', 'version' => '8.2.0.0',
'reference' => 'fb74303f96c53800aaec99a27e376aa9ece968cd', 'reference' => '442d585955ab048673c765a8afca680b1ea38e07',
'type' => 'wordpress-plugin', 'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../woocommerce/subscriptions-core', 'install_path' => __DIR__ . '/../woocommerce/subscriptions-core',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'woocommerce/woocommerce-subscriptions' => array( 'woocommerce/woocommerce-subscriptions' => array(
'pretty_version' => 'dev-release/7.3.1', 'pretty_version' => 'dev-release/7.4.0',
'version' => 'dev-release/7.3.1', 'version' => 'dev-release/7.4.0',
'reference' => 'bbd68f5de02c5b7fe412d87e548a957d7bfb8abf', 'reference' => 'f038fe29017b9110f7f39251b9c042a8c076125a',
'type' => 'wordpress-plugin', 'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),

View File

@@ -430,7 +430,12 @@ a.close-subscriptions-search {
max-width: 34%; max-width: 34%;
float: right; float: right;
} }
.variable_subscription_pricing_2_3.variable_subscription_trial p.form-row { .variable_subscription_pricing_2_3.variable_subscription_pricing p.form-row {
margin-bottom: 0;
}
#variable_product_options
.variable_subscription_pricing_2_3
p._subscription_price_field {
margin-bottom: 0; margin-bottom: 0;
} }
#variable_product_options #variable_product_options
@@ -460,6 +465,11 @@ a.close-subscriptions-search {
.wc_input_subscription_payment_sync_month { .wc_input_subscription_payment_sync_month {
max-width: 86%; max-width: 86%;
} }
#variable_product_options
.variable_subscription_pricing_2_3 .multiple_fields .wrap {
display: flex;
gap: 12px;
}
@media screen and ( max-width: 1190px ) { @media screen and ( max-width: 1190px ) {
#variable_product_options #variable_product_options
.variable_subscription_pricing_2_3 .variable_subscription_pricing_2_3
@@ -473,7 +483,6 @@ a.close-subscriptions-search {
.variable_subscription_pricing_2_3 .variable_subscription_pricing_2_3
p._subscription_price_field { p._subscription_price_field {
width: 100%; width: 100%;
margin-bottom: 0;
} }
#variable_product_options #variable_product_options
.variable_subscription_pricing_2_3 .variable_subscription_pricing_2_3
@@ -922,3 +931,6 @@ table.form-table input#woocommerce_subscriptions_customer_notifications_offset {
#wcs_order_price_lock > .woocommerce-help-tip { #wcs_order_price_lock > .woocommerce-help-tip {
margin: 0 8px 0 2px; margin: 0 8px 0 2px;
} }
.show_if_subscription .select2-selection, .show_if_variable-subscription .select2-selection {
font-size: 14px;
}

View File

@@ -1,6 +1,9 @@
@media only screen and ( max-width: 768px ) { @media only screen and ( max-width: 768px ) {
.subscription_details .button { .subscription_details .button {
box-sizing: border-box;
margin-bottom: 2px; margin-bottom: 2px;
padding-left: 0.5rem;
padding-right: 0.5rem;
width: 100%; width: 100%;
max-width: 200px; max-width: 200px;
text-align: center; text-align: center;

View File

@@ -396,7 +396,7 @@ jQuery( function ( $ ) {
// Add the subscription price fields above the standard price fields // Add the subscription price fields above the standard price fields
$( this ).insertBefore( $regularPriceRow ); $( this ).insertBefore( $regularPriceRow );
$trialSignUpRow.insertBefore( $( this ) ); $trialSignUpRow.insertAfter( $( this ) );
// Replace the regular price field with the trial period field // Replace the regular price field with the trial period field
$regularPriceRow $regularPriceRow

View File

@@ -1,7 +1,14 @@
*** WooCommerce Subscriptions Core Changelog *** *** WooCommerce Subscriptions Core Changelog ***
= 8.2.0 - 2025-04-14 =
* Update - Increase the number of args accepted by wcs_get_subscriptions(), to bring about parity with wc_get_orders().
* Dev - Update wcs_maybe_prefix_key() and wcs_maybe_unprefix_key() to support an array of keys.
* Fix - Prevent sending renewal reminders for orders with a 0 total.
* Fix - Ensure the second parameter passed to the 'get_edit_post_link' filter is an integer.
* Fix - Prevent WooCommerce Subscriptions buttons from overflowing in the View subscription page
= 8.1.1 - 2025-03-30 = = 8.1.1 - 2025-03-30 =
* Update - Display the subscriptions parent order icon in the WooCommerce Orders list table by default. * Update - Display the subscriptions parent order icon in the WooCommerce Orders list table by default
= 8.1.0 - 2025-03-24 = = 8.1.0 - 2025-03-24 =
* Update - Improved subscription search performance for WP Post stores by removing unnecessary _order_key and _billing_email meta queries. * Update - Improved subscription search performance for WP Post stores by removing unnecessary _order_key and _billing_email meta queries.
@@ -20,6 +27,7 @@
* Fix - Prevent empty strings being saved in related orders cache ID meta when backfilling order data to the WP Posts table. * Fix - Prevent empty strings being saved in related orders cache ID meta when backfilling order data to the WP Posts table.
* Fix - Correctly load product names with HTML on the cart and checkout shipping rates. * Fix - Correctly load product names with HTML on the cart and checkout shipping rates.
* Dev - Fix Node version mismatch between package.json and .nvmrc (both are now set to v16.17.1). * Dev - Fix Node version mismatch between package.json and .nvmrc (both are now set to v16.17.1).
* Add - Hooks to add more columns to the Related Orders table on admin
= 8.0.1 - 2025-02-13 = = 8.0.1 - 2025-02-13 =
* Fix - Revert a change released in 7.2.0 which triggered the "woocommerce_cart_item_name" filter with the wrong number of parameters. * Fix - Revert a change released in 7.2.0 which triggered the "woocommerce_cart_item_name" filter with the wrong number of parameters.

View File

@@ -316,7 +316,8 @@ class WC_Subscriptions_Admin {
?> ?>
</label> </label>
<span class="wrap"> <span class="wrap">
<input type="text" id="_subscription_price" name="_subscription_price" class="wc_input_price wc_input_subscription_price" placeholder="<?php echo esc_attr_x( 'e.g. 5.90', 'example price', 'woocommerce-subscriptions' ); ?>" step="any" min="0" value="<?php echo esc_attr( wc_format_localized_price( $chosen_price ) ); ?>" /> <?php // Translators: %s: formatted example price value. ?>
<input type="text" id="_subscription_price" name="_subscription_price" class="wc_input_price wc_input_subscription_price" placeholder="<?php echo esc_attr( sprintf( _x( 'e.g. %s', 'example price', 'woocommerce-subscriptions' ), wc_format_localized_price( '5.90' ) ) ); ?>" step="any" min="0" value="<?php echo esc_attr( wc_format_localized_price( $chosen_price ) ); ?>" />
<label for="_subscription_period_interval" class="wcs_hidden_label"><?php esc_html_e( 'Subscription interval', 'woocommerce-subscriptions' ); ?></label> <label for="_subscription_period_interval" class="wcs_hidden_label"><?php esc_html_e( 'Subscription interval', 'woocommerce-subscriptions' ); ?></label>
<select id="_subscription_period_interval" name="_subscription_period_interval" class="wc_input_subscription_period_interval wc-enhanced-select"> <select id="_subscription_period_interval" name="_subscription_period_interval" class="wc_input_subscription_period_interval wc-enhanced-select">
<?php foreach ( wcs_get_subscription_period_interval_strings() as $value => $label ) { ?> <?php foreach ( wcs_get_subscription_period_interval_strings() as $value => $label ) { ?>
@@ -354,7 +355,8 @@ class WC_Subscriptions_Admin {
'class' => 'wc_input_subscription_intial_price wc_input_subscription_initial_price wc_input_price short', 'class' => 'wc_input_subscription_intial_price wc_input_subscription_initial_price wc_input_price short',
// translators: %s is a currency symbol / code // translators: %s is a currency symbol / code
'label' => sprintf( __( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ), 'label' => sprintf( __( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ),
'placeholder' => _x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ), 'placeholder' => // translators: %s the formatted example price value.
esc_attr( sprintf( _x( 'e.g. %s', 'example price', 'woocommerce-subscriptions' ), wc_format_localized_price( '5.90' ) ) ),
'description' => __( 'Optionally include an amount to be charged at the outset of the subscription. The sign-up fee will be charged immediately, even if the product has a free trial or the payment dates are synced.', 'woocommerce-subscriptions' ), 'description' => __( 'Optionally include an amount to be charged at the outset of the subscription. The sign-up fee will be charged immediately, even if the product has a free trial or the payment dates are synced.', 'woocommerce-subscriptions' ),
'desc_tip' => true, 'desc_tip' => true,
'type' => 'text', 'type' => 'text',

View File

@@ -8,61 +8,79 @@
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly exit; // Exit if accessed directly
} }
?>
<tr>
<td>
<?php // translators: placeholder is an order number. ?>
<a href="<?php echo esc_url( $order->get_edit_order_url() ); ?>" aria-label="<?php echo esc_attr( sprintf( __( 'Edit order number %s', 'woocommerce-subscriptions' ), $order->get_order_number() ) ); ?>">
<?php
// translators: placeholder is an order number.
echo sprintf( esc_html_x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), esc_html( $order->get_order_number() ) );
?>
</a>
</td>
<td>
<?php echo esc_html( $order->get_meta( '_relationship' ) ); ?>
</td>
<td>
<?php
$date_created = $order->get_date_created();
if ( $date_created ) { // Order number column
// translators: placeholder is an order number.
$order_number = '<a href="' . esc_url( $order->get_edit_order_url() ) . '" aria-label="' . esc_attr( sprintf( __( 'Edit order number %s', 'woocommerce-subscriptions' ), $order->get_order_number() ) ) . '">' .
// translators: placeholder is an order number.
sprintf( esc_html_x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), esc_html( $order->get_order_number() ) ) .
'</a>';
// Relationship column
$relationship = esc_html( $order->get_meta( '_relationship' ) );
// Date created column
$date_created = $order->get_date_created();
if ( $date_created ) {
$t_time = $order->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ); $t_time = $order->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );
$date_to_display = ucfirst( wcs_get_human_time_diff( $date_created->getTimestamp() ) ); $date_to_display = ucfirst( wcs_get_human_time_diff( $date_created->getTimestamp() ) );
} else { } else {
$t_time = __( 'Unpublished', 'woocommerce-subscriptions' ); $t_time = __( 'Unpublished', 'woocommerce-subscriptions' );
$date_to_display = $t_time; $date_to_display = $t_time;
} }
if ( ! wcs_is_custom_order_tables_usage_enabled() ) { if ( ! wcs_is_custom_order_tables_usage_enabled() ) {
// Backwards compatibility for third-parties using the generic WP post time filter. // Backwards compatibility for third-parties using the generic WP post time filter.
// Only apply this filter if HPOS is not enabled, as the filter is not compatible with HPOS. // Only apply this filter if HPOS is not enabled, as the filter is not compatible with HPOS.
$date_to_display = apply_filters( 'post_date_column_time', $date_to_display, get_post( $order->get_id() ) ); $date_to_display = apply_filters( 'post_date_column_time', $date_to_display, get_post( $order->get_id() ) );
} }
?> $date_created = '<abbr title="' . esc_attr( $t_time ) . '">' . esc_html( apply_filters( 'wc_subscriptions_related_order_date_column', $date_to_display, $order ) ) . '</abbr>';
<abbr title="<?php echo esc_attr( $t_time ); ?>">
<?php echo esc_html( apply_filters( 'wc_subscriptions_related_order_date_column', $date_to_display, $order ) ); ?> // Status column
</abbr> $classes = array(
</td>
<td>
<?php
$classes = array(
'order-status', 'order-status',
sanitize_html_class( 'status-' . $order->get_status() ), sanitize_html_class( 'status-' . $order->get_status() ),
); );
if ( wcs_is_subscription( $order ) ) { if ( wcs_is_subscription( $order ) ) {
$status_name = wcs_get_subscription_status_name( $order->get_status() ); $status_name = wcs_get_subscription_status_name( $order->get_status() );
$classes[] = 'subscription-status'; $classes[] = 'subscription-status';
} else { } else {
$status_name = wc_get_order_status_name( $order->get_status() ); $status_name = wc_get_order_status_name( $order->get_status() );
} }
printf( '<mark class="%s"><span>%s</span></mark>', esc_attr( implode( ' ', $classes ) ), esc_html( $status_name ) ); $status_html = '<mark class="' . esc_attr( implode( ' ', $classes ) ) . '"><span>' . esc_html( $status_name ) . '</span></mark>';
?>
</td> // Total column
$total = '<span class="amount">' . wp_kses(
$order->get_formatted_order_total(),
array(
'small' => array(),
'span' => array(
'class' => array(),
),
'del' => array(),
'ins' => array(),
)
) . '</span>';
$columns = array(
$order_number,
$relationship,
$date_created,
$status_html,
$total,
);
$columns = apply_filters( 'wcs_related_orders_table_row_columns', $columns );
?>
<tr>
<?php foreach ( $columns as $column ) { ?>
<td> <td>
<span class="amount"><?php echo wp_kses( $order->get_formatted_order_total(), array( 'small' => array(), 'span' => array( 'class' => array() ), 'del' => array(), 'ins' => array() ) ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound ?></span> <?php echo wp_kses_post( $column ); ?>
</td> </td>
<?php } ?>
</tr> </tr>

View File

@@ -10,16 +10,24 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly exit; // Exit if accessed directly
} }
$columns = array(
esc_html__( 'Order Number', 'woocommerce-subscriptions' ),
esc_html__( 'Relationship', 'woocommerce-subscriptions' ),
esc_html__( 'Date', 'woocommerce-subscriptions' ),
esc_html__( 'Status', 'woocommerce-subscriptions' ),
esc_html_x( 'Total', 'table heading', 'woocommerce-subscriptions' ),
);
$columns = apply_filters( 'wcs_related_orders_table_header_columns', $columns );
?> ?>
<div class="woocommerce_subscriptions_related_orders"> <div class="woocommerce_subscriptions_related_orders">
<table> <table>
<thead> <thead>
<tr> <tr>
<th><?php esc_html_e( 'Order Number', 'woocommerce-subscriptions' ); ?></th> <?php foreach ( $columns as $row ) { ?>
<th><?php esc_html_e( 'Relationship', 'woocommerce-subscriptions' ); ?></th> <th><?php echo wp_kses_post( $row ); ?></th>
<th><?php esc_html_e( 'Date', 'woocommerce-subscriptions' ); ?></th> <?php } ?>
<th><?php esc_html_e( 'Status', 'woocommerce-subscriptions' ); ?></th>
<th><?php echo esc_html_x( 'Total', 'table heading', 'woocommerce-subscriptions' ); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -16,7 +16,7 @@ class WC_Subscriptions_Core_Plugin {
* The version of subscriptions-core library. * The version of subscriptions-core library.
* @var string * @var string
*/ */
protected $library_version = '8.1.1'; // WRCS: DEFINED_VERSION. protected $library_version = '8.2.0'; // WRCS: DEFINED_VERSION.
/** /**
* The subscription scheduler instance. * The subscription scheduler instance.

View File

@@ -156,6 +156,9 @@ class WC_Subscriptions_Email_Notifications {
switch ( current_action() ) { switch ( current_action() ) {
case 'woocommerce_scheduled_subscription_customer_notification_renewal': case 'woocommerce_scheduled_subscription_customer_notification_renewal':
$subscription = wcs_get_subscription( $subscription_id ); $subscription = wcs_get_subscription( $subscription_id );
if ( $subscription->get_total() <= 0 ) {
break;
}
if ( $subscription->is_manual() ) { if ( $subscription->is_manual() ) {
$notification = $emails['WCS_Email_Customer_Notification_Manual_Renewal']; $notification = $emails['WCS_Email_Customer_Notification_Manual_Renewal'];
} else { } else {
@@ -248,7 +251,7 @@ class WC_Subscriptions_Email_Notifications {
$actions['wcs_customer_notification_subscription_expiration'] = esc_html__( 'Send upcoming subscription expiration notification', 'woocommerce-subscriptions' ); $actions['wcs_customer_notification_subscription_expiration'] = esc_html__( 'Send upcoming subscription expiration notification', 'woocommerce-subscriptions' );
} }
if ( in_array( 'next_payment', $valid_notifications, true ) ) { if ( in_array( 'next_payment', $valid_notifications, true ) && $theorder->get_total() > 0 ) {
$actions['wcs_customer_notification_renewal'] = esc_html__( 'Send upcoming renewal notification', 'woocommerce-subscriptions' ); $actions['wcs_customer_notification_renewal'] = esc_html__( 'Send upcoming renewal notification', 'woocommerce-subscriptions' );
} }

View File

@@ -56,19 +56,24 @@ function wcs_date_input( $timestamp = 0, $args = array() ) {
} }
/** /**
* Get the edit post link without checking if the user can edit that post or not. * Get the admin edit link for an order.
*
* Note: this function does not check if the user has permission to edit the order.
* *
* @param int $post_id
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0 * @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
* @since 7.4.0 - Added official support for WC_Order objects as the first parameter.
*
* @param int|WC_Order $order The order ID or WC_Order object.
* @return string The edit order URL or an empty string if the order is not found or is not a valid order type.
*/ */
function wcs_get_edit_post_link( $post_id ) { function wcs_get_edit_post_link( $order ) {
$object = wc_get_order( $post_id ); // works for both WC Order and WC Subscription objects. $order = is_a( $order, 'WC_Abstract_Order' ) ? $order : wc_get_order( $order );
if ( ! $object || ! in_array( $object->get_type(), array( 'shop_order', 'shop_subscription' ), true ) ) { if ( ! $order || ! in_array( $order->get_type(), array( 'shop_order', 'shop_subscription' ), true ) ) {
return ''; return '';
} }
return apply_filters( 'get_edit_post_link', $object->get_edit_order_url(), $post_id, '' ); return apply_filters( 'get_edit_post_link', $order->get_edit_order_url(), $order->get_id(), '' );
} }
/** /**
@@ -162,26 +167,36 @@ function wcs_get_rounding_precision() {
} }
/** /**
* Add a prefix to a string if it doesn't already have it * Add a prefix to a string or array of strings if it doesn't already have it
* *
* @param string * @param string|array $key The key or array of keys to add the prefix to.
* @param string * @param string $prefix The prefix to add.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0 * @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @return string *
* @return string|array The key or array of keys with the prefix added.
*/ */
function wcs_maybe_prefix_key( $key, $prefix = '_' ) { function wcs_maybe_prefix_key( $key, $prefix = '_' ) {
return ( substr( $key, 0, strlen( $prefix ) ) != $prefix ) ? $prefix . $key : $key; if ( is_array( $key ) ) {
return array_map( __FUNCTION__, $key, array_fill( 0, count( $key ), $prefix ) );
}
return ( substr( $key, 0, strlen( $prefix ) ) !== $prefix ) ? $prefix . $key : $key;
} }
/** /**
* Remove a prefix from a string if has it * Remove a prefix from a string or array of strings if it has it.
* *
* @param string $key * @param string|array $key The key or array of keys to remove the prefix from.
* @param string $prefix * @param string $prefix The prefix to remove.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0 * @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @return string *
* @return string|array The key or array of keys with the prefix removed.
*/ */
function wcs_maybe_unprefix_key( $key, $prefix = '_' ) { function wcs_maybe_unprefix_key( $key, $prefix = '_' ) {
if ( is_array( $key ) ) {
return array_map( __FUNCTION__, $key, array_fill( 0, count( $key ), $prefix ) );
}
return ( substr( $key, 0, strlen( $prefix ) ) === $prefix ) ? substr( $key, strlen( $prefix ) ) : $key; return ( substr( $key, 0, strlen( $prefix ) ) === $prefix ) ? substr( $key, strlen( $prefix ) ) : $key;
} }

View File

@@ -15,36 +15,19 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
?> ?>
<div class="variable_subscription_trial variable_subscription_pricing_2_3 show_if_variable-subscription variable_subscription_trial_sign_up" style="display: none">
<p class="form-row form-row-first form-field show_if_variable-subscription sign-up-fee-cell">
<label for="variable_subscription_sign_up_fee[<?php echo esc_attr( $loop ); ?>]"><?php printf( esc_html__( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) ); ?></label>
<input type="text" class="wc_input_price wc_input_subscription_intial_price wc_input_subscription_initial_price" name="variable_subscription_sign_up_fee[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_price( WC_Subscriptions_Product::get_sign_up_fee( $variation_product ) ) ); ?>" placeholder="<?php echo esc_attr_x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ); ?>">
</p>
<p class="form-row form-row-last show_if_variable-subscription">
<label for="variable_subscription_trial_length[<?php echo esc_attr( $loop ); ?>]">
<?php esc_html_e( 'Free trial', 'woocommerce-subscriptions' ); ?>
<?php // translators: placeholder is trial period validation message if passed an invalid value (e.g. "Trial period can not exceed 4 weeks") ?>
<?php echo wcs_help_tip( sprintf( _x( 'An optional period of time to wait before charging the first recurring payment. Any sign up fee will still be charged at the outset of the subscription. %s', 'Trial period dropdown\'s description in pricing fields', 'woocommerce-subscriptions' ), self::get_trial_period_validation_message() ) ); ?>
</label>
<input type="text" class="wc_input_subscription_trial_length" name="variable_subscription_trial_length[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( WC_Subscriptions_Product::get_trial_length( $variation_product ) ); ?>">
<label for="variable_subscription_trial_period[<?php echo esc_attr( $loop ); ?>]" class="wcs_hidden_label"><?php esc_html_e( 'Subscription trial period:', 'woocommerce-subscriptions' ); ?></label>
<select name="variable_subscription_trial_period[<?php echo esc_attr( $loop ); ?>]" class="wc_input_subscription_trial_period wc-enhanced-select">
<?php foreach ( wcs_get_available_time_periods() as $key => $value ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $key, WC_Subscriptions_Product::get_trial_period( $variation_product ) ); ?>><?php echo esc_html( $value ); ?></option>
<?php endforeach; ?>
</select>
</p>
</div>
<div class="variable_subscription_pricing variable_subscription_pricing_2_3 show_if_variable-subscription" style="display: none"> <div class="variable_subscription_pricing variable_subscription_pricing_2_3 show_if_variable-subscription" style="display: none">
<p class="form-row form-row-first form-field show_if_variable-subscription _subscription_price_field"> <p class="form-row multiple_fields form-row-first form-field show_if_variable-subscription _subscription_price_field">
<label for="variable_subscription_price[<?php echo esc_attr( $loop ); ?>]"> <label for="variable_subscription_price[<?php echo esc_attr( $loop ); ?>]">
<?php <?php
// translators: placeholder is a currency symbol / code // translators: placeholder is a currency symbol / code
printf( esc_html__( 'Subscription price (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) ); printf( esc_html__( 'Subscription price (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) );
?> ?>
<?php echo wcs_help_tip( __( 'Choose the subscription price, billing interval and period.', 'woocommerce-subscriptions' ) ); ?>
</label> </label>
<input type="text" class="wc_input_price wc_input_subscription_price" name="variable_subscription_price[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_price( WC_Subscriptions_Product::get_regular_price( $variation_product ) ) ); ?>" placeholder="<?php echo esc_attr_x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ); ?>">
<span class="wrap">
<?php // Translators: %s: formatted example price value. ?>
<input type="text" class="wc_input_price wc_input_subscription_price" name="variable_subscription_price[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_price( WC_Subscriptions_Product::get_regular_price( $variation_product ) ) ); ?>" placeholder="<?php echo esc_attr( sprintf( _x( 'e.g. %s', 'example price', 'woocommerce-subscriptions' ), esc_attr( wc_format_localized_price( '9.90' ) ) ) ); ?>">
<label for="variable_subscription_period_interval[<?php echo esc_attr( $loop ); ?>]" class="wcs_hidden_label"><?php esc_html_e( 'Billing interval:', 'woocommerce-subscriptions' ); ?></label> <label for="variable_subscription_period_interval[<?php echo esc_attr( $loop ); ?>]" class="wcs_hidden_label"><?php esc_html_e( 'Billing interval:', 'woocommerce-subscriptions' ); ?></label>
<select name="variable_subscription_period_interval[<?php echo esc_attr( $loop ); ?>]" class="wc_input_subscription_period_interval wc-enhanced-select"> <select name="variable_subscription_period_interval[<?php echo esc_attr( $loop ); ?>]" class="wc_input_subscription_period_interval wc-enhanced-select">
@@ -59,9 +42,10 @@ if ( ! defined( 'ABSPATH' ) ) {
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $key, $billing_period ); ?>><?php echo esc_html( $value ); ?></option> <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $key, $billing_period ); ?>><?php echo esc_html( $value ); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</span>
</p> </p>
<p class="form-row form-row-last show_if_variable-subscription _subscription_length_field" style="display: none"> <p class="form-row form-row-last form-field show_if_variable-subscription _subscription_length_field" style="display: none">
<label for="variable_subscription_length[<?php echo esc_attr( $loop ); ?>]"> <label for="variable_subscription_length[<?php echo esc_attr( $loop ); ?>]">
<?php esc_html_e( 'Stop renewing after', 'woocommerce-subscriptions' ); ?> <?php esc_html_e( 'Stop renewing after', 'woocommerce-subscriptions' ); ?>
<?php echo wcs_help_tip( _x( 'Automatically stop renewing the subscription after this length of time. This length is in addition to any free trial or amount of time provided before a synchronised first renewal date.', 'Subscription Length dropdown\'s description in pricing fields', 'woocommerce-subscriptions' ) ); ?> <?php echo wcs_help_tip( _x( 'Automatically stop renewing the subscription after this length of time. This length is in addition to any free trial or amount of time provided before a synchronised first renewal date.', 'Subscription Length dropdown\'s description in pricing fields', 'woocommerce-subscriptions' ) ); ?>
@@ -73,3 +57,30 @@ if ( ! defined( 'ABSPATH' ) ) {
</select> </select>
</p> </p>
</div> </div>
<div class="variable_subscription_trial variable_subscription_pricing_2_3 show_if_variable-subscription variable_subscription_trial_sign_up" style="display: none">
<p class="form-row form-row-first form-field show_if_variable-subscription sign-up-fee-cell">
<label for="variable_subscription_sign_up_fee[<?php echo esc_attr( $loop ); ?>]">
<?php printf( esc_html__( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) ); ?>
<?php echo wcs_help_tip( __( 'Optionally include an amount to be charged at the outset of the subscription. The sign-up fee will be charged immediately, even if the product has a free trial or the payment dates are synced.', 'woocommerce-subscriptions' ) ); ?>
</label>
<input type="text" class="wc_input_price wc_input_subscription_intial_price wc_input_subscription_initial_price" name="variable_subscription_sign_up_fee[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_price( WC_Subscriptions_Product::get_sign_up_fee( $variation_product ) ) ); ?>" placeholder="<?php echo esc_attr_x( 'e.g.', 'example price', 'woocommerce-subscriptions' ); ?> <?php echo esc_attr( wc_format_localized_price( '9.90' ) ); ?>">
</p>
<p class="form-row multiple_fields form-field form-row-last show_if_variable-subscription">
<label for="variable_subscription_trial_length[<?php echo esc_attr( $loop ); ?>]">
<?php esc_html_e( 'Free trial', 'woocommerce-subscriptions' ); ?>
<?php // translators: placeholder is trial period validation message if passed an invalid value (e.g. "Trial period can not exceed 4 weeks") ?>
<?php echo wcs_help_tip( sprintf( _x( 'An optional period of time to wait before charging the first recurring payment. Any sign up fee will still be charged at the outset of the subscription. %s', 'Trial period dropdown\'s description in pricing fields', 'woocommerce-subscriptions' ), self::get_trial_period_validation_message() ) ); ?>
</label>
<span class="wrap">
<input type="text" class="wc_input_subscription_trial_length" name="variable_subscription_trial_length[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( WC_Subscriptions_Product::get_trial_length( $variation_product ) ); ?>">
<label for="variable_subscription_trial_period[<?php echo esc_attr( $loop ); ?>]" class="wcs_hidden_label"><?php esc_html_e( 'Subscription trial period:', 'woocommerce-subscriptions' ); ?></label>
<select name="variable_subscription_trial_period[<?php echo esc_attr( $loop ); ?>]" class="wc_input_subscription_trial_period wc-enhanced-select">
<?php foreach ( wcs_get_available_time_periods() as $key => $value ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $key, WC_Subscriptions_Product::get_trial_period( $variation_product ) ); ?>><?php echo esc_html( $value ); ?></option>
<?php endforeach; ?>
</select>
</span>
</p>
</div>

View File

@@ -393,12 +393,14 @@ function wcs_sanitize_subscription_status_key( $status_key ) {
/** /**
* Gets a list of subscriptions that match a certain set of query arguments. * Gets a list of subscriptions that match a certain set of query arguments.
* *
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0. * @since 1.0.0 Migrated from WooCommerce Subscriptions v2.0.
* @since 7.3.0 Any additional arguments are passed across to WooCommerce for use in the final query.
* *
* @param array $args { * @param array $args {
* A set of name value pairs to query for subscriptions - similar to args supported by wc_get_orders(). * A set of name value pairs to query for subscriptions - similar to args supported by wc_get_orders().
* You can also provide other keys, such as but not limited to those supported by WooCommerce order queries.
* *
* @type string $subscriptions_per_page The number of subscriptions to return. Set to -1 for unlimited. Default 10. * @type int $subscriptions_per_page The number of subscriptions to return. Set to -1 for unlimited. Default 10.
* @type int $paged The page of subscriptions to return. Default 1. * @type int $paged The page of subscriptions to return. Default 1.
* @type int $offset An optional number of subscription to displace or pass over. Default 0. * @type int $offset An optional number of subscription to displace or pass over. Default 0.
* @type string $orderby The field which the subscriptions should be ordered by. Can be 'start_date', 'trial_end_date', 'end_date', 'status' or 'order_id'. Defaults to 'start_date'. * @type string $orderby The field which the subscriptions should be ordered by. Can be 'start_date', 'trial_end_date', 'end_date', 'status' or 'order_id'. Defaults to 'start_date'.
@@ -407,15 +409,13 @@ function wcs_sanitize_subscription_status_key( $status_key ) {
* @type int $product_id To restrict subscriptions to those which contain a certain product ID. Default 0 - No product restriction. * @type int $product_id To restrict subscriptions to those which contain a certain product ID. Default 0 - No product restriction.
* @type int $variation_id To restrict subscriptions to those which contain a certain product variation ID. Default 0 - No variation restriction. * @type int $variation_id To restrict subscriptions to those which contain a certain product variation ID. Default 0 - No variation restriction.
* @type int $order_id To restrict subscriptions to those which have a certain parent order ID. Default 0 - No parent order restriction. * @type int $order_id To restrict subscriptions to those which have a certain parent order ID. Default 0 - No parent order restriction.
* @type string $subscription_status The status of the subscriptions to return. Can be 'any', 'active', 'on-hold', 'pending', 'cancelled', 'expired', 'trash', 'pending-cancel'. Default 'any'. * @type string|string[] $subscription_status The status of the subscriptions to return. Can be 'any', 'active', 'on-hold', 'pending', 'cancelled', 'expired', 'trash', 'pending-cancel'. Default 'any'.
* } * }
* *
* @return WC_Subscription[] An array of WC_Subscription objects keyed by their ID matching the query args. * @return WC_Subscription[] An array of WC_Subscription objects keyed by their ID matching the query args.
*/ */
function wcs_get_subscriptions( $args ) { function wcs_get_subscriptions( $args ) {
$args = wp_parse_args( $default_args = array(
$args,
array(
'subscriptions_per_page' => 10, 'subscriptions_per_page' => 10,
'paged' => 1, 'paged' => 1,
'offset' => 0, 'offset' => 0,
@@ -427,16 +427,24 @@ function wcs_get_subscriptions( $args ) {
'order_id' => 0, 'order_id' => 0,
'subscription_status' => array( 'any' ), 'subscription_status' => array( 'any' ),
'meta_query_relation' => 'AND', 'meta_query_relation' => 'AND',
)
); );
$provided_args = wp_parse_args( $args );
$working_args = array_merge( $default_args, $provided_args );
$extra_args = array_diff_key( $provided_args, $default_args );
// If the order ID arg is not a shop_order then there's no need to proceed with the query. // If the order ID arg is not a shop_order then there's no need to proceed with the query.
if ( 0 !== $args['order_id'] && 'shop_order' !== WC_Data_Store::load( 'order' )->get_order_type( $args['order_id'] ) ) { if ( 0 !== $working_args['order_id'] && 'shop_order' !== WC_Data_Store::load( 'order' )->get_order_type( $working_args['order_id'] ) ) {
return array(); return array();
} }
// Ensure subscription_status is an array. // Support the direct use of 'status'.
$args['subscription_status'] = $args['subscription_status'] ? (array) $args['subscription_status'] : []; if ( isset( $extra_args['status'] ) ) {
$working_args['subscription_status'] = $extra_args['status'];
}
// Ensure the status argument is an array.
$working_args['subscription_status'] = $working_args['subscription_status'] ? (array) $working_args['subscription_status'] : [];
// Grab the native post stati, removing pending and adding any. // Grab the native post stati, removing pending and adding any.
$builtin = get_post_stati( [ '_builtin' => true ] ); $builtin = get_post_stati( [ '_builtin' => true ] );
@@ -444,7 +452,7 @@ function wcs_get_subscriptions( $args ) {
$builtin['any'] = 'any'; $builtin['any'] = 'any';
// Make sure statuses start with 'wc-'. // Make sure statuses start with 'wc-'.
foreach ( $args['subscription_status'] as &$status ) { foreach ( $working_args['subscription_status'] as &$status ) {
if ( isset( $builtin[ $status ] ) ) { if ( isset( $builtin[ $status ] ) ) {
continue; continue;
} }
@@ -455,23 +463,25 @@ function wcs_get_subscriptions( $args ) {
// Prepare the args for WC_Order_Query. // Prepare the args for WC_Order_Query.
$query_args = array( $query_args = array(
'type' => 'shop_subscription', 'type' => 'shop_subscription',
'status' => $args['subscription_status'], 'status' => $working_args['subscription_status'],
'limit' => $args['subscriptions_per_page'], 'limit' => $working_args['limit'] ?? $working_args['subscriptions_per_page'],
'page' => $args['paged'], 'offset' => $working_args['offset'] > 0 ? $working_args['offset'] : null,
'offset' => $args['offset'] > 0 ? $args['offset'] : null, 'order' => $working_args['order'],
'order' => $args['order'],
'return' => 'ids', 'return' => 'ids',
// just in case we need to filter or order by meta values later // just in case we need to filter or order by meta values later
'meta_query' => isset( $args['meta_query'] ) ? $args['meta_query'] : array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => isset( $working_args['meta_query'] ) ? $working_args['meta_query'] : array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
); );
// Remove Subscriptions-specific args which exist as aliases of regular order query args.
unset( $query_args['subscription_status'], $query_args['subscriptions_per_page'] );
// Maybe only get subscriptions created by a certain order // Maybe only get subscriptions created by a certain order
if ( 0 !== $args['order_id'] && is_numeric( $args['order_id'] ) ) { if ( 0 !== $working_args['order_id'] && is_numeric( $working_args['order_id'] ) ) {
$query_args['parent'] = $args['order_id']; $query_args['parent'] = $working_args['order_id'];
} }
// Map subscription specific orderby values to internal keys. // Map subscription specific orderby values to internal keys.
switch ( $args['orderby'] ) { switch ( $working_args['orderby'] ) {
case 'status': case 'status':
wcs_deprecated_argument( __FUNCTION__, 'subscriptions-core 5.0.0', 'The "status" orderby value is deprecated.' ); wcs_deprecated_argument( __FUNCTION__, 'subscriptions-core 5.0.0', 'The "status" orderby value is deprecated.' );
break; break;
@@ -481,7 +491,7 @@ function wcs_get_subscriptions( $args ) {
case 'trial_end_date': case 'trial_end_date':
case 'end_date': case 'end_date':
// We need to orderby post meta value: http://www.paulund.co.uk/order-meta-query // We need to orderby post meta value: http://www.paulund.co.uk/order-meta-query
$date_type = str_replace( '_date', '', $args['orderby'] ); $date_type = str_replace( '_date', '', $working_args['orderby'] );
$query_args = array_merge( $query_args, array( $query_args = array_merge( $query_args, array(
'orderby' => 'meta_value', 'orderby' => 'meta_value',
'meta_key' => wcs_get_date_meta_key( $date_type ), 'meta_key' => wcs_get_date_meta_key( $date_type ),
@@ -494,51 +504,56 @@ function wcs_get_subscriptions( $args ) {
); );
break; break;
default: default:
$query_args['orderby'] = $args['orderby']; $query_args['orderby'] = $working_args['orderby'];
break; break;
} }
// Maybe filter to a specific customer. // Maybe filter to a specific customer.
if ( 0 !== $args['customer_id'] && is_numeric( $args['customer_id'] ) ) { if ( 0 !== $working_args['customer_id'] && is_numeric( $working_args['customer_id'] ) ) {
// When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache and query by post__in for improved performance. // When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache and query by post__in for improved performance.
if ( ! wcs_is_custom_order_tables_usage_enabled() ) { if ( ! wcs_is_custom_order_tables_usage_enabled() ) {
$users_subscription_ids = WCS_Customer_Store::instance()->get_users_subscription_ids( $args['customer_id'] ); $users_subscription_ids = WCS_Customer_Store::instance()->get_users_subscription_ids( $working_args['customer_id'] );
$query_args = WCS_Admin_Post_Types::set_post__in_query_var( $query_args, $users_subscription_ids ); $query_args = WCS_Admin_Post_Types::set_post__in_query_var( $query_args, $users_subscription_ids );
} else { } else {
$query_args['customer_id'] = $args['customer_id']; $query_args['customer_id'] = $working_args['customer_id'];
} }
} }
// It's more efficient to filter the results by product ID or variation ID rather than querying for via a "post__in" clause. // It's more efficient to filter the results by product ID or variation ID rather than querying for via a "post__in" clause.
// This can only work where we know that the results will be sufficiently limited by the other query args. ie when we're querying by customer_id or order_id. // This can only work where we know that the results will be sufficiently limited by the other query args. ie when we're querying by customer_id or order_id.
// We store the filters in a separate array so that we can apply them after the query has been run. // We store the filters in a separate array so that we can apply them after the query has been run.
$query_controller = new WC_Subscription_Query_Controller( $args ); $query_controller = new WC_Subscription_Query_Controller( $working_args );
// We need to restrict subscriptions to those which contain a certain product/variation. // We need to restrict subscriptions to those which contain a certain product/variation.
if ( $query_controller->has_product_query() ) { if ( $query_controller->has_product_query() ) {
if ( $query_controller->should_filter_query_results() ) { if ( $query_controller->should_filter_query_results() ) {
// We will filter the results and apply any paging, limit and offset after the query has been run. // We will filter the results and apply any paging, limit and offset after the query has been run.
unset( $args['product_id'], $args['variation_id'], $query_args['limit'], $query_args['paged'], $query_args['offset'] ); unset( $working_args['product_id'], $working_args['variation_id'], $query_args['limit'], $query_args['paged'], $query_args['offset'] );
// We need to get all subscriptions otherwise the limit could be filled with subscriptions that don't contain the product. // We need to get all subscriptions otherwise the limit could be filled with subscriptions that don't contain the product.
$query_args['limit'] = -1; $query_args['limit'] = -1;
} else { } else {
$subscriptions_for_product = wcs_get_subscriptions_for_product( array( $args['product_id'], $args['variation_id'] ) ); $subscriptions_for_product = wcs_get_subscriptions_for_product( array( $working_args['product_id'], $working_args['variation_id'] ) );
$query_args = WCS_Admin_Post_Types::set_post__in_query_var( $query_args, $subscriptions_for_product ); $query_args = WCS_Admin_Post_Types::set_post__in_query_var( $query_args, $subscriptions_for_product );
} }
} }
if ( ! empty( $query_args['meta_query'] ) ) { if ( ! empty( $query_args['meta_query'] ) ) {
$query_args['meta_query']['relation'] = $args['meta_query_relation']; $query_args['meta_query']['relation'] = $working_args['meta_query_relation'];
} }
// We add any extra args, that are not specific to Subscriptions-queries, to the final set of query args.
// Merge order is important to prevent callers from overriding fields such as 'type', or where they might
// interfere with clever things we are doing re pagination.
$query_args = array_merge( $extra_args, $query_args );
/** /**
* Filters the query arguments used to retrieve subscriptions in wcs_get_subscriptions(). * Filters the query arguments used to retrieve subscriptions in wcs_get_subscriptions().
* *
* @param array $query_args The query arguments used to retrieve subscriptions. * @param array $query_args The query arguments used to retrieve subscriptions.
* @param array $args The original wcs_get_subscription() $args parameter. * @param array $working_args The original wcs_get_subscription() $args parameter.
*/ */
$query_args = apply_filters( 'woocommerce_get_subscriptions_query_args', $query_args, $args ); $query_args = apply_filters( 'woocommerce_get_subscriptions_query_args', $query_args, $working_args );
$subscriptions = array(); $subscriptions = array();
foreach ( wcs_get_orders_with_meta_query( $query_args ) as $subscription_id ) { foreach ( wcs_get_orders_with_meta_query( $query_args ) as $subscription_id ) {
@@ -551,7 +566,7 @@ function wcs_get_subscriptions( $args ) {
$subscriptions = $query_controller->paginate_results( $subscriptions ); $subscriptions = $query_controller->paginate_results( $subscriptions );
} }
return apply_filters( 'woocommerce_got_subscriptions', $subscriptions, $args ); return apply_filters( 'woocommerce_got_subscriptions', $subscriptions, $working_args );
} }
/** /**

View File

@@ -6,5 +6,5 @@
* Author: Automattic * Author: Automattic
* Author URI: https://woocommerce.com/ * Author URI: https://woocommerce.com/
* Requires WP: 5.6 * Requires WP: 5.6
* Version: 8.1.1 * Version: 8.2.0
*/ */

View File

@@ -5,7 +5,7 @@
* Description: Sell products and services with recurring payments in your WooCommerce Store. * Description: Sell products and services with recurring payments in your WooCommerce Store.
* Author: WooCommerce * Author: WooCommerce
* Author URI: https://woocommerce.com/ * Author URI: https://woocommerce.com/
* Version: 7.3.1 * Version: 7.4.0
* Requires Plugins: woocommerce * Requires Plugins: woocommerce
* *
* WC requires at least: 8.7.1 * WC requires at least: 8.7.1
@@ -78,7 +78,7 @@ class WC_Subscriptions {
public static $plugin_file = __FILE__; public static $plugin_file = __FILE__;
/** @var string */ /** @var string */
public static $version = '7.3.1'; // WRCS: DEFINED_VERSION. public static $version = '7.4.0'; // WRCS: DEFINED_VERSION.
/** @var string */ /** @var string */
public static $wc_minimum_supported_version = '7.7'; public static $wc_minimum_supported_version = '7.7';
@@ -149,7 +149,7 @@ class WC_Subscriptions {
$trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1 ); $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1 );
$file = $trace[0]['file']; $file = $trace[0]['file'];
$line = $trace[0]['line']; $line = $trace[0]['line'];
trigger_error( "Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR ); //phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped trigger_error( "Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} }
} }
} }