Forfaits club
Cette page documente les règles Convex de forfaits déclarés sur une compétition.
Sources principales :
convex/schema.ts
convex/competition/club_forfeits.ts
convex/competition/club_forfeit_impacts.ts
convex/competition/club_forfeit_sanctions.ts
convex/competition/match_forfeits.ts
convex/competition/bet_settlement.ts
convex/lib/audit_logs.ts
convex/social/announcements.ts
club_forfeits et statut
Les champs clés vérifiés : leagueId, season, clubId, effectiveAt, declaredAt, declaredById, scope, mercatoStartAt, status, reason, adminNote, cancelledAt, cancelledById, cancelReason.
Statuts :
Idempotence métier : la déclaration d’un forfait actif existant retourne l’enregistrement existant.
Détermination de scope
Deux scopes existent :
FULL_COMPETITION
POST_MERCATO_ONLY
Quand une fenêtre de mercato est connue, le scope est dérivé : avant mercato = FULL_COMPETITION, à partir de mercato = POST_MERCATO_ONLY.
Application sportive (applyClubForfeit)
Le moteur applique le forfait sur les matchs actifs où le club est impliqué, selon le scope :
FULL_COMPETITION : tous les matchs en cours de saison,
POST_MERCATO_ONLY : seulement à partir de mercatoStartAt si défini.
Scores imposés :
- forfait simple : club forfaité perd (
0), adversaire marque 1,
- double forfait : score
0-0.
Écriture d’impact :
- scores/évolution match en
resultOrigin = CLUB_FORFEIT,
forfeitClubId, forfeitCreatedResult,
- snapshots de restauration :
previousHomeScore, previousAwayScore, previousResultOrigin, previousMatchStatus,
isDisputed = false, validatedAt posé,
- match repassé
validated.
Répercussions associées
- Invalidation des stats match (
match_player_stats, match_team_stats) avec invalidatedReason = CLUB_FORFEIT.
- Annulation paris :
cancelPendingBetsForMatch avec reason = MATCH_FORFEITED (uniquement PENDING).
- Recalcul classement via
recalculateLeagueStandingsCore.
- Sanctions automatiques via
generateForfeitSanctions.
- Annonce globale
sourceType = CLUB_FORFEIT.
Réversion (revertClubForfeitImpacts)
La réversion re-cible uniquement les impacts signés par le forfait (resultOrigin = CLUB_FORFEIT, forfeitClubId).
- si
forfeitCreatedResult === true : suppression du résultat puis restauration status depuis snapshot previousMatchStatus,
- sinon : restauration
homeScore / awayScore et flags de forfeit,
- réactivation des stats invalidées par ce forfeit,
- recalcul des standings,
- levée des sanctions liées.
Cas complexe :
- si l’adversaire est encore en forfait actif, la restauration de score peut être ignorée avec warning.
Les paris annulés lors du forfeit ne sont pas réouverts ; seuls les PENDING ont été annulés.
Forfait ponctuel de match
declareMatchForfeit couvre le cas isolé d’un match forfait, distinct du
forfait club-compétition : il ne crée pas de sanction pluri-saison, n’annonce pas
un forfait global et n’impacte qu’un seul match.
Règles vérifiées :
- table d’historique
match_forfeits ;
- résultat écrit en
resultOrigin = MATCH_FORFEIT avec forfeitClubId ;
- club domicile forfait : score
0-3 ;
- club extérieur forfait : score
3-0 ;
- refus si un litige
OPEN existe sur le match ;
- match repassé
validated, isDisputed = false ;
- si un résultat validé existait, son impact classement est retiré avant
application du score forfait ;
- les stats match (
match_player_stats, match_team_stats) sont invalidées
avec invalidatedReason = MATCH_FORFEIT ;
- les paris
PENDING de la vague courante sont annulés via
cancelPendingBetsForMatch et reason = MATCH_FORFEITED ;
- les paris déjà
WON / LOST ne sont pas modifiés.
cancelMatchForfeit annule le forfait actif, restaure le snapshot du
match/résultat, corrige le classement et réactive uniquement les stats invalidées
par ce forfait ponctuel. Les paris annulés par le forfait restent annulés, par
cohérence avec la réversion des forfaits club.
Sanctions
generateForfeitSanctions applique un COMPETITION_BAN :
- destinataires GM +
MANAGER / CO_MANAGER actifs,
- déduplication par utilisateur quand le GM apparaît aussi comme manager,
- scope principal :
["SPORT_ELIGIBILITY"],
banScope par défaut PLAYER_AND_STAFF,
- périodes : de
baseSeason sur seasonCount saisons (défaut 3, cap 10),
- traçage source (
sourceType, sourceForfeitId, sourceClubId).
Chaque sanction active créée déclenche une notification in-app
COMPETITION_BAN_ACTIVATED. Une tentative email est planifiée si l’utilisateur
a une adresse valide. Les utilisateurs sans email valide gardent la notification
in-app. Un rerun idempotent ne recrée ni sanction ni message.
liftForfeitSanctions met fin à la sanction en mode manuel (END_MANUAL, ENDED_MANUAL) avec raison par défaut "Forfait annulé".
Les chemins de fin supportés (cancelForfeitSanction et cancelClubForfeit
via revertClubForfeitImpacts) déclenchent COMPETITION_BAN_ENDED en in-app et
planifient un email si une adresse valide existe. Les répétitions ne dupliquent
pas les notifications.
Surfaces frontend et limites de périmètre
- Le forfait club-compétition se pilote dans l’administration d’une ligue via
club-forfeit-dialog.
- Le forfait ponctuel se déclare dans la section admin d’une fiche match et se
suit dans
/admin/match-forfeits.
- Les résultats attribués affichent
ForfeitBadge /
ForfeitResultMention sur les surfaces match compatibles.
- Les annonces de forfait club sont publiées en bannière globale puis archivées
lors de la réversion.
- Les emails de sanctions ciblent les responsables dédupliqués ; les
utilisateurs sans email valide conservent la notification in-app.
- L’UI d’annulation précise que le snapshot sportif est restauré mais que les
paris annulés restent annulés.
Le dépôt ne contient pas d’automatisation d’accès à un terrain EA ou de
« replay » externe : ces opérations restent hors du périmètre applicatif CEL.Last modified on July 18, 2026