Syncing WooCommerce Memberships with Discourse groups when Discourse is the SSO Provider

I was asked by a client to sync WooCommerce Memberships with Discourse Groups when Discourse is the SSO Provider. Wordpress development is not my forte, but this is what I came up with. It may be useful for someone else (@pfaffman).

Notes:

  • Change MEMBERSHIP_PLAN_ID to the id of the plan you’d like to sync
  • Change DISCOURSE_GROUP_ID to the id of the group you’d like to sync
  • This will not work for Wordpress users with an email address that is not associated with a Discourse account, however if Discourse is the SSO provider this is unlikely to be the case.

functions.php

use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_ID = 35;
const DISCOURSE_GROUP_ID = 41;
const ACTIVE_STATUSES = array('wcm-active');

function update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status) {
	$options = DiscourseUtilities::get_options();
	$base_url = $options['url'];
	$api_key = $options['api-key'];
    $api_username = $options['publish-username'];

	if ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {
	  return new \WP_Error( 'discourse_configuration_error', 'The WP Discourse plugin has not been properly configured.' );
	}

	$discourse_user_id = get_user_meta($user_id, 'discourse_sso_user_id', true);
	$user_info = get_userdata($user_id);
	$user_email = $user_info->user_email;
	$logger = wc_get_logger();

	$logger->info( sprintf('%s membership of %s changed to %s' , $user_email, $membership_plan_name, $status ) );

	if (in_array($status, ACTIVE_STATUSES)) {
		$action = 'PUT';
	} else {
		$action = 'DELETE';
	}

	$external_url = esc_url_raw( $base_url . "/groups/". DISCOURSE_GROUP_ID ."/members" );

	$logger->info( sprintf('Sending %s request to %s with %s', $action, $external_url, $user_email) );

	$external_url = add_query_arg( array(
		'api_key'      => $api_key,
		'api_username' => $api_username,
		'user_emails'  => rawurlencode($user_email)
	), $external_url );

	$response = wp_remote_request($external_url,
          array(
    	    'method' => $action
          )
	);

	$logger->info( sprintf( 'Response from Discourse: %s %s' , 
        wp_remote_retrieve_response_code($response), 
        wp_remote_retrieve_response_message($response) ) );

	if ( ! DiscourseUtilities::validate( $response ) ) {

		return new \WP_Error( 'discourse_response_error', 'There has been an error in retrieving the user data from Discourse.' );
	}
};

