Sync WooCommerce Memberships with Discourse groups

This Plugin allows you to Sync WooCommerce Memberships with Discourse Groups.

Pre-requisites

For this to work you have to have DiscourseConnect enabled, with either Wordpress or Discourse as the DiscourseConnect provider.

Steps

  1. Install this Wordpress plugin GitHub - paviliondev/discourse-woocommerce.

  2. Use the Wordpress theme editor to update these numbers to the WooCommerce plan_id and Discourse group id and group_name you want to sync

    $member_group_map[] = (object) array('plan_id' => 51, 'group_id' => 43, 'group_name' => 'group1');
    $member_group_map[] = (object) array('plan_id' => 62, 'group_id' => 44, 'group_name' => 'group2');
    

    You can add as many entries as you like (or remove existing ones).

Optional Step

  1. Install WP Crontrol – WordPress plugin | WordPress.org and schedule run_full_wc_membership_sync to run every 24 hours. This will do a full sync to ensure consistency.

Technical Notes

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.

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@email.com 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@email.com
2019-05-23T07:01:57+00:00 INFO Response from Discourse: 200 OK

Usage Notes

:point_right: The Sync will not work if you click “Delete User Membership” in the Wordpress admin panel. There is no hook in WooCommerce that fires when that is clicked. Change the status of the membership instead.

18 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

8 Likes

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

6 Likes

Much appreciated! Thank you @DNSTARS

1 Like

What changes should I make in my case? I have a little bit different situation. Here are the differencies:

  1. I have Wordpress as a SSO Client, users login via Discourse.
  2. I installed a WP plugin ‘WooCommerce Groups’: Groups WooCommerce Archives - WooCommerce Docs . It’s similar to WooCommerce Memberships but with less functions. I think all core function are the same.
  3. I have multiple groups in Discourse (more than 3) and that’s why 3 groups of access in WP.

Can you provide me with corrections for this code or some advice? Thanks!
May be, you can help? @pfaffman @simon

Ah, I didn’t write a question there…
So I need to activate this integration between WP and Discourse.

What is working:

  1. Wp is working as a SSO client.
  2. Memberships are selling through WP Groups plugin and expiring => users are added to necessary groups.
  3. Users are deleted from groups on expiration.

I need:

  • when user is added to a group in WP I need to add this user to specific group in Discourse
  • when is deleted from that group in WP - delete in Discourse
    Multiple groups in WP and Discourse

I noticed that the update_discourse_group_access function is adding the API authentication credentials to the URL. This will not work on any up to date version of Discourse. Do you have an updated version of the code that is using header based authentication? If not, I can edit the code, but I will not be able to test my edits, so it’s a little risky.

I have a clean Discourse and Wordpress sites, can test your code without any risk.

2 Likes

Here is Angus’s code, updated to use header based authentication. I have not made any other changes to it. I have also not tested the code. I won’t update the OP until someone gets a chance to test it.

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.' );
	}

	$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 ) );

	$response = wp_remote_request( $external_url,
		array(
			'method'  => $action,
			'headers' => array(
				'Api-Key'      => sanitize_key( $api_key ),
				'Api-Username' => sanitize_text_field( $api_username ),
			),
			'body'    => array( 'user_emails' => $user_email ),
		)
	);

	$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 );

The code is setting a $discourse_user_id variable, but that variable is not being used anywhere. It could probably be removed from the code.

I’m not so sure about that. The code hooks into the wc_memberships_user_membership_saved action hook. It seems likely that that hook is added by the WooCommerce Memberships plugin. There is probably a similar hook available in the WooCommerce Groups plugin, but it is unlikely to have the same name and parameters.

3 Likes

‘Groups for Woocommerce’ plugin uses ‘WP Group’ plugin to operate groups functions, here is it’s API: Actions | Itthinx Documentation

As for Groups WooCommerce

Actions

The extension does not invoke any specific actions at current.

To simplify I switched to WooCommerce Memberships and use original code + changes made by @simon.

I changed IDs to my data:

const MEMBERSHIP_PLAN_ID = 347;
const DISCOURSE_GROUP_ID = 42;

At this point I can’t sinc but get some info in log:

> 2020-05-14T13:12:34+00:00 INFO test@gmail.com membership of PAID1 changed to wcm-active
> 2020-05-14T13:12:34+00:00 INFO Sending PUT request to https://site/groups/42/members with test@gmail.com
> 2020-05-14T13:12:34+00:00 INFO Response from Discourse: 400 Bad Request

ID for the Discourse group I found here
site/groups/PAID1.json

