Skip to main content

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 :
  • ACTIVE
  • CANCELLED
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