Auto EOT not working consistently

We’re encountering an issue where some members are correctly expired once their membership term ends, but some of them are not expired.

I tried manually loading https://[domain-name]/?s2member_auto_eot_system_via_cron=1, which, if I understand correctly, should force all members who have passed their term to be marked as expired. However, when I load this page manually, nothing changes with the members.

Something we have observed is that the occurrence of members being expired correctly happens more regularly on our live website, which sees more traffic than our dev site (on the dev site, it is more frequent that members ARE NOT expired correctly).

Does anyone have any ideas why the auto EOT would be working inconsistently?

Can someone point me to a piece of PHP code that would change any members who’s EOT has already passed to Expired Members? With this, I could try running it manually at least, or perhaps set up a custom cron job.

Thank you.

That’s the problem with WP’s own cronjobs. They are dependent on traffic to the site. If you don’t have it, they don’t run.

You can try replacing them with a real cronjob, as explained here: https://www.siteground.com/tutorials/wordpress/real-cron-job/

Whether that will work for EOTs that are already out of date, though, I don’t know. I suspect you might have to deal with those manually.

Thank you, Tim. I can do that.

However, shouldn’t manually loading https://[domain-name]/?s2member_auto_eot_system_via_cron=1 expire all members who are passed EOT, even older ones?

I’m not sure what you mean by “manually loading.” You could try a plugin that enables you to run cronjobs manually, such as WP Crontrol.

Additionally, I don’t know how far back s2Member’s check for expired subscriptions goes.

By “manually loading”, I mean loading the https://[domain-name]/?s2member_auto_eot_system_via_cron=1 url from my web browser, instead of from a cron job. My understanding is that loading that url should clear any members who have passed EOT.

I did try running https://[domain-name]/?s2member_auto_eot_system_via_cron=1 as a server based cron job, and created a new member that would expire within a few minutes, and the user did not expire, so that doesn’t seem to address the issue.

I have had a look at a dev site of mine, using WP Crontrol, and the function to run the auto-delete system isn’t displayed. That suggests it isn’t running.

Could you try installing the same plugin, and then go to Tools->Cron Events to see if you can find an s2Member function displayed there?

Yes, I installed it and checked. No, I don’t see any s2Member function there. There is one called wp_scheduled_delete(), but I think that’s part of wp, not s2Member.

I have set up a test with the server cron job running the following every hour (is that frequent enough?), and set s2Member to allow me to run auto EOT myself through cron jobs:
wget https://[domain name]/?s2member_auto_eot_system_via_cron=1 -O /dev/null

I created three test users with expirations one hour from now, tomorrow, and the day after tomorrow. We will see if they get expired properly.

Do you know how to create a piece of PHP code and SQL query that would search for all members who’s EOT has already passed and then change their role to Expired Member?

No, I don’t. It sounds like there is a bug in s2Member. Would you like to open an issue on Github or shall I?

A problem for us is that we’re running an outdated version of s2Member, and I don’t think we can update because of some customization that was done. Would it still be relevant to post to Github, considering this situation?

No, it wouldn’t, unless the customization is in an mu-plugin.

I will see if I can come up with some code to deal with this, but that might take a couple of weeks.

I have now opened a Github issue. I will see what I can do about writing some code for your situation but, as I said, it might take a while.

Thanks, Tim, much appreciated. I’ve got a start on the code. This finds the users who’s EOT has already passed:

SELECT * FROM [database].usermeta WHERE meta_key LIKE ‘%s2member_auto_eot_time%’ AND meta_value < now()

And the following should retrieve all users who aren’t ‘subscriber’. Subscriber is the role of expired members, as far as I can tell, so it should list all members who ARE NOT expired:

SELECT user_id FROM [database].usermeta WHERE meta_key LIKE ‘%capabilities%’ AND meta_value NOT LIKE ‘%subscriber%’

