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:
DELETED -> erreur ACCOUNT_DELETED;
DEACTIVATED -> erreur ACCOUNT_DEACTIVATED;
- 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.
| Surface | ADMIN | MODERATOR | USER |
|---|
requireAdminActor par défaut | oui | non | non |
Surfaces admin avec allowModerator: true | oui | oui | non |
| Publication report refusé / adminDirect | oui | non | non |
Publication season_awards | oui | non | non |
Publication publishTotw | oui | non | non |
| Gestion de compte (delete/deactivate/reactivate) | oui | non | non |
Changement de rôle (USER/ADMIN/MODERATOR) | oui | non | non |
| Override lineup | oui | oui | non |
| Gestion de reports côté équipe | oui | oui | non |
| Actions standard club/ligue/match | selon logique métier | selon logique métier + surfaces allowModerator | selon 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 :
- É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 :
sourceType | plafond croissance |
|---|
ARTICLE | 2 |
TRANSFER | 1 |
TOTW | 1 |
SEASON_AWARD | 1 |
AWARD | 1 |
ADMIN, LEGACY, SYSTEM, indéfini | illimité |
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.
| Source | sourceType | placement | priorité | TTL / endsAt | href | Déclencheur | Archivage |
|---|
Forfait club (FULL_COMPETITION) | CLUB_FORFEIT | BANNER_GLOBAL | 80 | 14 j | — | applyClubForfeit | revertClubForfeitImpacts |
Forfait club (POST_MERCATO_ONLY) | CLUB_FORFEIT | BANNER_GLOBAL | 60 | 14 j | — | applyClubForfeit | revertClubForfeitImpacts |
| Sanction utilisateur | USER_SANCTION | BANNER_GLOBAL | 55 | 7 j (ou expiresAt si fourni) | — | createSanction (chemin admin manuel, pas le chemin forfait) | deleteSanction |
| TOTW | TOTW | ALL_GLOBAL | 20 | 7 j | /totw | publishTotw | deleteTotw |
| Palmarès | SEASON_AWARD | ALL_GLOBAL | 18 | 7 j | /palmares/{awardId} | publishSeasonAwards | unpublishSeasonAwards |
| Transfert accepté | TRANSFER | BANNER_GLOBAL | 12 | 48 h | /mercato | respond (acceptation transfert) | aucun (expiration via TTL) |
| Article publié | ARTICLE | BANNER_GLOBAL | 10 | 48 h | /journal/{slug} | publish | unpublish / 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