Aller au contenu principal

Plan d'Implémentation - Paiements & Relances

📋 Vue d'ensemble

Ce plan détaille l'implémentation du système de paiements & relances pour améliorer la gestion des factures en retard et automatiser les relances.

Dernière mise à jour : 2026-01-27

✅ Progrès d'Implémentation

Phase 1 : Détection automatique OVERDUE ✅ COMPLÉTÉ

  • Service créé : backend/src/billing/invoice-overdue/invoice-overdue.service.ts

    • Méthode markOverdueInvoices() pour détecter et marquer les factures en retard
    • Méthode markInvoiceAsOverdue() pour marquer une facture spécifique
    • Validation des transitions avec BillingLifecycleValidator
    • Réconciliation des paiements avant marquage OVERDUE
  • Scheduler créé : backend/src/billing/invoice-overdue/invoice-overdue.scheduler.ts

    • Job quotidien à 4h du matin (@Cron("0 4 * * *"))
    • Utilise executeWithTimeoutAndRetry pour la gestion des timeouts
    • Logging structuré avec scope "INVOICE_OVERDUE_SCHEDULER"
  • Intégration : Services ajoutés à BillingCoreModule et BillingModule

Phase 2 : Système de relances ✅ COMPLÉTÉ (Backend)

  • Migration créée : infra/liquibase/changes/0141_create_invoice_reminders/

    • Table invoice_reminders avec tous les champs nécessaires
    • Indexes pour performance
    • Foreign key vers invoices(id, owner_id)
  • Service créé : backend/src/billing/reminders/invoice-reminders.service.ts

    • getRemindersForInvoice() - Récupérer l'historique des relances
    • shouldSendReminder() - Calculer si une relance doit être envoyée
    • sendReminder() - Envoyer une relance (automatique ou manuelle)
    • sendAutomaticReminders() - Envoyer toutes les relances automatiques
    • Utilise le template payment-reminder avec remplacement de variables
    • Génère des liens client portail pour le paiement
    • Crée des notifications pour l'utilisateur
  • Scheduler créé : backend/src/billing/reminders/invoice-reminders.scheduler.ts

    • Job quotidien à 5h du matin (@Cron("0 5 * * *"))
    • S'exécute après la détection OVERDUE (4h)
  • Contrôleur créé : backend/src/billing/reminders/invoice-reminders.controller.ts

    • GET /invoices/:invoiceId/reminders - Historique des relances
    • POST /invoices/:invoiceId/reminders - Envoyer une relance manuelle
  • Types de base de données : InvoiceReminderTable ajouté à database.types.ts

  • Frontend : À faire (voir Phase 4)

Phase 3 : Amélioration PayPal ✅ COMPLÉTÉ

  • Service de notifications créé : backend/src/paypal/paypal-notification.service.ts

    • sendPaymentSuccessEmailToOwner() - Notification au propriétaire de la facture
    • sendPaymentSuccessEmailToClient() - Notification au client (contact)
    • sendPaymentFailedEmailToOwner() - Notification d'échec au propriétaire
    • sendPaymentPendingEmail() - Notification de paiement en attente
  • Webhooks améliorés : backend/src/paypal/paypal-webhooks.controller.ts

    • Logging détaillé avec scope "PAYPAL_WEBHOOK"
    • Logging des détails de signature webhook (pour debugging)
    • Gestion améliorée des erreurs avec contexte complet
    • Support des statuts PENDING, FAILED, DECLINED
    • Notifications automatiques intégrées dans le webhook
    • Logging des événements non gérés pour monitoring
  • Intégration dans le service de paiement : backend/src/paypal/paypal-payment.service.ts

    • Notification PENDING lors de la création d'un paiement
    • Notifications SUCCESS/FAILED lors de la capture manuelle

Phase 4 : Interface Frontend ✅ COMPLÉTÉ

  • Hook créé : frontend/src/client/quotes-invoices/useInvoiceReminders.ts

    • useInvoiceReminders() - Récupérer l'historique des relances pour une facture
    • useSendInvoiceReminder() - Envoyer une relance manuelle
    • Invalidation automatique des queries après envoi
  • Composant créé : frontend/src/pages/invoices/InvoiceViewPage/components/RemindersSection.tsx

    • Affichage de l'historique des relances envoyées
    • Bouton pour envoyer une relance manuelle (visible uniquement pour factures OVERDUE)
    • Badge "Manuelle" pour les relances envoyées manuellement
    • Affichage des détails (numéro, date, destinataire, sujet)
    • Section masquée si facture non-overdue et aucune relance envoyée
  • Intégration : Composant ajouté à InvoiceView.tsx

    • Section affichée après la description de la facture
    • Utilise les traductions i18n (fr-FR, en-US)
  • Traductions ajoutées :

    • sessions.quotesInvoices.invoices.reminders.* dans translation.json (fr et en)

