> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cel-eleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ea stripe

# EA FC, Stripe et premium

Cette page documente les parties implémentées dans le code vérifiées pour EA FC et Stripe.

Sources principales :

* `convex/matches/ea_stats_sync.ts`
* `convex/matches/ea_score_sync.ts`
* `convex/admin/cron_runners.ts`
* `convex/awards/recipients.ts`
* `convex/stripe/customerIndex.ts`
* `convex/schema.ts`

## EA FC

### Pipeline score sync (matchs)

La synchronisation de score EA est pilotée par le cron `runMatchEaScoreSync` (`convex/admin/cron_runners.ts`) :

* il lit les matchs éligibles via `getEligibleMatchesForEaSync`,
* traite les `matchIds` par lots avec `scoreConcurrency`,
* appelle `resolveEaScoreAction` pour chaque match,
* en parallèle, il prépare `statsSyncCandidates` (matchs confirmés sans `match_player_stats` et sans lignes unmatched),
  puis planifie `internal.matches.ea_stats_sync.fetchAndPersistEaPlayerStats` si nécessaire.

Dans `resolveEaScoreAction` (`convex/matches/ea_score_sync.ts`) :

* récupère les matchs EA des deux clubs,
* construit une fenêtre de correspondance autour du kickoff (`DEFAULT_MATCH_TOLERANCE_MINUTES`),
* si rien : retry avec fenêtre large `MATCH_HISTORY_DAYS`,
* sans candidat -> `persistUnavailable` (`ea.score.unavailable`),
* plusieurs candidats -> `persistAmbiguous` (`ea.score.ambiguous`),
* divergence entre score EA et score soumis / score joueur -> `ea.potential_draw_review_required`,
* candidate unique valide -> `persistConfirmedScore` (`finalizeEaConfirmedScore`).

`notifyAdmins` émet les événements en sortie de sync score :

* `ea.score.confirmed`
* `ea.score.ambiguous`
* `ea.score.unavailable`
* `ea.score.error`

### Pipeline stats sync + matching

`fetchAndPersistEaPlayerStats` (`ea_stats_sync.ts`) traite les lignes EA d’un match :

* résolution des profils via :
  * compo soumise,
  * actif de l’effectif,
  * `eaPlayerId`,
  * `eaGamertagNormalized`.
* ligne matchée : insertion dans `match_player_stats`,
* ligne non résolue : insertion/mise à jour dans `match_ea_unmatched_player_stats` avec `reviewStatus` + `reasonCode`,
* recalc MOTM possible via `replaceMotmAwardRecipientForMatch` quand des stats sont matchées (`auditReason: 'EA_STATS_PERSISTED'`).

`reviewComparisons` (stocké avec la ligne unmatched) regroupe les comparaisons de résolution `lineup` / `active_roster` pour le debug admin.

### Unmatched review (vérifié)

Types vérifiés :

* `ACTION_REQUIRED`
* `IGNORED`
* `FRAUD`

Codes de raison vérifiés :

* `AMBIGUOUS_LINEUP_MATCH`
* `AMBIGUOUS_ACTIVE_ROSTER_MATCH`
* `PLAYER_NOT_IN_SUBMITTED_LINEUP`
* `PLAYER_NOT_IN_ACTIVE_ROSTER`
* `LEGACY_UNCLASSIFIED`

Règles de classification :

* plusieurs candidats dans la compo => `ACTION_REQUIRED + AMBIGUOUS_LINEUP_MATCH`
* plusieurs candidats actifs résolus => `ACTION_REQUIRED + AMBIGUOUS_ACTIVE_ROSTER_MATCH`
* un seul actif + absent de compo => `IGNORED + PLAYER_NOT_IN_SUBMITTED_LINEUP`
* aucun actif => `FRAUD + PLAYER_NOT_IN_ACTIVE_ROSTER`

`autoMapUnmatchedPlayerStats` :

* relit uniquement les `ACTION_REQUIRED`,
* peut basculer en `match_player_stats` si résolution univoque,
* supprime la ligne unmatched après mapping,
* met à jour les compteurs `autoMappedCount`, `unresolvedCount`, `ambiguousCount`,
* relance le recalcul MOTM (`auditReason: 'EA_STATS_AUTO_MAPPED'`) quand un mapping auto réussit.

`backfillMappedEaStats` existe en backfill technique (batch de `50` avec pagination auto).

### Point à vérifier

* La portée des règles métier autour du statut du match (par ex. validation finale selon l’état compétition) n’est pas détaillée dans ces modules.

## Stripe premium (vérifié dans `convex/stripe/customerIndex.ts`)

### Statuts et éligibilité

Tableau des statuts connus :

* `trialing`
* `active`
* `past_due`
* `canceled`
* `unpaid`
* `incomplete`
* `incomplete_expired`
* `paused`

Éligibilité premium effective :

* statut membre de l’ensemble `{trialing, active}`,
* `livemode` cohérent entre subscription et customer résolu,
* filtrage optionnel par `STRIPE_PREMIUM_PRICE_IDS` si défini.

Règles `STRIPE_PREMIUM_PRICE_IDS` :

* variable optionnelle ; parsing en set de chaînes commençant par `price_`,
* si variable absente/vides : pas de filtrage par priceId.

`livemode` :

* pour un owner, `resolveEffectiveLivemode` vaut `true` si au moins un customer Stripe lié est en `livemode: true`,
* la souscription doit matcher ce mode.

### Jobs / synchronisation

* `reconcilePremiumEntitlements` :
  * batch par défaut `25`,
  * max `50`,
  * pagination + replanification automatique jusqu’à fin.
* `listForAdmin` (admin/modérateurs) :
  * filtre options `searchEmail`, `hasSubscription`, `livemode`,
  * scan candidats limité à `MAX_ADMIN_SCAN = 500`.
* événements gérés :
  * `customer.created`
  * `customer.updated`
  * `customer.deleted`
  * `customer.subscription.created`
  * `customer.subscription.updated`
  * `customer.subscription.deleted`
  * `checkout.session.completed`
