espace-paie-odentas/lib/emailLoggingService.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

456 lines
No EOL
12 KiB
TypeScript

// lib/emailLoggingService.ts
import { createSbServiceRole } from "@/lib/supabaseServer";
export type EmailType =
| 'contract-created'
| 'contract-updated'
| 'contract-cancelled'
| 'employee-created'
| 'invitation'
| 'auto-declaration-invitation'
| 'invoice'
| 'signature-request'
| 'signature-request-employer'
| 'signature-request-employee'
| 'signature-request-employee-amendment' // Signature avenant par salarié
| 'signature-request-salarie' // Demande signature salarié (depuis Lambda DocuSeal)
| 'bulk-signature-notification'
| 'salary-transfer-notification'
| 'contribution-notification' // Notification de cotisations
| 'notification'
| 'support-reply' // Réponse support
| 'support-ticket-created' // Nouveau ticket
| 'support-ticket-reply' // Réponse utilisateur
| 'contact-support' // Formulaire contact public
| 'amendment-completed-employer' // Avenant signé (notification employeur)
| 'amendment-completed-employee' // Avenant signé (notification salarié)
| 'account-activation'
| 'access-updated'
| 'access-revoked'
| 'password-created'
| 'password-changed'
| 'twofa-enabled'
| 'twofa-disabled'
| 'bulk-email'
| 'system-notification'
| 'other';
export type EmailStatus =
| 'pending'
| 'sending'
| 'sent'
| 'delivered'
| 'bounce'
| 'complaint'
| 'failed';
export interface EmailLogData {
// Informations obligatoires
senderEmail: string;
recipientEmail: string;
subject: string;
emailType: EmailType;
// Contenu
htmlContent?: string;
textContent?: string;
templateName?: string;
templateData?: Record<string, any>;
// Destinataires additionnels
ccEmails?: string[];
bccEmails?: string[];
// Informations utilisateur
senderUserId?: string;
senderName?: string;
// SES
sesMessageId?: string;
sesRegion?: string;
sesConfigurationSet?: string;
sesSourceArn?: string;
sesReturnPath?: string;
// Contexte
organizationId?: string;
contractId?: string;
ticketId?: string;
userAgent?: string;
ipAddress?: string;
// Métadonnées
tags?: Record<string, any>;
context?: Record<string, any>;
// Statut
emailStatus?: EmailStatus;
}
export interface EmailLogUpdate {
emailStatus?: EmailStatus;
sesMessageId?: string;
bounceReason?: string;
complaintReason?: string;
failureReason?: string;
sentAt?: Date;
deliveredAt?: Date;
}
class EmailLoggingService {
private sb = createSbServiceRole();
/**
* Extraire l'adresse email pure d'une chaîne qui peut contenir un nom d'affichage
* Ex: "Odentas <paie@odentas.fr>" -> "paie@odentas.fr"
*/
private extractPureEmail(emailString: string): string {
// Rechercher un email entre < >
const match = emailString.match(/<([^>]+)>/);
if (match) {
return match[1].trim();
}
// Si pas de < >, retourner la chaîne nettoyée
return emailString.trim().replace(/['"]/g, '');
}
/**
* Créer un nouveau log d'email
*/
async logEmail(data: EmailLogData): Promise<string | null> {
try {
const cleanedSenderEmail = this.extractPureEmail(data.senderEmail);
const cleanedRecipientEmail = this.extractPureEmail(data.recipientEmail);
const logEntry = {
sender_email: cleanedSenderEmail,
recipient_email: cleanedRecipientEmail,
cc_emails: data.ccEmails?.map(email => this.extractPureEmail(email)) || null,
bcc_emails: data.bccEmails || null,
subject: data.subject,
html_content: data.htmlContent || null,
text_content: data.textContent || null,
template_name: data.templateName || null,
template_data: data.templateData || null,
email_type: data.emailType,
email_status: data.emailStatus || 'pending',
// Informations utilisateur
sender_user_id: data.senderUserId || null,
sender_name: data.senderName || null,
// SES
ses_message_id: data.sesMessageId || null,
ses_region: data.sesRegion || process.env.AWS_REGION || 'eu-west-3',
ses_configuration_set: data.sesConfigurationSet || null,
ses_source_arn: data.sesSourceArn || null,
ses_return_path: data.sesReturnPath || null,
// Contexte
organization_id: data.organizationId || null,
contract_id: data.contractId || null,
ticket_id: data.ticketId || null,
user_agent: data.userAgent || null,
ip_address: data.ipAddress || null,
// Métadonnées
tags: data.tags || {},
context: data.context || {},
created_at: new Date().toISOString()
};
const { data: result, error } = await this.sb
.from('email_logs')
.insert(logEntry)
.select('id')
.single();
if (error) {
console.error('Erreur lors de la création du log email:', error);
return null;
}
return result?.id || null;
} catch (error) {
console.error('Exception lors de la création du log email:', error);
return null;
}
}
/**
* Mettre à jour un log d'email existant
*/
async updateEmailLog(logId: string, updates: EmailLogUpdate): Promise<boolean> {
try {
const updateData: any = {};
if (updates.emailStatus) {
updateData.email_status = updates.emailStatus;
}
if (updates.sesMessageId) {
updateData.ses_message_id = updates.sesMessageId;
}
if (updates.bounceReason) {
updateData.bounce_reason = updates.bounceReason;
}
if (updates.complaintReason) {
updateData.complaint_reason = updates.complaintReason;
}
if (updates.failureReason) {
updateData.failure_reason = updates.failureReason;
}
if (updates.sentAt) {
updateData.sent_at = updates.sentAt.toISOString();
}
if (updates.deliveredAt) {
updateData.delivered_at = updates.deliveredAt.toISOString();
}
const { error } = await this.sb
.from('email_logs')
.update(updateData)
.eq('id', logId);
if (error) {
console.error('Erreur lors de la mise à jour du log email:', error);
return false;
}
return true;
} catch (error) {
console.error('Exception lors de la mise à jour du log email:', error);
return false;
}
}
/**
* Mettre à jour un log par SES Message ID
*/
async updateEmailLogBySesId(sesMessageId: string, updates: EmailLogUpdate): Promise<boolean> {
try {
const updateData: any = {};
if (updates.emailStatus) {
updateData.email_status = updates.emailStatus;
}
if (updates.bounceReason) {
updateData.bounce_reason = updates.bounceReason;
}
if (updates.complaintReason) {
updateData.complaint_reason = updates.complaintReason;
}
if (updates.failureReason) {
updateData.failure_reason = updates.failureReason;
}
if (updates.sentAt) {
updateData.sent_at = updates.sentAt.toISOString();
}
if (updates.deliveredAt) {
updateData.delivered_at = updates.deliveredAt.toISOString();
}
const { error } = await this.sb
.from('email_logs')
.update(updateData)
.eq('ses_message_id', sesMessageId);
if (error) {
console.error('Erreur lors de la mise à jour du log email par SES ID:', error);
return false;
}
return true;
} catch (error) {
console.error('Exception lors de la mise à jour du log email par SES ID:', error);
return false;
}
}
/**
* Récupérer les logs d'email avec filtres
*/
async getEmailLogs(options: {
limit?: number;
offset?: number;
emailType?: EmailType;
emailStatus?: EmailStatus;
senderEmail?: string;
recipientEmail?: string;
organizationId?: string;
dateFrom?: Date;
dateTo?: Date;
searchTerm?: string;
} = {}) {
try {
let query = this.sb
.from('email_logs')
.select(`
id,
created_at,
sent_at,
delivered_at,
sender_email,
sender_name,
recipient_email,
cc_emails,
bcc_emails,
subject,
email_type,
email_status,
ses_message_id,
organization_id,
contract_id,
ticket_id,
bounce_reason,
complaint_reason,
failure_reason,
tags,
context
`);
// Filtres
if (options.emailType) {
query = query.eq('email_type', options.emailType);
}
if (options.emailStatus) {
query = query.eq('email_status', options.emailStatus);
}
if (options.senderEmail) {
query = query.eq('sender_email', options.senderEmail);
}
if (options.recipientEmail) {
query = query.eq('recipient_email', options.recipientEmail);
}
if (options.organizationId) {
query = query.eq('organization_id', options.organizationId);
}
if (options.dateFrom) {
query = query.gte('created_at', options.dateFrom.toISOString());
}
if (options.dateTo) {
query = query.lte('created_at', options.dateTo.toISOString());
}
if (options.searchTerm) {
query = query.or(`subject.ilike.%${options.searchTerm}%,recipient_email.ilike.%${options.searchTerm}%,sender_email.ilike.%${options.searchTerm}%`);
}
// Pagination et tri
query = query
.order('created_at', { ascending: false })
.range(options.offset || 0, (options.offset || 0) + (options.limit || 50) - 1);
const { data, error } = await query;
if (error) {
console.error('Erreur lors de la récupération des logs email:', error);
return { data: [], error };
}
return { data: data || [], error: null };
} catch (error) {
console.error('Exception lors de la récupération des logs email:', error);
return { data: [], error };
}
}
/**
* Récupérer les statistiques d'email
*/
async getEmailStats(options: {
dateFrom?: Date;
dateTo?: Date;
organizationId?: string;
} = {}) {
try {
let query = this.sb
.from('email_stats_summary')
.select('*');
if (options.dateFrom) {
query = query.gte('date', options.dateFrom.toISOString().split('T')[0]);
}
if (options.dateTo) {
query = query.lte('date', options.dateTo.toISOString().split('T')[0]);
}
const { data, error } = await query;
if (error) {
console.error('Erreur lors de la récupération des stats email:', error);
return { data: [], error };
}
return { data: data || [], error: null };
} catch (error) {
console.error('Exception lors de la récupération des stats email:', error);
return { data: [], error };
}
}
/**
* Nettoyer les anciens logs (à exécuter périodiquement)
*/
async cleanupOldLogs(daysToKeep: number = 90): Promise<number> {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const { data, error } = await this.sb
.from('email_logs')
.delete()
.lt('created_at', cutoffDate.toISOString())
.select('id');
if (error) {
console.error('Erreur lors du nettoyage des logs email:', error);
return 0;
}
return data?.length || 0;
} catch (error) {
console.error('Exception lors du nettoyage des logs email:', error);
return 0;
}
}
}
// Instance singleton
export const emailLogger = new EmailLoggingService();
// Helpers pour extraction de données SES
export function extractSesDataFromResponse(sesResponse: any) {
return {
sesMessageId: sesResponse.MessageId,
sesRegion: process.env.AWS_REGION || 'eu-west-3',
};
}
export function extractUserContextFromRequest(request: any) {
return {
userAgent: request.headers.get('user-agent') || undefined,
ipAddress: request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
undefined,
};
}