diff --git a/adminpages/emailsettings.php b/adminpages/emailsettings.php index efbe27f134..5514b030a7 100644 --- a/adminpages/emailsettings.php +++ b/adminpages/emailsettings.php @@ -33,6 +33,8 @@ pmpro_setOption("email_member_notification"); + pmpro_setOption("churned_email_days", null, 'intval'); + //assume success $msg = true; $msgt = "Your email settings have been updated."; @@ -49,6 +51,8 @@ $email_member_notification = get_option( "pmpro_email_member_notification"); + $churned_email_days = get_option("pmpro_churned_email_days", 30); + if(empty($from_email)) { $parsed = parse_url(home_url()); @@ -186,6 +190,18 @@
++ + ', '' ); ?> +
+diff --git a/classes/class.pmproemail.php b/classes/class.pmproemail.php index 5dfb0352b0..8e17f3603d 100644 --- a/classes/class.pmproemail.php +++ b/classes/class.pmproemail.php @@ -731,6 +731,27 @@ function sendMembershipExpiredEmail( $user = NULL, $membership_id = NULL ) { $email = new PMPro_Email_Template_Membership_Expired( $user, $membership_id ); return $email->send(); } + + /** + * Send the member a churned email 30 days after their membership has expired. + * + * @param object $user The WordPress user object. + * @param int $membership_id The member's membership level ID. + * @return bool Whether the email was sent successfully. + */ + function sendMembershipChurnedEmail( $user = NULL, $membership_id = NULL ) { + global $current_user; + if( !$user ) { + $user = $current_user; + } + //Bail if still we don't have a user. + if( !$user ) { + return false; + } + + $email = new PMPro_Email_Template_Membership_Churned( $user, $membership_id ); + return $email->send(); + } /** * Send the member an email when their membership has ended. diff --git a/classes/email-templates/class-pmpro-email-template-membership-churned.php b/classes/email-templates/class-pmpro-email-template-membership-churned.php new file mode 100644 index 0000000000..4e4ff55338 --- /dev/null +++ b/classes/email-templates/class-pmpro-email-template-membership-churned.php @@ -0,0 +1,193 @@ +user = $user; + $this->membership_level_id = $membership_level_id; + } + + /** + * Get the email template slug. + * + * @return string The email template slug. + */ + public static function get_template_slug() { + return 'membership_churned'; + } + + /** + * Get the "nice name" of the email template. + * + * @return string The "nice name" of the email template. + */ + public static function get_template_name() { + return esc_html__( 'Membership Churned', 'paid-memberships-pro' ); + } + + /** + * Get "help text" to display to the admin when editing the email template. + * + * @return string The "help text" to display to the admin when editing the email template. + */ + public static function get_template_description() { + $days = intval(get_option("pmpro_churned_email_days", 30)); + /* translators: %d: number of days after membership expiration */ + return sprintf(esc_html__( 'This email is sent to former members %d days after their membership expires (churned members).', 'paid-memberships-pro' ), $days); + } + + /** + * Get the default subject for the email. + * + * @return string The default subject for the email. + */ + public static function get_default_subject() { + return esc_html__( 'We miss you at !!sitename!! - Special offer inside', 'paid-memberships-pro' ); + } + + /** + * Get the default body content for the email. + * + * @return string The default body content for the email. + */ + public static function get_default_body() { + $days = intval(get_option("pmpro_churned_email_days", 30)); + + // Convert days to friendly text with flexible ranges + $time_period = ''; + if ($days <= 7) { + $time_period = 'a week'; + } elseif ($days <= 14) { + $time_period = 'two weeks'; + } elseif ($days <= 31) { + $time_period = 'a month'; + } elseif ($days <= 62) { + $time_period = 'two months'; + } elseif ($days <= 93) { + $time_period = 'three months'; + } elseif ($days <= 186) { + $time_period = 'six months'; + } elseif ($days <= 366) { + $time_period = 'a year'; + } else { + $time_period = 'some time'; + } + + return wp_kses_post( sprintf( __( '
Hi !!display_name!!,
+ +We noticed it has been %s since your membership at !!sitename!! expired, and we would love to have you back.
+ +We value your past membership and would like to offer you the opportunity to rejoin our community.
+ +View our current membership offerings here: !!levels_url!!
+ +Log in to manage your account here: !!login_url!!
+ +Thank you for considering rejoining us!
', 'paid-memberships-pro' ), $time_period ) ); + } + + /** + * Get the email address to send the email to. + * + * @return string The email address to send the email to. + */ + public function get_recipient_email() { + return $this->user->user_email; + } + + /** + * Get the name of the email recipient. + * + * @return string The name of the email recipient. + */ + public function get_recipient_name() { + return $this->user->display_name; + } + + + /** + * Get the email template variables for the email paired with a description of the variable. + * + * @return array The email template variables for the email (key => value pairs). + */ + public static function get_email_template_variables_with_description() { + return array( + '!!display_name!!' => esc_html__( 'The display name of the user.', 'paid-memberships-pro' ), + '!!user_login!!' => esc_html__( 'The username of the user.', 'paid-memberships-pro' ), + '!!user_email!!' => esc_html__( 'The email address of the user.', 'paid-memberships-pro' ), + '!!membership_id!!' => esc_html__( 'The ID of the membership level.', 'paid-memberships-pro' ), + '!!membership_level_name!!' => esc_html__( 'The name of the membership level.', 'paid-memberships-pro' ), + ); + } + + /** + * Get the email template variables for the email. + * + * @return array The email template variables for the email (key => value pairs). + */ + public function get_email_template_variables() { + global $wpdb; + // If we don't have a level ID, query the user's most recently expired level from the database. + if ( empty( $this->membership_id ) ) { + $membership_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT membership_id FROM $wpdb->pmpro_memberships_users + WHERE user_id = %d + AND status = 'expired' + ORDER BY enddate DESC + LIMIT 1", + $this->user->ID + ) + ); + + // If we still don't have a level ID, bail. + if ( empty( $membership_id ) ) { + $membership_id = 0; + } + } + + // Get the membership level object. + $membership_level = pmpro_getLevel( $membership_id ); + + return array( + "subject" => $this->get_default_subject(), + "name" => $this->user->display_name, + "display_name" => $this->user->display_name, + "user_login" => $this->user->user_login, + "user_email" => $this->user->user_email, + "membership_id" => ( ! empty( $membership_level ) && ! empty( $membership_level->id ) ) ? $membership_level->id : 0, + "membership_level_name" => ( ! empty( $membership_level ) && ! empty( $membership_level->name ) ) ? $membership_level->name : '[' . esc_html( 'deleted', 'paid-memberships-pro' ) . ']', + ); + } +} + +/** + * Register the email template. + * + * @param array $email_templates The email templates (template slug => email template class name) + * @return array The modified email templates array. + */ +function pmpro_email_templates_membership_churned( $email_templates ) { + $email_templates['membership_churned'] = 'PMPro_Email_Template_Membership_Churned'; + return $email_templates; +} +add_filter( 'pmpro_email_templates', 'pmpro_email_templates_membership_churned' ); diff --git a/includes/crons.php b/includes/crons.php index 6af1ddb129..d65326e6d2 100644 --- a/includes/crons.php +++ b/includes/crons.php @@ -32,6 +32,10 @@ function pmpro_get_crons() { 'interval' => 'daily', 'timestamp' => strtotime( '10:30:00' ) - ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ), ], + 'pmpro_cron_churned_emails' => [ + 'interval' => 'daily', + 'timestamp' => strtotime( '11:00:00' ) - ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ), + ], 'pmpro_license_check_key' => [ 'interval' => 'monthly', ], diff --git a/includes/email.php b/includes/email.php index 4b4abb81f6..c691735018 100644 --- a/includes/email.php +++ b/includes/email.php @@ -366,6 +366,10 @@ function pmpro_email_templates_send_test() { $send_email = 'sendInvoiceEmail'; $params = array($test_user, $test_order); break; + case 'membership_churned'; + $send_email = 'sendMembershipChurnedEmail'; + $params = array($test_user, $test_order->membership_id ); + break; case 'membership_expired'; $send_email = 'sendMembershipExpiredEmail'; $params = array($test_user, $test_order->membership_id ); diff --git a/paid-memberships-pro.php b/paid-memberships-pro.php index 3dc472231d..e0f77ef480 100644 --- a/paid-memberships-pro.php +++ b/paid-memberships-pro.php @@ -78,6 +78,7 @@ require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-billing-failure-admin.php' ); // billing failure email template require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-cancel-on-next-payment-date.php' ); //cancel auto renewals email template require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-cancel-on-next-payment-date-admin.php' ); //cancel auto renewals admin email template +require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-membership-churned.php' ); //churned member email template require_once( PMPRO_DIR . '/includes/filters.php' ); // filters, hacks, etc, moved into the plugin require_once( PMPRO_DIR . '/includes/reports.php' ); // load reports for admin (reports may also include tracking code, etc) diff --git a/scheduled/churnedemails.php b/scheduled/churnedemails.php new file mode 100644 index 0000000000..851e1f0e29 --- /dev/null +++ b/scheduled/churnedemails.php @@ -0,0 +1,2 @@ +prepare( + "SELECT DISTINCT + mu.user_id, + mu.membership_id, + mu.startdate, + mu.enddate, + um.meta_value AS notice + FROM {$wpdb->pmpro_memberships_users} AS mu + LEFT JOIN {$wpdb->usermeta} AS um ON um.user_id = mu.user_id + AND um.meta_key = CONCAT( 'pmpro_churned_notice_', mu.membership_id ) + WHERE ( um.meta_value IS NULL ) + AND ( mu.status = 'expired' ) + AND ( mu.enddate IS NOT NULL ) + AND ( mu.enddate <> '0000-00-00 00:00:00' ) + AND ( mu.enddate <= %s ) + AND ( mu.membership_id <> 0 OR mu.membership_id <> NULL ) + ORDER BY mu.enddate", + $interval, + ); + + if ( defined( 'PMPRO_CRON_LIMIT' ) ) { + $sqlQuery .= " LIMIT " . PMPRO_CRON_LIMIT; + } + + $expired_users = $wpdb->get_results( $sqlQuery ); + + foreach( $expired_users as $user_data ) { + $send_email = apply_filters( 'pmpro_send_churned_email', true, $user_data->user_id ); + + if ( $send_email ) { + // Send the churned email + $pmproemail = new PMProEmail(); + $user = get_userdata( $user_data->user_id ); + + if ( ! empty( $user ) ) { + $pmproemail->sendMembershipChurnedEmail( $user, $user_data->membership_id ); + + if ( WP_DEBUG ) { + error_log( sprintf( esc_html__( "Churned member email sent to %s.", 'paid-memberships-pro' ), $user->user_email ) ); + } + } + } + + // Delete any old user meta for this key to prevent duplicate user meta rows + delete_user_meta( $user_data->user_id, 'pmpro_churned_notice' ); + + // Update user meta so we don't email them again + update_user_meta( $user_data->user_id, 'pmpro_churned_notice_' . $user_data->membership_id, $today ); + } +} + /** * Delete old files in wp-content/uploads/pmpro-register-helper/tmp every day. */