377 lines
12 KiB
JavaScript
377 lines
12 KiB
JavaScript
#!/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: 'Installation af håndværkerspil i alle vinduerne gennem huset. I alt 12 vinduer á 1.2x1.0m',
|
|
area: 15,
|
|
type: 'spilinstallation',
|
|
materials: ['Træ-spil', 'Lim', 'Skruer', 'Kraftig 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);
|
|
});
|