Synchroniser WooCommerce Memberships avec les groupes Discourse

Ce plugin vous permet de synchroniser les adhésions WooCommerce avec les groupes Discourse.

Prérequis

Pour que cela fonctionne, vous devez avoir DiscourseConnect activé, avec soit WordPress, soit Discourse comme fournisseur DiscourseConnect.

Étapes

  1. Installez ce plugin WordPress GitHub - paviliondev/discourse-woocommerce · GitHub.

  2. Utilisez l’éditeur de thème WordPress pour mettre à jour ces valeurs avec l’plan_id WooCommerce et l’id et le group_name du groupe Discourse que vous souhaitez synchroniser :

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

    Vous pouvez ajouter autant d’entrées que vous le souhaitez (ou supprimer celles existantes).

Étape facultative

  1. Installez WP Crontrol – plugin WordPress | WordPress.org et programmez run_full_wc_membership_sync pour s’exécuter toutes les 24 heures. Cela effectuera une synchronisation complète afin d’assurer la cohérence.

Notes techniques

La seule méthode fiable pour capturer à la fois la création d’une adhésion et le changement de statut dans WooCommerce consiste à utiliser le hook d’administration wc_memberships_user_membership_saved. Vous pouvez l’utiliser en combinaison avec le hook wc_memberships_user_membership_status_changed, mais cela ne devrait pas être nécessaire.

Ce code utilise le journal de WooCommerce pour enregistrer des informations dans /wp-content/uploads/wc-logs/{log_file}. Si cela ne fonctionne pas, vérifiez la sortie à cet endroit. Vous verrez des journaux ressemblant à ceci, qui montrent les étapes de la logique :

2019-05-23T07:01:57+00:00 INFO Exécution de handle_wc_membership_saved 1, 92, 1
2019-05-23T07:01:57+00:00 INFO L'adhésion de angus@email.com à VIP Membership a changé pour wcm-active
2019-05-23T07:01:57+00:00 INFO Envoi d'une requête PUT vers http://localhost:3000/groups/41/members avec angus@email.com
2019-05-23T07:01:57+00:00 INFO Réponse de Discourse : 200 OK

Notes d’utilisation

:point_right: La synchronisation ne fonctionnera pas si vous cliquez sur « Supprimer l’adhésion de l’utilisateur » dans le panneau d’administration WordPress. Aucun hook WooCommerce ne se déclenche lors de cette action. Modifiez plutôt le statut de l’adhésion.

19 « J'aime »

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 « J'aime »

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: Manage group membership in Discourse with WP Discourse SSO.

4 « J'aime »

In this case Discourse is the SSO Provider :slight_smile:

2 « J'aime »

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 « J'aime »

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

6 « J'aime »

Much appreciated! Thank you @DNSTARS

1 « J'aime »

Quelles modifications devrais-je apporter dans mon cas ? Ma situation est un peu différente. Voici les différences :

  1. J’utilise WordPress comme client SSO ; les utilisateurs se connectent via Discourse.
  2. J’ai installé le plugin WP « WooCommerce Groups » : Groups WooCommerce Documentation - WooCommerce. Il est similaire à WooCommerce Memberships mais avec moins de fonctionnalités. Je pense que toutes les fonctions principales sont identiques.
  3. J’ai plusieurs groupes dans Discourse (plus de 3), d’où la nécessité de 3 groupes d’accès dans WP.

Pouvez-vous me proposer des corrections pour ce code ou me donner des conseils ? Merci !
Peut-être pouvez-vous m’aider ? @pfaffman @simon

Ah, je n’avais pas posé de question là-dessus…
Donc, je dois activer cette intégration entre WP et Discourse.

Ce qui fonctionne :

  1. WP fonctionne comme un client SSO.
  2. Les adhésions sont vendues via le plugin WP Groups et expirent, ce qui entraîne l’ajout des utilisateurs aux groupes nécessaires.
  3. Les utilisateurs sont retirés des groupes à l’expiration.

