@@ -1194,7 +1194,13 @@ public function daily_membership_expiring_soon_check() {
11941194 $ email_service = new EmailService ();
11951195 foreach ( $ subscriptions as $ subscription ) {
11961196
1197- $ user_id = $ subscription ['member_id ' ];
1197+ $ user_id = $ subscription ['member_id ' ];
1198+
1199+ // Skip memberships already scheduled to cancel; no "expiring soon / renew" reminder for them.
1200+ if ( get_user_meta ( $ user_id , 'urm_pending_cancel_ ' . ( $ subscription ['subscription_id ' ] ?? '' ), true ) ) {
1201+ continue ;
1202+ }
1203+
11981204 $ checked_date = get_user_meta ( $ user_id , 'urm_expiring_reminder_sent_for_date ' , true );
11991205
12001206 if ( $ checked_date === $ subscription ['next_billing_date ' ] ) {
@@ -1211,22 +1217,23 @@ public function daily_membership_expiring_soon_check() {
12111217 * @return void
12121218 */
12131219 public function daily_membership_ended_check () {
1214- $ date = new \DateTime ( 'today ' );
1215- $ check_date = $ date ->modify ( '-1 day ' )->format ( 'Y-m-d H:i:s ' );
1220+ // Look back a window (not a single day) and dedupe per subscription, so a late status flip,
1221+ // cron-order drift, or a missed cron day can't permanently skip the ended email.
1222+ $ lookback_days = (int ) apply_filters ( 'urm_ended_email_lookback_days ' , 7 );
1223+ $ check_date = ( new \DateTime ( "- {$ lookback_days } days " ) )->format ( 'Y-m-d H:i:s ' );
12161224 $ subscriptions = $ this ->members_subscription_repository ->get_expired_subscriptions ( $ check_date );
12171225
12181226 if ( empty ( $ subscriptions ) ) {
12191227 return ;
12201228 }
12211229 $ email_service = new EmailService ();
12221230 foreach ( $ subscriptions as $ subscription ) {
1223- $ user_id = $ subscription ['member_id ' ];
1224- $ checked_date = get_user_meta ( $ user_id , 'urm_expired_reminder_sent_for_date ' , true );
1225- if ( $ checked_date === $ subscription ['expiry_date ' ] ) {
1231+ $ sent_key = 'urm_ended_email_sent_ ' . ( $ subscription ['subscription_id ' ] ?? '' );
1232+ if ( get_user_meta ( $ subscription ['member_id ' ], $ sent_key , true ) ) {
12261233 continue ;
12271234 }
12281235 $ email_service ->send_email ( $ subscription , 'membership_ended ' );
1229- update_user_meta ( $ subscription ['member_id ' ], ' urm_expired_reminder_sent_for_date ' , $ subscription ['expiry_date ' ] );
1236+ update_user_meta ( $ subscription ['member_id ' ], $ sent_key , $ subscription ['expiry_date ' ] );
12301237 }
12311238 }
12321239
@@ -1271,7 +1278,9 @@ public function daily_membership_expiration_check() {
12711278 ( new NewPaypalService () )->cancel_suspended_subscription ( $ subscription ['gateway_subscription_id ' ] );
12721279 }
12731280 delete_user_meta ( $ user_id , 'urm_pending_cancel_ ' . $ subscription_id );
1274- $ update_result = $ this ->members_subscription_repository ->update ( $ subscription_id , array ( 'status ' => 'expired ' ) );
1281+ // A pending-cancel subscription reaching its date is a cancellation, not a natural expiry.
1282+ $ new_status = $ pending_cancel_meta ? 'canceled ' : 'expired ' ;
1283+ $ update_result = $ this ->members_subscription_repository ->update ( $ subscription_id , array ( 'status ' => $ new_status ) );
12751284
12761285 if ( $ update_result ) {
12771286 ++$ expired_count ;
@@ -1293,13 +1302,14 @@ public function daily_membership_expiration_check() {
12931302 array ( 'source ' => 'urm-membership-expiration ' )
12941303 );
12951304
1296- // Prepare data to trigger subscription expired event .
1305+ // Cancellation email already went out at cancel time; the membership_ended cron only targets ' expired', so a canceled sub gets no expiry email either .
12971306 $ payload = array (
12981307 'subscription_id ' => $ subscription_id ,
12991308 'member_id ' => $ user_id ,
1300- 'event_type ' => 'expired ' ,
1309+ 'event_type ' => $ pending_cancel_meta ? ' canceled ' : 'expired ' ,
13011310 'meta ' => array (
13021311 'membership_id ' => $ membership_id ,
1312+ 'mode ' => 'expiry ' ,
13031313 ),
13041314 );
13051315
0 commit comments