The part I’m stuck on right now is how to join these two based on the user_id field: I want to get a list of all user_ids that have a record matching the first query and the second query. But, because these are in the same table, I don’t know how to do the join.

Update: I may have figured this out. Here’s the query I’ve got currently, and it seems to work as

CREATE TEMPORARY TABLE IF NOT EXISTS usermeta_temp1 AS (SELECT * FROM [database].usermeta WHERE meta_key LIKE ‘%s2member_auto_eot_time%’ AND meta_value < now());
CREATE TEMPORARY TABLE IF NOT EXISTS usermeta_temp2 AS (SELECT user_id FROM [database].usermeta WHERE meta_key LIKE ‘%capabilities%’ AND meta_value NOT LIKE ‘%subscriber%’);
SELECT * FROM usermeta_temp1 INNER JOIN usermeta_temp2 ON usermeta_temp1.user_id = usermeta_temp2.user_id;

What do you think?

Nile, it isn’t the way I’d do it, because I’d prefer to use WP functions rather than interacting directly with the database, but that doesn’t mean your way is wrong. I was thinking more along these lines:

<?php

function kts_delete_members_at_eot() {
	$args = array(
		'role__in' => array( 's2member_level1', 's2member_level2', 's2member_level3', 's2member_level4' ),
	);
	$users = get_users( $args );

	foreach ( $users as $user ) {
		$eot = get_user_meta( $user->ID, 's2member_auto_eot_time', true );
		if ( current_time( 'mysql' ) > $eot ) {
			wp_delete_user( $user->ID );
		}
	}
}
add_action( 'init', 'kts_delete_members_at_eot' );

Note: that’s completely untested.

But if your method works, it works.

Thanks, Tim. I wold prefer to use Wordpress instead of interacting directly with the database, but wasn’t having much success figuring out how. Thank you for your example of concept.

Two questions about your method:

  1. Does it loop through every single user in the database? If yes, this seems really inefficient – it would be much faster if a search could be done to retrieve only users who’s EOT had already passed and who were not of role = subscriber.

  2. Does it delete the user who’s EOT has already passed? We need our code to change their role to ‘subscriber’, so the user is still in the database, but becomes an expired member.

Nile, on (1), the code is going to loop through every user to some extent, whether you do it by including or excluding roles (and whether via WP or directly within the database). The only question really is whether it’s faster to do it by including roles or by excluding them. But, when it comes to the deletion part, the list of users with the relevant roles is already stored as a variable, and so there’s no further call to the database for every user: only for those with the relevant roles.

If you think doing it by excluding roles would be better, you could use 'role__not_in' instead of 'role__in'. But that will then include administrators and the like, unless you add them into this array too.

On (2), yes, it deletes a member whose EOT has passed. If you want just to change the role, then you’d need something like $user->set_role( 'subscriber');

The real inefficiency of my code (if it works) is the fact that it will run on every page load because of the init hook. It should really be a cron function, but I don’t have time at the moment to work that bit out.

I have now tested some code. This seems to work:

<?php
function kts_delete_members_at_eot() {
	$args = array(
		'role__in' => array( 's2member_level1', 's2member_level2', 's2member_level3', 's2member_level4' ),
		'meta_key' => 'wp_s2member_auto_eot_time',
		'fields' => 'all_with_meta',
	);
	$users = get_users( $args );
	if ( empty( $users ) ) {
		return;
	}
	$time = time();

	foreach ( $users as $user ) {
		$eot = get_user_option( 'wp_s2member_auto_eot_time', $user->ID );
		if ( empty( $eot ) ) {
			return;
		}
		if ( $time > $eot ) {
			//wp_delete_user( $user->ID ); // deletes member
			$user->set_role( 'subscriber' ); // changes member's role
		}
	}
}
add_action( 'init', 'kts_delete_members_at_eot' );

You can add this as an mu-plugin. It will run on every page load. I will now experiment with getting it to run as a cronjob.