Phase 6 : Configuration des relances ✅ COMPLÉTÉ

  • Migration créée : infra/liquibase/changes/0142_create_user_reminder_settings/

    • Table user_reminder_settings pour personnaliser les paramètres par utilisateur
    • Champs : enabled, first_reminder_days, second_reminder_days, third_reminder_days, max_reminders, reminder_template_key
    • Valeurs par défaut : 7, 14, 30 jours, max 3 relances
  • Types de base de données : UserReminderSettingsTable ajouté à database.types.ts

  • Service mis à jour : backend/src/billing/reminders/invoice-reminders.service.ts

    • Fonction getReminderSettings() pour récupérer les settings utilisateur (avec fallback sur defaults)
    • shouldSendReminder() utilise maintenant les settings utilisateur
    • sendReminder() utilise le template configuré par l'utilisateur
    • Les valeurs par défaut sont utilisées si l'utilisateur n'a pas de settings personnalisés
    • Méthodes getReminderSettingsForUser() et updateReminderSettingsForUser() pour gérer les settings
  • Contrôleur créé : backend/src/billing/reminders/invoice-reminders.controller.ts

    • ReminderSettingsController avec endpoints :
      • GET /reminder-settings - Récupérer les settings de l'utilisateur
      • POST /reminder-settings - Mettre à jour les settings de l'utilisateur
    • Validation avec Zod (updateReminderSettingsSchema)
  • DTO créé : backend/src/billing/reminders/dto/update-reminder-settings.dto.ts

    • Validation Zod pour les paramètres de relances
  • Hooks React Query créés : frontend/src/client/billing/useReminderSettings.ts

    • useReminderSettings() - Récupérer les settings
    • useUpdateReminderSettings() - Mettre à jour les settings
  • Page de settings créée : frontend/src/pages/settings/ReminderSettingsPage/ReminderSettingsPage.tsx

    • Formulaire avec Switch pour activer/désactiver
    • Inputs pour les délais (J+7, J+14, J+30)
    • Input pour le nombre max de relances
    • Design cohérent avec les autres pages de settings
  • Route ajoutée : /settings/reminders dans settings.routes.tsx

  • Navigation ajoutée :

    • Lien dans UserMenuMobile (menu mobile)
    • Lien dans UserDropdown (menu desktop)
  • Traductions ajoutées :

    • settings.reminders.* dans translation.json (fr et en)
    • settings.tabs.reminders pour le menu

✅ État Actuel

Ce qui existe déjà

  1. Paiements partiels

    • Statut PARTIALLY_PAID implémenté
    • PaymentReconciliationService calcule automatiquement balance_due
    • Support des paiements multiples par facture
  2. Intégration PayPal

    • Création de paiements via PayPal Orders
    • Webhooks pour capturer les paiements
    • Stockage des paiements dans paypal_payments
  3. Intégration Stripe

    • Création de Payment Intents
    • Webhooks pour synchroniser les statuts
    • Stockage des paiements dans stripe_payments
  4. Template de relance

    • Template email payment-reminder.html existe
    • Notification type PAYMENT_LATE défini

Ce qui manque

  1. Job périodique pour marquer OVERDUE : Pas de scheduler pour détecter automatiquement les factures en retard
  2. Système de relances automatiques : Pas de service pour envoyer des relances automatiques
  3. ⚠️ Webhooks PayPal améliorés : Logging et gestion d'erreurs à améliorer
  4. ⚠️ Notifications client PayPal : Notifications à améliorer (succès, échec, en attente)
  5. Suivi des relances : Pas de table pour historiser les relances envoyées
  6. Configuration des relances : Pas de paramètres configurables (délai, fréquence, max)

🎯 Objectifs

  1. Détection automatique des factures en retard

    • Job quotidien pour marquer les factures OVERDUE
    • Basé sur due_date < today et statut non payé
  2. Système de relances automatiques

    • Relances progressives (J+7, J+14, J+30)
    • Limite de relances par facture
    • Historique des relances envoyées
  3. Amélioration PayPal

    • Webhooks avec logging détaillé
    • Notifications client améliorées
    • Meilleure gestion des erreurs
  4. Interface utilisateur

    • Visualisation des relances envoyées
    • Envoi manuel de relances
    • Configuration des paramètres de relance

📐 Architecture

1. Base de données

Table invoice_reminders

CREATE TABLE invoice_reminders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
reminder_number INTEGER NOT NULL, -- 1, 2, 3, etc.
sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
sent_to_email TEXT NOT NULL,
subject TEXT NOT NULL,
template_key TEXT NOT NULL, -- 'payment-reminder'
metadata JSONB, -- Données du template, statut d'envoi, etc.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

