Remboursements & Annulations - Guide Complet
Date de création : 11 novembre 2025
Statut : 🔵 En cours de rédaction
Propriétaire : Équipe
Dernière mise à jour : 11 novembre 2025
đź“– Guide d'Utilisation
Ce document couvre : - Politique de remboursement : Définition et implémentation - Gestion des annulations : Immédiate vs avec délai - Conformité légale : Délais par juridiction - Implémentation technique : Stripe Refunds API - User stories : Cas d'usage détaillés
1. Politique de Remboursement
1.1 Définition de la Politique
Questions Ă se poser :
| Question | Réponse Recommandée | Notes |
|---|---|---|
| Délai de remboursement | 14-30 jours | RGPD/CCPA standard |
| Conditions | Remboursement complet | Sauf abus |
| Exceptions | Après 7 jours d'utilisation | À définir |
| Processus | Automatique ou manuel ? | Automatique recommandé |
| Moyen de remboursement | Carte de crédit | Même moyen que paiement |
1.2 Politique Recommandée pour Yinshi
Proposition :
POLITIQUE DE REMBOURSEMENT
1. Délai de rétractation : 14 jours calendaires
- À partir de la date d'achat
- Applicable Ă tous les abonnements
2. Conditions de remboursement :
- Remboursement complet du montant payé
- Aucune question posée
- Remboursement sur le moyen de paiement original
3. Exceptions :
- Après 7 jours d'utilisation active
- En cas d'abus (multiples remboursements)
- Après annulation volontaire
4. Processus :
- Utilisateur demande remboursement dans l'app
- Remboursement automatique via Stripe
- Email de confirmation
- Accès révoqué immédiatement
5. Délais de traitement :
- Remboursement initié : 1-2 jours
- Apparition sur compte : 5-10 jours (selon banque)
1.3 Obligations Légales par Juridiction
France (RGPD + Code de la Consommation)
| Obligation | Détail |
|---|---|
| Délai de rétractation | 14 jours calendaires |
| Droit de rétractation | Gratuit, sans justification |
| Information | Avant achat |
| Exceptions | Services numériques après livraison (avec consentement) |
Source : https://www.cnil.fr/fr/droit-de-retractation
EU (Directive 2011/83/EU)
| Obligation | Détail |
|---|---|
| Délai de rétractation | 14 jours calendaires |
| Droit de rétractation | Gratuit, sans justification |
| Information | Avant achat |
| Exceptions | Services numériques après livraison (avec consentement) |
Source : https://ec.europa.eu/info/business-economy-euro/consumer-rights-and-requirements/harmonised-rules-consumer-rights_en
USA (CCPA + FTC)
| Obligation | Détail |
|---|---|
| Délai de remboursement | Varie selon État (30-60 jours) |
| Droit de remboursement | Selon politique de l'entreprise |
| Information | Avant achat |
| Exceptions | Services numériques livrés |
Source : https://www.ftc.gov/business-guidance/resources/complying-coppa-frequently-asked-questions
Brésil (LGPD + Code de la Consommation)
| Obligation | Détail |
|---|---|
| Délai de rétractation | 7 jours calendaires |
| Droit de rétractation | Gratuit, sans justification |
| Information | Avant achat |
| Exceptions | Services numériques après livraison |
Source : https://www.gov.br/cidadania/pt-br/acesso-a-informacao/lgpd
1.4 Implémentation Stripe
Étape 1 : Configurer Stripe
// Créer un produit avec politique de remboursement
const product = await stripe.products.create({
name: 'Yinshi Premium',
description: 'Accès premium à Yinshi',
metadata: {
refund_policy: '14_days',
refund_conditions: 'full_refund_no_questions'
}
});
// Créer un prix
const price = await stripe.prices.create({
product: product.id,
unit_amount: 999, // €9.99
currency: 'eur',
recurring: {
interval: 'month',
interval_count: 1
}
});
Étape 2 : Implémenter les Remboursements
// Cloud Function : Traiter une demande de remboursement
exports.processRefund = functions.https.onCall(async (data, context) => {
if (!context.auth) throw new Error('Non authentifié');
const { chargeId, reason } = data;
const uid = context.auth.uid;
try {
// Vérifier que l'utilisateur demande un remboursement pour son propre paiement
const userDoc = await admin.firestore()
.collection('customers')
.doc(uid)
.get();
if (!userDoc.exists) throw new Error('Utilisateur non trouvé');
// Créer le remboursement
const refund = await stripe.refunds.create({
charge: chargeId,
reason: reason, // 'requested_by_customer', 'duplicate', 'fraudulent'
metadata: {
user_id: uid,
refund_reason: reason
}
});
// Logger le remboursement
await admin.firestore()
.collection('audit_logs')
.add({
timestamp: admin.firestore.FieldValue.serverTimestamp(),
user_id: uid,
action: 'refund_requested',
result: 'success',
details: {
refund_id: refund.id,
amount: refund.amount,
reason: reason
}
});
// Envoyer email de confirmation
await sendRefundConfirmationEmail(uid, refund);
return { success: true, refund_id: refund.id };
} catch (error) {
console.error('Erreur remboursement:', error);
throw new Error('Erreur lors du remboursement');
}
});
Étape 3 : Interface Utilisateur
Écran de demande de remboursement :
class RefundRequestScreen extends StatefulWidget {
@override
_RefundRequestScreenState createState() => _RefundRequestScreenState();
}
class _RefundRequestScreenState extends State<RefundRequestScreen> {
String? selectedReason;
final reasons = [
'Pas satisfait du service',
'Erreur de paiement',
'Paiement en double',
'Autre'
];
void _requestRefund() async {
final functions = FirebaseFunctions.instance;
try {
final result = await functions.httpsCallable('processRefund').call({
'chargeId': 'ch_...',
'reason': selectedReason
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Remboursement demandé avec succès'))
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'))
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Demander un remboursement')),
body: Column(
children: [
Text('Pourquoi demandez-vous un remboursement ?'),
...reasons.map((reason) => RadioListTile(
title: Text(reason),
value: reason,
groupValue: selectedReason,
onChanged: (value) => setState(() => selectedReason = value),
)),
ElevatedButton(
onPressed: _requestRefund,
child: Text('Demander remboursement')
)
]
)
);
}
}
2. Gestion des Annulations
2.1 Types d'Annulations
Type 1 : Annulation Immédiate
User clicks "Cancel subscription"
↓
Confirmation demandée
↓
Abonnement annulé immédiatement
↓
Accès révoqué immédiatement
↓
Remboursement au prorata (optionnel)
Cas d'usage : - Utilisateur change d'avis immédiatement - Utilisateur trouve une meilleure offre - Utilisateur n'est pas satisfait
Type 2 : Annulation avec Fin de Période
User clicks "Cancel at end of period"
↓
Confirmation demandée
↓
Abonnement marqué comme "cancel_at_period_end"
↓
Accès maintenu jusqu'à la fin de la période
↓
Abonnement annulé automatiquement à la fin
↓
Pas de remboursement
Cas d'usage : - Utilisateur veut finir sa période payée - Utilisateur veut réfléchir avant d'annuler - Utilisateur veut garder l'accès jusqu'à la fin
2.2 Implémentation Stripe
Annulation Immédiate
// Cloud Function : Annuler immédiatement
exports.cancelSubscriptionImmediate = functions.https.onCall(
async (data, context) => {
if (!context.auth) throw new Error('Non authentifié');
const uid = context.auth.uid;
try {
// Récupérer l'abonnement
const customerDoc = await admin.firestore()
.collection('customers')
.doc(uid)
.get();
const subscriptionId = customerDoc.data().subscriptionId;
// Annuler l'abonnement
const subscription = await stripe.subscriptions.del(subscriptionId);
// Mettre Ă jour Firestore
await admin.firestore()
.collection('customers')
.doc(uid)
.update({
subscriptionStatus: 'canceled',
canceledAt: admin.firestore.FieldValue.serverTimestamp()
});
// Logger
await admin.firestore()
.collection('audit_logs')
.add({
timestamp: admin.firestore.FieldValue.serverTimestamp(),
user_id: uid,
action: 'subscription_canceled_immediate',
result: 'success'
});
return { success: true };
} catch (error) {
throw new Error('Erreur annulation: ' + error.message);
}
}
);
Annulation à la Fin de Période
// Cloud Function : Annuler à la fin de période
exports.cancelSubscriptionAtPeriodEnd = functions.https.onCall(
async (data, context) => {
if (!context.auth) throw new Error('Non authentifié');
const uid = context.auth.uid;
try {
// Récupérer l'abonnement
const customerDoc = await admin.firestore()
.collection('customers')
.doc(uid)
.get();
const subscriptionId = customerDoc.data().subscriptionId;
// Annuler à la fin de période
const subscription = await stripe.subscriptions.update(
subscriptionId,
{ cancel_at_period_end: true }
);
// Mettre Ă jour Firestore
await admin.firestore()
.collection('customers')
.doc(uid)
.update({
subscriptionStatus: 'active',
cancelAtPeriodEnd: true,
cancelAtPeriodEndDate: new Date(subscription.current_period_end * 1000)
});
// Logger
await admin.firestore()
.collection('audit_logs')
.add({
timestamp: admin.firestore.FieldValue.serverTimestamp(),
user_id: uid,
action: 'subscription_canceled_at_period_end',
result: 'success',
details: {
cancelAtDate: subscription.current_period_end
}
});
return { success: true };
} catch (error) {
throw new Error('Erreur annulation: ' + error.message);
}
}
);
2.3 Interface Utilisateur
class CancelSubscriptionScreen extends StatefulWidget {
@override
_CancelSubscriptionScreenState createState() =>
_CancelSubscriptionScreenState();
}
class _CancelSubscriptionScreenState extends State<CancelSubscriptionScreen> {
bool cancelImmediately = false;
void _cancelSubscription() async {
final functions = FirebaseFunctions.instance;
try {
if (cancelImmediately) {
await functions.httpsCallable('cancelSubscriptionImmediate').call();
} else {
await functions.httpsCallable('cancelSubscriptionAtPeriodEnd').call();
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Abonnement annulé'))
);
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'))
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Annuler l\'abonnement')),
body: Column(
children: [
Text('Comment voulez-vous annuler ?'),
RadioListTile(
title: Text('Annuler immédiatement'),
subtitle: Text('Accès révoqué immédiatement'),
value: true,
groupValue: cancelImmediately,
onChanged: (value) => setState(() => cancelImmediately = value ?? false),
),
RadioListTile(
title: Text('Annuler à la fin de la période'),
subtitle: Text('Accès maintenu jusqu\'à la fin'),
value: false,
groupValue: cancelImmediately,
onChanged: (value) => setState(() => cancelImmediately = value ?? false),
),
ElevatedButton(
onPressed: _cancelSubscription,
child: Text('Confirmer annulation')
)
]
)
);
}
}
3. Conformité Légale - Délais d'Annulation
3.1 Délais par Juridiction
France
| Aspect | Délai | Source |
|---|---|---|
| Droit de rétractation | 14 jours | Code de la Consommation |
| Annulation d'abonnement | Immédiate | Pas de délai légal |
| Remboursement | 14 jours après demande | RGPD |
| Notification | Avant achat | Obligatoire |
Détails : - Droit de rétractation : 14 jours calendaires à partir de l'achat - Annulation : Peut être annulée à tout moment (sauf délai de rétractation) - Remboursement : Doit être effectué dans les 14 jours suivant la demande
Source : https://www.cnil.fr/fr/droit-de-retractation
EU (Général)
| Aspect | Délai | Source |
|---|---|---|
| Droit de rétractation | 14 jours | Directive 2011/83/EU |
| Annulation d'abonnement | Immédiate | Pas de délai légal |
| Remboursement | 14 jours après demande | RGPD |
| Notification | Avant achat | Obligatoire |
Détails : - Identique à la France - S'applique à tous les États membres
Source : https://ec.europa.eu/info/business-economy-euro/consumer-rights-and-requirements/harmonised-rules-consumer-rights_en
USA (Général)
| Aspect | Délai | Source |
|---|---|---|
| Droit de remboursement | Varie (30-60 jours) | Selon État |
| Annulation d'abonnement | Immédiate | Pas de délai légal |
| Remboursement | 30-60 jours | Selon État |
| Notification | Avant achat | Obligatoire |
Détails : - Varie selon l'État - Californie (CCPA) : 30 jours minimum - New York : 30 jours minimum - Autres États : Varie
Source : https://www.ftc.gov/business-guidance/resources/complying-coppa-frequently-asked-questions
Brésil
| Aspect | Délai | Source |
|---|---|---|
| Droit de rétractation | 7 jours | Code de la Consommation |
| Annulation d'abonnement | Immédiate | Pas de délai légal |
| Remboursement | 7 jours après demande | LGPD |
| Notification | Avant achat | Obligatoire |
Détails : - Droit de rétractation : 7 jours calendaires - Plus court que l'EU
Source : https://www.gov.br/cidadania/pt-br/acesso-a-informacao/lgpd
3.2 Tableau Récapitulatif
| Juridiction | Rétractation | Annulation | Remboursement | Notes |
|---|---|---|---|---|
| France | 14 jours | Immédiate | 14 jours | RGPD + Code Consommation |
| EU | 14 jours | Immédiate | 14 jours | Directive 2011/83/EU |
| USA | 30-60 jours | Immédiate | 30-60 jours | Varie selon État |
| Brésil | 7 jours | Immédiate | 7 jours | LGPD |
3.3 Implémentation dans le Système
// Déterminer la juridiction de l'utilisateur
function getRefundPolicy(country) {
const policies = {
'FR': { refund_days: 14, cancellation: 'immediate' },
'DE': { refund_days: 14, cancellation: 'immediate' },
'IT': { refund_days: 14, cancellation: 'immediate' },
'ES': { refund_days: 14, cancellation: 'immediate' },
'US': { refund_days: 30, cancellation: 'immediate' },
'CA': { refund_days: 30, cancellation: 'immediate' },
'BR': { refund_days: 7, cancellation: 'immediate' },
'default': { refund_days: 14, cancellation: 'immediate' }
};
return policies[country] || policies['default'];
}
// Vérifier si l'utilisateur peut demander un remboursement
function canRequestRefund(purchaseDate, country) {
const policy = getRefundPolicy(country);
const daysSincePurchase = Math.floor(
(Date.now() - purchaseDate) / (1000 * 60 * 60 * 24)
);
return daysSincePurchase <= policy.refund_days;
}
3.4 Notification de Renouvellement Automatique
⚠️ À valider avec un juriste : les obligations exactes de notification avant reconduction tacite peuvent varier selon le pays (ex : France / Loi Chatel, autres lois locales). Cette section donne une base produit/technique à affiner.
Principes généraux : - L'utilisateur doit comprendre clairement que son abonnement est à renouvellement automatique. - L'utilisateur doit être informé avant l'achat de la fréquence, du montant et du caractère reconductible. - Pour certains pays et types d'abonnement (ex : abonnements annuels), il est recommandé d'envoyer un rappel avant le renouvellement.
Ce que nous faisons côté produit : - Sur l'écran de souscription : - Afficher prix, fréquence (mensuel/annuel) et mention explicite "abonnement à renouvellement automatique". - Lier vers les CGV et la politique de remboursement. - Après souscription : - Envoyer un email de confirmation indiquant : - le type d'abonnement, - la date du prochain renouvellement, - le montant qui sera prélevé, - un lien pour annuler. - Pour les abonnements annuels : - Mettre en place un rappel email X jours avant la date de renouvellement (ex : 7 à 30 jours, à calibrer juridiquement).
Pseudo-code pour le rappel de renouvellement (annuel) :
// Tâche planifiée quotidienne (Cloud Scheduler)
async function sendRenewalReminders() {
const today = new Date();
const reminderWindowDays = 15; // À ajuster par pays / juriste
const subscriptions = await db
.collection('subscriptions')
.where('status', '==', 'active')
.where('interval', '==', 'year')
.get();
for (const sub of subscriptions.docs) {
const data = sub.data();
const renewalDate = data.current_period_end.toDate();
const diffDays = Math.floor(
(renewalDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
);
if (diffDays === reminderWindowDays) {
await sendRenewalReminderEmail({
userId: data.userId,
renewalDate,
amount: data.amount,
currency: data.currency,
});
}
}
}
Points à valider avec un juriste : - Délai minimal / maximal entre le rappel et le renouvellement (ex : 1 mois avant pour les contrats > 1 an). - Pays où un rappel explicite est juridiquement requis vs recommandé. - Contenu légal minimal de l'email (mentions obligatoires).
4. User Stories
US-R1 : Demander un Remboursement
En tant qu'utilisateur
Je veux demander un remboursement
Pour récupérer mon argent si je ne suis pas satisfait
Critères d'acceptation :
- [ ] Bouton "Demander remboursement" visible dans les paramètres
- [ ] Formulaire avec raison du remboursement
- [ ] Vérification du délai de rétractation (14 jours)
- [ ] Remboursement automatique via Stripe
- [ ] Email de confirmation envoyé
- [ ] Audit log créé
- [ ] Accès révoqué après remboursement
US-R2 : Annuler Immédiatement
En tant qu'utilisateur
Je veux annuler mon abonnement immédiatement
Pour arrêter les prélèvements
Critères d'acceptation :
- [ ] Bouton "Annuler maintenant" visible
- [ ] Confirmation demandée
- [ ] Abonnement annulé immédiatement
- [ ] Accès révoqué immédiatement
- [ ] Email de confirmation envoyé
- [ ] Audit log créé
US-R3 : Annuler à la Fin de Période
En tant qu'utilisateur
Je veux annuler mon abonnement à la fin de la période
Pour garder l'accès jusqu'à la fin
Critères d'acceptation :
- [ ] Bouton "Annuler à la fin de la période" visible
- [ ] Confirmation demandée
- [ ] Abonnement marqué comme "cancel_at_period_end"
- [ ] Accès maintenu jusqu'à la fin
- [ ] Date d'annulation affichée
- [ ] Email de confirmation envoyé
- [ ] Audit log créé
US-R4 : Voir la Politique de Remboursement
En tant qu'utilisateur
Je veux voir la politique de remboursement
Pour comprendre mes droits
Critères d'acceptation :
- [ ] Politique affichée dans les paramètres
- [ ] Politique claire et lisible
- [ ] Délai de rétractation indiqué
- [ ] Conditions indiquées
- [ ] Lien vers CGV complets
5. Checklist d'Implémentation
Phase 1 : Recherche & Définition
- [ ] Recherche juridique complétée (délais par pays)
- [ ] Politique de remboursement définie
- [ ] Politique d'annulation définie
- [ ] Validée par juriste (optionnel)
- [ ] Ajoutée aux CGV
Phase 2 : Implémentation
- [ ] Remboursements implémentés (Stripe Refunds API)
- [ ] Annulation immédiate implémentée
- [ ] Annulation à fin de période implémentée
- [ ] Interface utilisateur créée
- [ ] Emails de confirmation implémentés
Phase 3 : Tests
- [ ] Tests unitaires passés
- [ ] Tests d'intégration passés
- [ ] Tests avec Stripe CLI passés
- [ ] Tests manuels passés
- [ ] Audit trails complets
Phase 4 : Déploiement
- [ ] Code review complétée
- [ ] Documentation mise Ă jour
- [ ] Équipe support formée
- [ ] PrĂŞt pour production
6. Ressources
- Stripe Refunds : https://stripe.com/docs/refunds
- Stripe Subscriptions : https://stripe.com/docs/billing/subscriptions/overview
- CNIL - Droit de rétractation : https://www.cnil.fr/fr/droit-de-retractation
- Directive 2011/83/EU : https://ec.europa.eu/info/business-economy-euro/consumer-rights-and-requirements/harmonised-rules-consumer-rights_en
- CCPA : https://oag.ca.gov/privacy/ccpa
- LGPD : https://www.gov.br/cidadania/pt-br/acesso-a-informacao/lgpd
Document créé par Cascade - Remboursements & Annulations pour Yinshi