Aller au contenu principal

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 :

TypeQuestionUsage
"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 (si OuvertureDuVoteUtc est 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 EnvoyerJeton renvoie 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
}
}

ResultatsVisibles est false pendant la campagne si l'indicateur de fonctionnalité AfficherResultatsVoteActif est désactivé — dans ce cas, seuls les taux de participation sont exposés. Les résultats par option deviennent accessibles après FermetureDuVoteUtc.

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 : UpsertVotant par courriel est idempotent. EnvoyerJeton renvoie 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 UpsertVotant en 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, pas IListeElectoraleClient.MettreAJourDroitDeVote (unitaire).
  • Fenêtre de vote : OuvertureDuVoteUtc et FermetureDuVoteUtc sont 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 EnvoiLienVote doit exister côté MCM avant EnvoyerJeton. Vérifiez en pré-production.

Voir aussi