#!/usr/bin/env node /** * 🔨 CARPENTER AGENT - API INTEGRATION * * A backend API-based carpenter agent that: * - Generates realistic carpenter quotes via REST API * - Creates reusable test data fixtures * - Validates API response and quote data * - Outputs test quotes in JSON format for regression testing * * Usage: node carpenter-agent-api.js [--save-fixtures] */ const axios = require('axios'); const fs = require('fs'); const path = require('path'); // Configuration const BASE_URL = process.env.BASE_URL || 'http://localhost:4032'; const API_BASE = BASE_URL; // Carpenter profiles const carpenters = { lars: { name: 'Lars Nielsen', experience: '15 år', specialty: 'Tag og terrasser', phone: '40 40 40 40', email: 'lars@tommer.dk' }, jannick: { name: 'Jannick Andersen', experience: '12 år', specialty: 'Indendørs renovering', phone: '41 41 41 41', email: 'jannick@tommer.dk' }, alexander: { name: 'Alexander Ørneby Andersen', experience: '8 år', specialty: 'Moderne bygge- og renoveringsprojekter', phone: '42 42 42 42', email: 'alexander@tommer.dk' } }; // Realistic project templates const projectTemplates = { 'roofing': { category: '🏠 Tag', projects: [ { description: 'Udbedring af skadede tagsten på 35m² tørvehus. Der skal skiftes ca. 50 stk. beskadige sten og lægges nye dragsten', area: 35, type: 'tagdækning', materials: ['Tagsten', 'Dragsten', 'Bly', 'Mørtelmasse'] }, { description: 'Komplette tagrenovering på 120m² stråtækt hus. Udskiftning af hele overtækningen med traditionel stråtækning', area: 120, type: 'stråtag', materials: ['Strå', 'Mørtelmasse', 'Bæreevne'] } ] }, 'flooring': { category: '🏠 Gulv', projects: [ { description: 'Lægning af egetparket på 45m² i stue, gang og soveværelse. Oprindelige undergulv skal forbedres og udjævnes', area: 45, type: 'parket', materials: ['Egetparket', 'Lim', 'Spackling', 'Finér'] }, { description: 'Klikgulv i bambus på 28m² køkkken og spisestue. Moderne design med varmedækkende underlag', area: 28, type: 'klikgulv', materials: ['Bambusklikgulv', 'Underlags-matte', 'Lim', 'Kant-lister'] }, { description: 'Terrazzo-lægning på 15m² badeværelse med varmekabler under. Professionel polering efter lægning', area: 15, type: 'terrazzo', materials: ['Terrazzo', 'Varmekabler', 'Mørtelmasse', 'Fugemasse'] } ] }, 'furniture': { category: '🛏️ Møbler', projects: [ { description: 'Indvendig garderobe med skydedøre i soveværelset. Størrelse 2.4x1.8m med 2 skydedøre og hylder', area: 4, type: 'garderobe', materials: ['Plywood', 'Skydedøre', 'Hylder', 'Søm og skruer'] }, { description: 'Køkkenskabe til omdisponering af køkken. Nye skabe under arbejdspladen og nye hylder over med belysning', area: 8, type: 'køkkenskabe', materials: ['Massivt træ', 'Hylder', 'Beslag', 'LED-belysning'] } ] }, 'doors': { category: '🚪 Døre', projects: [ { description: 'Installation af 3 nye indvendige glas-ali dører til åbent køkkenkoncept. Dørene er 1m brede', area: 3, type: 'dørinstallation', materials: ['Glas-ali dører', 'Beslag', 'Lim', 'Lister'] } ] }, 'windows': { category: '🪟 Vinduer', projects: [ { description: 'Montering og justering af vindueskarme/lister i hele huset. I alt 12 vinduer á 1.2x1.0m', area: 15, type: 'vinduesmontering', materials: ['Vindueslister', 'Fugemasse', 'Skruer', 'Maling/lak'] } ] } }; /** * Generate realistic pricing based on complexity and area */ function generateQuoteData(carpenter, project, projectCategory) { const basePricePerM2 = 450; // DKK const complexityMultiplier = { low: 1.0, medium: 1.5, high: 2.0, veryHigh: 2.5 }; const complexity = ['low', 'medium', 'high'][Math.floor(Math.random() * 3)]; const multiplier = complexityMultiplier[complexity]; const laborCost = project.area * basePricePerM2 * multiplier; const materialCost = laborCost * 0.4; // Materials are ~40% of labor const totalCost = laborCost + materialCost; return { carpenter, projectCategory, project, complexity, costs: { labor: Math.round(laborCost), materials: Math.round(materialCost), total: Math.round(totalCost) }, timeline: `${Math.ceil(project.area / 20)}-${Math.ceil(project.area / 10)} dage`, notes: `Tilbud fra erfaren tømrer ${carpenter.name} (${carpenter.experience})\n\nKategori: ${projectCategory}\nOmråde: ${project.area}m²\nKompleksitet: ${complexity}` }; } /** * Create a quote via API */ async function createQuoteViaAPI(quoteData) { try { const payload = { customerInfo: { name: `Test Customer - ${new Date().toISOString().split('T')[0]}`, email: 'test@example.com', phone: '12345678' }, projectInfo: { title: `${quoteData.projectCategory} - ${quoteData.project.type}`, description: quoteData.project.description, area: quoteData.project.area, notes: quoteData.notes }, materials: (quoteData.project.materials || []).map((mat, idx) => ({ name: mat, quantity: Math.floor(Math.random() * 100) + 10, unit: 'pcs', price: Math.random() * 500 + 50 })), labor: [ { description: 'Timefakturering', hours: Math.ceil(quoteData.project.area / 5), hourlyRate: 450 } ], totals: { materials: quoteData.costs.materials, labor: quoteData.costs.labor, total: quoteData.costs.total } }; // Try to POST to the API const response = await axios.post(`${API_BASE}/api/ordrestyring/offers/create`, payload, { timeout: 5000, headers: { 'Content-Type': 'application/json' } }); return { success: true, data: response.data, quotation: payload }; } catch (error) { // API might not be available, but we still have the quote data return { success: false, error: error.message, quotation: quoteData, note: 'Quote created locally (API not available)' }; } } /** * Main agent function */ async function runCarpenterAgent(saveFixtures = false) { console.log('\n' + '='.repeat(80)); console.log('🔨 CARPENTER AGENT - TEST DATA GENERATOR'); console.log('='.repeat(80) + '\n'); const outputDir = 'test-results/carpenter-test-data'; if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } let generatedQuotes = []; let quoteIndex = 0; // For each carpenter for (const [carpenterId, carpenter] of Object.entries(carpenters)) { console.log(`\n👷 CARPENTER: ${carpenter.name}`); console.log(` Experience: ${carpenter.experience} | Specialty: ${carpenter.specialty}`); console.log(` Email: ${carpenter.email} | Phone: ${carpenter.phone}`); console.log('-'.repeat(80)); // For each project category for (const [categoryKey, categoryData] of Object.entries(projectTemplates)) { console.log(`\n ${categoryData.category}`); // For each project in category for (const project of categoryData.projects) { quoteIndex++; // Generate quote const quote = generateQuoteData(carpenter, project, categoryData.category); // Try to create via API console.log(` 📝 Generating quote #${quoteIndex}...`); const apiResult = await createQuoteViaAPI(quote); // Prepare test data file const testData = { quoteNumber: quoteIndex, timestamp: new Date().toISOString(), carpenter: carpenter, ...quote, ...apiResult }; // Save individual quote file const filename = `quote-${quoteIndex.toString().padStart(3, '0')}-${carpenterId}-${project.type}.json`; const filepath = path.join(outputDir, filename); fs.writeFileSync(filepath, JSON.stringify(testData, null, 2)); generatedQuotes.push(testData); // Log result const status = apiResult.success ? '✅' : '⚠️'; console.log(` ${status} Quote saved: ${filename}`); console.log(` 💰 Total: DKK ${testData.costs.total.toLocaleString('da-DK')}`); console.log(` ⏱️ Timeline: ${testData.timeline}`); } } } // Generate comprehensive summary and fixtures const summary = { generatedAt: new Date().toISOString(), totalQuotes: generatedQuotes.length, carpenters: Object.keys(carpenters), categories: Object.keys(projectTemplates), quotes: generatedQuotes.map(q => ({ quoteNumber: q.quoteNumber, carpenter: q.carpenter.name, projectCategory: q.projectCategory, projectType: q.project.type, area: q.project.area, complexity: q.complexity, total: q.costs.total, timeline: q.timeline })), usageInstructions: { purpose: 'This test data can be used for regression testing and validation', files: `${generatedQuotes.length} individual quote files in ${outputDir}`, format: 'JSON with all quote details including carpenter info, project details, costs, and API responses', recommendations: [ 'Use for regression testing of quote generation', 'Validate quote calculations against new pricing models', 'Test quote export and PDF generation', 'Load test with multiple quotes' ] } }; // Save summary const summaryFile = path.join(outputDir, 'QUOTES_SUMMARY.json'); fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2)); // If fixtures requested, create a fixtures file if (saveFixtures) { const fixtures = { carpenters, projectTemplates, sampleQuotes: generatedQuotes.slice(0, 5).map(q => ({ quoteNumber: q.quoteNumber, carpenter: q.carpenter, project: q.project, costs: q.costs })) }; const fixturesFile = path.join(outputDir, 'FIXTURES.json'); fs.writeFileSync(fixturesFile, JSON.stringify(fixtures, null, 2)); console.log(`\n✅ Fixtures saved to: ${fixturesFile}`); } // Print summary console.log('\n' + '='.repeat(80)); console.log('📊 CARPENTER AGENT - SUMMARY'); console.log('='.repeat(80)); console.log(`✅ Total quotes generated: ${generatedQuotes.length}`); console.log(`👷 Carpenters: ${Object.keys(carpenters).length}`); console.log(`📁 Output directory: ${outputDir}`); console.log(`📄 Summary file: ${summaryFile}`); console.log('\nQuote Distribution:'); // Print distribution Object.entries(projectTemplates).forEach(([key, cat]) => { const count = generatedQuotes.filter(q => q.projectCategory === cat.category).length; console.log(` ${cat.category}: ${count} quotes`); }); console.log('\n💾 Test Data Ready for:'); console.log(' ✓ Regression testing'); console.log(' ✓ Quote validation'); console.log(' ✓ API integration testing'); console.log(' ✓ UI/UX testing'); console.log('='.repeat(80) + '\n'); return summary; } // Run the agent const args = process.argv.slice(2); const saveFixtures = args.includes('--save-fixtures') || args.includes('-f'); runCarpenterAgent(saveFixtures) .then(() => { console.log('✅ Carpenter agent completed successfully!\n'); process.exit(0); }) .catch(error => { console.error('\n❌ Error running carpenter agent:', error); process.exit(1); });