espace-paie-odentas/app/api/webhooks/docuseal-amendment-completed/route.ts
odentas 2520a73602 feat: Migration Lambda avenant completion vers Next.js API
- Créé route /api/webhooks/docuseal-amendment-completed
- Ajouté templates emails amendment-completed-employer et amendment-completed-employee
- Intégration système emails universel v2 (Handlebars)
- Logging dans Supabase email_logs (plus d'Airtable)
- Types ajoutés à EmailTypeV2 et EmailType
- Documentation complète dans LAMBDA_MIGRATION_AVENANT_COMPLETION.md
- Script SQL migration dans MIGRATION_SQL_EMAIL_TYPES.md

Migration complète AWS Lambda → Next.js cdg1 (RGPD compliant)
2025-10-23 19:39:38 +02:00

226 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from "next/server";
import { createSbServiceRole } from "@/lib/supabaseServer";
import { sendUniversalEmailV2, EmailDataV2 } from "@/lib/emailTemplateService";
export const dynamic = "force-dynamic";
/**
* Webhook appelé quand TOUTES les parties ont signé l'avenant (employeur + salarié)
* Envoie les emails de confirmation finaux aux deux parties
*
* Flux :
* 1. Employeur signe → Lambda postDocuSealAvenantSalarie → /api/webhooks/docuseal-amendment
* 2. Salarié signe → DocuSeal webhook → Cette route
* 3. Envoi emails de confirmation à l'employeur ET au salarié
*/
export async function POST(request: NextRequest) {
console.log("🎉 [WEBHOOK AVENANT COMPLETED] Début du traitement");
try {
const body = await request.json();
console.log("📦 [WEBHOOK AVENANT COMPLETED] Body reçu:", JSON.stringify(body, null, 2));
// Extraire les données du webhook DocuSeal
const eventType = body.event_type;
const submissionId = body.data?.submission?.id || body.data?.submission_id;
const templateExternalId = body.data?.template?.external_id;
console.log("🔍 [WEBHOOK AVENANT COMPLETED] Event type:", eventType);
console.log("🔍 [WEBHOOK AVENANT COMPLETED] Submission ID:", submissionId);
console.log("🔍 [WEBHOOK AVENANT COMPLETED] Template external ID:", templateExternalId);
// Vérifier que c'est bien un événement de complétion
if (eventType !== "submission.completed") {
console.log(" [WEBHOOK AVENANT COMPLETED] Event type non géré:", eventType);
return NextResponse.json({ message: "Event type ignoré" }, { status: 200 });
}
if (!submissionId) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] submission_id manquant");
return NextResponse.json(
{ error: "submission_id requis" },
{ status: 400 }
);
}
const supabase = createSbServiceRole();
// 1. Récupérer l'avenant depuis Supabase via le docuseal_submission_id
console.log("🔍 [WEBHOOK AVENANT COMPLETED] Recherche de l'avenant...");
const { data: avenant, error: avenantError } = await supabase
.from("avenants")
.select(`
*,
cddu_contracts (
*,
salaries (
prenom,
nom,
adresse_mail
),
organizations (
id,
name,
code_employeur
)
)
`)
.eq("docuseal_submission_id", submissionId)
.maybeSingle();
if (avenantError || !avenant) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Avenant non trouvé:", avenantError);
return NextResponse.json(
{ error: "Avenant non trouvé", details: avenantError?.message },
{ status: 404 }
);
}
console.log("✅ [WEBHOOK AVENANT COMPLETED] Avenant trouvé:", {
id: avenant.id,
numero: avenant.numero_avenant,
contract_id: avenant.contract_id,
});
// 2. Mettre à jour le statut de l'avenant
console.log("🔄 [WEBHOOK AVENANT COMPLETED] Mise à jour du statut...");
const { error: updateError } = await supabase
.from("avenants")
.update({
signature_status: "completed",
completed_at: new Date().toISOString(),
})
.eq("id", avenant.id);
if (updateError) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur mise à jour:", updateError);
} else {
console.log("✅ [WEBHOOK AVENANT COMPLETED] Statut mis à jour: completed");
}
// 3. Préparer les données pour les emails
const contract = avenant.cddu_contracts;
const salarie = contract?.salaries;
const organization = contract?.organizations;
if (!contract || !salarie || !organization) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Données manquantes:", {
hasContract: !!contract,
hasSalarie: !!salarie,
hasOrganization: !!organization,
});
return NextResponse.json(
{ error: "Données du contrat/salarié/organisation manquantes" },
{ status: 400 }
);
}
// Formater la date
const formatDate = (dateStr?: string | null) => {
if (!dateStr) return "-";
try {
const parts = dateStr.split("-");
if (parts.length === 3) {
const [y, m, d] = parts;
return `${d}/${m}/${y}`;
}
return dateStr;
} catch {
return dateStr;
}
};
const startDate = formatDate(contract.start_date);
const employerEmail = organization.email || "hello@odentas.fr"; // Fallback si pas d'email
const employeeEmail = salarie.adresse_mail;
// Récupérer le prénom du signataire depuis l'organisation (profiles)
let employerFirstName = "Employeur";
if (organization.id) {
const { data: profiles } = await supabase
.from("profiles")
.select("first_name, last_name")
.eq("organization_id", organization.id)
.limit(1)
.maybeSingle();
if (profiles?.first_name) {
employerFirstName = profiles.first_name;
}
}
// 4. Envoyer l'email à l'EMPLOYEUR
console.log("📧 [WEBHOOK AVENANT COMPLETED] Envoi email employeur...");
const employerEmailData: EmailDataV2 = {
firstName: employerFirstName,
organizationName: organization.name || "Votre structure",
employerCode: organization.code_employeur || "Non spécifié",
contractReference: contract.contract_number || "Non spécifié",
startDate: startDate,
profession: contract.fonction || "Non spécifié",
productionName: contract.analytique || "Non spécifié",
ctaUrl: "https://paie.odentas.fr/",
ctaText: "Accès à l'Espace Paie",
numeroAvenant: avenant.numero_avenant,
salarieName: `${salarie.prenom} ${salarie.nom}`,
contractType: "CDDU (contrat intermittent)",
};
try {
const employerMessageId = await sendUniversalEmailV2({
type: "amendment-completed-employer",
toEmail: employerEmail,
data: employerEmailData,
});
console.log("✅ [WEBHOOK AVENANT COMPLETED] Email employeur envoyé:", employerMessageId);
} catch (emailError: any) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur email employeur:", emailError);
}
// 5. Envoyer l'email au SALARIÉ
console.log("📧 [WEBHOOK AVENANT COMPLETED] Envoi email salarié...");
const employeeEmailData: EmailDataV2 = {
firstName: salarie.prenom || "Salarié",
organizationName: organization.name || "Votre employeur",
matricule: contract.matricule || "Non spécifié",
contractReference: contract.contract_number || "Non spécifié",
startDate: startDate,
profession: contract.fonction || "Non spécifié",
productionName: contract.analytique || "Non spécifié",
ctaUrl: `https://paie.odentas.fr/dl-avenant?avenant_id=${avenant.id}`,
ctaText: "Télécharger l'avenant",
numeroAvenant: avenant.numero_avenant,
contractType: "CDDU (contrat intermittent)",
};
try {
const employeeMessageId = await sendUniversalEmailV2({
type: "amendment-completed-employee",
toEmail: employeeEmail,
data: employeeEmailData,
});
console.log("✅ [WEBHOOK AVENANT COMPLETED] Email salarié envoyé:", employeeMessageId);
} catch (emailError: any) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur email salarié:", emailError);
}
return NextResponse.json({
success: true,
message: "Emails de confirmation envoyés",
avenantId: avenant.id,
numeroAvenant: avenant.numero_avenant,
});
} catch (error: any) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur:", error);
return NextResponse.json(
{
error: "Erreur lors du traitement",
details: error.message,
},
{ status: 500 }
);
}
}