CONSTRAINT invoice_reminders_invoice_owner_fkey
FOREIGN KEY (invoice_id, owner_id)
REFERENCES invoices(id, owner_id) ON DELETE CASCADE
);

CREATE INDEX idx_invoice_reminders_invoice_id ON invoice_reminders(invoice_id);
CREATE INDEX idx_invoice_reminders_owner_id ON invoice_reminders(owner_id);
CREATE INDEX idx_invoice_reminders_sent_at ON invoice_reminders(sent_at);

Table user_reminder_settings (optionnel, pour configuration par user)

CREATE TABLE user_reminder_settings (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT true,
first_reminder_days INTEGER NOT NULL DEFAULT 7, -- Jours après due_date
second_reminder_days INTEGER NOT NULL DEFAULT 14,
third_reminder_days INTEGER NOT NULL DEFAULT 30,
max_reminders INTEGER NOT NULL DEFAULT 3,
reminder_template_key TEXT NOT NULL DEFAULT 'payment-reminder',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

2. Services Backend

InvoiceOverdueService

  • Détecte les factures en retard (due_date < today)
  • Met à jour le statut vers OVERDUE
  • Appelle PaymentReconciliationService si nécessaire

InvoiceReminderService

  • Calcule quelles factures nécessitent une relance
  • Envoie les relances par email
  • Enregistre l'historique dans invoice_reminders
  • Respecte les limites (max relances, délais)

PaypalWebhookService (amélioration)

  • Logging détaillé de tous les événements webhook
  • Meilleure gestion des erreurs
  • Notifications client améliorées

3. Schedulers

BillingScheduler

  • @Cron("0 4 * * *") - Tous les jours à 4h du matin
    • Marquer les factures OVERDUE
    • Envoyer les relances automatiques

4. Contrôleurs

InvoiceRemindersController

  • GET /invoices/:id/reminders - Historique des relances
  • POST /invoices/:id/reminders - Envoyer une relance manuelle
  • GET /reminder-settings - Récupérer la configuration
  • PUT /reminder-settings - Mettre à jour la configuration

🔧 Implémentation

Phase 1 : Détection automatique OVERDUE

Fichiers à créer/modifier :

  1. backend/src/billing/invoice-overdue/invoice-overdue.service.ts

    - markOverdueInvoices(): Promise<number>
    - markInvoiceAsOverdue(invoiceId, ownerId): Promise<void>
  2. backend/src/billing/invoice-overdue/invoice-overdue.scheduler.ts

    @Cron("0 4 * * *")
    async markOverdueInvoices()
  3. Migration : Ajouter colonne last_overdue_check_at à invoices (optionnel)

Phase 2 : Système de relances

Fichiers à créer :

  1. backend/src/billing/reminders/invoice-reminders.service.ts

    - sendReminder(invoiceId, ownerId, reminderNumber): Promise<void>
    - getRemindersForInvoice(invoiceId, ownerId): Promise<Reminder[]>
    - shouldSendReminder(invoiceId, ownerId): Promise<{should: boolean, reminderNumber: number}>
    - sendAutomaticReminders(): Promise<number>
  2. backend/src/billing/reminders/invoice-reminders.controller.ts

    • Endpoints REST pour gérer les relances
  3. backend/src/billing/reminders/invoice-reminders.scheduler.ts

    @Cron("0 5 * * *") // 5h du matin, après OVERDUE
    async sendAutomaticReminders()
  4. Migration : Créer table invoice_reminders

Phase 3 : Amélioration PayPal

Fichiers à modifier :

  1. backend/src/paypal/paypal-webhooks.service.ts (nouveau ou améliorer existant)

    • Logging détaillé avec paypal_webhook_events
    • Gestion d'erreurs améliorée
    • Notifications client pour tous les statuts
  2. backend/src/paypal/paypal-notification.service.ts (nouveau)

    • Envoyer notifications client (succès, échec, en attente)
    • Utiliser EmailService ou NotificationsService

Phase 4 : Interface Frontend

Fichiers à créer/modifier :

  1. frontend/src/pages/invoices/InvoiceViewPage/components/RemindersSection.tsx

    • Afficher l'historique des relances
    • Bouton pour envoyer une relance manuelle
  2. frontend/src/hooks/useInvoiceReminders.ts

    • React Query hooks pour les relances
  3. frontend/src/pages/settings/ReminderSettingsPage.tsx (optionnel)

    • Configuration des paramètres de relance

📝 Détails d'Implémentation

1. Détection OVERDUE

// invoice-overdue.service.ts
async markOverdueInvoices(): Promise<number> {
const today = new Date();
today.setHours(0, 0, 0, 0);

const invoices = await this.db.kysely
.selectFrom("invoices")
.selectAll()
.where("due_date", "<", today)
.where((eb) =>
eb.or([
eb("status", "=", InvoiceStatusValues.ISSUED),
eb("status", "=", InvoiceStatusValues.SENT),
eb("status", "=", InvoiceStatusValues.PARTIALLY_PAID),
])
)
.execute();

let updatedCount = 0;
for (const invoice of invoices) {
await this.markInvoiceAsOverdue(invoice.id, invoice.owner_id);
updatedCount++;
}

return updatedCount;
}

2. Calcul des relances

// invoice-reminders.service.ts
async shouldSendReminder(
invoiceId: string,
ownerId: string
): Promise<{ should: boolean; reminderNumber: number }> {
const invoice = await this.getInvoice(invoiceId, ownerId);
if (!invoice || invoice.status !== InvoiceStatusValues.OVERDUE) {
return { should: false, reminderNumber: 0 };
}

const reminders = await this.getRemindersForInvoice(invoiceId, ownerId);
const lastReminder = reminders[0]; // Plus récent

const settings = await this.getReminderSettings(ownerId);
const daysSinceDue = Math.floor(
(Date.now() - new Date(invoice.due_date).getTime()) / (1000 * 60 * 60 * 24)
);

// Déterminer quel numéro de relance envoyer
let reminderNumber = 1;
if (lastReminder) {
reminderNumber = lastReminder.reminder_number + 1;
}

if (reminderNumber > settings.max_reminders) {
return { should: false, reminderNumber };
}

// Vérifier le délai depuis la dernière relance
const daysSinceLastReminder = lastReminder
? Math.floor((Date.now() - new Date(lastReminder.sent_at).getTime()) / (1000 * 60 * 60 * 24))
: daysSinceDue;

const requiredDays = this.getRequiredDaysForReminder(reminderNumber, settings);

return {
should: daysSinceLastReminder >= requiredDays,
reminderNumber,
};
}

3. Envoi de relance

async sendReminder(
invoiceId: string,
ownerId: string,
reminderNumber: number
): Promise<void> {
const invoice = await this.getInvoice(invoiceId, ownerId);
const contact = await this.getContact(invoice.contact_id);
const settings = await this.getReminderSettings(ownerId);

// Générer le contenu de l'email
const emailContent = await this.emailTemplatesService.renderTemplate(
settings.reminder_template_key,
{
invoiceNumber: invoice.number,
amount: invoice.amount,
balanceDue: invoice.balance_due,
dueDate: invoice.due_date,
contactName: contact.first_name + " " + contact.last_name,
reminderNumber,
},
"fr-FR" // ou récupérer depuis user settings
);

// Envoyer l'email
await this.emailService.sendEmail({
to: contact.email,
subject: emailContent.subject,
html: emailContent.html,
});

// Enregistrer la relance
await this.db.kysely
.insertInto("invoice_reminders")
.values({
invoice_id: invoiceId,
owner_id: ownerId,
reminder_number: reminderNumber,
sent_to_email: contact.email,
subject: emailContent.subject,
template_key: settings.reminder_template_key,
metadata: {
balanceDue: invoice.balance_due,
amount: invoice.amount,
},
})
.execute();

// Créer une notification pour l'utilisateur
await this.notificationsService.createForUser(ownerId, {
title: "Relance envoyée",
body: `Relance #${reminderNumber} envoyée pour la facture ${invoice.number}`,
type: "OTHER",
metadata: {
invoice_id: invoiceId,
navigation: {
link: `/invoices/${invoiceId}`,
},
},
});
}

📊 Ordre d'Implémentation Recommandé

  1. Phase 1 : Détection automatique OVERDUE (1-2 jours)

    • Service + Scheduler
    • Tests unitaires
  2. Phase 2 : Système de relances (3-4 jours)

    • Migration table invoice_reminders
    • Service de relances
    • Scheduler
    • Tests
  3. Phase 3 : Amélioration PayPal (1-2 jours)

    • Webhooks améliorés
    • Notifications client
  4. Phase 4 : Interface Frontend (2-3 jours)

    • Composants React
    • Hooks React Query
    • Intégration dans InvoiceViewPage

Total estimé : 7-11 jours de développement


🧪 Tests

Tests unitaires

  • invoice-overdue.service.spec.ts

    • Test marquage OVERDUE
    • Test non-marquage si déjà payée
    • Test non-marquage si due_date future
  • invoice-reminders.service.spec.ts

    • Test calcul shouldSendReminder
    • Test envoi relance
    • Test limite max relances
    • Test délais entre relances

Tests d'intégration

  • Test scheduler OVERDUE
  • Test scheduler relances
  • Test webhook PayPal avec notifications

📚 Documentation

  • Mettre à jour docs/BILLING_LIFECYCLE.md avec les détails des relances
  • Créer backend/src/billing/reminders/README.md
  • Mettre à jour docs/PRIORITES.md une fois complété

🔗 Références