espace-paie-odentas/app/api/emails/signature-avenant-salarie/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

239 lines
7.5 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 { sendUniversalEmailV2 } from '@/lib/emailTemplateService';
import { ENV } from '@/lib/cleanEnv';
import { createSbServiceRole } from '@/lib/supabaseServer';
/**
* POST /api/emails/signature-avenant-salarie
*
* Route API appelée par la Lambda postDocuSealAvenantSalarie pour envoyer l'email de signature au salarié
* Utilise le système universel d'emails v2 avec logging automatique
*
* Authentification : API Key dans le header X-API-Key
*/
export async function POST(request: NextRequest) {
console.log('=== API Email Signature Avenant Salarié ===');
try {
// 1. Vérification de l'authentification via API Key
const apiKey = request.headers.get('X-API-Key');
const validApiKey = ENV.LAMBDA_API_KEY;
if (!validApiKey) {
console.error('❌ Configuration error: LAMBDA_API_KEY not configured');
return NextResponse.json(
{ error: 'Server configuration error' },
{ status: 500 }
);
}
if (!apiKey || apiKey !== validApiKey) {
console.error('❌ Unauthorized: Invalid or missing API key');
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
console.log('✅ Authentication successful');
// 2. Récupération et validation des données
const data = await request.json();
console.log('📦 Données reçues:', {
employeeEmail: data.employeeEmail,
reference: data.reference,
structure: data.structure,
firstName: data.firstName,
matricule: data.matricule,
avenantId: data.avenantId
});
const {
employeeEmail,
signatureLink,
reference,
firstName: providedFirstName,
organizationName: providedOrgName,
matricule,
typecontrat,
startDate,
profession,
productionName,
organizationId,
contractId,
avenantId,
numeroAvenant
} = data;
// Validation des champs requis
if (!employeeEmail || !signatureLink || !reference) {
console.error('❌ Champs requis manquants');
return NextResponse.json(
{
error: 'Champs manquants',
required: ['employeeEmail', 'signatureLink', 'reference']
},
{ status: 400 }
);
}
// 3. Récupération du vrai nom de l'organisation depuis Supabase
let organizationName = providedOrgName || 'Employeur';
if (organizationId || contractId) {
console.log('🔍 Récupération du nom de l\'organisation depuis la base de données...');
try {
const supabase = createSbServiceRole();
let actualOrgId = organizationId;
// Si on n'a que le contractId, récupérer l'org_id depuis le contrat
if (!actualOrgId && contractId) {
const { data: contractData } = await supabase
.from('cddu_contracts')
.select('org_id')
.eq('id', contractId)
.single();
if (contractData?.org_id) {
actualOrgId = contractData.org_id;
console.log('✅ org_id récupéré depuis le contrat:', actualOrgId);
}
}
// Récupérer le nom de l'organisation
if (actualOrgId) {
const { data: orgData, error: orgError } = await supabase
.from('organizations')
.select('name')
.eq('id', actualOrgId)
.single();
if (!orgError && orgData?.name) {
organizationName = orgData.name;
console.log('✅ Nom de l\'organisation trouvé:', organizationName);
} else {
console.warn('⚠️ Nom de l\'organisation non trouvé, utilisation du fallback');
}
}
} catch (err) {
console.error('⚠️ Erreur lors de la récupération du nom de l\'organisation:', err);
}
}
// 4. Récupération du prénom depuis Supabase si non fourni
let firstName = providedFirstName;
if (!firstName && (matricule || employeeEmail)) {
console.log('🔍 Récupération du prénom depuis la table salaries...');
try {
const supabase = createSbServiceRole();
// Recherche par matricule ou email
let query = supabase
.from('salaries')
.select('prenom')
.limit(1);
if (organizationId) {
query = query.eq('employer_id', organizationId);
}
// Priorité au matricule
if (matricule) {
query = query.or(`code_salarie.eq.${matricule},num_salarie.eq.${matricule}`);
} else if (employeeEmail) {
query = query.eq('adresse_mail', employeeEmail);
}
const { data: salaryData, error: salaryError } = await query;
if (!salaryError && salaryData && salaryData[0]?.prenom) {
firstName = salaryData[0].prenom;
console.log('✅ Prénom trouvé dans Supabase:', firstName);
} else {
console.warn('⚠️ Prénom non trouvé dans Supabase');
firstName = 'Salarié(e)'; // Fallback
}
} catch (err) {
console.error('⚠️ Erreur lors de la récupération du prénom:', err);
firstName = 'Salarié(e)'; // Fallback
}
} else if (!firstName) {
firstName = 'Salarié(e)'; // Fallback si aucune donnée disponible
}
// 5. Mise à jour du last_employee_notification_at dans la table avenants
if (avenantId) {
console.log('🔄 Mise à jour de last_employee_notification_at pour l\'avenant:', avenantId);
try {
const supabase = createSbServiceRole();
const { error: updateError } = await supabase
.from('avenants')
.update({
last_employee_notification_at: new Date().toISOString(),
signature_status: 'pending_employee'
})
.eq('id', avenantId);
if (updateError) {
console.error('❌ Erreur mise à jour last_employee_notification_at:', updateError);
} else {
console.log('✅ last_employee_notification_at mis à jour');
}
} catch (err) {
console.error('⚠️ Erreur lors de la mise à jour:', err);
}
}
// 6. Envoi de l'email via le système universel v2
console.log('📧 Envoi de l\'email via le système universel v2...');
try {
await sendUniversalEmailV2({
type: 'signature-request-employee-amendment',
toEmail: employeeEmail,
subject: `Signature électronique requise Avenant ${numeroAvenant || reference}`,
data: {
signatureLink,
firstName,
organizationName,
matricule,
contractReference: reference,
contractType: typecontrat || 'CDDU',
startDate,
profession,
productionName,
numeroAvenant
}
});
console.log('✅ Email envoyé avec succès');
return NextResponse.json({
success: true,
message: 'Email envoyé avec succès'
});
} catch (emailError: any) {
console.error('❌ Erreur lors de l\'envoi de l\'email:', emailError);
return NextResponse.json(
{
error: 'Erreur lors de l\'envoi de l\'email',
details: emailError.message
},
{ status: 500 }
);
}
} catch (error: any) {
console.error('❌ Erreur serveur:', error);
return NextResponse.json(
{
error: 'Erreur serveur',
details: error.message
},
{ status: 500 }
);
}
}