function handle_wc_membership_saved($membership_plan, $args) {
	$logger = wc_get_logger();

	$logger->info( sprintf('Running handle_wc_membership_saved %s, %s, %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );

	$user_id = $args['user_id'];
	$membership = wc_memberships_get_user_membership($args['user_membership_id']);
	$membership_plan_id = $membership->plan->id;

	if ($membership && $membership_plan_id == MEMBERSHIP_PLAN_ID) {
		$membership_plan_name = $membership_plan->name;
		$status = $membership->status;
		update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);
	}
};

add_action('wc_memberships_user_membership_saved', 'handle_wc_membership_saved', 10, 2);

Technical notes:

  1. The only reliable way to capture both membership creation and status change in WooCommerce is using the wc_memberships_user_membership_saved admin hook. You can use this in combination with the wc_memberships_user_membership_status_changed hook, but it should not be necessary.

  2. This code uses the WooCommerce logger to log info to /wp-content/uploads/wc-logs/{log_file}. If it is not working check the output there. You will see a logs that look like this, showing you the stages of the logic:

    2019-05-23T07:01:57+00:00 INFO Running handle_wc_membership_saved 1, 92, 1
    2019-05-23T07:01:57+00:00 INFO angus@mcleod.org.au membership of VIP Membership changed to wcm-active
    2019-05-23T07:01:57+00:00 INFO Sending PUT request to http://localhost:3000/groups/41/members with angus@mcleod.org.au
    2019-05-23T07:01:57+00:00 INFO Response from Discourse: 200 OK
    

p.s. @Simon_Cossar if you could quickly cast your eyes over the above it would be much appreciated!

9 Likes

Thanks, @angus. Funny that it seems we were both doing this at the same time.

You managed to figure out what was in those $args. I couldn’t find the user_id in it, I think.

1 Like

It looks correct to me. Since the site is using WordPress as the SSO provider, you could use the static add_user_to_discourse_group and remove_user_from_discourse_group functions to handle changing the group status on Discourse. That would let you do something like:

	if ( in_array( $status, ACTIVE_STATUSES ) ) {
		DiscourseUtilities::add_user_to_discourse_group( $user_id, 'your_discourse_group_name' );
	} else {
		DiscourseUtilities::remove_user_from_discourse_group( $user_id, 'your_discourse_group_name' );
	}

Both of those functions take the user’s WordPress ID as the first argument, and a comma separated list of group names (with no spaces between the names) as the second argument. Those functions will take care of all the API credentials for you, so using them would simplify the code a bit. There are more details about those functions here: Managing Discourse group membership with WP Discourse.

4 Likes

In this case Discourse is the SSO Provider :slight_smile:

2 Likes

Just an update here.

The updated version of this integration has the following structure:

  1. Sync on every WooCommerce Membership status change

  2. Batch sync every relevant WooCommerce Member every 24 hours to ensure consistency.

  3. In terms of matching wordpress users with discourse users, the code first checks if the wordpress user has a discourse account associated with it. If so, it uses the discourse user id. If not it attempts the sync using the wordpress user’s email. The sync can fail if

    3.1 the user has never associated their wordpress and discourse accounts; and
    3.2 they have used different emails on wordpress and discourse.

We’re successfully running this in production on a client’s site. So far we’ve had only 2 users who have failed to sync, i.e. where both 3.1 and 3.2 were true.

Implementation

The updated implementation is

  1. Functions.php methods (update MEMBERSHIP_PLAN_ID and DISCOURSE_GROUP_ID)
Functions.php

use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_ID = 61128;
const DISCOURSE_GROUP_ID = 62;
const ACTIVE_STATUSES = array(‘wcm-active’);

function update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status) {
$options = DiscourseUtilities::get_options();
$base_url = $options[‘url’];
$api_key = $options[‘api-key’];
$api_username = $options[‘publish-username’];

if ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {
  return new \WP_Error( 'discourse_configuration_error', 'The WP Discourse plugin has not been properly configured.' );
}

$discourse_user_id = get_user_meta($user_id, 'discourse_sso_user_id', true);
$user_info = get_userdata($user_id);
$user_email = $user_info->user_email;
$logger = wc_get_logger();

$logger->info( sprintf('%s membership of %s is %s' , $user_email, $membership_plan_name, $status ) );

if (in_array($status, ACTIVE_STATUSES)) {
	$action = 'PUT';
} else {
	$action = 'DELETE';
}

$external_url = esc_url_raw( $base_url . "/groups/". DISCOURSE_GROUP_ID ."/members" );

$args = array(
‘api_key’ => $api_key,
‘api_username’ => $api_username
);

if ($discourse_user_id) {
$args[‘user_id’] = $discourse_user_id;
} else {
$args[‘user_emails’] = $user_email;
}

$logger->info( sprintf(‘Sending %s request to %s with %s’, $action, $external_url, http_build_query($args)) );

$external_url = add_query_arg($args, $external_url);

$response = wp_remote_request($external_url,
 array(
	 'method' => $action
 )
);

$logger->info( sprintf( 'Response from Discourse: %s %s' ,
    wp_remote_retrieve_response_code($response),
    wp_remote_retrieve_response_message($response) ) );

if ( ! DiscourseUtilities::validate( $response ) ) {

	return new \WP_Error( 'discourse_response_error', 'There has been an error in retrieving the user data from Discourse.' );
}

};

function handle_wc_membership_saved($membership_plan, $args) {
$logger = wc_get_logger();

$logger->info( sprintf('Running handle_wc_membership_saved %s, %s, %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );

$user_id = $args['user_id'];
$membership = wc_memberships_get_user_membership($args['user_membership_id']);
$membership_plan_id = $membership->plan->id;

if ($membership && $membership_plan_id == MEMBERSHIP_PLAN_ID) {
	$membership_plan_name = $membership_plan->name;
	$status = $membership->status;
	update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);
}

};

add_action(‘wc_memberships_user_membership_saved’, ‘handle_wc_membership_saved’, 10, 2);

function full_wc_membership_sync() {
$allusers = get_users();
$logger = wc_get_logger();

$logger->info( sprintf('Running full_wc_membership_sync') );

foreach ( $allusers as $user ) {
	$user_id = $user->id;
	$membership = wc_memberships_get_user_membership($user_id, MEMBERSHIP_PLAN_ID);
	$membership_plan_id = $membership->plan->id;

	$logger->info( sprintf('Checking membership of %s', $user->user_login) );

	if ($membership && $membership_plan_id === MEMBERSHIP_PLAN_ID) {
		$membership_plan_name = $membership->plan->name;
		$status = $membership->status;
		$logger->info( sprintf('Updating group access of %s', $user->user_login) );

		update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);

		$logger->info( sprintf('Sleeping for 5 seconds') );
		sleep(5);
	}
}

}

add_action(‘run_full_wc_membership_sync’, ‘full_wc_membership_sync’);

  1. Install WP Crontrol – WordPress plugin | WordPress.org and schedule “run_full_wc_membership_sync” to run every 24 hours.

You can review the logs of the sync in WooCommerce > Status > Logs.

@DNSTARS @pfaffman

5 Likes

What a baller, sending some magical internet money your way in thanks. :+1:

5 Likes

Much appreciated! Thank you @DNSTARS

1 Like