Règles métier vérifiées
Cette page ne liste que les règles explicitement observées dans le code vérifié. Les états qui dépendent d’une infrastructure externe sont documentés dans les runbooks plutôt que présentés comme règles métier.Match reports (demande de report)
Sources:convex/competition/match_postponement_mutations.ts.
match_reports.statusvérifiés:PENDING,ACCEPTED,COUNTER_PROPOSED,CANCELLED,REJECTED.statuspeut être absent sur des données legacy.- Avant mutération, les rows legacy sans statut conforme sont filtrées via
hasLegacyMatchReportStatus.
requestPostponement:
- Seuls les membres staff du club demandeur (
MANAGER,CO_MANAGER,COACH) peuvent créer. - Matchs admissibles:
scheduledouprovisional. - Blocage si une demande
PENDINGouCOUNTER_PROPOSEDexiste déjà sur le même match. - Blocage si le match a déjà un report
ACCEPTED. - Transition vers
PENDING.
acceptPostponement:
- Réservé à l’adversaire du demandeur.
- Sources valides:
PENDINGetCOUNTER_PROPOSED. - En cas de
COUNTER_PROPOSED, seul le demandeur initial peut accepter. - Transition vers
ACCEPTED, puismatches.scheduledAtetmatches.status = scheduled.
counterProposePostponement:
- Réservé à l’équipe adverse, pas à l’initiateur.
- Source stricte
PENDING. - Transition vers
COUNTER_PROPOSEDaveccounterProposedDate.
cancelPostponement:
- Sources:
PENDING,COUNTER_PROPOSED. - Exécutable par le club initiateur.
- Transition vers
CANCELLED.
rejectPostponement:
- Réservé
ADMIN. - Sources:
PENDING,COUNTER_PROPOSED. - Transition vers
REJECTED.
adminDirectPostponement:
- Réservé
ADMIN. - Match admissible si
scheduledouprovisional. - Aucune création si un report
PENDING,COUNTER_PROPOSEDouACCEPTEDexiste déjà. - Crée directement un report en
ACCEPTED.
isReportQuotaConsumedne compte comme consommé que le statutACCEPTED.effectiveReportsTotalinclut bonus premium viaPREMIUM_REPORTS_BONUS = 2.
Compositions d’avant-match
Source:convex/competition/match_lineups.ts.
LINEUP_DEADLINE_MS = 20 * 60 * 1000.- Statut
match_lineups.status:draft,submitted. match_lineups.statuspeut être forcé en visibilité/validation parADMIN/MODERATORviahasLineupOverridePrivileges.- Sans override:
- match
scheduled, - délai max 20 minutes depuis le coup d’envoi.
- match
submitexige min7joueurs (minPlayers: 7).- Un draft ne peut pas écraser une version
submitted. - Positions détaillées autorisées:
GK,DG,DC,DD,MDC,MC,MOC,MG,MD,AG,AD,BU. saveDraft/submit:resolveManagedClubIdForMutation(..., includeCoach: true).- Vues:
ADMIN/MODERATORpeuvent lireallVersions,- profils non-admin lisent la dernière version
submitteduniquement.
- Actions auditées override:
SAVE_DRAFT_LINEUP_OVERRIDE,SUBMIT_LINEUP_OVERRIDE.
Gestion de compte
Sources:convex/auth.ts, convex/auth/guards.ts, convex/auth/accountStatus.ts, convex/lib/rate_limit.ts, convex/social/users.ts, convex/admin/users.ts.
-
Par défaut, absence de
accountStatus=ACTIVE. -
assertAccountAccessAllowedapplique l’ordre:DELETED-> erreurACCOUNT_DELETED;DEACTIVATED-> erreurACCOUNT_DEACTIVATED;- sanctions
APP_ACCESSsi présentes.
-
transitionAccountStatusmet à jour:users.accountStatus,users.accountStatusUpdatedAt,user_profiles.isActiveàfalsehors statut actif.
-
Changement vers statut non-
ACTIVE=> invalidation session viainvalidateUserSessions. -
Self-service
deleteOwnAccount:- refusé si l’acteur gère un club actif,
- écrit
DELETE_USER(SELF_SERVICE_DELETE,source=self-service), selfDeletedAt,- purge auth et anonymisation.
-
Admin only:
deleteUser,deactivateUser,reactivateUser.selfimpossible à supprimer, désactiver, réactiver par la logique API.reactivateUserrefusé siselfDeletedAtdéjà présent.
-
setUserBanStateet certains patches utilisentallowModerator: true, mais sans marge sur rôle/utilisateur premium/username. -
changePassword:- Si le compte possède déjà un credential
password, le mot de passe courant est obligatoire (CURRENT_PASSWORD_REQUIRED), puis vérifié avant mutation (INVALID_CURRENT_PASSWORD). - Les erreurs de validation du mot de passe actuel sont renvoyées avec un code métier dédié, sans modification de credential.
- Pour un compte OAuth sans password, le premier mot de passe peut être défini sans
currentPasswordvia le même flux. - La route HTTP
POST /api/users/change-passwordreprend ces règles, rejette les corps mal formés et vérifie la cohérence acteur/session (ACTOR_MISMATCH,EMAIL_MISMATCH) avant de lancer l’action.
- Si le compte possède déjà un credential
-
Connexion
password:- Le flux
signInpasse par le limiteur applicatifcheckAuthRateLimitavec une clé par email normalisé avant vérification du credential. - Une clé saturée renvoie
TOO_MANY_ATTEMPTSsans générer de session.
- Le flux
Paris et settlement
Sources:convex/competition/bet_settlement.ts, convex/admin/bets.ts,
convex/admin/cron_runners.ts.
settleBetsForMatch,reopenBetsForMatch,cancelBetsForMatchetcancelPendingBetsForMatchparcourent toutes les pages de paris du match en lots bornés, sans plafond global à 500.- Le filtrage conserve la vague courante du match et laisse inchangés les autres matchs, autres vagues et statuts non éligibles.
cancelBetsForMatch({ advanceWave: true })avance la vague après une annulation effective et ne réavance pas un retry horodaté identique.createOrUpdateBet,settleBet,requestBulkSettleBetsetrunBetBulkSettlementexposent les erreurs actionnables via payload typé (UNAUTHORIZED,NOT_FOUND,FORBIDDEN,CONFLICT,BAD_REQUEST) sans détail de handler.
Avatars et assets de profil
Sources:convex/social/users.ts, convex/social/profile.ts, convex/lib/user_avatar.ts, convex/web.ts.
- La photo de profil standard est enregistrée via
updateUserProfile.avatarUrldansusers.image. - Les utilisateurs non premium peuvent définir une photo de profil standard, limitée aux uploads
/uploads/profilesen PNG, JPG ou WEBP statique, 5 Mo maximum. - Les GIF et images animées sont refusés pour la photo standard (
upload_objects.isAnimatedet MIME type). - Pour un utilisateur non premium, l’avatar public ne peut pas venir de
oauthImageni d’un miroir OAuth (users.image === users.oauthImage). - Les assets créateur (
user_profiles.avatarUrl,bannerUrl,logoUrl) restent gérés parupdatePremiumAssetset nécessitent Premium. - Les bannières, logos et indicateurs animés du profil public restent masqués pour les non-premium.
Matrice Auth / Admin / Modérateur
Sources:convex/admin/_shared.ts, convex/admin/users.ts, convex/social/_shared.ts, convex/competition/match_lineups.ts, convex/competition/match_postponement_mutations.ts.
Awards, TOTS, TOTW
Sources:convex/competition/season_awards.ts, convex/competition/totw.ts.
publishSeasonAwards:
- Restriction
ADMIN. league.statusrequisCOMPLETEDouARCHIVED.mvpSelectionKeyrequis etbestGkSelectionKeyrequis.totsSelectionKeyoptionnel.replaceExistingsupporté.- Notifications et emails activés par défaut.
season_awards, award_recipients):
TOTS_FORMULA_VERSION = 'tots-v3'.- Limites vérifiées: individuel
70, distinction10, collectif20. - Bonus:
TOTS_TOTW_BONUS = 2etTOTS_MATCH_MVP_BONUS = 2. - Seuils:
TOTS_MIN_SEASON_MATCHES = 10,TOTS_MIN_POSITION_MATCHES = 15. TOTS_TOTW_MAX = 6,TOTS_MATCH_MVP_MAX = 4.- La régularité ne donne plus de points séparés; elle est portée par les seuils d’éligibilité saison/poste.
Felicitations palmarès (B4)
Source métier:convex/social/award_congrats.ts.
congratulate({ awardId, entryId, reaction, actorUserId? })- authentification requise (
requireActorUser). - résout
awardIdetentryIdviaidmétier (by_external_id). - vérifie que
entry.seasonAwardId === award.id. reactionstrictement dansMERITE | QUELLE_SAISON | FIER | RENDEZVOUS.- idempotent si la même réaction est déjà posée par l’utilisateur pour l’entrée.
- en cas de changement de réaction, remplacement atomique: l’ancienne est décrémentée, la nouvelle incrémentée, total inchangé.
- authentification requise (
uncongratulate({ awardId, entryId, actorUserId? })- authentification requise (
requireActorUser). - suppression de la réaction de l’utilisateur s’il en existe une.
- décrémente le compteur
award_congratulation_counts. - idempotent si aucune réaction n’existe.
- authentification requise (
getAwardCongratulations({ awardId, actorUserId? })- résolution actor via
resolveActorUserId(valeurmine: nullsi déconnecté). - retourne un objet indexé par
entryId:{ count, byReaction, mine }. - bornage de lecture sur les entrées de l’award (
take(250)).
- résolution actor via
award_congratulation_countsest la source de vérité de lecture (pas decollect().lengthpour les compteurs).by_entry_usersert au calculmine.award_congratulationscontient au plus 1 ligne active par (entryId,congratulatorUserId).
- Réservé
ADMIN. publishTotwexige preview et période résolue (periodStartAt/periodEndAtnon nuls).- Requiert au moins 11 joueurs dans la sélection.
- Républication: remplacement de l’entrée existante pour la même période.
- Version active en production:
TOTW_CALC_VERSION = 'totw-v3'.
Bannière d’annonces (feed de croissance)
Sources :convex/social/announcements.ts (listBannerAnnouncements, upsertAnnouncementBySource, archiveAnnouncementBySource, BANNER_PRIORITY, BANNER_TTL_MS), web/src/components/announcements/AnnouncementTicker.tsx.
Eligibilité bannière
AnnouncementTicker affiche les annonces dont :
status = ACTIVE,placementestBANNER_GLOBALouALL_GLOBAL,- la fenêtre temporelle
[startsAt, endsAt]est valide à l’instant de lecture.
Gouvernance du feed (listBannerAnnouncements)
Le feed applique deux étages, dans l’ordre :
- Étage ops (
priority ≥ 50) — triés par priorité décroissante, toujours placés en tête. - Étage croissance (
priority < 50) — triés par récence décroissante, soumis à un plafond par source :
Plafond global : 6 messages affichés au total (ops + croissance, par ordre ops-first).
Émetteurs automatiques
Les annonces systèmes sont créées/mises à jour viaupsertAnnouncementBySource (dédupliqué par la clé composite (sourceType, sourceId)) et retirées via archiveAnnouncementBySource (passage en ARCHIVED, idempotent). Leur déclencheur et leur archivage sont décrits ci-dessous.
Tracking clics
Chaque clic sur un CTA de la bannière est capturé via PostHog (posthog.capture('banner_cta_click', { announcementId, tag, sourceType })). Le lien de destination est augmenté du paramètre from=banner (ajout de ?from=banner ou &from=banner selon la présence d’un ? existant).
Messagerie / notifications
Sources:convex/social/recipient_groups.ts, convex/messaging/notifications.ts, convex/messaging/bus.ts.
- Groupes résolus par code:
ADMINS:ADMINMODERATORS:MODERATORSTAFF:ADMIN,MODERATORUSERS:USERALL_ACTIVE_USERS:USER,ADMIN,MODERATOR
resolveRecipientGroupMembersapplique un filtreCONTACTviahasRestrictionEffect.- Côté bus:
MAX_MATCH_TARGETED_IN_APP_RECIPIENTS = 40.- Les événements match ciblés doivent émettre exactement un plan in-app.
recipientUserIds/managerUserIdsdoivent correspondre strictement au ciblage.
- Sources qui explicitent
recipientUserIdsrequis:messaging/match.scheduling.*messaging/match.postponement.*messaging/league.calendar.deleted
- Les événements de match ciblent explicitement des utilisateurs (
users/userIds), pas de groupe implicite.
Sanctions (vérifié)
Sources:convex/schema.ts, convex/admin/_shared.ts, convex/admin/users.ts, convex/lib/person_sanctions.ts.
targetTypevérifiés:PERSON,CLUB.effectScopesvérifiés:APP_ACCESS,SPORT_ELIGIBILITY,VISIBILITY,CONTACT.- États vérifiés:
ACTIVE,ENDED_MANUAL,ENDED_EXPIRED,ENDED_REPLACED. - Sources de fin:
MANUAL,AUTO_EXPIRED,REPLACED. - Typologies vérifiées (liste schéma):
BAN_PLAYER,SUSPENSION,BAN_CLUB,PENDING_DISCIPLINARY,POINTS_DEDUCTION,FINE,FORFAIT,PERSON_BAN,PERSON_SUSPENSION,PERSON_DISCIPLINARY_REVIEW,COMPETITION_BAN.
COMPETITION_BAN de forfait club
Sources: convex/competition/club_forfeit_sanctions.ts,
convex/messaging/events.ts, convex/messaging/bus.ts.
- Création automatique et manuelle : une sanction active par utilisateur
responsable dédupliqué (GM,
MANAGER,CO_MANAGER). - Activation : notification in-app
COMPETITION_BAN_ACTIVATEDpour chaque utilisateur, plus email si l’adresse est valide. - Fin supportée :
cancelForfeitSanctionetcancelClubForfeitvialiftForfeitSanctionsenvoientCOMPETITION_BAN_ENDED, plus email si l’adresse est valide. - Métadonnées garanties :
sanctionId,sanctionType: COMPETITION_BAN,sourceForfeitId,leagueId,clubId, état de cycle de vie. - Reruns idempotents : pas de sanction, notification ou email supplémentaire pour une même sanction déjà créée ou déjà terminée.
Recherche (invariants)
- Index utilisés via logique métier: annonces, articles, audits, litiges, clubs, matchs, notifications, sanctions, transferts, profils utilisateurs, utilisateurs.
Restrictions personne et livraisons
Les quatre effets de sanction ont les conséquences runtime vérifiées suivantes :APP_ACCESS: blocage d’authentification et invalidation de sessions ;SPORT_ELIGIBILITY: effet utilisé pour les bans compétition/forfaits et exposé comme exclusion sportive dans l’administration ;VISIBILITY: exclusion des utilisateurs/profils des listes publiques et du listing des agents libres ;CONTACT: exclusion des groupes de destinataires et des contacts managers utilisés par la messagerie compétition.
sanctions-moderation.md. La queue email, les
retries, le backoff et le push navigateur sont documentés dans
messaging-notifications-emails.md.
Statuts de Match
Source métier :web/src/lib/match-status.ts, convex/schema.ts.
Les matchs ont un cycle de vie représenté par le champ status :
scheduled: Programméprovisional: Provisoirepending_validation: En attente de validationcompleted: Terminévalidated: Validédisputed: Litigieuxcancelled: Annulé
- Litige autorisé (DISPUTE_ALLOWED) :
scheduled,pending_validation,completed,validated - Soumission de résultat autorisée (RESULT_SUBMISSION_ALLOWED) :
scheduled,pending_validation - Édition administrateur (EDITABLE_ADMIN) :
scheduled,provisional,pending_validation,completed,cancelled - Gestion de composition d’équipe (Lineup) :
scheduled