J’ai besoin de :

  • Lorsqu’un utilisateur est ajouté à un groupe dans WP, je dois l’ajouter au groupe correspondant dans Discourse.
  • Lorsqu’il est retiré de ce groupe dans WP, je dois le supprimer dans Discourse.
    Plusieurs groupes dans WP et Discourse

J’ai remarqué que la fonction update_discourse_group_access ajoute les informations d’authentification de l’API dans l’URL. Cela ne fonctionnera sur aucune version récente de Discourse. Avez-vous une version mise à jour du code utilisant une authentification basée sur les en-têtes ? Sinon, je peux modifier le code, mais je ne pourrai pas tester mes modifications, ce qui est un peu risqué.

J’ai des sites Discourse et WordPress propres, je peux donc tester votre code sans aucun risque.

2 « J'aime »

Voici le code d’Angus, mis à jour pour utiliser une authentification basée sur les en-têtes. Je n’ai apporté aucune autre modification. Je n’ai pas non plus testé le code. Je ne mettrai pas à jour le message d’origine tant que quelqu’un n’aura pas eu l’occasion de le tester.

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', 'Le plugin WP Discourse n\'a pas été correctement configuré.' );
	}

	$user_info         = get_userdata( $user_id );
	$user_email        = $user_info->user_email;
	$logger            = wc_get_logger();

	$logger->info( sprintf( '%s membership de %s changé en %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( 'Envoi de la requête %s vers %s avec %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( 'Réponse de 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', 'Une erreur s\'est produite lors de la récupération des données utilisateur depuis Discourse.' );
	}
}

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

	$logger->info( sprintf( 'Exécution de 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 );

Le code définit une variable $discourse_user_id, mais cette variable n’est utilisée nulle part. Elle pourrait probablement être supprimée du code.

Je ne suis pas si sûr de cela. Le code se connecte au crochet d’action wc_memberships_user_membership_saved. Il est probable que ce crochet soit ajouté par le plugin WooCommerce Memberships. Il existe probablement un crochet similaire disponible dans le plugin WooCommerce Groups, mais il est peu probable qu’il ait le même nom et les mêmes paramètres.

3 « J'aime »

L’extension ‘Groups for Woocommerce’ utilise l’extension ‘WP Group’ pour gérer les fonctions de groupes. Voici son API : Actions | Itthinx Documentation

En ce qui concerne Groups WooCommerce

Actions

L’extension n’appelle actuellement aucune action spécifique.

Pour simplifier, j’ai basculé vers WooCommerce Memberships et j’utilise le code original modifié par @simon.

J’ai remplacé les IDs par mes propres données :

const MEMBERSHIP_PLAN_ID = 347;
const DISCOURSE_GROUP_ID = 42;

À ce stade, je ne parviens pas à synchroniser, mais je reçois des informations dans le journal :

> 2020-05-14T13:12:34+00:00 INFO test@gmail.com : le statut d'adhésion de PAID1 est passé à wcm-active
> 2020-05-14T13:12:34+00:00 INFO Envoi d'une requête PUT vers https://site/groups/42/members avec test@gmail.com
> 2020-05-14T13:12:34+00:00 INFO Réponse de Discourse : 400 Bad Request

L’ID du groupe Discourse a été trouvé ici :
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 « J'aime »

Donc, deux questions restent en suspens :

  1. Comment synchroniser sans erreur ?
  2. Que se passe-t-il si j’ai plus de trois types d’adhésion dans WP et de groupes dans Discourse ? Ce code ne concerne qu’un seul type.

Cela semble provenir d’un bug dans ma mise à jour du code. Je m’en occuperai plus tard aujourd’hui.

2 « J'aime »

J’ai mis à jour l’exemple de code ici : Sync WooCommerce Memberships with Discourse groups - #11 by simon. Il y avait une erreur d’encodage dans le code que j’ai publié hier. J’ai testé que l’appel API dans le code fonctionne correctement, mais je n’ai pas testé le code avec le plugin Woocommerce Memberships.

Il devrait être possible de le faire. Il est possible que quelqu’un puisse vous aider ici, mais vous auriez peut-être plus de chance en engageant quelqu’un pour effectuer le travail. Vous pourriez essayer de créer un sujet dans notre catégorie Marketplace pour trouver un développeur capable de réaliser ce travail.

2 « J'aime »

@simon, j’ai réussi à faire fonctionner ! Merci à toute l’équipe ! :vulcan_salute:

1 « J'aime »

@simon, dans votre code, je n’ai pas trouvé la partie utilisée pour la synchronisation par lots que @angus a employée.

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

$logger->info( sprintf('Exécution de run_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('Vérification de l'appartenance de %s', $user->user_login) );

	if ($membership && $membership_plan_id === MEMBERSHIP_PLAN_ID) {
		$membership_plan_name = $membership->plan->name;
		$status = $membership->status;
		$logger->info( sprintf('Mise à jour de l'accès au groupe pour %s', $user->user_login) );

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

		$logger->info( sprintf('Mise en veille pendant 5 secondes') );
		sleep(5);
	}
}

}

add_action('run_full_wc_membership_sync', 'full_wc_membership_sync');  

Devrais-je ajouter ce code à la fin du vôtre si je souhaite activer une synchronisation quotidienne ?

Lorsque j’essaie d’ajouter cet événement cron dans WP via le plugin WordPress Crontrol avec le nom de crochet :

run_full_wc_membership_sync

J’obtiens une erreur :

Qu’est-ce qui pourrait en être la cause ?

Résolu. Cela peut aider quelqu’un. J’ai dû sélectionner « À » dans le champ « Prochaine exécution » et y entrer une date comme 2020-05-10 10:00:00.

Test de synchronisation effectué : cela fonctionne maintenant.

Voici une solution fonctionnelle si vous disposez de plusieurs plans d’adhésion WooCommerce et de groupes dans Discourse. Vous devez remplacer les IDs de vos plans d’adhésion et les IDs de vos groupes Discourse par les vôtres.

  1. Les IDs des plans d’adhésion WooCommerce se trouvent dans l’URL lors de la modification du plan dans WordPress : https://site/wp-admin/post.php?post=**441**&action=edit
  2. Les IDs des groupes Discourse se trouvent ici : https://site/groups/group_name.json

Code fonctionnel à inclure dans functions.php :

Résumé

//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', 'Le plugin WP Discourse n''a pas été correctement configuré.' );
}

$user_info         = get_userdata( $user_id );
$user_email        = $user_info->user_email;
$logger            = wc_get_logger();

$logger->info( sprintf( 'L''adhésion %s de %s a changé pour %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( 'Envoi de la requête %s vers %s avec %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( 'Réponse de 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', 'Une erreur s''est produite lors de la récupération des données utilisateur depuis Discourse.' );
}

}

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

$logger->info( sprintf( 'Exécution de 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('Exécution de 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('Vérification de l''adhésion de %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('Mise à jour de l''accès au groupe pour %s', $user->user_login) );

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

	  $logger->info( sprintf('Pause de 5 secondes') );
      
	  sleep(5);
   }
}

}

add_action(‘run_full_wc_membership_sync’, ‘full_wc_membership_sync’);

Vous devez modifier la première colonne avec les IDs des plans d’adhésion et la deuxième colonne avec les IDs des groupes Discourse.

Résumé
                                    "347"  => "42",
                                    "357"  => "43",
                                    "441"  => "44"
7 « J'aime »

Salut @Ed_Bobkov, j’espérais que tu pourrais m’aider sur ce sujet. Voici quelques points sur ma situation actuelle…

  1. J’ai WordPress configuré comme client SSO ; les utilisateurs se connectent via Discourse ou WordPress.
  2. Je n’utilise actuellement aucun plugin de « memberships » sur WordPress.
  3. Je vais avoir des dizaines de groupes dans Discourse, un pour chaque type de membre.
  4. J’avais prévu de créer manuellement un nouveau groupe d’abonnement dans WP et un groupe correspondant dans Discourse à chaque fois qu’un nouveau groupe d’abonnement sera disponible pour nos utilisateurs.
  5. Comme tu l’as dit : « Je dois activer cette intégration entre WP et Discourse. »
  6. Je suppose que je dois installer le plugin « Woocommerce Memberships » ?

Quelles sont mes prochaines étapes ?