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 éléments à confirmer sont regroupés dans la section finale.

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/guards.ts, convex/auth/accountStatus.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.

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.
SurfaceADMINMODERATORUSER
requireAdminActor par défautouinonnon
Surfaces admin avec allowModerator: trueouiouinon
Publication report refusé / adminDirectouinonnon
Publication season_awardsouinonnon
Publication publishTotwouinonnon
Gestion de compte (delete/deactivate/reactivate)ouinonnon
Changement de rôle (USER/ADMIN/MODERATOR)ouinonnon
Override lineupouiouinon
Gestion de reports côté équipeouiouinon
Actions standard club/ligue/matchselon logique métierselon logique métier + surfaces allowModeratorselon logique métier

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 :
sourceTypeplafond croissance
ARTICLE2
TRANSFER1
TOTW1
SEASON_AWARD1
AWARD1
ADMIN, LEGACY, SYSTEM, indéfiniillimité
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.
SourcesourceTypeplacementprioritéTTL / endsAthrefDéclencheurArchivage
Forfait club (FULL_COMPETITION)CLUB_FORFEITBANNER_GLOBAL8014 japplyClubForfeitrevertClubForfeitImpacts
Forfait club (POST_MERCATO_ONLY)CLUB_FORFEITBANNER_GLOBAL6014 japplyClubForfeitrevertClubForfeitImpacts
Sanction utilisateurUSER_SANCTIONBANNER_GLOBAL557 j (ou expiresAt si fourni)createSanction (chemin admin manuel, pas le chemin forfait)deleteSanction
TOTWTOTWALL_GLOBAL207 j/totwpublishTotwdeleteTotw
PalmarèsSEASON_AWARDALL_GLOBAL187 j/palmares/{awardId}publishSeasonAwardsunpublishSeasonAwards
Transfert acceptéTRANSFERBANNER_GLOBAL1248 h/mercatorespond (acceptation transfert)aucun (expiration via TTL)
Article publiéARTICLEBANNER_GLOBAL1048 h/journal/{slug}publishunpublish / archive

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.

Recherche (invariants)

  • Index utilisés via logique métier: annonces, articles, audits, litiges, clubs, matchs, notifications, sanctions, transferts, profils utilisateurs, utilisateurs.

Zones non vérifiées / à confirmer

  • Les règles de livraison email (queues/retry/backoff précis) ne sont pas exhaustivement couvertes ici.
  • Le détail complet de la visibilité utilisateur hors restriction CONTACT n’est pas reconstruit intégralement dans ce document métier.
Last modified on June 25, 2026