Publication IA: - 4 templates (magazine, brief, essay, simple) avec CSS riche - Rewrite IA (article/exercises/tutorial/reference/mixed) - Modération avec timeout 12s + fallback safe - Quotas publish_enhance par tier (basic=2, pro=15, business=100) - Détection contenu stale (hash) - Migration DB publishedContent/publishedTemplate/publishedSourceHash Fixes: - cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast - _isShared ajouté au type Note (champ virtuel serveur) - callout colors PDF export: extraction fonction pure testable - admin/published: guard note.userId null - Cmd+S fonctionne en mode dialog (pas seulement fullPage) i18n: - 23 clés publish* traduites dans les 15 locales - Extension Web Clipper: 13 locales mise à jour Tests: - callout-colors.test.ts (6 tests) - note-visible-in-view.test.ts (5 tests) - entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs - 199/199 tests passent Tracker: user-stories.md sync avec sprint-status.yaml
212 lines
8.7 KiB
Markdown
212 lines
8.7 KiB
Markdown
# 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.
|