espace-paie-odentas/lib/webhookDeduplication.ts
odentas 6170365fc0 feat: Ajout programme de parrainage, bannières promo, déduplication webhooks et avenants signés
- Programme de parrainage (referrals):
  * Page /parrainage pour clients et staff
  * API /api/referrals (GET, POST)
  * Table referrals avec tracking complet
  * Email template avec design orange/gradient
  * Réductions: 30€ HT parrain, 20€ HT filleul

- Bannières promotionnelles (promo_banners):
  * Page staff /staff/offres-promo pour gérer les bannières
  * API /api/promo-banners (CRUD complet)
  * Composant PromoBanner affiché en haut de l'espace
  * Compte à rebours optionnel
  * Customisation couleurs (gradient, texte, CTA)

- Déduplication des webhooks DocuSeal:
  * Table webhook_events pour tracker les webhooks traités
  * Helper checkAndMarkWebhookProcessed()
  * Intégré dans docuseal-amendment et docuseal-amendment-completed
  * Prévient les doublons d'emails

- Avenants signés:
  * API GET /api/contrats/[id]/avenants
  * Affichage des avenants signés dans DocumentsCard
  * Génération d'URLs presignées S3

- Brouillons d'emails groupés:
  * Table bulk_email_drafts pour sauvegarder les brouillons
  * Template HTML bulk-email-template.html

- Améliorations ContractsGrid:
  * Ajout filtre par production (dépendant de la structure)
  * Tri par production

- Templates emails:
  * referral-template.html (parrainage)
  * bulk-email-template.html (emails groupés staff)
2025-10-31 23:31:53 +01:00

90 lines
2.8 KiB
TypeScript

import { createSbServiceRole } from "./supabaseServer";
/**
* Vérifie si un webhook a déjà été traité pour éviter les doublons
* Utilise une clé unique basée sur submission_id + event_type + timestamp
*
* @param submissionId - ID de la soumission DocuSeal
* @param eventType - Type d'événement (ex: "avenant_employer_signed")
* @param timestamp - Timestamp de l'événement (optionnel, sinon on utilise l'heure actuelle)
* @param payload - Données du webhook (optionnel, pour debug)
* @returns true si le webhook est nouveau, false si déjà traité
*/
export async function checkAndMarkWebhookProcessed(
submissionId: string,
eventType: string,
timestamp?: string,
payload?: any
): Promise<boolean> {
const supabase = createSbServiceRole();
// Créer une clé unique
// On utilise submission_id + event_type (pas de timestamp car DocuSeal peut renvoyer le même timestamp)
const webhookKey = `${submissionId}_${eventType}`;
console.log(`🔍 [WEBHOOK DEDUP] Vérification: ${webhookKey}`);
try {
// Essayer d'insérer l'événement
const { data, error } = await supabase
.from("webhook_events")
.insert({
webhook_key: webhookKey,
event_type: eventType,
submission_id: submissionId,
payload: payload || null,
})
.select()
.single();
if (error) {
// Si l'erreur est une violation de contrainte unique, c'est un doublon
if (error.code === "23505") {
console.log(`⚠️ [WEBHOOK DEDUP] Doublon détecté: ${webhookKey}`);
return false; // Webhook déjà traité
}
// Autre erreur
console.error(`❌ [WEBHOOK DEDUP] Erreur lors de l'insertion:`, error);
// En cas d'erreur technique, on laisse passer pour éviter de bloquer
return true;
}
console.log(`✅ [WEBHOOK DEDUP] Nouveau webhook: ${webhookKey}`);
return true; // Nouveau webhook, on peut traiter
} catch (error) {
console.error(`❌ [WEBHOOK DEDUP] Exception:`, error);
// En cas d'exception, on laisse passer pour éviter de bloquer
return true;
}
}
/**
* Vérifie si un webhook a déjà été traité (lecture seule)
* Utile pour vérifier sans marquer comme traité
*/
export async function isWebhookProcessed(
submissionId: string,
eventType: string
): Promise<boolean> {
const supabase = createSbServiceRole();
const webhookKey = `${submissionId}_${eventType}`;
try {
const { data, error } = await supabase
.from("webhook_events")
.select("id")
.eq("webhook_key", webhookKey)
.maybeSingle();
if (error) {
console.error(`❌ [WEBHOOK DEDUP] Erreur vérification:`, error);
return false;
}
return !!data; // true si trouvé, false sinon
} catch (error) {
console.error(`❌ [WEBHOOK DEDUP] Exception:`, error);
return false;
}
}