espace-paie-odentas/app/api/webhooks/docuseal-amendment-completed/route.ts

309 lines
12 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;
const submitters = body.data?.submitters || body.data?.submission?.submitters || [];
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);
console.log("🔍 [WEBHOOK AVENANT COMPLETED] Submitters dans payload:", JSON.stringify(submitters, null, 2));
// Vérifier que c'est bien un événement de complétion FINALE (form.completed)
// "submission.completed" est envoyé à CHAQUE signature individuelle
// "form.completed" est envoyé UNE SEULE FOIS quand TOUS ont signé
if (eventType !== "form.completed") {
console.log(" [WEBHOOK AVENANT COMPLETED] Event type non géré (attendu: form.completed):", eventType);
return NextResponse.json({ message: "Event type ignoré - attendu form.completed" }, { status: 200 });
}
if (!submissionId) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] submission_id manquant");
return NextResponse.json(
{ error: "submission_id requis" },
{ status: 400 }
);
}
// Récupérer les submitters via l'API DocuSeal car le payload ne les contient pas tous
console.log("🔍 [WEBHOOK AVENANT COMPLETED] Récupération des submitters via API DocuSeal...");
let allSubmitters: any[] = [];
try {
const docusealResponse = await fetch(
`${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/docuseal/submissions/${submissionId}`,
{
method: 'GET',
cache: 'no-store',
}
);
if (docusealResponse.ok) {
const submission = await docusealResponse.json();
allSubmitters = submission.submitters || [];
console.log("✅ [WEBHOOK AVENANT COMPLETED] Submitters récupérés:", allSubmitters.map((s: any) => ({
role: s.role,
email: s.email,
status: s.status
})));
} else {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur récupération submitters");
}
} catch (error) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur API DocuSeal:", error);
}
// IMPORTANT: Vérifier que TOUS les submitters ont signé (status = 'completed')
const allSigned = allSubmitters.length > 0 && allSubmitters.every((s: any) => s.status === 'completed');
if (!allSigned) {
console.log("⏸️ [WEBHOOK AVENANT COMPLETED] Tous les submitters n'ont pas encore signé");
console.log("📋 [WEBHOOK AVENANT COMPLETED] Statuts:", allSubmitters.map((s: any) => ({
role: s.role,
email: s.email,
status: s.status
})));
return NextResponse.json({
message: "En attente de toutes les signatures",
submitters: allSubmitters.map((s: any) => ({ role: s.role, status: s.status }))
}, { status: 200 });
}
console.log("✅ [WEBHOOK AVENANT COMPLETED] Toutes les signatures sont complètes !");
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
)
)
`)
.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: "signed",
})
.eq("id", avenant.id);
if (updateError) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur mise à jour avenant:", updateError);
} else {
console.log("✅ [WEBHOOK AVENANT COMPLETED] Statut avenant mis à jour: signed");
}
// 2b. Mettre à jour le contrat pour marquer l'avenant comme signé
if (avenant.contract_id) {
console.log("🔄 [WEBHOOK AVENANT COMPLETED] Mise à jour du contrat...");
const { error: contractUpdateError } = await supabase
.from("cddu_contracts")
.update({
avenant_signe: true,
avenant_signe_date: new Date().toISOString(),
})
.eq("id", avenant.contract_id);
if (contractUpdateError) {
console.error("❌ [WEBHOOK AVENANT COMPLETED] Erreur mise à jour contrat:", contractUpdateError);
} else {
console.log("✅ [WEBHOOK AVENANT COMPLETED] Contrat mis à jour: avenant_signe = true");
}
}
// 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 employeeEmail = salarie.adresse_mail;
// Récupérer les infos employeur depuis organization_details
let employerCode = "Non spécifié";
let employerFirstName = "Employeur";
let employerEmail = "hello@odentas.fr";
if (organization.id) {
const { data: orgDetails } = await supabase
.from("organization_details")
.select("code_employeur, prenom_signataire, email_signature")
.eq("org_id", organization.id)
.maybeSingle();
if (orgDetails) {
if (orgDetails.code_employeur) {
employerCode = orgDetails.code_employeur;
}
if (orgDetails.prenom_signataire) {
employerFirstName = orgDetails.prenom_signataire;
}
if (orgDetails.email_signature) {
employerEmail = orgDetails.email_signature;
}
}
}
// 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: employerCode,
handlerName: "Renaud BREVIERE-ABRAHAM",
contractReference: contract.contract_number || "Non spécifié",
startDate: startDate,
profession: contract.profession || "Non spécifié",
productionName: contract.analytique || contract.production_name || "Non spécifié",
ctaUrl: "https://paie.odentas.fr/",
ctaText: "Accès à l'Espace Paie",
numeroAvenant: avenant.numero_avenant,
employeeName: `${salarie.prenom} ${salarie.nom}`,
matricule: contract.matricule || contract.employee_matricule || "Non spécifié",
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é...");
// Récupérer le slug DocuSeal du salarié depuis l'avenant (stocké lors de la signature employeur)
const employeeDocusealSlug = avenant.employee_docuseal_slug || "";
console.log("🔑 [WEBHOOK AVENANT COMPLETED] Slug salarié:", employeeDocusealSlug);
const employeeEmailData: EmailDataV2 = {
firstName: salarie.prenom || "Salarié",
organizationName: organization.name || "Votre employeur",
matricule: contract.matricule || contract.employee_matricule || "Non spécifié",
contractReference: contract.contract_number || "Non spécifié",
startDate: startDate,
profession: contract.profession || "Non spécifié",
productionName: contract.analytique || contract.production_name || "Non spécifié",
ctaUrl: employeeDocusealSlug
? `https://paie.odentas.fr/signature-salarie/?docuseal_id=${employeeDocusealSlug}`
: `https://paie.odentas.fr/`,
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 }
);
}
}