espace-paie-odentas/app/api/staff/amendments/[id]/send-signature/route.ts
odentas 5b72941777 feat: Système complet de gestion des avenants avec signatures électroniques
 Nouvelles fonctionnalités
- Page de gestion des avenants (/staff/avenants)
- Page de détail d'un avenant (/staff/avenants/[id])
- Création d'avenants (objet, durée, rémunération)
- Génération automatique de PDF d'avenant
- Signature électronique via DocuSeal (employeur puis salarié)
- Changement manuel du statut d'un avenant
- Suppression d'avenants

🔧 Routes API
- POST /api/staff/amendments/create - Créer un avenant
- POST /api/staff/amendments/generate-pdf - Générer le PDF
- POST /api/staff/amendments/[id]/send-signature - Envoyer en signature
- POST /api/staff/amendments/[id]/change-status - Changer le statut
- POST /api/webhooks/docuseal-amendment - Webhook après signature employeur
- GET /api/signatures-electroniques/avenants - Liste des avenants en signature

📧 Système email universel v2
- Migration vers le système universel v2 pour les emails d'avenants
- Template 'signature-request-employee-amendment' pour salariés
- Insertion automatique dans DynamoDB pour la Lambda
- Mise à jour automatique du statut dans Supabase

🗄️ Base de données
- Table 'avenants' avec tous les champs (objet, durée, rémunération)
- Colonnes de notification (last_employer_notification_at, last_employee_notification_at)
- Liaison avec cddu_contracts

🎨 Composants
- AvenantDetailPageClient - Détail complet d'un avenant
- ChangeStatusModal - Changement de statut manuel
- SendSignatureModal - Envoi en signature
- DeleteAvenantModal - Suppression avec confirmation
- AvenantSuccessModal - Confirmation de création

📚 Documentation
- AVENANT_EMAIL_SYSTEM_MIGRATION.md - Guide complet de migration

🐛 Corrections
- Fix parsing défensif dans Lambda AWS
- Fix récupération des données depuis DynamoDB
- Fix statut MFA !== 'verified' au lieu de === 'unverified'
2025-10-23 15:30:11 +02:00

