# Memento — Spécification Technique & Design du Système de Parrainage (Referral) Ce document présente l'architecture complète d'un système de parrainage (referral) natif pour **Memento**, intégré à notre base de données PostgreSQL (Prisma) et à Stripe. --- ## 1. Objectifs du Système 1. **Viralité (PLG)** : Encourager les utilisateurs existants à partager Memento pour acquérir de nouveaux clients sans budget publicitaire (CAC proche de 0). 2. **Double Récompense (Win-Win)** : - **Le Filleul (Invité)** obtient une réduction immédiate lors de son premier abonnement (ex. `-10 % sur son abonnement PRO`). - **Le Parrain (Hôte)** obtient une récompense lors du premier paiement de son filleul (ex. **1 mois gratuit** appliqué directement sur sa prochaine facture Stripe, ou **+100 crédits IA** récurrents). 3. **Simplicité & Automatisation** : Le parrainage doit se faire par lien unique et être validé automatiquement via notre webhook Stripe actuel. --- ## 2. Architecture de Données (Prisma) Pour suivre les relations de parrainage et distribuer les récompenses de manière sécurisée et sans doublons, nous proposons d'étendre le schéma Prisma actuel. ```prisma // memento-note/prisma/schema.prisma model User { // ... champs existants ... // Système de Parrainage referralCode String @unique // Code unique généré pour l'utilisateur (ex: "SEPEHR50") referredById String? // ID du parrain qui l'a invité referredBy User? @relation("UserReferrals", fields: [referredById], references: [id]) referrals User[] @relation("UserReferrals") referralRewards ReferralReward[] // Historique des récompenses distribuées } model ReferralReward { id String @id @default(cuid()) userId String // Le parrain récompensé refereeId String // Le filleul qui a déclenché la récompense rewardType String // "FREE_MONTH" (Stripe) ou "AI_CREDITS" (Redis) status String @default("PENDING") // "PENDING", "COMPLETED", "FAILED" stripeTxId String? // ID de la transaction de crédit Stripe si applicable createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } ``` --- ## 3. Le Parcours Utilisateur pas à pas ### 3.1 Génération et Partage du Lien 1. Chaque utilisateur inscrit reçoit automatiquement un code unique lors de la création de son compte (généré à partir de son nom + 4 chiffres uniques, ou un cuid court). 2. Un nouvel onglet **"Parrainage"** est ajouté dans `/settings/billing` : - Affiche son code promo de parrainage : `MOMENTO-SEPEHR-1234` - Affiche son lien d'invitation unique : `https://memento-note.com/signup?ref=MOMENTO-SEPEHR-1234` - Affiche un bouton de partage rapide (LinkedIn, X, Email, Clipboard). ### 3.2 Inscription du Filleul (Cookie & localStorage) 1. Le filleul clique sur le lien `https://memento-note.com/signup?ref=MOMENTO-SEPEHR-1234`. 2. Le frontend intercepte le paramètre `ref` dans l'URL et le stocke de manière persistante dans un cookie ou dans le `localStorage` (durée : 30 jours). 3. Lorsque le filleul valide son formulaire d'inscription : - Le serveur lit le code promo de parrainage dans la requête. - S'il est valide, il associe le `referredById` du nouveau compte `User` à l'ID du parrain. ### 3.3 Achat de l'Abonnement et Déclenchement de la Récompense 1. Lors du checkout sur le plan Pro, le filleul utilise le code promo Stripe `MOMENTO-SEPEHR-1234` (qui est configuré dans votre Stripe Dashboard comme un code promotionnel avec une réduction de 10%). 2. Une fois le paiement validé, Stripe émet l'événement `checkout.session.completed` à notre webhook `/api/billing/webhook`. 3. Notre helper `sync-subscription-from-stripe.ts` traite l'activation de l'abonnement : - Il vérifie en base de données si le nouvel abonné possède un parrain (`referredById !== null`). - Il vérifie si une récompense pour ce couple Parrain/Filleul a déjà été créée (évite le double-déclenchement). - Il déclenche la distribution des récompenses. --- ## 4. Implémentation Technique des Récompenses ### Option A (Recommandée) : Offrir 1 mois gratuit sur Stripe au Parrain Pour récompenser le parrain de manière entièrement automatisée, nous utilisons le système de **Solde Client (Customer Balance)** de Stripe. On ajoute un crédit négatif (ex. `-9.90 €`) sur le compte client Stripe du parrain. Lors de son prochain renouvellement, Stripe déduira automatiquement ce crédit, ce qui lui offrira son mois gratuit. ```typescript // lib/billing/referral.ts import { stripe } from '@/lib/stripe'; import { prisma } from '@/lib/prisma'; export async function processReferralReward(refereeUserId: string) { // 1. Récupérer le filleul et son parrain const referee = await prisma.user.findUnique({ where: { id: refereeUserId }, include: { subscription: true } }); if (!referee || !referee.referredById) return; const parrainId = referee.referredById; // 2. Vérifier si la récompense est déjà octroyée const existingReward = await prisma.referralReward.findFirst({ where: { userId: parrainId, refereeId: refereeUserId } }); if (existingReward) return; // 3. Récupérer l'abonnement Stripe du parrain const parrainSub = await prisma.subscription.findUnique({ where: { userId: parrainId } }); if (!parrainSub || !parrainSub.stripeCustomerId) { // Si le parrain n'est pas encore client Stripe ou n'a pas d'abonnement actif, // on peut lui créditer des crédits IA en bonus à la place await awardAiCredits(parrainId, 200); // ex: +200 crédits IA return; } try { // 4. Créer une transaction de solde Stripe (Créditer son compte de 9,90 €) const amountInCents = 990; // Le prix du plan PRO const balanceTransaction = await stripe.customers.createBalanceTransaction( parrainSub.stripeCustomerId, { amount: -amountInCents, // Un montant NÉGATIF crédite le client Stripe currency: 'eur', description: `Récompense de parrainage pour l'invitation de ${referee.email}`, } ); // 5. Enregistrer la récompense dans notre base PostgreSQL await prisma.referralReward.create({ data: { userId: parrainId, refereeId: refereeUserId, rewardType: 'FREE_MONTH', status: 'COMPLETED', stripeTxId: balanceTransaction.id } }); console.log(`[Referral] Parrain ${parrainId} récompensé avec succès d'un mois gratuit !`); } catch (err) { console.error('[Referral] Erreur lors de l\'octroi du crédit Stripe :', err); await prisma.referralReward.create({ data: { userId: parrainId, refereeId: refereeUserId, rewardType: 'FREE_MONTH', status: 'FAILED' } }); } } ``` ### Option B : Offrir des crédits IA (Redis) au Parrain Si le parrain a un compte gratuit, on peut simplement augmenter son quota de crédits IA ou lui offrir des crédits "boost" à vie. ```typescript // lib/billing/referral.ts import { redis } from '@/lib/redis'; async function awardAiCredits(userId: string, amount: number) { // Ajouter des crédits bonus dans Redis pour la période en cours const period = getCurrentPeriodKey(); const key = `usage:${userId}:semantic_search:bonus`; await redis.incrby(key, amount); await prisma.referralReward.create({ data: { userId, refereeId: refereeUserId, rewardType: 'AI_CREDITS', status: 'COMPLETED' } }); } ``` --- ## 5. Intégration Webhook Stripe (Finalisation) Dans le webhook `app/api/billing/webhook/route.ts`, lors du traitement de `checkout.session.completed`, il suffit d'appeler notre fonction de parrainage : ```typescript // app/api/billing/webhook/route.ts if (event.type === 'checkout.session.completed') { const session = event.data.object as Stripe.Checkout.Session; const userId = session.metadata?.userId; if (userId) { // 1. Synchroniser l'abonnement await syncSubscriptionFromStripe(subscription, userId); // 2. Traiter le parrainage si applicable await processReferralReward(userId); } } ``` --- ## 6. Plan de Lancement & Coupon Stripe (No-Code) Pour que ce système fonctionne immédiatement : 1. Créez un **Coupon** général de `10%` sur Stripe nommé "Parrainage Filleul". 2. Dans Stripe, créez un **Code de Promotion orienté client** attaché à ce coupon. 3. Configurez ce code promo avec le motif suivant : **Autoriser les codes de promotion générés par les utilisateurs** ou créez un code parrain par défaut. 4. Dans le code de l'application, nous générons automatiquement le code de parrainage de l'utilisateur lors de son premier partage et l'associons à son compte.