const fs = require('fs'); const path = require('path'); const { PrismaClient } = require('@prisma/client'); const { PrismaPg } = require('@prisma/adapter-pg'); const { Pool } = require('pg'); require('dotenv').config(); // Base configurations - Target the dev server directly via localhost port const BASE_URL = "http://127.0.0.1:3001"; let prisma; let pool; const deployedWorkflowIds = []; let n8nUrl; let n8nApiKey; // Helper to run query with admin credentials function getPrisma() { if (!prisma) { const dbUrl = new URL(process.env.DATABASE_URL_DEV || process.env.DATABASE_URL); pool = new Pool({ host: dbUrl.hostname, port: dbUrl.port ? parseInt(dbUrl.port) : 5432, user: decodeURIComponent(dbUrl.username), password: decodeURIComponent(dbUrl.password), database: dbUrl.pathname.substring(1).split('?')[0], ssl: false }); const adapter = new PrismaPg(pool); prisma = new PrismaClient({ adapter }); } return prisma; } async function runAsAdmin(queryFn) { const db = getPrisma(); return db.$transaction(async (tx) => { await tx.$executeRawUnsafe(`SET LOCAL app.current_user_role = 'admin';`); return queryFn(tx); }); } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Custom simple assertion let passed = 0; let failed = 0; function assert(condition, message) { if (condition) { console.log(` ✓ PASS: ${message}`); passed++; } else { console.error(` ✗ FAIL: ${message}`); failed++; } } async function cleanupDb() { console.log("Cleaning up integration test records in test database..."); try { await runAsAdmin(async (tx) => { // Clean sales results await tx.salesResult.deleteMany({ where: { OR: [ { idempotencyKey: { startsWith: 'key-integration' } }, { period: '2026-07' } ] } }); // Clean import jobs if (tx.salesImportJob) { await tx.salesImportJob.deleteMany({ where: { idempotencyKey: { startsWith: 'key-integration' } } }); } // Clean settlements await tx.settlement.deleteMany({ where: { period: '2026-07' } }); // Clean goals await tx.goal.deleteMany({ where: { period: '2026-07' } }); // Clean test plans await tx.calculationRule.deleteMany({ where: { plan: { code: 'PLAN-INT-P5' } } }); await tx.compensationPlan.deleteMany({ where: { code: 'PLAN-INT-P5' } }); // Clean audit logs await tx.auditLog.deleteMany({ where: { action: 'CREATE', targetTable: 'sales_results' } }); }); } catch (err) { console.error("Error during DB cleanup:", err); } } async function runTests() { console.log("=== STARTING REAL N8N E2E INTEGRATION TEST SUITE ==="); // 1. Clean DB await cleanupDb(); // 2. Fetch n8n API configuration const mcpConfigPath = path.join(__dirname, '../.agents/mcp_config.json'); if (!fs.existsSync(mcpConfigPath)) { console.error("Error: .agents/mcp_config.json not found."); process.exit(1); } const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8')); const n8nEnv = mcpConfig.mcpServers.n8n.env; n8nUrl = n8nEnv.N8N_API_URL || "https://n8n.gaboggamer.online"; n8nApiKey = n8nEnv.N8N_API_KEY; if (!n8nApiKey) { console.error("Error: N8N_API_KEY is missing in mcp_config.json."); process.exit(1); } // 3. Fetch existing workflows to retrieve the project ID dynamically console.log("Fetching project ID from existing workflows..."); let projectId; try { const listRes = await fetch(`${n8nUrl}/api/v1/workflows`, { method: 'GET', headers: { 'X-N8N-API-KEY': n8nApiKey } }); if (listRes.ok) { const listData = await listRes.json(); const firstWf = listData.data.find(w => w.shared && w.shared.length > 0); if (firstWf) { projectId = firstWf.shared[0].projectId; console.log(`Resolved project ID dynamically: ${projectId}`); } } } catch (err) { console.warn("Warning: Could not fetch project ID dynamically, falling back to default.", err.message); } // 4. Fetch credentials dynamically to resolve AI accounts console.log("Fetching credentials from n8n to resolve AI accounts..."); let deepSeekCred = null; let geminiCred = null; try { const credsRes = await fetch(`${n8nUrl}/api/v1/credentials`, { method: 'GET', headers: { 'X-N8N-API-KEY': n8nApiKey } }); if (credsRes.ok) { const credsData = await credsRes.json(); const credentials = Array.isArray(credsData.data) ? credsData.data : (credsData.data.credentials || []); const deepSeekCreds = credentials.filter(c => c.type === 'deepSeekApi'); if (deepSeekCreds.length > 0) { deepSeekCred = deepSeekCreds.find(c => c.name.toLowerCase().includes('deepseek')) || deepSeekCreds[0]; } const geminiCreds = credentials.filter(c => c.type === 'googlePalmApi'); if (geminiCreds.length > 0) { geminiCred = geminiCreds.find(c => c.name.toLowerCase().includes('gemini') || c.name.toLowerCase().includes('google')) || geminiCreds[0]; } } } catch (err) { console.warn("Warning: Could not fetch credentials dynamically:", err.message); } // 5. Verify the external dev server is running console.log(`Verifying target dev server at ${BASE_URL}...`); try { const res = await fetch(`${BASE_URL}/api/auth/me`); if (!res.ok) { console.warn(`Warning: Target server returned status ${res.status}`); } } catch (e) { console.error(`Error: Dev server is unreachable at ${BASE_URL}. Error:`, e.message); await cleanup(); process.exit(1); } // ========================================== // TEST CASE 1: SALES IMPORT WORKFLOW // ========================================== console.log("\n[Test 1] Deploying and triggering Sales Import Workflow..."); const importWorkflowPath = path.join(__dirname, '../n8n/sales_import_workflow.json'); if (!fs.existsSync(importWorkflowPath)) { console.error("Error: n8n/sales_import_workflow.json not found."); process.exit(1); } const importWfJson = JSON.parse(fs.readFileSync(importWorkflowPath, 'utf8')); const importWebhookPath = `calculate-commissions-${Date.now()}`; // Configure dev app urls and webhooks for (const node of importWfJson.nodes) { if (node.id === 'Set-Dev-Env' || node.id === 'Set-Prod-Env') { node.parameters.values.string = [ { name: 'appUrl', value: 'http://special-hotel-dev:3000' }, { name: 'signature', value: process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc=' } ]; } if (node.type === 'n8n-nodes-base.webhook') { node.parameters.path = importWebhookPath; } if (node.type === '@n8n/n8n-nodes-langchain.lmChatDeepSeek' && deepSeekCred) { node.credentials = { deepSeekApi: { id: deepSeekCred.id, name: deepSeekCred.name } }; } if (node.type === '@n8n/n8n-nodes-langchain.lmChatGoogleGemini' && geminiCred) { node.credentials = { googlePalmApi: { id: geminiCred.id, name: geminiCred.name } }; } } const importWfName = `Semillero E2E Integration: Sales Import - ${Date.now()}`; const importCreateRes = await fetch(`${n8nUrl}/api/v1/workflows`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': n8nApiKey }, body: JSON.stringify({ name: importWfName, nodes: importWfJson.nodes, connections: importWfJson.connections, settings: importWfJson.settings || {}, projectId }) }); if (!importCreateRes.ok) { console.error("Failed to deploy import workflow:", await importCreateRes.text()); process.exit(1); } const importWfData = await importCreateRes.json(); deployedWorkflowIds.push(importWfData.id); // Activate import workflow await fetch(`${n8nUrl}/api/v1/workflows/${importWfData.id}/activate`, { method: 'POST', headers: { 'X-N8N-API-KEY': n8nApiKey } }); await sleep(3000); const importIdempotencyKey = `key-integration-import-${Date.now()}`; const uploadRes = await fetch(`${n8nUrl}/webhook/${importWebhookPath}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-n8n-signature': process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc=' }, body: JSON.stringify({ idempotencyKey: importIdempotencyKey, uploaderId: 1, sales: [ { username: 'colaborador_mde', hotelCode: 'EST-MDE', period: '2026-06', amount: 15000, salesCount: 5, transactionId: `TX-INT-COMM-${Date.now()}` } ] }) }); assert(uploadRes.status === 200 || uploadRes.status === 202, `Import webhook response code: ${uploadRes.status}`); const uploadData = await uploadRes.json(); assert(uploadData.success && uploadData.code === 'IMPORT_ACCEPTED', "Import accepted by n8n"); let importCallbackCompleted = false; for (let i = 0; i < 20; i++) { await sleep(2000); const dbSales = await runAsAdmin(tx => tx.salesResult.findMany({ where: { idempotencyKey: { startsWith: importIdempotencyKey } } })); if (dbSales.length > 0) { assert(dbSales.length === 1, "Sales result saved to dev database by n8n callback"); assert(parseFloat(dbSales[0].amount) === 15000.00, "Imported amount verified"); importCallbackCompleted = true; break; } } assert(importCallbackCompleted, "Sales import integration completed successfully"); // ========================================== // TEST CASE 2: SETTLEMENT CALCULATION WORKFLOW // ========================================== console.log("\n[Test 2] Deploying and triggering Settlement Calculation Workflow..."); const calcWorkflowPath = path.join(__dirname, '../n8n/settlement_calculation_workflow.json'); if (!fs.existsSync(calcWorkflowPath)) { console.error("Error: n8n/settlement_calculation_workflow.json not found."); await cleanup(); process.exit(1); } const calcWfJson = JSON.parse(fs.readFileSync(calcWorkflowPath, 'utf8')); const calcWebhookPath = `calculate-settlements-${Date.now()}`; // Configure dev app urls and webhooks for (const node of calcWfJson.nodes) { if (node.id === 'Set-Dev-Env' || node.id === 'Set-Prod-Env') { node.parameters.values.string = [ { name: 'appUrl', value: 'http://special-hotel-dev:3000' }, { name: 'signature', value: process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc=' } ]; } if (node.type === 'n8n-nodes-base.webhook') { node.parameters.path = calcWebhookPath; } if (node.type === '@n8n/n8n-nodes-langchain.lmChatDeepSeek' && deepSeekCred) { node.credentials = { deepSeekApi: { id: deepSeekCred.id, name: deepSeekCred.name } }; } if (node.type === '@n8n/n8n-nodes-langchain.lmChatGoogleGemini' && geminiCred) { node.credentials = { googlePalmApi: { id: geminiCred.id, name: geminiCred.name } }; } } const calcWfName = `Semillero E2E Integration: Settlement Calc - ${Date.now()}`; const calcCreateRes = await fetch(`${n8nUrl}/api/v1/workflows`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': n8nApiKey }, body: JSON.stringify({ name: calcWfName, nodes: calcWfJson.nodes, connections: calcWfJson.connections, settings: calcWfJson.settings || {}, projectId }) }); if (!calcCreateRes.ok) { console.error("Failed to deploy calculation workflow:", await calcCreateRes.text()); await cleanup(); process.exit(1); } const calcWfData = await calcCreateRes.json(); deployedWorkflowIds.push(calcWfData.id); // Activate calculation workflow await fetch(`${n8nUrl}/api/v1/workflows/${calcWfData.id}/activate`, { method: 'POST', headers: { 'X-N8N-API-KEY': n8nApiKey } }); await sleep(3000); // Seed DB records for the calculation E2E test console.log("Seeding test database with plan, rules, goal, and sales results for calculation E2E test..."); const adminUser = (await runAsAdmin(tx => tx.user.findMany())).find(u => u.username === 'admin'); const colaboradorMde = (await runAsAdmin(tx => tx.user.findMany())).find(u => u.username === 'colaborador_mde'); const testPlan = await runAsAdmin(async (tx) => { // Delete existing PLAN-INT-P5 if any await tx.calculationRule.deleteMany({ where: { plan: { code: 'PLAN-INT-P5' } } }); await tx.compensationPlan.deleteMany({ where: { code: 'PLAN-INT-P5' } }); const plan = await tx.compensationPlan.create({ data: { name: 'Integration Test Plan Phase 5', code: 'PLAN-INT-P5', validityStart: new Date('2026-01-01'), type: 'SCALE', status: 'ACTIVE', version: 1, createdBy: adminUser.id } }); await tx.calculationRule.create({ data: { planId: plan.id, type: 'TIER', minAchievement: 0.0, maxAchievement: 2.0, rate: 0.02, payoutAmount: 0.0 } }); return plan; }); // Seed Goal and SalesResult for period 2026-07 await runAsAdmin(async (tx) => { await tx.goal.deleteMany({ where: { period: '2026-07', targetId: colaboradorMde.id } }); await tx.goal.create({ data: { targetType: 'INDIVIDUAL', targetId: colaboradorMde.id, period: '2026-07', amount: 10000.00 } }); await tx.salesResult.deleteMany({ where: { period: '2026-07', userId: colaboradorMde.id } }); await tx.salesResult.create({ data: { source: 'API', hotelId: colaboradorMde.hotelId, userId: colaboradorMde.id, period: '2026-07', amount: 5000.00, salesCount: 2, idempotencyKey: 'key-integration-sales-p5', uploadedBy: adminUser.id, status: 'PENDING' } }); }); // Trigger calculation webhook (commit mode) console.log(`Triggering settlement calculation webhook directly at ${n8nUrl}/webhook/${calcWebhookPath}...`); const calcRes = await fetch(`${n8nUrl}/webhook/${calcWebhookPath}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-n8n-signature': process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc=' }, body: JSON.stringify({ period: '2026-07', simulateOnly: false, uploaderId: adminUser.id }) }); assert(calcRes.status === 200 || calcRes.status === 201, `Calculation webhook response code: ${calcRes.status}`); const calcData = await calcRes.json(); assert(calcData.success && calcData.code === 'SETTLEMENTS_CALCULATED', "Calculation execution successful"); // Verify settlement created in database const finalSettlement = await runAsAdmin(tx => tx.settlement.findFirst({ where: { userId: colaboradorMde.id, period: '2026-07', planId: testPlan.id } })); assert(finalSettlement !== null, "Settlement record created in database for colaborador_mde in 2026-07"); if (finalSettlement) { assert(finalSettlement.status === 'PENDING', "Settlement status is set to PENDING"); assert(parseFloat(finalSettlement.calculatedCommission) === 100.00, "Calculated commission is correctly 100.00 (5,000 * 2%)"); assert(parseFloat(finalSettlement.totalPayout) === 100.00, "Total payout is correctly 100.00"); } await cleanup(); console.log(`\n=== REAL N8N INTEGRATION TEST RESULTS: ${passed} PASSED, ${failed} FAILED ===`); if (failed > 0) { process.exit(1); } else { process.exit(0); } } async function cleanup() { console.log("\nCleaning up integration test processes..."); // 1. Delete all deployed workflows from n8n for (const id of deployedWorkflowIds) { if (failed > 0) { console.log(`[TEST FAILED] Skipping test workflow ${id} deletion to allow troubleshooting in n8n.gaboggamer.online.`); } else { console.log(`Deleting n8n test workflow ${id}...`); try { const delRes = await fetch(`${n8nUrl}/api/v1/workflows/${id}`, { method: 'DELETE', headers: { 'X-N8N-API-KEY': n8nApiKey } }); if (delRes.ok) { console.log(`Test workflow ${id} deleted successfully from n8n.`); } else { console.error(`Failed to delete test workflow ${id}:`, await delRes.text()); } } catch (e) { console.error(`Error deleting workflow ${id}:`, e); } } } // 3. Clean DB records await cleanupDb(); // 4. Disconnect Prisma try { if (prisma) { await prisma.$disconnect(); } if (pool) { await pool.end(); } } catch (e) {} } process.on('SIGINT', async () => { await cleanup(); process.exit(1); }); runTests().catch(async err => { console.error("Fatal E2E integration test runner error:", err); failed++; await cleanup(); process.exit(1); });