Cycle de vie d'une campagne de vote
Ce guide couvre le parcours complet d'une campagne Votez, de la création à la fermeture. Il fait le lien entre les endpoints et leur orchestration.
Module requis : Votez (voir authentication.md).
Clients SDK impliqués : ICampagnesClient, IQuestionsClient, IVotantsClient, IListeElectoraleClient.
1. Vue d'ensemble
┌────────────────────────────────────────────────────────────────┐
│ 1. AddCampagne → Brouillon (Guid campagneId) │
│ 2. AddQuestion + AddOption ×N │
│ 3. UpsertVotant ×N (ou webhook liste_electorale) │
│ 4. PublierCampagne → Publiée │
│ 5. EnvoyerJeton ×N → courriels envoyés │
│ ↓ │
│ votants votent (UI MCM — pas de B2B) │
│ ↓ │
│ 6. GetCampagneStats → TauxParticipation, résultats│
│ 7. DepublierCampagne (optionnel) │
└────────────────────────────────────────────────────────────────┘
Les modifications structurelles (questions, options, ordre) sont bloquées une fois la campagne publiée. Ajuster avant PublierCampagne.
2. Créer la campagne
var dto = new B2BCreateCampagneDto
{
Titre = "Vote d'acceptation — Convention 2026",
OuvertureDuVoteUtc = new DateTime(2026, 5, 1, 14, 0, 0, DateTimeKind.Utc),
FermetureDuVoteUtc = new DateTime(2026, 5, 8, 14, 0, 0, DateTimeKind.Utc),
TextePageAccueil = "<p>Bienvenue sur le vote d'acceptation...</p>",
LibelleAbstention = "Je m'abstiens",
OrdreReponsesAleatoire = false
};
var created = await campagnesClient.AddCampagne(dto);
if (created.IsError) return;
Guid campagneId = created.Value;
Champs requis : Titre, OuvertureDuVoteUtc, FermetureDuVoteUtc. La campagne démarre en état Brouillon.
Les dates sont en UTC. Le slug est généré à partir du titre.
3. Ajouter les questions
Trois types de questions supportés, déclarés par une string :
TypeQuestion | Usage |
|---|---|
"Proposition" | Question Oui/Non/Abstention (ajoutez deux options Oui/Non) |
"ChoixLibre" | Choix multiple avec MinReponses/MaxReponses paramétrables |
"Election" | Élection de candidats (options = candidats) |
var q = await questionsClient.AddQuestion(campagneId, new B2BCreateQuestionDto
{
Nom = "acceptation-convention",
Texte = "Acceptez-vous la convention collective 2026-2029 ?",
TypeQuestion = "Proposition",
MinReponses = 1,
MaxReponses = 1,
AbstentionPermise = true,
Options =
[
new() { Texte = "Oui" },
new() { Texte = "Non" }
]
});
if (q.IsError) return;
int questionId = q.Value;
Vous pouvez aussi créer les options séparément avec AddOption (utile pour charger un grand nombre de candidats).
Réordonner les questions
await questionsClient.ReorderQuestions(campagneId, [questionId2, questionId1, questionId3]);
Les IDs doivent être la liste complète des questions dans l'ordre voulu.
4. Importer les votants
Deux stratégies :
4a. Upsert unitaire (petite campagne, sources multiples)
foreach (var source in sources)
{
var r = await votantsClient.UpsertVotant(campagneId, new B2BUpsertVotantDto
{
Identifiant = source.NoMembre, // optionnel — stocké comme métadonnée
Courriel = source.Email, // clé de déduplication
Nom = source.Nom,
Prenom = source.Prenom,
DroitDeVote = true,
Poids = 1.0m, // pondération du vote
Commentaire = null
});
if (r.IsError) logger.LogWarning("{Email}: {Err}", source.Email, r.FirstError.Description);
}
VotantId est null → création. Passer VotantId fait une mise à jour.
4b. Liste électorale générée côté MCM
Pour les campagnes dont les votants sont dérivés des employés MCM (via un filtre syndicat / employeur), la liste est créée côté back-office admin. Dans ce cas, vous recevez un webhook campagne.liste_electorale_cree une fois le traitement batch terminé. Voir webhooks.md.
Mise à jour en lot du droit de vote
Idéal pour appliquer une liste d'exclus (ex. Militantes). Prend en charge 40 000+ IDs par appel, ignore les votants radiés, n'écrit que les différences.
var exclus = await militantesClient.ListerExclus();
var result = await votantsClient.MettreAJourDroitsDeVote(campagneId,
new B2BMettreAJourDroitsDeVoteDto
{
VotantIds = exclus.Select(x => x.VotantId).ToList(),
DroitDeVote = false
});
if (result.IsError) return;
logger.LogInformation("Retiré: {N}, inconnus: {M}",
result.Value.NombreMisAJour, result.Value.VotantIdsNonTrouves.Count);
Radier un votant (cas rare)
await votantsClient.RadierVotant(campagneId, votantId);
Un votant radié est exclu des mises à jour en lot, des envois de jeton, et ne peut plus voter.
5. Publier la campagne
var pub = await campagnesClient.PublierCampagne(campagneId);
if (pub.IsError) return;
Après PublierCampagne :
- Les questions, options et leur ordre sont gelés.
- L'état passe à
Planifie(siOuvertureDuVoteUtcest futur) ou directement àOuvert. - Les votants peuvent désormais recevoir leur jeton et voter.
Pour annuler : DepublierCampagne(campagneId). Possible tant qu'aucun vote n'a été enregistré.
6. Envoyer les jetons de vote
Par votant
await listeElectoraleClient.EnvoyerJeton(campagneId, votantId);
Utilise le template de courriel EnvoiLienVote configuré sur la campagne.
En masse
Pas d'endpoint de masse explicite ; boucler en parallèle avec un throttle raisonnable :
var liste = await listeElectoraleClient.GetListeElectorale(campagneId);
var avecDroit = liste.Value.Where(v => v.DroitDeVote && !v.AVote);
await Parallel.ForEachAsync(avecDroit, new ParallelOptions { MaxDegreeOfParallelism = 10 },
async (v, _) =>
{
await listeElectoraleClient.EnvoyerJeton(campagneId, v.Id);
});
Les envois sont idempotents côté MCM au niveau d'un votant donné — rappeler
EnvoyerJetonrenvoie le courriel.
7. Suivre la campagne
Liste des votants en temps réel
var liste = await listeElectoraleClient.GetListeElectorale(campagneId,
new B2BListeElectoraleSearchRequest
{
// filtres optionnels (ex: SyndicatIdExterne, AVote)
});
foreach (var item in liste.Value)
Console.WriteLine($"{item.Prenom} {item.Nom} — droit={item.DroitDeVote} a_voté={item.AVote}");
B2BListeElectoraleItem expose EmployeIdExterne et SyndicatIdExterne pour corréler avec votre système externe.
Statistiques
var stats = await campagnesClient.GetCampagneStats(campagneId);
if (stats.IsError) return;
Console.WriteLine($"Participation: {stats.Value.TauxParticipation}%");
Console.WriteLine($"Votants éligibles: {stats.Value.TotalVotantsAvecDroitDeVote}");
Console.WriteLine($"Ont voté: {stats.Value.VotantsAyantVote}");
if (stats.Value.ResultatsVisibles)
{
foreach (var question in stats.Value.Questions)
{
// Parcourir les options par question
}
}
ResultatsVisiblesestfalsependant la campagne si l'indicateur de fonctionnalitéAfficherResultatsVoteActifest désactivé — dans ce cas, seuls les taux de participation sont exposés. Les résultats par option deviennent accessibles aprèsFermetureDuVoteUtc.
Filtrage par syndicat (stats scopées) :
var stats = await campagnesClient.GetCampagneStats(campagneId, syndicatIdExterne: "SYND001");
8. Fin de campagne et archivage
Quand DateTime.UtcNow > FermetureDuVoteUtc :
- L'état devient automatiquement
Ferme. - Les résultats détaillés sont visibles (
ResultatsVisibles = true). - Aucune action B2B requise — la fermeture est temporelle.
L'archivage (EstArchive) se fait côté back-office MCM, pas via l'API B2B.
9. Cas d'usage complet (end-to-end)
public class VoteWorkflow(
ICampagnesClient campagnes,
IQuestionsClient questions,
IVotantsClient votants,
IListeElectoraleClient listeElectorale)
{
public async Task<Guid> CreerEtPublier(IEnumerable<(string Email, string Prenom, string Nom)> electeurs)
{
// 1. Campagne
var camp = await campagnes.AddCampagne(new B2BCreateCampagneDto
{
Titre = "Vote d'acceptation 2026",
OuvertureDuVoteUtc = DateTime.UtcNow.AddDays(1),
FermetureDuVoteUtc = DateTime.UtcNow.AddDays(8)
});
if (camp.IsError) throw new Exception(camp.FirstError.Description);
var id = camp.Value;
// 2. Question + options
await questions.AddQuestion(id, new B2BCreateQuestionDto
{
Nom = "acceptation",
Texte = "Acceptez-vous la convention ?",
TypeQuestion = "Proposition",
MinReponses = 1, MaxReponses = 1, AbstentionPermise = true,
Options = [new() { Texte = "Oui" }, new() { Texte = "Non" }]
});
// 3. Votants
foreach (var e in electeurs)
{
await votants.UpsertVotant(id, new B2BUpsertVotantDto
{
Courriel = e.Email, Nom = e.Nom, Prenom = e.Prenom
});
}
// 4. Publier
await campagnes.PublierCampagne(id);
// 5. Jetons
var liste = await listeElectorale.GetListeElectorale(id);
foreach (var v in liste.Value.Where(v => v.DroitDeVote))
await listeElectorale.EnvoyerJeton(id, v.Id);
return id;
}
}
10. Bonnes pratiques
- Idempotence :
UpsertVotantpar courriel est idempotent.EnvoyerJetonrenvoie sans doublon. Les modifications de questions après publication retournent une erreur — verrouiller votre workflow en conséquence. - Éviter la perte de votants : importez via
UpsertVotanten lot plutôt qu'en parallèle massif ; MCM ne garantit pas l'ordre si 2 appels identiques sont simultanés. - Droit de vote : utilisez
IVotantsClient.MettreAJourDroitsDeVote(lot) pour 10+ votants, pasIListeElectoraleClient.MettreAJourDroitDeVote(unitaire). - Fenêtre de vote :
OuvertureDuVoteUtcetFermetureDuVoteUtcsont strictes ; MCM refuse les votes hors fenêtre. Prévoyez une marge de sécurité (1h+ après). - Webhook
liste_electorale_cree: abonnez-vous si votre liste est générée côté MCM ; cela évite d'interroger périodiquement. - Templates courriel : le template
EnvoiLienVotedoit exister côté MCM avantEnvoyerJeton. Vérifiez en pré-production.
Voir aussi
ICampagnesClient(référence)IQuestionsClient(référence)IVotantsClient(référence)IListeElectoraleClient(référence)- Webhooks —
campagne.liste_electorale_cree - Endpoints HTTP