8.7 KiB
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 Momento, intégré à notre base de données PostgreSQL (Prisma) et à Stripe.
1. Objectifs du Système
- Viralité (PLG) : Encourager les utilisateurs existants à partager Momento pour acquérir de nouveaux clients sans budget publicitaire (CAC proche de 0).
- 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).
- Le Filleul (Invité) obtient une réduction immédiate lors de son premier abonnement (ex.
- 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.
// 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
- 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).
- 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).
- Affiche son code promo de parrainage :
3.2 Inscription du Filleul (Cookie & localStorage)
- Le filleul clique sur le lien
https://memento-note.com/signup?ref=MOMENTO-SEPEHR-1234. - Le frontend intercepte le paramètre
refdans l'URL et le stocke de manière persistante dans un cookie ou dans lelocalStorage(durée : 30 jours). - 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
referredByIddu nouveau compteUserà l'ID du parrain.
3.3 Achat de l'Abonnement et Déclenchement de la Récompense
- 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%). - Une fois le paiement validé, Stripe émet l'événement
checkout.session.completedà notre webhook/api/billing/webhook. - Notre helper
sync-subscription-from-stripe.tstraite 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.
- Il vérifie en base de données si le nouvel abonné possède un parrain (
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.
// 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.
// 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 :
// 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 :
- Créez un Coupon général de
10%sur Stripe nommé "Parrainage Filleul". - Dans Stripe, créez un Code de Promotion orienté client attaché à ce coupon.
- 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.
- 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.