import { prisma } from '@/lib/prisma'; import { LeantimeService } from './leantime-service'; import { OutlineService } from './outline-service'; import { RocketChatService } from './rocketchat-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; constructor() { this.leantimeService = new LeantimeService(); this.outlineService = new OutlineService(); this.rocketChatService = new RocketChatService(); } /** * 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; // 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 } }; // 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 } // 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 and penpotProjectId will be added when implemented } }); // 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 } }; } catch (error) { console.error('Error setting up integrations:', error); // Rollback any created resources await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId); // 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 */ private async rollbackIntegrations( leantimeProjectId?: number, outlineCollectionId?: string, rocketChatChannelId?: 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 }; 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}`); } catch (rocketChatError) { console.error('Error during Rocket.Chat rollback:', rocketChatError); console.log(`⚠️ Note: Rocket.Chat channel ${rocketChatChannelId} may need to be deleted manually`); } } // 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) { 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}`); } } }