Files
tilbudgivern/carpenter-agent-api.js

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}\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);
});