Skip to main content

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.status vérifiés: PENDING, ACCEPTED, COUNTER_PROPOSED, CANCELLED, REJECTED.
  • status peut ê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: scheduled ou provisional.
  • Blocage si une demande PENDING ou COUNTER_PROPOSED existe 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: PENDING et COUNTER_PROPOSED.
  • En cas de COUNTER_PROPOSED, seul le demandeur initial peut accepter.
  • Transition vers ACCEPTED, puis matches.scheduledAt et matches.status = scheduled.
counterProposePostponement:
  • Réservé à l’équipe adverse, pas à l’initiateur.
  • Source stricte PENDING.
  • Transition vers COUNTER_PROPOSED avec counterProposedDate.
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 scheduled ou provisional.
  • Aucune création si un report PENDING, COUNTER_PROPOSED ou ACCEPTED existe déjà.
  • Crée directement un report en ACCEPTED.
Quota de reports:
  • isReportQuotaConsumed ne compte comme consommé que le statut ACCEPTED.
  • effectiveReportsTotal inclut bonus premium via PREMIUM_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.status peut être forcé en visibilité/validation par ADMIN/MODERATOR via hasLineupOverridePrivileges.
  • Sans override:
    • match scheduled,
    • délai max 20 minutes depuis le coup d’envoi.
  • submit exige min 7 joueurs (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/MODERATOR peuvent lire allVersions,
    • profils non-admin lisent la dernière version submitted uniquement.
  • 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.
  • assertAccountAccessAllowed applique l’ordre:
    1. DELETED -> erreur ACCOUNT_DELETED;
    2. DEACTIVATED -> erreur ACCOUNT_DEACTIVATED;
    3. sanctions APP_ACCESS si présentes.
  • transitionAccountStatus met à jour:
    • users.accountStatus,
    • users.accountStatusUpdatedAt,
    • user_profiles.isActive à false hors statut actif.
  • Changement vers statut non-ACTIVE => invalidation session via invalidateUserSessions.
  • 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.
    • self impossible à supprimer, désactiver, réactiver par la logique API.
    • reactivateUser refusé si selfDeletedAt déjà présent.
  • setUserBanState et certains patches utilisent allowModerator: 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 currentPassword via le même flux.
    • La route HTTP POST /api/users/change-password reprend 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.
  • Connexion password:
    • Le flux signIn passe par le limiteur applicatif checkAuthRateLimit avec une clé par email normalisé avant vérification du credential.
    • Une clé saturée renvoie TOO_MANY_ATTEMPTS sans générer de session.

Paris et settlement

Sources: convex/competition/bet_settlement.ts, convex/admin/bets.ts, convex/admin/cron_runners.ts.
  • settleBetsForMatch, reopenBetsForMatch, cancelBetsForMatch et cancelPendingBetsForMatch parcourent 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, requestBulkSettleBets et runBetBulkSettlement exposent 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.avatarUrl dans users.image.
  • Les utilisateurs non premium peuvent définir une photo de profil standard, limitée aux uploads /uploads/profiles en PNG, JPG ou WEBP statique, 5 Mo maximum.
  • Les GIF et images animées sont refusés pour la photo standard (upload_objects.isAnimated et MIME type).
  • Pour un utilisateur non premium, l’avatar public ne peut pas venir de oauthImage ni d’un miroir OAuth (users.image === users.oauthImage).
  • Les assets créateur (user_profiles.avatarUrl, bannerUrl, logoUrl) restent gérés par updatePremiumAssets et 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.status requis COMPLETED ou ARCHIVED.
  • mvpSelectionKey requis et bestGkSelectionKey requis.
  • totsSelectionKey optionnel.
  • replaceExisting supporté.
  • Notifications et emails activés par défaut.
TOTS (season_awards, award_recipients):
  • TOTS_FORMULA_VERSION = 'tots-v3'.
  • Limites vérifiées: individuel 70, distinction 10, collectif 20.
  • Bonus: TOTS_TOTW_BONUS = 2 et TOTS_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 awardId et entryId via id métier (by_external_id).
    • vérifie que entry.seasonAwardId === award.id.
    • reaction strictement dans MERITE | 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é.
  • 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.
  • getAwardCongratulations({ awardId, actorUserId? })
    • résolution actor via resolveActorUserId (valeur mine: null si déconnecté).
    • retourne un objet indexé par entryId: { count, byReaction, mine }.
    • bornage de lecture sur les entrées de l’award (take(250)).
Invariants B4:
  • award_congratulation_counts est la source de vérité de lecture (pas de collect().length pour les compteurs).
  • by_entry_user sert au calcul mine.
  • award_congratulations contient au plus 1 ligne active par (entryId, congratulatorUserId).
TOTW:
  • Réservé ADMIN.
  • publishTotw exige preview et période résolue (periodStartAt / periodEndAt non 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,
  • placement est BANNER_GLOBAL ou ALL_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 :
  1. Étage ops (priority ≥ 50) — triés par priorité décroissante, toujours placés en tête.
  2. É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 via upsertAnnouncementBySource (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: ADMIN
    • MODERATORS: MODERATOR
    • STAFF: ADMIN, MODERATOR
    • USERS: USER
    • ALL_ACTIVE_USERS: USER, ADMIN, MODERATOR
  • resolveRecipientGroupMembers applique un filtre CONTACT via hasRestrictionEffect.
  • Côté bus:
    • MAX_MATCH_TARGETED_IN_APP_RECIPIENTS = 40.
    • Les événements match ciblés doivent émettre exactement un plan in-app.
    • recipientUserIds/managerUserIds doivent correspondre strictement au ciblage.
  • Sources qui explicitent recipientUserIds requis:
    • 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.
  • targetType vérifiés: PERSON, CLUB.
  • effectScopes vé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_ACTIVATED pour chaque utilisateur, plus email si l’adresse est valide.
  • Fin supportée : cancelForfeitSanction et cancelClubForfeit via liftForfeitSanctions envoient COMPETITION_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.
Le détail du cycle de vie est dans 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 : Provisoire
  • pending_validation : En attente de validation
  • completed : Terminé
  • validated : Validé
  • disputed : Litigieux
  • cancelled : Annulé
Règles associées aux statuts :
  • 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
Last modified on July 18, 2026