Skip to content

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