324 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
import axios from "axios";
import { sendUniversalEmailV2 } from "@/lib/emailTemplateService";
export const dynamic = "force-dynamic";
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
console.log("🔵 [SEND SIGNATURE] Début de l'envoi en signature pour avenant:", id);
const supabase = createRouteHandlerClient({ cookies });
// Vérifier l'authentification
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
console.error("❌ [SEND SIGNATURE] Erreur authentification:", authError);
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
console.log("✅ [SEND SIGNATURE] Utilisateur authentifié:", user.id);
// Vérifier que l'utilisateur est staff
const { data: staffUser } = await supabase
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
if (!staffUser?.is_staff) {
console.error("❌ [SEND SIGNATURE] Utilisateur non staff");
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
}
console.log("✅ [SEND SIGNATURE] Utilisateur est staff");
// Récupérer l'avenant avec les données du contrat, salarié et organisation
console.log("🔍 [SEND SIGNATURE] Récupération de l'avenant avec jointures...");
const { data: avenant, error: avenantError } = await supabase
.from("avenants")
.select(`
*,
cddu_contracts (
*,
salaries (
prenom,
nom,
adresse_mail
),
organizations (
id,
name,
organization_details (
email_signature,
prenom_signataire,
code_employeur
)
)
)
`)
.eq("id", id)
.single();
console.log("📊 [SEND SIGNATURE] Résultat requête avenant:", {
error: avenantError,
found: !!avenant,
hasContract: !!avenant?.cddu_contracts,
hasSalarie: !!avenant?.cddu_contracts?.salaries,
hasOrg: !!avenant?.cddu_contracts?.organizations,
hasOrgDetails: !!avenant?.cddu_contracts?.organizations?.organization_details
});
if (avenantError || !avenant) {
console.error("❌ [SEND SIGNATURE] Avenant non trouvé:", avenantError);
return NextResponse.json({ error: "Avenant non trouvé" }, { status: 404 });
}
const contract = avenant.cddu_contracts;
const salarie = contract.salaries;
const organization = contract.organizations;
const orgDetails = organization.organization_details;
console.log("📋 [SEND SIGNATURE] Données récupérées:", {
contractId: contract?.id,
salariePrenom: salarie?.prenom,
salarieNom: salarie?.nom,
salarieEmail: salarie?.adresse_mail,
orgName: organization?.name,
orgEmail: orgDetails?.email_signature,
pdfS3Key: avenant.pdf_s3_key
});
// Vérifier que le PDF existe
if (!avenant.pdf_s3_key) {
console.error("❌ [SEND SIGNATURE] PDF S3 key manquant");
return NextResponse.json(
{ error: "Le PDF de l'avenant doit être généré avant l'envoi en signature" },
{ status: 400 }
);
}
// Vérifier que les détails de l'organisation sont disponibles
if (!orgDetails || !orgDetails.email_signature) {
console.error("❌ [SEND SIGNATURE] Email signature manquant:", {
hasOrgDetails: !!orgDetails,
emailSignature: orgDetails?.email_signature
});
return NextResponse.json(
{ error: "Email de signature de l'organisation non configuré" },
{ status: 400 }
);
}
console.log("✅ [SEND SIGNATURE] Validations passées, récupération du PDF depuis S3...");
// Récupérer le PDF depuis S3
const s3Client = new S3Client({
region: process.env.AWS_REGION || "eu-west-3",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const s3Object = await s3Client.send(
new GetObjectCommand({
Bucket: "odentas-docs",
Key: avenant.pdf_s3_key,
})
);
const pdfBuffer = await s3Object.Body?.transformToByteArray();
if (!pdfBuffer) {
console.error("❌ [SEND SIGNATURE] Impossible de lire le PDF depuis S3");
return NextResponse.json({ error: "Impossible de lire le PDF" }, { status: 500 });
}
console.log("✅ [SEND SIGNATURE] PDF récupéré depuis S3, taille:", pdfBuffer.length, "bytes");
const pdfBase64 = Buffer.from(pdfBuffer).toString("base64");
console.log("🔵 [SEND SIGNATURE] Création du template DocuSeal...");
// Créer le template DocuSeal
const templateResponse = await axios.post(
"https://api.docuseal.eu/templates/pdf",
{
name: `AVENANT - CDDU - ${contract.contract_number}`,
documents: [{ name: avenant.numero_avenant, file: pdfBase64 }],
},
{
headers: {
"X-Auth-Token": process.env.DOCUSEAL_TOKEN!,
"Content-Type": "application/json",
},
}
);
const templateId = templateResponse.data.id;
console.log("✅ [SEND SIGNATURE] Template DocuSeal créé:", templateId);
// Insérer les données dans DynamoDB pour la Lambda
console.log("🔵 [SEND SIGNATURE] Insertion des données dans DynamoDB...");
const dynamoDBClient = new DynamoDBClient({
region: process.env.AWS_REGION || "eu-west-3",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const formatDateForDisplay = (dateStr?: string) => {
if (!dateStr) return "-";
const [y, m, d] = dateStr.split("-");
return `${d}/${m}/${y}`;
};
try {
await dynamoDBClient.send(
new PutItemCommand({
TableName: "DocuSealNotification",
Item: {
submission_id: { S: avenant.numero_avenant }, // Clé primaire : numéro de l'avenant
employeeEmail: { S: salarie.adresse_mail },
reference: { S: contract.contract_number },
salarie: { S: `${salarie.prenom} ${salarie.nom}` },
date: { S: formatDateForDisplay(contract.date_debut) },
poste: { S: contract.fonction || "Non spécifié" },
analytique: { S: contract.analytique || "Non spécifié" },
structure: { S: organization.name },
prenom_salarie: { S: salarie.prenom },
prenom_signataire: { S: orgDetails.prenom_signataire || "Non spécifié" },
code_employeur: { S: orgDetails.code_employeur || "Non spécifié" },
matricule: { S: contract.matricule || "Non spécifié" },
created_at: { S: new Date().toISOString() },
},
})
);
console.log("✅ [SEND SIGNATURE] Données insérées dans DynamoDB");
} catch (dynamoError: any) {
console.error("❌ [SEND SIGNATURE] Erreur DynamoDB:", dynamoError);
// On continue quand même pour créer la soumission DocuSeal
}
console.log("🔵 [SEND SIGNATURE] Création de la soumission DocuSeal avec emails:", {
employeur: orgDetails.email_signature,
salarie: salarie.adresse_mail
});
// Créer la soumission DocuSeal
const submissionResponse = await axios.post(
"https://api.docuseal.eu/submissions",
{
template_id: templateId,
send_email: false,
submitters: [
{ role: "Employeur", email: orgDetails.email_signature },
{ role: "Salarié", email: salarie.adresse_mail },
],
},
{
headers: {
"X-Auth-Token": process.env.DOCUSEAL_TOKEN!,
"Content-Type": "application/json",
},
}
);
console.log("✅ [SEND SIGNATURE] Soumission DocuSeal créée:", submissionResponse.data);
const employerSubmission = submissionResponse.data.find((sub: any) => sub.role === "Employeur");
const employeeSubmission = submissionResponse.data.find((sub: any) => sub.role === "Salarié");
console.log("📋 [SEND SIGNATURE] Soumissions extraites:", {
hasEmployer: !!employerSubmission,
hasEmployee: !!employeeSubmission,
employerSlug: employerSubmission?.slug,
employeeSlug: employeeSubmission?.slug
});
if (!employerSubmission || !employeeSubmission) {
console.error("❌ [SEND SIGNATURE] Soumissions DocuSeal incomplètes");
return NextResponse.json(
{ error: "Impossible de créer les soumissions DocuSeal" },
{ status: 500 }
);
}
const employerSlug = employerSubmission.slug;
const signatureLink = `https://staging.paie.odentas.fr/odentas-sign?docuseal_id=${employerSlug}`;
console.log("✅ [SEND SIGNATURE] Lien de signature généré:", signatureLink);
console.log("🔵 [SEND SIGNATURE] Mise à jour de l'avenant dans Supabase...");
// Mettre à jour l'avenant avec les informations DocuSeal
await supabase
.from("avenants")
.update({
docuseal_template_id: templateId,
docuseal_submission_id: employerSubmission.submission_id,
employer_docuseal_slug: employerSlug,
employee_docuseal_slug: employeeSubmission.slug,
signature_status: "pending_employer",
statut: "pending", // Passer de "draft" à "pending"
last_employer_notification_at: new Date().toISOString(),
})
.eq("id", id);
console.log("✅ [SEND SIGNATURE] Avenant mis à jour dans Supabase");
console.log("🔵 [SEND SIGNATURE] Envoi de l'email à l'employeur...");
// Formater la date de début
const formatDate = (dateStr?: string) => {
if (!dateStr) return "-";
const [y, m, d] = dateStr.split("-");
return `${d}/${m}/${y}`;
};
// Envoyer l'email via le système universel
try {
await sendUniversalEmailV2({
type: 'signature-request-employer',
toEmail: orgDetails.email_signature,
subject: `Signature électronique requise Avenant ${avenant.numero_avenant}`,
data: {
firstName: orgDetails.prenom_signataire || undefined,
organizationName: organization.name,
employerCode: orgDetails.code_employeur || "Non spécifié",
handlerName: "Renaud BREVIERE-ABRAHAM",
documentType: `Avenant ${avenant.numero_avenant}`,
employeeName: `${salarie.prenom} ${salarie.nom}`,
contractReference: contract.contract_number,
status: 'En attente',
ctaUrl: signatureLink,
contractId: avenant.id, // Pour traçabilité dans email_logs
},
});
console.log("✅ [SEND SIGNATURE] Email envoyé avec succès");
} catch (emailError: any) {
console.error("❌ [SEND SIGNATURE] Erreur envoi email:", emailError);
// On continue quand même car DocuSeal est créé
}
console.log("🎉 [SEND SIGNATURE] Processus terminé avec succès");
return NextResponse.json({
success: true,
message: "Avenant envoyé en signature à l'employeur",
signatureLink,
});
} catch (error: any) {
console.error("❌❌❌ [SEND SIGNATURE] ERREUR FATALE:", error);
console.error("Stack trace:", error.stack);
return NextResponse.json(
{ error: "Erreur serveur", details: error.message },
{ status: 500 }
);
}
}