Repository navigation
Reconnaître un achat fait sans compte : compte anonyme, fusion à la connexion et purge (API) #532
Description
Contexte
Epic : #11
API › Conventions (compte anonyme, account_required) · API › Compte (POST /me/merge-anonymous) · Modele-de-Donnees › users · Modele-de-Donnees › route_unlocks · Securite-et-RGPD · D-072 · D-075 · D-099
Maquette : sans objet — API et modèle de données, aucun écran.
Contenu réécrit le 2026-10-20 par le chef de projet, après D-099 (décision d'Ilan du 2026-10-19 sur #531, qui amende D-096) : un achat fait sans compte appartient à un compte anonyme Firebase, créé par l'app juste avant sa première opération du store faite sans compte (achat ou restauration) ; son uid est l'identifiant RevenueCat. route_unlocks ne change pas : user_id reste non nul, la clé reste (user_id, route_id), plus de revenuecat_app_user_id sur la ligne ni de rattachement au logIn. Le contenu précédent (migration de route_unlocks) est abandonné.
Bloqué par #341 (vérifié le 2026-10-20) : la fusion déplace des lignes route_unlocks qui portent source et store_transaction_id, et un droit Premium qui porte plan_source, colonnes ajoutées par la PR #499. La migration de ce ticket suit celle de la PR #499.
Comportement attendu
- Migration Drizzle versionnée sur
users:is_anonymous(boolean, défautfalse, lu dans le jeton, fournisseuranonymous, repasse àfalsequand l'app lie une identité parlinkWithCredential) ;merged_into_id(uuid nullable, compte connecté dans lequel le compte anonyme a été fusionné) ;revenuecat_app_user_id=firebase_uid, compte anonyme compris. - Authentification (
apps/api/src/auth/plugin.ts) : aujourd'hui tout jeton anonyme est refusé en 401 « Sign in required », y compris suroptionalAuth(constaté surmainle 2026-10-20). À la place :optionalAuthaccepte un jeton anonyme, etcanAccessRoutereste la seule règle de droit pour la fiche et la recherche, réponse privée (D-075) ;requireAuthetrequireRoleappelés avec un jeton anonyme répondent 403account_required, nouveau code stable. Aucune ligneuserscréée pour un jeton anonyme qui n'a fait aucun achat, hors la règle du webhook ci-dessous. POST /me/merge-anonymous{ anonymousIdToken }(compte connecté) : l'API vérifie le jeton (signé par Firebase, fournisseuranonymous, non expiré), puis déplace en une transaction les déblocages et le droit Premium du compte anonyme vers le compte connecté (décision d'Ilan du 2026-09-29 : les achats passent au compte connecté, téléphone partagé compris), sans doublon quand le compte avait déjà le parcours ; un droit Premium offert (plan_source = gift) n'est pas écrasé ; posemerged_into_id. Idempotent : un second appel avec le même jeton répond 200 sans rien déplacer.- Webhook RevenueCat (service partagé avec Brancher RevenueCat et construire l'écran Premium (E-17) #67) : ligne
usersanonyme créée au premier événement si elle n'existe pas encore ; un événement qui nomme un compte fusionné s'applique àmerged_into_id;TRANSFERdéplace les achats comme la fusion. - Purge : comptes anonymes sans achat supprimés 7 jours après leur création (ligne
userset compte Firebase, par l'Admin SDK) ; l'app qui reçoit alors un 401 oublie le compte et repart sans jeton. Le nettoyage automatique des comptes anonymes de Firebase (Identity Platform) reste désactivé. Conservation d'un compte anonyme qui porte un achat : en attente de la décision d'Ilan (Securite-et-RGPD, « à fixer »).
Critères d'acceptation
- Un jeton anonyme qui a débloqué un parcours lit la fiche et la card en entier (
isLocked: false) ; la même requête sans jeton les reçoit verrouillées (tests API) - Un jeton anonyme sur une route qui demande un compte répond 403
account_required(test) - Fusion : déblocages et droit Premium passent au compte connecté, sans doublon,
plan_source = giftintact ; un jeton non anonyme, d'un autre projet Firebase ou expiré est refusé (tests) - Un événement RevenueCat sur un compte anonyme fusionné s'applique au compte connecté ;
TRANSFERdéplace les achats (tests du webhook) - Purge : un compte anonyme sans achat de plus de 7 jours est supprimé, un compte anonyme qui porte un achat est gardé (test)
-
packages/sharedporteaccount_requiredet le schéma dePOST /me/merge-anonymous, sans seconde définition - API, Modele-de-Donnees et le code disent la même chose
Notes techniques
- Niveau 3 : authentification et droits d'achat. Retour arrière à décrire dans la PR.
Activity
IlanAMG commentedon Oct 19, 2026
Chef de projet (point du 19/10/2026) : priorité P1 → P0. Il bloque #67 (P0). Chaîne #52 → #341 → #532 → #67 (P0, v0.4, RevenueCat) : un bloqueur ne peut être ni moins prioritaire ni planifié après ce qu'il bloque.
IlanAMG commentedon Oct 20, 2026
Chef de projet (point du 20/10/2026) : D-099 (décision d'Ilan sur #531) change le contenu de ce ticket : plus de migration de
route_unlocks(user_idreste non nul), à la placeusers.is_anonymous,users.merged_into_id,POST /me/merge-anonymous, 403account_required, webhook et purge à 7 jours. Le corps actuel (D-096) est périmé : à réécrire par MANAGER avant que CODE ne le prenne. La raison du blocage par #341 (migration sur la même table que la PR #499) tombe ; la dépendance est laissée en place en attendant la décision d'Ilan (point du jour).[-]Enregistrer un déblocage acheté sans compte et le rattacher au compte (route_unlocks)[/-][+]Reconnaître un achat fait sans compte : compte anonyme, fusion à la connexion et purge (API)[/+]IlanAMG commentedon Oct 20, 2026
Chef de projet (2026-10-20) : corps et titre réécrits après D-099. La dépendance à #341 est gardée : la fusion lit
route_unlocks.sourceetusers.plan_source, ajoutés par la PR #499. Le correctif de l'authentification facultative (jeton anonyme refusé en 401 surmain,apps/api/src/auth/plugin.ts) est porté ici.