import { prisma } from '@/lib/prisma'; import { LeantimeService } from './leantime-service'; import { OutlineService } from './outline-service'; import { RocketChatService } from './rocketchat-service'; import { GiteaService } from './gitea-service'; import axios from 'axios'; interface IntegrationResult { success: boolean; error?: string; data?: { leantimeProjectId?: number; outlineCollectionId?: string; rocketChatChannelId?: string; giteaRepositoryUrl?: string; penpotProjectId?: string; }; } export class IntegrationService { private leantimeService: LeantimeService; private outlineService: OutlineService; private rocketChatService: RocketChatService; private giteaService: GiteaService; constructor() { this.leantimeService = new LeantimeService(); this.outlineService = new OutlineService(); this.rocketChatService = new RocketChatService(); this.giteaService = new GiteaService(); } /** * Set up all integrations for a mission * @param missionId The mission ID * @returns Integration result */ async setupIntegrationsForMission(missionId: string): Promise { try { // Get complete mission data with users const mission = await prisma.mission.findUnique({ where: { id: missionId }, include: { missionUsers: { include: { user: true } }, attachments: true } }); if (!mission) { throw new Error(`Mission not found: ${missionId}`); } // These fields will store the IDs of created resources let leantimeProjectId: number | undefined; let outlineCollectionId: string | undefined; let rocketChatChannelId: string | undefined; let giteaRepositoryUrl: string | undefined; // Track which integrations succeeded and which failed const integrationStatus = { leantime: { success: false, id: undefined as number | undefined, error: undefined as string | undefined }, outline: { success: false, id: undefined as string | undefined, error: undefined as string | undefined }, rocketchat: { success: false, id: undefined as string | undefined, error: undefined as string | undefined }, gitea: { success: false, url: undefined as string | undefined, error: undefined as string | undefined } }; // A flag to determine if we should consider this a success or failure overall let criticalFailure = false; try { // Step 1: Create Leantime project (Consider this a critical integration) try { console.log('Starting Leantime project creation...'); leantimeProjectId = await this.leantimeService.createProject(mission); console.log(`Leantime project created with ID: ${leantimeProjectId}`); integrationStatus.leantime.success = true; integrationStatus.leantime.id = leantimeProjectId; } catch (leantimeError) { console.error('Error creating Leantime project:', leantimeError); integrationStatus.leantime.success = false; integrationStatus.leantime.error = leantimeError instanceof Error ? leantimeError.message : String(leantimeError); criticalFailure = true; throw leantimeError; // Leantime is critical, so we rethrow } // Add a delay to avoid rate limits (extended to 3 seconds) console.log('Waiting 3 seconds before proceeding to Outline integration...'); await new Promise(resolve => setTimeout(resolve, 3000)); // Step 2: Create Outline collection (Consider this non-critical) try { console.log('Starting Outline collection creation...'); outlineCollectionId = await this.outlineService.createCollection(mission); console.log(`Outline collection created with ID: ${outlineCollectionId}`); integrationStatus.outline.success = true; integrationStatus.outline.id = outlineCollectionId; } catch (outlineError) { console.error('Error creating Outline collection:', outlineError); integrationStatus.outline.success = false; integrationStatus.outline.error = outlineError instanceof Error ? outlineError.message : String(outlineError); // Check if it's an authentication error (401) if (axios.isAxiosError(outlineError) && outlineError.response?.status === 401) { console.log('⚠️ Outline authentication error. Please check your API credentials.'); } else if (axios.isAxiosError(outlineError) && outlineError.response?.status === 429) { console.log('⚠️ Outline rate limiting error (429). The integration will be skipped for now.'); } // Don't set criticalFailure - Outline is non-critical } // Add a delay to avoid rate limits (extended to 3 seconds) console.log('Waiting 3 seconds before proceeding to Rocket.Chat integration...'); await new Promise(resolve => setTimeout(resolve, 3000)); // Step 3: Create Rocket.Chat channel (Consider this non-critical) try { console.log('Starting Rocket.Chat channel creation...'); rocketChatChannelId = await this.rocketChatService.createChannel(mission); console.log(`Rocket.Chat channel created with ID: ${rocketChatChannelId}`); integrationStatus.rocketchat.success = true; integrationStatus.rocketchat.id = rocketChatChannelId; } catch (rocketChatError) { console.error('Error creating Rocket.Chat channel:', rocketChatError); integrationStatus.rocketchat.success = false; integrationStatus.rocketchat.error = rocketChatError instanceof Error ? rocketChatError.message : String(rocketChatError); // Check for rate limiting if (axios.isAxiosError(rocketChatError) && rocketChatError.response?.status === 429) { console.log('⚠️ Rocket.Chat rate limiting error (429). The integration will be skipped for now.'); } // Don't set criticalFailure - Rocket.Chat is non-critical } // Add a delay to avoid rate limits (extended to 3 seconds) console.log('Waiting 3 seconds before proceeding to Gitea integration...'); await new Promise(resolve => setTimeout(resolve, 3000)); // Step 4: Create Gitea repository (Consider this non-critical) // Only create if Gite or Calcul services are selected if (mission.services && (mission.services.includes('Gite') || mission.services.includes('Calcul'))) { try { console.log('Starting Gitea repository creation...'); const repoData = await this.giteaService.createRepository(mission); if (repoData && repoData.html_url) { giteaRepositoryUrl = repoData.html_url; } else { giteaRepositoryUrl = this.giteaService.getRepositoryUrl( mission.name .toLowerCase() .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, '') ); } console.log(`Gitea repository created at: ${giteaRepositoryUrl}`); integrationStatus.gitea.success = true; integrationStatus.gitea.url = giteaRepositoryUrl; } catch (giteaError) { console.error('Error creating Gitea repository:', giteaError); integrationStatus.gitea.success = false; integrationStatus.gitea.error = giteaError instanceof Error ? giteaError.message : String(giteaError); // Don't set criticalFailure - Gitea is non-critical } } else { console.log('Skipping Gitea repository creation - neither Gite nor Calcul services selected'); integrationStatus.gitea.success = false; integrationStatus.gitea.error = 'Skipped - service not selected'; } // Update the mission with the integration IDs (only for successful integrations) await prisma.mission.update({ where: { id: missionId }, data: { leantimeProjectId: integrationStatus.leantime.success ? leantimeProjectId?.toString() : undefined, outlineCollectionId: integrationStatus.outline.success ? outlineCollectionId : undefined, rocketChatChannelId: integrationStatus.rocketchat.success ? rocketChatChannelId : undefined, giteaRepositoryUrl: integrationStatus.gitea.success ? giteaRepositoryUrl : undefined } }); // Output a summary of integration results console.log('Integration results:', JSON.stringify(integrationStatus, null, 2)); // If we get here without a critical failure, we consider it a success // even if some non-critical integrations failed return { success: !criticalFailure, data: { leantimeProjectId, outlineCollectionId, rocketChatChannelId, giteaRepositoryUrl } }; } catch (error) { console.error('Error setting up integrations:', error); // Rollback any created resources await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId, giteaRepositoryUrl); // Delete the mission itself since a critical integration failed try { await prisma.mission.delete({ where: { id: missionId } }); console.log(`Mission ${missionId} deleted due to critical integration failure.`); } catch (deleteError) { console.error('Error deleting mission after integration failure:', deleteError); } return { success: false, error: `Integration failed: ${error instanceof Error ? error.message : String(error)}` }; } } catch (error) { console.error('Error in integration setup:', error); return { success: false, error: `Integration setup failed: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Rollback integrations if any step fails * @param leantimeProjectId The Leantime project ID to delete * @param outlineCollectionId The Outline collection ID to delete * @param rocketChatChannelId The Rocket.Chat channel ID to delete * @param giteaRepositoryUrl The Gitea repository URL to extract owner/repo from for deletion */ public async rollbackIntegrations( leantimeProjectId?: number, outlineCollectionId?: string, rocketChatChannelId?: string, giteaRepositoryUrl?: string ): Promise { console.log('⚠️ Rolling back integrations due to an error...'); // Track what we've successfully rolled back const rollbackStatuses = { leantime: false, outline: false, rocketchat: false, gitea: false }; try { // Attempt to delete Leantime project if (leantimeProjectId) { try { console.log(`Attempting to delete Leantime project: ${leantimeProjectId}`); const leantimeSuccess = await this.leantimeService.deleteProject(leantimeProjectId); rollbackStatuses.leantime = leantimeSuccess; console.log(`Leantime project deletion ${leantimeSuccess ? 'successful' : 'failed'}: ${leantimeProjectId}`); // Add a longer delay to avoid rate limiting (3 seconds) console.log('Waiting 3 seconds before next rollback operation...'); await new Promise(resolve => setTimeout(resolve, 3000)); } catch (leantimeError) { console.error('Error during Leantime rollback:', leantimeError); console.log(`⚠️ Note: Leantime project ${leantimeProjectId} may need to be deleted manually`); } } // Attempt to delete Outline collection if (outlineCollectionId) { try { console.log(`Attempting to delete Outline collection: ${outlineCollectionId}`); const outlineSuccess = await this.outlineService.deleteCollection(outlineCollectionId); rollbackStatuses.outline = outlineSuccess; console.log(`Outline collection deletion ${outlineSuccess ? 'successful' : 'failed'}: ${outlineCollectionId}`); // Add a longer delay to avoid rate limiting (3 seconds) console.log('Waiting 3 seconds before next rollback operation...'); await new Promise(resolve => setTimeout(resolve, 3000)); } catch (outlineError) { console.error('Error during Outline rollback:', outlineError); console.log(`⚠️ Note: Outline collection ${outlineCollectionId} may need to be deleted manually`); } } // Attempt to delete Rocket.Chat channel if (rocketChatChannelId) { try { console.log(`Attempting to delete Rocket.Chat channel: ${rocketChatChannelId}`); const rocketChatSuccess = await this.rocketChatService.deleteChannel(rocketChatChannelId); rollbackStatuses.rocketchat = rocketChatSuccess; console.log(`Rocket.Chat channel deletion ${rocketChatSuccess ? 'successful' : 'failed'}: ${rocketChatChannelId}`); // Add a longer delay to avoid rate limiting (3 seconds) console.log('Waiting 3 seconds before next rollback operation...'); await new Promise(resolve => setTimeout(resolve, 3000)); } catch (rocketChatError) { console.error('Error during Rocket.Chat rollback:', rocketChatError); console.log(`⚠️ Note: Rocket.Chat channel ${rocketChatChannelId} may need to be deleted manually`); } } // Attempt to delete Gitea repository if (giteaRepositoryUrl) { try { // Extract owner and repo from URL const urlParts = giteaRepositoryUrl.split('/'); // Make sure we have enough parts in the URL before accessing array elements if (urlParts.length >= 2) { const owner = urlParts[urlParts.length - 2]; const repo = urlParts[urlParts.length - 1]; console.log(`Attempting to delete Gitea repository: ${owner}/${repo}`); const giteaSuccess = await this.giteaService.deleteRepository(owner, repo); rollbackStatuses.gitea = giteaSuccess; console.log(`Gitea repository deletion ${giteaSuccess ? 'successful' : 'failed'}: ${owner}/${repo}`); } else { console.log(`Invalid Gitea repository URL format: ${giteaRepositoryUrl}`); } } catch (giteaError) { console.error('Error during Gitea rollback:', giteaError); console.log(`⚠️ Note: Gitea repository at ${giteaRepositoryUrl} may need to be deleted manually`); } } else { console.log('No Gitea repository was created, skipping rollback'); // Mark as successful since there's nothing to delete rollbackStatuses.gitea = true; } // Provide a summary of rollback operations console.log('Rollback summary:', JSON.stringify(rollbackStatuses)); // If any rollbacks failed, provide a note if (!rollbackStatuses.leantime || !rollbackStatuses.outline || !rollbackStatuses.rocketchat || !rollbackStatuses.gitea) { console.log('⚠️ Some resources may need to be deleted manually.'); } } catch (error) { console.error('Error during rollback:', error); console.log('⚠️ Resources may need to be deleted manually:'); if (leantimeProjectId) console.log(`- Leantime project: ${leantimeProjectId}`); if (outlineCollectionId) console.log(`- Outline collection: ${outlineCollectionId}`); if (rocketChatChannelId) console.log(`- Rocket.Chat channel: ${rocketChatChannelId}`); if (giteaRepositoryUrl) console.log(`- Gitea repository: ${giteaRepositoryUrl}`); } } }