> {"group":{"id":42,"automatic":false,"name":"PAID1","user_count":0,"mentionable_level":0,"messageable_level":0,"visibility_level":0,"automatic_membership_email_domains":"","primary_group":false,"title":"","grant_trust_level":null,"incoming_email":null,"has_messages":false,"flair_url":"","flair_bg_color":"","flair_color":"","bio_raw":"","bio_cooked":null,"bio_excerpt":null,"public_admission":false,"public_exit":false,"allow_membership_requests":false,"full_name":"France-PAID1","default_notification_level":3,"membership_request_template":"","is_group_user":false,"is_group_owner":true,"members_visibility_level":0,"can_see_members":true,"publish_read_state":false,"is_group_owner_display":false,"mentionable":true,"messageable":false},"extras":{"visible_group_names":["admins","moderators","PAID1","PAID2","PAID3","staff","trust_level_0","trust_level_1","trust_level_2","trust_level_3","trust_level_4"]}}
2 Likes

So 2 questions are open:

  1. How to sync without error?
  2. What if I have more than 3 membership types in WP and groups in Discourse? this code is for 1 type only.

That looks like it comes from a bug in my update to the code. I’ll take a look at it later today.

2 Likes

I have updated the code example here: Syncing WooCommerce Memberships with Discourse groups when Discourse is the SSO Provider. There was an encoding error in the code I posted yesterday. I’ve tested that the API call in the code is working correctly, but have not tested the code with the Woocommerce Memberships plugin.

It should be possible to do that. It is possible that someone can help you here, but you might have better luck by hiring someone to do the work for you. You could try creating a topic in our #marketplace category to find a developer to do the work.

2 Likes

@simon, got this working! Thank you, team! :vulcan_salute:

1 Like

@simon, in your code I didn’t find a part used for batch syncing that @angus used.

/*run_full_wc_membership_sync*/
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');  

Should I add this code to the end of your code if I want to activate an everyday syncing?

When I try to add this cron event to WP in Wordpress Crontrol plugin with a hook name:

run_full_wc_membership_sync

I got an error:

What may cause it?

Solved. May help smbd. I needed to choose ‘At’ in the ‘Next Run’ field and put there a date like 2020-05-10 10:00:00.

Tested syncing - now it is working.

Here is a working solution if you have multiple WooCommerce membership plans and groups in Discourse. You need to change your membership IDs and Discourse Group IDs to yours.

  1. WooCommerce membership plans IDs can be found from the url while editing the plan in Wordpress: https://site/wp-admin/post.php?post=441&action=edit
  2. Discourse Groups IDs can be found here: https://site/groups/group_name.json

Working code to include in functions.php:

Summary

//wp+discourse
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_DISCOURSE_GROUP = [
“347” => “42”,
“357” => “43”,
“441” => “44”
];

//const ACTIVE_STATUSES = array( ‘wcm-active’ );
const ACTIVE_STATUSES = array( ‘wcm-active’, ‘wcm-free_trial’ );

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.' );
}

$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/" . MEMBERSHIP_PLAN_DISCOURSE_GROUP[$membership_plan_id] . "/members" );

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

$response = wp_remote_request( $external_url,
	array(
		'method'  => $action,
		'headers' => array(
			'Api-Key'      => sanitize_key( $api_key ),
			'Api-Username' => sanitize_text_field( $api_username ),
		),
		'body'    => array( 'user_emails' => $user_email ),
	)
);

$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 && isset(MEMBERSHIP_PLAN_DISCOURSE_GROUP[$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 );

/run_full_wc_membership_sync/
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;

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

   if ($membership  && isset(MEMBERSHIP_PLAN_DISCOURSE_GROUP[$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’);

You need to change in the first column - Membership plans ids, 2nd column - Discourse groups ids.

Summary
                                    "347"  => "42",
                                    "357"  => "43",
                                    "441"  => "44"
7 Likes

Hey @Ed_Bobkov, I was hoping you might be able to help me on this. Here’s a few statements about where I am right now…

  1. I have Wordpress as a SSO Client, users login via Discourse or Wordpress.
  2. I currently don’t have a “memberships” plugin that I’m using on Wordpress
  3. I will have dozens of groups in Discourse for each different type of member
  4. I was planning on manually creating a new membership group in WP, and a corresponding group in Discourse, whenever we have a new membership group available to our users.
  5. As you stated, “I need to activate this integration between WP and Discourse.”
  6. I’m guessing I need to get “Woocommerce Memberships” plugin?

What are my next steps?