- 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)
226 lines
7.8 KiB
TypeScript
226 lines
7.8 KiB
TypeScript
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 }
|
||
);
|
||
}
|
||
}
|