perf: memo GridCard, fuse save fns, fix slash tab active color
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m32s
CI / Deploy production (on server) (push) Has been skipped

This commit is contained in:
Antigravity
2026-06-14 14:06:05 +00:00
parent a8785ed4f1
commit a623454347
120 changed files with 12301 additions and 785 deletions

View File

@@ -0,0 +1,211 @@
# 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
1. **Viralité (PLG)** : Encourager les utilisateurs existants à partager Momento 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.