import { env } from '@/lib/env'; import { logger } from '@/lib/logger'; export class N8nService { private webhookUrl: string; private rollbackWebhookUrl: string; private apiKey: string; constructor() { // Use consistent webhook URLs without -test suffix this.webhookUrl = process.env.N8N_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-created'; this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-rollback'; this.apiKey = process.env.N8N_API_KEY || ''; if (!this.apiKey) { logger.error('N8N_API_KEY is not set in environment variables'); } } async triggerMissionDeletion(data: any): Promise { try { const deleteWebhookUrl = process.env.N8N_DELETE_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook-test/mission-delete'; logger.debug('Triggering n8n mission deletion workflow', { missionId: data.missionId, name: data.name, hasRepoName: !!data.repoName }); logger.debug('Using deletion webhook URL', { url: deleteWebhookUrl }); logger.debug('API key present', { present: !!this.apiKey }); const response = await fetch(deleteWebhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey }, body: JSON.stringify(data), }); logger.debug('Deletion webhook response', { status: response.status }); if (!response.ok) { const errorText = await response.text(); logger.error('Deletion webhook error response', { status: response.status, error: errorText.substring(0, 200) // Truncate to avoid logging huge responses }); // Try to parse the error response as JSON for more details try { const errorJson = JSON.parse(errorText); logger.error('Parsed error response', { code: errorJson.code, message: errorJson.message }); } catch (e) { logger.error('Error response is not JSON'); } throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); } const responseText = await response.text(); logger.debug('N8nService - Deletion raw response received', { length: responseText.length }); // Try to parse the response as JSON try { const result = JSON.parse(responseText); logger.debug('Parsed deletion workflow result', { success: result.success || !result.error, hasError: !!result.error }); // Check if the response contains error information if (result.error || result.message?.includes('failed')) { return { success: false, error: result.message || result.error }; } return { success: true, results: result }; } catch (parseError) { logger.debug('Response is not JSON, treating as workflow trigger confirmation'); return { success: true, results: { confirmed: true } }; } } catch (error) { logger.error('Error triggering n8n deletion workflow', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async triggerMissionCreation(data: any): Promise { try { logger.debug('N8nService - Input data', { hasServices: Array.isArray(data.services), services: data.services, hasGite: data.services?.includes('Gite'), missionId: data.missionId }); // Clean and validate the data const cleanData = { name: data.name, oddScope: Array.isArray(data.oddScope) ? data.oddScope : [data.oddScope], niveau: data.niveau || 'default', intention: data.intention?.trim() || '', missionType: data.missionType || 'default', donneurDOrdre: data.donneurDOrdre || 'default', projection: data.projection || 'default', services: Array.isArray(data.services) ? data.services : [], participation: data.participation || 'default', profils: Array.isArray(data.profils) ? data.profils : [], guardians: data.guardians || {}, volunteers: Array.isArray(data.volunteers) ? data.volunteers : [], creatorId: data.creatorId, missionId: data.missionId, // ✅ Include missionId so N8N can use it logoPath: data.logoPath, // Include logoPath if present logoUrl: data.logoUrl, // Include logoUrl if present logo: data.logo, // Include logo data if present config: { ...data.config, // Preserve original config N8N_API_KEY: this.apiKey, MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://api.slm-lab.net/api' } }; // Log the cleaned data logger.debug('Sending cleaned data to n8n', { name: cleanData.name, missionId: cleanData.missionId, oddScope: cleanData.oddScope, niveau: cleanData.niveau, missionType: cleanData.missionType, donneurDOrdre: cleanData.donneurDOrdre, projection: cleanData.projection, services: cleanData.services, participation: cleanData.participation, profils: cleanData.profils, hasGuardians: !!cleanData.guardians, volunteersCount: cleanData.volunteers.length, hasLogo: !!cleanData.logoPath }); logger.debug('Using webhook URL', { url: this.webhookUrl }); logger.debug('API key present', { present: !!this.apiKey }); const response = await fetch(this.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey }, body: JSON.stringify(cleanData), }); logger.debug('Webhook response', { status: response.status }); if (!response.ok) { const errorText = await response.text(); logger.error('Webhook error response', { status: response.status, error: errorText.substring(0, 200) // Truncate to avoid logging huge responses }); // Try to parse the error response as JSON for more details try { const errorJson = JSON.parse(errorText); logger.error('Parsed error response', { code: errorJson.code, message: errorJson.message }); } catch (e) { logger.error('Error response is not JSON'); } throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); } const responseText = await response.text(); logger.debug('N8nService - Raw response received', { length: responseText.length }); // Try to parse the response as JSON try { const result = JSON.parse(responseText); logger.debug('Parsed workflow result', { success: !result.error && !result.message?.includes('failed'), hasError: !!result.error }); // Check if the response contains error information if (result.error || result.message?.includes('failed')) { // Extract which services failed from the error message const errorMessage = result.message || result.error; const failedServices = { gitRepo: errorMessage.includes('Git repository creation failed'), leantimeProject: errorMessage.includes('Leantime project creation failed'), docCollection: errorMessage.includes('Documentation collection creation failed'), rocketChatChannel: errorMessage.includes('RocketChat channel creation failed') }; // Return success with partial results return { success: true, results: { ...result, failedServices } }; } return { success: true, results: result }; } catch (parseError) { logger.debug('Response is not JSON, treating as workflow trigger confirmation'); return { success: true, results: { logoUrl: null, leantimeProjectId: null, outlineCollectionId: null, rocketChatChannelId: null, giteaRepositoryUrl: null } }; } } catch (error) { logger.error('Error triggering n8n workflow', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async triggerMissionRollback(data: any): Promise { try { logger.debug('Triggering n8n rollback workflow', { missionId: data.missionId, name: data.name }); logger.debug('Using rollback webhook URL', { url: this.rollbackWebhookUrl }); logger.debug('API key present', { present: !!this.apiKey }); const response = await fetch(this.rollbackWebhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey }, body: JSON.stringify(data), }); if (!response.ok) { const errorText = await response.text(); logger.error('Rollback webhook error response', { status: response.status, error: errorText.substring(0, 200) }); throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); } const result = await response.json(); logger.debug('Received response from n8n rollback', { success: !result.error, hasError: !!result.error }); return { success: true, results: result }; } catch (error) { logger.error('Error triggering n8n rollback workflow', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } }