billing-automation

Par wshobson · agents

Construisez des systèmes de facturation automatisés pour les paiements récurrents, la génération de factures, le cycle de vie des abonnements et la gestion des relances. À utiliser lors de la mise en place d'une facturation par abonnement, de l'automatisation de la facturation ou de la gestion de systèmes de paiement récurrents.

npx skills add https://github.com/wshobson/agents --skill billing-automation

Automatisation de la Facturation

Maîtrisez les systèmes de facturation automatisés incluant la facturation récurrente, la génération de factures, la gestion du recouvrement, la proratisation et le calcul fiscal.

Quand utiliser cette compétence

  • Implémenter la facturation des abonnements SaaS
  • Automatiser la génération et la livraison de factures
  • Gérer la récupération des paiements échoués (recouvrement)
  • Calculer les frais proratisés pour les changements de plan
  • Gérer la taxe de vente, la TVA et la GST
  • Traiter la facturation basée sur l'utilisation
  • Gérer les cycles de facturation et les renouvellements

Concepts fondamentaux

1. Cycles de facturation

Intervalles courants :

  • Mensuel (le plus courant pour SaaS)
  • Annuel (tarif réduit à long terme)
  • Trimestriel
  • Hebdomadaire
  • Personnalisé (basé sur l'utilisation, par siège)

2. États de l'abonnement

trial → active → past_due → canceled
              → paused → resumed

3. Gestion du recouvrement

Processus automatisé pour récupérer les paiements échoués via :

  • Calendriers de nouvelles tentatives
  • Notifications aux clients
  • Périodes de grâce
  • Restrictions de compte

4. Proratisation

Ajustement des frais lors de :

  • Amélioration/rétrogradation en milieu de cycle
  • Ajout/suppression de sièges
  • Changement de fréquence de facturation

Démarrage rapide

from billing import BillingEngine, Subscription

# Initialiser le moteur de facturation
billing = BillingEngine()

# Créer un abonnement
subscription = billing.create_subscription(
    customer_id="cus_123",
    plan_id="plan_pro_monthly",
    billing_cycle_anchor=datetime.now(),
    trial_days=14
)

# Traiter le cycle de facturation
billing.process_billing_cycle(subscription.id)

Gestion du cycle de vie des abonnements

from datetime import datetime, timedelta
from enum import Enum

class SubscriptionStatus(Enum):
    TRIAL = "trial"
    ACTIVE = "active"
    PAST_DUE = "past_due"
    CANCELED = "canceled"
    PAUSED = "paused"

class Subscription:
    def __init__(self, customer_id, plan, billing_cycle_day=None):
        self.id = generate_id()
        self.customer_id = customer_id
        self.plan = plan
        self.status = SubscriptionStatus.TRIAL
        self.current_period_start = datetime.now()
        self.current_period_end = self.current_period_start + timedelta(days=plan.trial_days or 30)
        self.billing_cycle_day = billing_cycle_day or self.current_period_start.day
        self.trial_end = datetime.now() + timedelta(days=plan.trial_days) if plan.trial_days else None

    def start_trial(self, trial_days):
        """Démarrer la période d'essai."""
        self.status = SubscriptionStatus.TRIAL
        self.trial_end = datetime.now() + timedelta(days=trial_days)
        self.current_period_end = self.trial_end

    def activate(self):
        """Activer l'abonnement après la période d'essai ou immédiatement."""
        self.status = SubscriptionStatus.ACTIVE
        self.current_period_start = datetime.now()
        self.current_period_end = self.calculate_next_billing_date()

    def mark_past_due(self):
        """Marquer l'abonnement comme en retard après un paiement échoué."""
        self.status = SubscriptionStatus.PAST_DUE
        # Déclencher le flux de recouvrement

    def cancel(self, at_period_end=True):
        """Annuler l'abonnement."""
        if at_period_end:
            self.cancel_at_period_end = True
            # Sera annulé à la fin de la période actuelle
        else:
            self.status = SubscriptionStatus.CANCELED
            self.canceled_at = datetime.now()

    def calculate_next_billing_date(self):
        """Calculer la prochaine date de facturation en fonction de l'intervalle."""
        if self.plan.interval == 'month':
            return self.current_period_start + timedelta(days=30)
        elif self.plan.interval == 'year':
            return self.current_period_start + timedelta(days=365)
        elif self.plan.interval == 'week':
            return self.current_period_start + timedelta(days=7)

Traitement du cycle de facturation

class BillingEngine:
    def process_billing_cycle(self, subscription_id):
        """Traiter la facturation pour un abonnement."""
        subscription = self.get_subscription(subscription_id)

        # Vérifier si la facturation est due
        if datetime.now() < subscription.current_period_end:
            return

        # Générer une facture
        invoice = self.generate_invoice(subscription)

        # Tenter le paiement
        payment_result = self.charge_customer(
            subscription.customer_id,
            invoice.total
        )

        if payment_result.success:
            # Paiement réussi
            invoice.mark_paid()
            subscription.advance_billing_period()
            self.send_invoice(invoice)
        else:
            # Paiement échoué
            subscription.mark_past_due()
            self.start_dunning_process(subscription, invoice)

    def generate_invoice(self, subscription):
        """Générer une facture pour la période de facturation."""
        invoice = Invoice(
            customer_id=subscription.customer_id,
            subscription_id=subscription.id,
            period_start=subscription.current_period_start,
            period_end=subscription.current_period_end
        )

        # Ajouter un élément de ligne d'abonnement
        invoice.add_line_item(
            description=subscription.plan.name,
            amount=subscription.plan.amount,
            quantity=subscription.quantity or 1
        )

        # Ajouter les frais basés sur l'utilisation si applicable
        if subscription.has_usage_billing:
            usage_charges = self.calculate_usage_charges(subscription)
            invoice.add_line_item(
                description="Usage charges",
                amount=usage_charges
            )

        # Calculer la taxe
        tax = self.calculate_tax(invoice.subtotal, subscription.customer)
        invoice.tax = tax

        invoice.finalize()
        return invoice

    def charge_customer(self, customer_id, amount):
        """Facturer le client en utilisant la méthode de paiement enregistrée."""
        customer = self.get_customer(customer_id)

        try:
            # Facturer via le processeur de paiement
            charge = stripe.Charge.create(
                customer=customer.stripe_id,
                amount=int(amount * 100),  # Convertir en centimes
                currency='usd'
            )

            return PaymentResult(success=True, transaction_id=charge.id)
        except stripe.error.CardError as e:
            return PaymentResult(success=False, error=str(e))

Gestion du recouvrement

class DunningManager:
    """Gérer la récupération des paiements échoués."""

    def __init__(self):
        self.retry_schedule = [
            {'days': 3, 'email_template': 'payment_failed_first'},
            {'days': 7, 'email_template': 'payment_failed_reminder'},
            {'days': 14, 'email_template': 'payment_failed_final'}
        ]

    def start_dunning_process(self, subscription, invoice):
        """Démarrer le processus de recouvrement pour un paiement échoué."""
        dunning_attempt = DunningAttempt(
            subscription_id=subscription.id,
            invoice_id=invoice.id,
            attempt_number=1,
            next_retry=datetime.now() + timedelta(days=3)
        )

        # Envoyer une notification d'échec initial
        self.send_dunning_email(subscription, 'payment_failed_first')

        # Planifier les nouvelles tentatives
        self.schedule_retries(dunning_attempt)

    def retry_payment(self, dunning_attempt):
        """Réessayer le paiement échoué."""
        subscription = self.get_subscription(dunning_attempt.subscription_id)
        invoice = self.get_invoice(dunning_attempt.invoice_id)

        # Tenter le paiement à nouveau
        result = self.charge_customer(subscription.customer_id, invoice.total)

        if result.success:
            # Paiement réussi
            invoice.mark_paid()
            subscription.status = SubscriptionStatus.ACTIVE
            self.send_dunning_email(subscription, 'payment_recovered')
            dunning_attempt.mark_resolved()
        else:
            # Toujours en échec
            dunning_attempt.attempt_number += 1

            if dunning_attempt.attempt_number < len(self.retry_schedule):
                # Planifier la prochaine tentative
                next_retry_config = self.retry_schedule[dunning_attempt.attempt_number]
                dunning_attempt.next_retry = datetime.now() + timedelta(days=next_retry_config['days'])
                self.send_dunning_email(subscription, next_retry_config['email_template'])
            else:
                # Tentatives épuisées, annuler l'abonnement
                subscription.cancel(at_period_end=False)
                self.send_dunning_email(subscription, 'subscription_canceled')

    def send_dunning_email(self, subscription, template):
        """Envoyer une notification de recouvrement au client."""
        customer = self.get_customer(subscription.customer_id)

        email_content = self.render_template(template, {
            'customer_name': customer.name,
            'amount_due': subscription.plan.amount,
            'update_payment_url': f"https://app.example.com/billing"
        })

        send_email(
            to=customer.email,
            subject=email_content['subject'],
            body=email_content['body']
        )

Proratisation

class ProrationCalculator:
    """Calculer les frais proratisés pour les changements de plan."""

    @staticmethod
    def calculate_proration(old_plan, new_plan, period_start, period_end, change_date):
        """Calculer la proratisation pour un changement de plan."""
        # Jours dans la période actuelle
        total_days = (period_end - period_start).days

        # Jours utilisés sur l'ancien plan
        days_used = (change_date - period_start).days

        # Jours restants sur le nouveau plan
        days_remaining = (period_end - change_date).days

        # Calculer les montants proratisés
        unused_amount = (old_plan.amount / total_days) * days_remaining
        new_plan_amount = (new_plan.amount / total_days) * days_remaining

        # Frais/crédit net
        proration = new_plan_amount - unused_amount

        return {
            'old_plan_credit': -unused_amount,
            'new_plan_charge': new_plan_amount,
            'net_proration': proration,
            'days_used': days_used,
            'days_remaining': days_remaining
        }

    @staticmethod
    def calculate_seat_proration(current_seats, new_seats, price_per_seat, period_start, period_end, change_date):
        """Calculer la proratisation pour les changements de sièges."""
        total_days = (period_end - period_start).days
        days_remaining = (period_end - change_date).days

        # Frais de sièges supplémentaires
        additional_seats = new_seats - current_seats
        prorated_amount = (additional_seats * price_per_seat / total_days) * days_remaining

        return {
            'additional_seats': additional_seats,
            'prorated_charge': max(0, prorated_amount),  # Aucun remboursement pour suppression de sièges en milieu de cycle
            'effective_date': change_date
        }

Calcul fiscal

class TaxCalculator:
    """Calculer la taxe de vente, la TVA, la GST."""

    def __init__(self):
        # Taux fiscaux par région
        self.tax_rates = {
            'US_CA': 0.0725,  # Taxe de vente en Californie
            'US_NY': 0.04,    # Taxe de vente à New York
            'GB': 0.20,       # TVA au Royaume-Uni
            'DE': 0.19,       # TVA en Allemagne
            'FR': 0.20,       # TVA en France
            'AU': 0.10,       # GST en Australie
        }

    def calculate_tax(self, amount, customer):
        """Calculer la taxe applicable."""
        # Déterminer la juridiction fiscale
        jurisdiction = self.get_tax_jurisdiction(customer)

        if not jurisdiction:
            return 0

        # Obtenir le taux fiscal
        tax_rate = self.tax_rates.get(jurisdiction, 0)

        # Calculer la taxe
        tax = amount * tax_rate

        return {
            'tax_amount': tax,
            'tax_rate': tax_rate,
            'jurisdiction': jurisdiction,
            'tax_type': self.get_tax_type(jurisdiction)
        }

    def get_tax_jurisdiction(self, customer):
        """Déterminer la juridiction fiscale en fonction de la localisation du client."""
        if customer.country == 'US':
            # États-Unis : Taxe basée sur l'état du client
            return f"US_{customer.state}"
        elif customer.country in ['GB', 'DE', 'FR']:
            # UE : TVA
            return customer.country
        elif customer.country == 'AU':
            # Australie : GST
            return 'AU'
        else:
            return None

    def get_tax_type(self, jurisdiction):
        """Obtenir le type de taxe pour la juridiction."""
        if jurisdiction.startswith('US_'):
            return 'Sales Tax'
        elif jurisdiction in ['GB', 'DE', 'FR']:
            return 'VAT'
        elif jurisdiction == 'AU':
            return 'GST'
        return 'Tax'

    def validate_vat_number(self, vat_number, country):
        """Valider un numéro de TVA européen."""
        # Utiliser l'API VIES pour la validation
        # Retourne True si valide, False sinon
        pass

Génération de factures

class Invoice:
    def __init__(self, customer_id, subscription_id=None):
        self.id = generate_invoice_number()
        self.customer_id = customer_id
        self.subscription_id = subscription_id
        self.status = 'draft'
        self.line_items = []
        self.subtotal = 0
        self.tax = 0
        self.total = 0
        self.created_at = datetime.now()

    def add_line_item(self, description, amount, quantity=1):
        """Ajouter un élément de ligne à la facture."""
        line_item = {
            'description': description,
            'unit_amount': amount,
            'quantity': quantity,
            'total': amount * quantity
        }
        self.line_items.append(line_item)
        self.subtotal += line_item['total']

    def finalize(self):
        """Finaliser la facture et calculer le total."""
        self.total = self.subtotal + self.tax
        self.status = 'open'
        self.finalized_at = datetime.now()

    def mark_paid(self):
        """Marquer la facture comme payée."""
        self.status = 'paid'
        self.paid_at = datetime.now()

    def to_pdf(self):
        """Générer une facture PDF."""
        from reportlab.pdfgen import canvas

        # Générer le PDF
        # Inclure : informations de l'entreprise, informations du client, éléments de ligne, taxe, total
        pass

    def to_html(self):
        """Générer une facture HTML."""
        template = """
        <!DOCTYPE html>
        <html>
        <head><title>Invoice #{invoice_number}</title></head>
        <body>
            <h1>Invoice #{invoice_number}</h1>
            <p>Date: {date}</p>
            <h2>Bill To:</h2>
            <p>{customer_name}<br>{customer_address}</p>
            <table>
                <tr><th>Description</th><th>Quantity</th><th>Amount</th></tr>
                {line_items}
            </table>
            <p>Subtotal: ${subtotal}</p>
            <p>Tax: ${tax}</p>
            <h3>Total: ${total}</h3>
        </body>
        </html>
        """

        return template.format(
            invoice_number=self.id,
            date=self.created_at.strftime('%Y-%m-%d'),
            customer_name=self.customer.name,
            customer_address=self.customer.address,
            line_items=self.render_line_items(),
            subtotal=self.subtotal,
            tax=self.tax,
            total=self.total
        )

Facturation basée sur l'utilisation

class UsageBillingEngine:
    """Suivre et facturer l'utilisation."""

    def track_usage(self, customer_id, metric, quantity):
        """Suivre un événement d'utilisation."""
        UsageRecord.create(
            customer_id=customer_id,
            metric=metric,
            quantity=quantity,
            timestamp=datetime.now()
        )

    def calculate_usage_charges(self, subscription, period_start, period_end):
        """Calculer les frais d'utilisation dans la période de facturation."""
        usage_records = UsageRecord.get_for_period(
            subscription.customer_id,
            period_start,
            period_end
        )

        total_usage = sum(record.quantity for record in usage_records)

        # Tarification échelonnée
        if subscription.plan.pricing_model == 'tiered':
            charge = self.calculate_tiered_pricing(total_usage, subscription.plan.tiers)
        # Tarification à l'unité
        elif subscription.plan.pricing_model == 'per_unit':
            charge = total_usage * subscription.plan.unit_price
        # Tarification en volume
        elif subscription.plan.pricing_model == 'volume':
            charge = self.calculate_volume_pricing(total_usage, subscription.plan.tiers)

        return charge

    def calculate_tiered_pricing(self, total_usage, tiers):
        """Calculer le coût en utilisant une tarification échelonnée."""
        charge = 0
        remaining = total_usage

        for tier in sorted(tiers, key=lambda x: x['up_to']):
            tier_usage = min(remaining, tier['up_to'] - tier['from'])
            charge += tier_usage * tier['unit_price']
            remaining -= tier_usage

            if remaining <= 0:
                break

        return charge

Skills similaires