🧬
Anatomie d'un objet user_access_json
Structure complète d'un élément du tableau
user_access_json est un tableau JSON retourné par la vue
user_feature_usage_json_cache via json_agg(). Il contient une entrée par feature disponible pour le plan de l'utilisateur. Flutter le stocke dans FFAppState().userFeatureRights.
user_access_json[i] — 1 objet feature
[ // tableau complet = toutes les features du plan
{
"feature_key": "group_create", // clé unique "access_type": "quota", // "flag" ou "quota" "limit_value": 3, // null si flag "used": 1, // COUNT(*) du log "remaining": 2, // limit_value - used "period_days": 30, // null si flag ou unlimited "is_enabled": true, // killswitch admin "has_access": true // ← résultat final },
{ ... }, // 10 autres features
]
{
"feature_key": "group_create", // clé unique "access_type": "quota", // "flag" ou "quota" "limit_value": 3, // null si flag "used": 1, // COUNT(*) du log "remaining": 2, // limit_value - used "period_days": 30, // null si flag ou unlimited "is_enabled": true, // killswitch admin "has_access": true // ← résultat final },
{ ... }, // 10 autres features
]
📋
Champs — cliquez pour détailler
feature_key
text
requis
Identifiant unique de la feature. Utilisé par
checkFeatureAccess(featureKey) pour trouver le bon objet dans le tableau. Source : subscription_access.feature_key.
access_type
text
requis
Détermine la logique de calcul de
has_access. Deux valeurs possibles : flag (booléen, pas de compteur) ou quota (limité avec COUNT sur le log).
limit_value
int4 | null
conditionnel
Nombre maximum d'utilisations autorisées pour les quotas.
null si access_type = "flag". Pour les flags, la valeur 1 dans subscription_access signifie "accès autorisé", 0 signifie "refusé".
used
int4
calculé
Nombre d'utilisations comptabilisées. Calculé par
COUNT(*) sur feature_usage_log filtré par member_uuid, feature_key et la fenêtre temporelle. Exception : pour eventOpened_join, la source est EventsRegistration.
remaining
int4 | null
calculé
Utilisations restantes =
limit_value - used. null pour les flags. Peut être négatif si des actions ont été effectuées sans log (incohérence). Utilisé uniquement pour l'affichage dans l'UI debug.
period_days
int4 | null
conditionnel
Durée de la fenêtre temporelle glissante en jours.
null pour les flags et les quotas unlimited. Pour 30, le COUNT(*) ne compte que les lignes créées dans les 30 derniers jours.
is_enabled
bool
requis
Killswitch admin. Si
false, checkFeatureAccess() retourne immédiatement false sans regarder has_access. Permet de désactiver une feature pour tous les plans sans toucher aux données.
has_access
bool | string | num
⭐ résultat
Le champ décisif — lu en dernier par
checkFeatureAccess(). Calculé par la vue : remaining > 0 pour un quota, ou limit_value = 1 pour un flag. Accepte 3 types : bool, String ("true"/"1"/"yes"), num (!= 0).
Origine des champs : 5 champs viennent directement de
subscription_access (feature_key, access_type, limit_value, period_days, is_enabled). 3 champs sont calculés par la vue (used, remaining, has_access).
🔧
Pipeline de construction
De la DB Postgres à Flutter
Le tableau
user_access_json est le résultat d'une chaîne de 5 étapes : 3 lectures de tables, 2 vues SQL imbriquées, puis une agrégation JSON. Tout est recalculé en temps réel à chaque requête Flutter.
TABLE · Base
subscription
→ premium_status de User
→ min_status de chaque règle
→ Détermine les règles applicables
→ min_status de chaque règle
→ Détermine les règles applicables
+
TABLE · Règles
subscription_access
feature_key
min_status
access_type
limit_value
period_days
is_enabled
min_status
access_type
limit_value
period_days
is_enabled
+
TABLE · Log
feature_usage_log
member_uuid
feature_key
created_at
→ COUNT(*) = used
feature_key
created_at
→ COUNT(*) = used
→
JOIN +
COUNT
COUNT
VUE · Live
user_feature_usage
calcule :
used (COUNT)
remaining
has_access
used (COUNT)
remaining
has_access
→
json_agg
GROUP BY
user
GROUP BY
user
VUE · JSON
…_json_cache
member_uuid
user_access_json
→ 1 ligne / user
→ array de features
user_access_json
→ 1 ligne / user
→ array de features
→
SELECT
Flutter
Flutter
Flutter · AppState
FFAppState()
.userFeatureRights
→ List<dynamic>
→ checkFeatureAccess()
→ bool
→ List<dynamic>
→ checkFeatureAccess()
→ bool
⏱️
Quand le tableau est rechargé
| Moment | Déclencheur | Code | Note |
|---|---|---|---|
| Chargement page Abonnements | initState() de SubscriptionsWidget | updateUserFeaturesRights() | Rechargement systématique |
| Bouton "Actualiser mon offre" | Tap utilisateur sur StripeWidget | updateUserFeaturesRights() | Retour depuis Stripe Checkout |
| Retour depuis /success | closeOrReturn() → /subscriptions | initState() → update | Après paiement validé |
| Page debug /oFeaturesRights | Chargement de la page debug | updateUserFeaturesRights() | Affichage temps réel |
| Après action soumise à quota | INSERT dans feature_usage_log | NON rechargé auto | ⚠ Cache potentiellement périmé |
⚠️ Point critique — fraîcheur du cache Flutter
Les vues Postgres recalculent toujours en temps réel. Le risque est côté Flutter :
FFAppState().userFeatureRights n'est pas rafraîchi automatiquement après chaque action. Si un utilisateur effectue 3 group_create sans que updateUserFeaturesRights() soit rappelée entre les deux, checkFeatureAccess() accordera encore l'accès côté UI même si le quota est dépassé côté DB.
🏷️
Types d'accès : flag vs quota
access_type détermine toute la logique de calcul
flag
Accès booléen
Oui ou non — pas de compteur. Le droit dépend uniquement du plan.
Exemple : send_notifications
"feature_key": "send_notifications",
"access_type": "flag",
"limit_value": null, // pas de limite
"used": 0,
"remaining": null,
"period_days": null,
"is_enabled": true,
"has_access": true // plan Joueur+ (status ≥ 1)
"access_type": "flag",
"limit_value": null, // pas de limite
"used": 0,
"remaining": null,
"period_days": null,
"is_enabled": true,
"has_access": true // plan Joueur+ (status ≥ 1)
Calcul dans la vue :
Pas d'entrée dans feature_usage_log — l'action n'est pas comptée.
has_access = (user.premium_status >= subscription_access.min_status)Pas d'entrée dans feature_usage_log — l'action n'est pas comptée.
Features de ce type :
group_join
view_stats
send_notifications
eventOpened_join*
group_files_access
group_members
* source spéciale : EventsRegistration, pas feature_usage_log
quota
Accès limité
N utilisations sur une période. Compteur via COUNT(*) sur le log.
Exemple : group_create (plan Joueur+)
"feature_key": "group_create",
"access_type": "quota",
"limit_value": 3, // max 3/mois
"used": 1, // COUNT(*) log
"remaining": 2, // 3 - 1
"period_days": 30,
"is_enabled": true,
"has_access": true // remaining > 0
"access_type": "quota",
"limit_value": 3, // max 3/mois
"used": 1, // COUNT(*) log
"remaining": 2, // 3 - 1
"period_days": 30,
"is_enabled": true,
"has_access": true // remaining > 0
Calcul dans la vue :
has_access = (remaining > 0) = used < limit_value
used = COUNT(*) FROM feature_usage_log WHERE member_uuid=? AND feature_key=? AND created_at > now() - interval(period_days)has_access = (remaining > 0) = used < limit_value
Features de ce type :
event_join
contact_add
create_event
place_create
group_create
Plan supérieur → limit_value = null → quota illimité → has_access = true
| Champ | flag | quota · limité | quota · illimité |
|---|---|---|---|
| access_type | "flag" | "quota" | "quota" |
| limit_value | null | ex: 3 | null |
| used | 0 | COUNT(*) log | COUNT(*) log |
| remaining | null | limit_value - used | null |
| period_days | null | ex: 30 | null |
| has_access | premium_status ≥ min_status | remaining > 0 | true (toujours) |
| Entrée dans feature_usage_log | Non | Oui — obligatoire | Oui — mais ne bloque pas |
La fonction
checkFeatureAccess(rights, featureKey) suit un arbre de décision strict avec 5 gardes successives. La première garde qui échoue retourne false immédiatement.
G1
rights == null || rights.isEmpty ?
→ true : return FALSE — cache vide (app non initialisée)
→ false : continuer
G2
rights.firstWhere(r => r['feature_key'] == featureKey) → null ?
→ null : return FALSE — feature absente du plan (min_status non atteint)
→ trouvé : continuer avec record
G3
record['is_enabled'] == false ?
→ true : return FALSE — killswitch admin actif
→ false/null : continuer
Note : uniquement si
is_enabled is bool. Si null, cette garde est ignorée.G4
record['has_access'] == null ?
→ null : return FALSE
→ non-null : continuer
G5
Quel est le type de has_access ?
bool → return has_access directement
String → "true"/"1"/"yes" → true, sinon false
num → != 0 → true, sinon false
autre type → return FALSE
En pratique, Postgres retourne toujours un
bool. Les cas String/num sont des sécurités pour la désérialisation JSON Dart.
custom_functions.dart · L.541
GitHub ↗
bool checkFeatureAccess(List<dynamic> rights, String featureKey) { try { // G1 — cache vide if (rights == null || rights.isEmpty) return false; // normalise la clé (trim, pas de toLower — clés sont case-sensitive) final wantedKey = featureKey.trim(); // G2 — feature absente du plan final record = rights.firstWhere( (r) => r != null && r['feature_key'] != null && r['feature_key'].toString().trim() == wantedKey, orElse: () => null, ); if (record == null) return false; // G3 — killswitch admin final isEnabled = record['is_enabled']; if (isEnabled is bool && isEnabled == false) return false; // G4 — has_access null final value = record['has_access']; if (value == null) return false; // G5 — conversion selon le type if (value is bool) return value; if (value is String) return ['true', '1', 'yes'].contains(value.toLowerCase().trim()); if (value is num) return value != 0; return false; // type inconnu } catch (_) { return false; } }
🧪
Simulateur live
Testez checkFeatureAccess() dans le navigateur
Ce simulateur reproduit exactement la logique de
checkFeatureAccess() — même ordre des gardes, même gestion des types. Modifiez les valeurs et observez quelle garde est franchie.
Saisie manuelle
Scénarios prédéfinis
📋
Les 11 feature keys — référence complète
FFAppConstants.featurekey · app_constants.dart
11 clés définies dans
FFAppConstants.featurekey. Chaque feature est présente dans le tableau user_access_json uniquement si premium_status ≥ min_status de la règle dans subscription_access.
| feature_key | Type | Source used | Gratuit status 0 |
Joueur+ status 1 |
Capitaine status 2 |
Pro status 3 |
has_access = true si… |
|---|---|---|---|---|---|---|---|
| event_join | quota | feature_usage_log | 5/30j | ✓ | ✓ | ✓ | used < 5 (gratuit) · illimité (≥1) |
| contact_add | quota | feature_usage_log | 10/30j | ✓ | ✓ | ✓ | used < 10 (gratuit) · illimité (≥1) |
| create_event | quota | feature_usage_log | 2/30j | ✓ | ✓ | ✓ | used < 2 (gratuit) · illimité (≥1) |
| group_join | flag | — | ✓ | ✓ | ✓ | ✓ | premium_status ≥ 0 → toujours vrai |
| view_stats | flag | — | ✗ | ✓ | ✓ | ✓ | premium_status ≥ 1 |
| send_notifications | flag | — | ✗ | ✓ | ✓ | ✓ | premium_status ≥ 1 |
| place_create | quota | feature_usage_log | ✗ | 3/30j | ✓ | ✓ | absent (0) · used < 3 (1) · illimité (≥2) |
| group_create | quota | feature_usage_log | ✗ | 3/30j | ✓ | ✓ | absent (0) · used < 3 (1) · illimité (≥2) |
| eventOpened_join | spécial | EventsRegistration | ✗ | ✓ | ✓ | ✓ | premium_status ≥ 1 · source ≠ feature_usage_log |
| group_files_access | flag | — | ✗ | ✗ | ✓ | ✓ | premium_status ≥ 2 |
| group_members | flag | — | ✗ | ✗ | ✓ | ✓ | premium_status ≥ 2 |
⚠️ Note sur eventOpened_join : Cette feature est déclarée comme
flag dans subscription_access, mais sa source de comptage est EventsRegistration (pas feature_usage_log). Le trigger eventsregistration_log_open_join est marqué TODO dans le code — implémentation incomplète.
🗄️
SQL des vues — reconstruction
Logique Postgres déduite du code Dart et de la structure DB
Les SQL ci-dessous sont reconstruits à partir du code Dart et du schéma — non extraits directement des vues Supabase. Ils reflètent la logique architecturale décrite dans le code.
user_feature_usage
user_feature_usage_json_cache
json_agg() détaillé
VUE
user_feature_usage — calcule used, remaining, has_access par user/feature
CREATE VIEW user_feature_usage AS SELECT u."Member_UUID" AS member_uuid, sa.feature_key, sa.access_type, sa.limit_value, sa.period_type, sa.period_days, sa.is_enabled, -- Comptage des utilisations selon le type de quota CASE WHEN sa.access_type = 'flag' THEN 0 WHEN sa.access_type = 'quota' AND sa.period_type = 'unlimited' THEN ( SELECT COUNT(*) FROM feature_usage_log ful WHERE ful.member_uuid = u."Member_UUID" AND ful.feature_key = sa.feature_key ) WHEN sa.access_type = 'quota' AND sa.period_type = 'monthly' THEN ( SELECT COUNT(*) FROM feature_usage_log ful WHERE ful.member_uuid = u."Member_UUID" AND ful.feature_key = sa.feature_key AND ful.created_at > NOW() - (sa.period_days * INTERVAL '1 day') ) -- cas spécial eventOpened_join WHEN sa.feature_key = 'eventOpened_join' THEN ( SELECT COUNT(*) FROM "EventsRegistration" er WHERE er.contact_uid = u."Member_UUID" AND er."registrationStatus" = 'present' AND er."External_Registration_Status" = 'invited' ) ELSE 0 END AS used, -- remaining = limit_value - used (null si flag ou illimité) CASE WHEN sa.limit_value IS NOT NULL THEN sa.limit_value - /* used calculé ci-dessus */ (/* sous-requête identique à used */) ELSE NULL END AS remaining, -- has_access = résultat final booléen CASE WHEN sa.access_type = 'flag' THEN (u.premium_status >= sa.min_status) WHEN sa.access_type = 'quota' AND sa.limit_value IS NULL THEN TRUE -- quota illimité pour ce plan WHEN sa.access_type = 'quota' AND sa.limit_value IS NOT NULL THEN (remaining > 0) ELSE FALSE END AS has_access FROM "User" u JOIN subscription_access sa ON u.premium_status >= sa.min_status WHERE sa.is_enabled = TRUE;
VUE
user_feature_usage_json_cache — agrège en JSON par utilisateur
CREATE VIEW user_feature_usage_json_cache AS SELECT member_uuid, json_agg( json_build_object( 'feature_key', feature_key, 'access_type', access_type, 'limit_value', limit_value, 'used', used, 'remaining', remaining, 'period_days', period_days, 'is_enabled', is_enabled, 'has_access', has_access ) ) AS user_access_json FROM user_feature_usage GROUP BY member_uuid;
1 ligne par utilisateur. Le champ
user_access_json est le tableau JSON que Flutter charge dans FFAppState().userFeatureRights via updateUserFeaturesRights().
Ce que retourne json_agg() pour un utilisateur
// user_access_json pour un utilisateur Joueur+ (premium_status=1) [ { "feature_key": "event_join", // présent car min_status=0 "access_type": "quota", "limit_value": null, // illimité au statut 1 "used": 7, "remaining": null, "period_days": null, "is_enabled": true, "has_access": true }, { "feature_key": "group_create", // présent car min_status=1 "access_type": "quota", "limit_value": 3, // quota actif "used": 3, // épuisé ! "remaining": 0, "period_days": 30, "is_enabled": true, "has_access": false // ← remaining = 0 }, { "feature_key": "group_files_access", // ABSENT — min_status=2, user=1 // ... ce record n'existe pas dans le tableau } // ... 8 autres features selon le plan ]
Point clé : Les features dont
min_status > premium_status de l'utilisateur sont complètement absentes du tableau (pas de record). C'est la garde G2 de checkFeatureAccess() qui retourne false dans ce cas.
📍
Utilisation dans le code Flutter
Tous les appels à checkFeatureAccess() identifiés
À ce jour, 3 appels à
checkFeatureAccess() sont présents dans le code Flutter (hors page debug). Chacun suit le même pattern : if (check) { action } else { rien / upsell }. Les features non encore protégées par ce mécanisme sont indiquées en bas de page.
favorites_widget.dart · L.265
group_create
Bouton "Créer un groupe" — affichage conditionnel
Au tap sur le bouton "+" dans la liste des groupes,
checkFeatureAccess() est appelée au moment de l'action (pas pour masquer le bouton). Si accès refusé → rien ne se passe (pas d'upsell visible).
onTap: () async { if (functions.checkFeatureAccess( FFAppState().userFeatureRights.toList(), 'group_create')) { await showDialog( // ✅ ouvre le dialog création groupe context: context, builder: (dialogContext) { ... } ); } // ❌ else : rien — pas d'upsell }
GitHub · L.265 ↗
⚠ Aucun message d'erreur si quota atteint — l'utilisateur ne sait pas pourquoi rien ne se passe
favorites_widget.dart · L.1412
place_create
Bouton "Créer un lieu" — action conditionnelle
Même pattern que
group_create : le check est au tap. Si accès refusé → rien ne se passe.
onTap: () async { if (functions.checkFeatureAccess( FFAppState().userFeatureRights.toList(), 'place_create')) { await showDialog( // ✅ ouvre le dialog création lieu context: context, builder: (dialogContext) { ... } ); } }
place_edit_widget.dart · L.1140
place_create
Bouton "Supprimer le lieu" — masquage conditionnel
Seul cas où le widget est masqué (pas juste bloqué au tap). Le bouton "Supprimer" dans place_edit n'est rendu que si l'utilisateur a
place_create. Pattern différent : utilise if (...) FFButtonWidget(...) directement dans le tree.
if (functions.checkFeatureAccess( FFAppState().userFeatureRights.toList(), 'place_create')) FFButtonWidget( // ✅ affiché seulement si accès onPressed: () async { await PlaceTable().delete(...); // supprimer le lieu }, text: 'Supprimer', )
GitHub · L.1140 ↗
✓ Bonne pratique : masquage visuel, pas juste blocage silencieux
📐
Patterns d'implémentation
✓ Pattern 1 — Masquage conditionnel (recommandé)
// Widget visible seulement si accès if (functions.checkFeatureAccess( FFAppState().userFeatureRights.toList(), 'group_create')) ElevatedButton( onPressed: () { ... }, child: Text('Créer un groupe'), )
Le bouton disparaît — l'utilisateur voit l'UI adaptée à son plan.
✓ Pattern 2 — Blocage avec upsell (à implémenter)
onTap: () async { if (functions.checkFeatureAccess( FFAppState().userFeatureRights.toList(), 'group_create')) { // ✅ action await showDialog(...); } else { // 💡 upsell context.pushNamed('/subscriptions'); } }
L'utilisateur voit le bouton mais est redirigé vers les abonnements. Améliore la conversion.
⚠️
Features sans protection côté Flutter
Les features ci-dessous ont des règles dans
subscription_access mais aucun appel à checkFeatureAccess() n'a été trouvé dans le code Flutter pour les protéger.
| feature_key | État actuel | Action recommandée |
|---|---|---|
| event_join | Règle DB présente, pas de check Flutter trouvé | Ajouter check avant inscription à un événement |
| contact_add | Règle DB présente, pas de check Flutter trouvé | Ajouter check avant ajout d'un contact |
| create_event | Règle DB présente, pas de check Flutter trouvé | Ajouter check avant création d'un événement |
| view_stats | Règle DB présente, pas de check Flutter trouvé | Masquer le tab stats ou protéger la page |
| send_notifications | Règle DB présente, pas de check Flutter trouvé | Conditionner l'envoi dans l'action Flutter |
| eventOpened_join | Trigger DB incomplet (TODO) | Compléter le trigger + ajouter check Flutter |
| group_join | Accessible à tous (status 0) — pas de check requis | – |
| group_files_access | Règle DB présente, pas de check Flutter trouvé | Conditionner l'accès aux fichiers dans le groupe |
| group_members | Règle DB présente, pas de check Flutter trouvé | Conditionner l'affichage de la liste membres |
📝
Enregistrer un usage — INSERT dans feature_usage_log
Quand et comment alimenter le compteur
Point critique :
checkFeatureAccess() retourne true tant que le compteur en DB n'est pas mis à jour. Si l'action est effectuée sans INSERT dans feature_usage_log, le quota ne se décrémente jamais — l'utilisateur peut contourner les limites indéfiniment.
État actuel — aucun INSERT côté Flutter identifié
Une analyse complète du code Dart ne révèle aucun appel à
Deux hypothèses pour les quotas actuellement actifs :
1. Les INSERTs sont gérés dans BuildShip (hors code Flutter) via des webhooks ou automations
2. Les INSERTs sont effectués côté Supabase Edge Function lors des actions critiques
À vérifier : les Edge Functions et les workflows BuildShip liés aux actions group_create, place_create, etc.
FeatureUsageLogTable().insert() dans les widgets ou actions. Le modèle Dart FeatureUsageLogTable est défini mais n'est utilisé que dans serialization_util.dart (désérialisation de paramètres de route).Deux hypothèses pour les quotas actuellement actifs :
1. Les INSERTs sont gérés dans BuildShip (hors code Flutter) via des webhooks ou automations
2. Les INSERTs sont effectués côté Supabase Edge Function lors des actions critiques
À vérifier : les Edge Functions et les workflows BuildShip liés aux actions group_create, place_create, etc.
✅
Comment implémenter un INSERT correct côté Flutter
Pattern complet — check + action + log + refresh
onTap: () async { // 1. Vérifier l'accès AVANT l'action if (!functions.checkFeatureAccess( FFAppState().userFeatureRights.toList(), 'group_create')) { context.pushNamed('/subscriptions'); // upsell return; } // 2. Effectuer l'action final newGroup = await GroupTable().insert({ 'name': groupName, 'owner_uuid': FFAppState().AuthUser.memberID, }); // 3. Enregistrer l'usage dans feature_usage_log await FeatureUsageLogTable().insert({ 'member_uuid': FFAppState().AuthUser.memberID, 'feature_key': 'group_create', // created_at : auto via default Postgres // metadata (optionnel) : 'metadata': {'group_uuid': newGroup.id}, }); // 4. Rafraîchir le cache AppState await action_blocks.updateUserFeaturesRights(context); // 5. Suite du flow... }
🗂️
Quelle source de log pour chaque feature ?
| feature_key | Source du COUNT | Qui doit faire l'INSERT | Moment |
|---|---|---|---|
| event_join | feature_usage_log | Flutter ou BuildShip | Après confirmation de participation |
| contact_add | feature_usage_log | Flutter ou BuildShip | Après ajout du contact confirmé |
| create_event | feature_usage_log | Flutter ou BuildShip | Après création de l'événement |
| place_create | feature_usage_log | Flutter ou BuildShip | Après enregistrement du lieu |
| group_create | feature_usage_log | Flutter ou BuildShip | Après création du groupe |
| eventOpened_join | EventsRegistration (direct) | Trigger DB (incomplet) | Quand registrationStatus → 'present' |
| group_join, view_stats… | — (flags) | Pas de log nécessaire | Pas de compteur |
Règle d'or : Le log doit être inséré après que l'action a réussi (pas avant). Si l'action échoue et que le log a déjà été inséré, le quota est décrémenté pour rien. En cas de rollback, supprimer la ligne de log correspondante.
La route
/oFeaturesRights est une page de diagnostic temps réel. Elle appelle updateUserFeaturesRights() au chargement puis affiche l'intégralité de FFAppState().userFeatureRights avec toutes les valeurs calculées par Postgres.
Initialisation (initState)
SchedulerBinding.instance .addPostFrameCallback((_) async { await action_blocks .updateUserFeaturesRights(context); safeSetState(() {}); });
La page force un rechargement complet des droits depuis Postgres à chaque ouverture.
Ce qui est affiché
TEXT
premiumPlan — nom du plan actifLABEL
feature_key (tronqué à 15 chars)COND
Flag= / max= selon access_typeCOUNT
used/period_days en vert ou rougeBOOL
L= remaining coloré vert/rouge📱
Maquette de l'affichage debug
🔬 Features Rights Debug
Plan actif : Joueur+
User_feature_usage_json (table) :
event_join
max=
7/null
L=
∞
group_create
max=
3/30
L=
0
send_notifica…
Flag=
0/7
place_create
max=
1/30
L=
2
Vert = checkFeatureAccess() → true · Rouge = false
Lecture de l'affichage : Le champ
used/period_days affiché correspond à $.used / $.period_days du JSON. Le champ L= affiche $.remaining. La couleur rouge/vert est calculée via checkFeatureAccess() sur chaque ligne du tableau. Pour les flags, seul used/period_days est affiché (pas de L=).
| Champ affiché | Source JSON | Couleur |
|---|---|---|
| "Flag= " ou "max=" | $.access_type == "flag" | Texte gris fixe |
| used/period_days | $.used / $.period_days | Vert si checkFeatureAccess=true, rouge sinon |
| L= (remaining) | $.remaining | Vert si checkFeatureAccess=true, rouge sinon (caché si flag) |
⚠️
Pièges & points de vigilance
Ce qui peut silencieusement mal tourner
CRITIQUE
Cache Flutter périmé après une action
FFAppState().userFeatureRights n'est pas mis à jour automatiquement après un INSERT dans feature_usage_log. Si l'utilisateur effectue 3 actions soumises à quota sans que la page Abonnements soit rechargée entre les deux, checkFeatureAccess() continuera à retourner true côté Flutter même si le quota est épuisé côté DB.
✓ Solution : appeler
updateUserFeaturesRights(context) immédiatement après chaque INSERT dans feature_usage_log.
CRITIQUE
Action sans log = quota qui ne se décrémente pas
Si un utilisateur crée un groupe mais qu'aucun INSERT n'est fait dans
feature_usage_log, la vue Postgres compte used = 0 en permanence et has_access = true pour toujours, même si la limite est 3. L'utilisateur peut créer autant de groupes qu'il veut.
✓ Solution : chaque action soumise à quota DOIT insérer une ligne dans
feature_usage_log après succès.
MOYEN
Blocage silencieux sans retour utilisateur
Les appels actuels (
favorites_widget.dart) ne font rien si l'accès est refusé — pas de message, pas de redirection. L'utilisateur tape sur le bouton et rien ne se passe. C'est une mauvaise UX qui peut être confondue avec un bug.
✓ Solution : ajouter un
else { context.pushNamed('/subscriptions'); } ou un SnackBar explicatif.
MOYEN
eventOpened_join — trigger incomplet (TODO)
Le trigger
eventsregistration_log_open_join sur la table EventsRegistration est marqué comme TODO dans le code. La feature eventOpened_join utilise une source de comptage différente (EventsRegistration, pas feature_usage_log), mais la logique de comptage dans la vue dépend de l'implémentation correcte de ce trigger.
✓ Solution : compléter le trigger Postgres + vérifier que la vue
user_feature_usage compte correctement les inscriptions à des événements ouverts.
FAIBLE
Quotas cumulatifs vs fenêtre glissante
Les quotas avec
period_type = 'unlimited' ne se réinitialisent jamais — ils sont cumulatifs à vie. Si un utilisateur Joueur+ a un quota de 3 groupes par mois (period_type = 'monthly'), les lignes de log antérieures à 30 jours ne sont plus comptées. Mais si period_type = 'unlimited', chaque groupe créé depuis la création du compte est compté.
⚠ À vérifier : les valeurs de
period_type dans subscription_access pour s'assurer que chaque quota utilise le type attendu.
FAIBLE
feature_key case-sensitive — pas de toLower
checkFeatureAccess() fait un trim() mais pas de toLowercase. La clé 'eventOpened_join' est différente de 'eventopenend_join'. Si la valeur dans feature_usage_log.feature_key ne correspond pas exactement à la casse de subscription_access.feature_key, le COUNT sera 0 et le quota jamais décrémenté.
✓ Solution : toujours utiliser les constantes
FFAppConstants.featurekey plutôt que des strings littérales.
✅
Checklist d'implémentation
| À vérifier | Statut actuel |
|---|---|
| Chaque action soumise à quota a un check checkFeatureAccess() | ⚠ Partiel — group_create et place_create uniquement |
| Chaque action protégée a un INSERT dans feature_usage_log | ✗ Non trouvé côté Flutter — vérifier BuildShip/Edge Functions |
| updateUserFeaturesRights() appelé après chaque INSERT | ✗ Non implémenté — cache potentiellement périmé |
| Upsell ou message d'erreur si accès refusé | ✗ Blocage silencieux dans les 2 appels existants |
| Trigger eventOpened_join fonctionnel | ✗ TODO — non implémenté |
| feature_keys utilisent les constantes FFAppConstants.featurekey | ✓ Oui — les 3 appels identifiés utilisent des strings exactes correctes |
| Masquage visuel des UI non accessibles (pas juste blocage au tap) | ⚠ Partiel — uniquement place_edit_widget.dart masque le bouton |