Files
tilbudgivern/tests/carpenter_quote_api_test.py

359 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
API-based Carpenter Quote Test
Tests quote creation through API calls with UI verification
This is more reliable than pure UI testing
"""
import requests
import json
import time
from datetime import datetime
class CarpenterQuoteAPITest:
def __init__(self):
self.api_url = 'http://localhost:4031'
self.test_data = {
'customer_name': f'Test Tømrerkunde API - {datetime.now().strftime("%Y%m%d-%H%M%S")}',
'customer_email': 'test-api@toemrer.dk',
'customer_phone': '42 46 81 10',
'customer_address': 'API Testvej 123, 4600 Køge',
'project_description': 'Terrasse 30 m² med douglasgran brædder',
}
self.project_id = None
self.results = {}
def run_all_tests(self):
"""Run all tests"""
print('\n' + '='*60)
print('🔨 CARPENTER QUOTE API TESTS')
print('='*60 + '\n')
try:
self.test_01_api_health()
self.test_02_create_project()
self.test_03_add_geometry()
self.test_04_get_smart_packages()
self.test_05_add_materials()
self.test_06_calculate_totals()
self.test_07_verify_realism()
self.test_08_save_results()
print('\n' + '='*60)
print('✅ ALL TESTS PASSED')
print('='*60 + '\n')
return True
except Exception as e:
print(f'\n❌ TEST FAILED: {e}\n')
return False
def test_01_api_health(self):
"""Test 1: API Health Check"""
print('🏥 Test 1: API Health Check')
response = requests.get(f'{self.api_url}/health')
assert response.status_code == 200, f'Health check failed: {response.status_code}'
data = response.json()
print(f' ✅ API Status: {data.get("status")}')
print(f' ✅ Service: {data.get("service")}')
def test_02_create_project(self):
"""Test 2: Create Customer Project"""
print('\n🏗️ Test 2: Create Customer Project')
payload = {
'projectName': 'Terrasse Projekt via API',
'customerName': self.test_data['customer_name'],
'address': self.test_data['customer_address'],
'zipcode': '4600',
'city': 'Køge'
}
response = requests.post(
f'{self.api_url}/api/customer-projects/projects',
json=payload
)
if response.status_code != 200:
print(f' ❌ Error response: {response.text}')
assert response.status_code == 200, f'Create project failed: {response.status_code}'
data = response.json()
assert data.get('success'), 'Project creation was not successful'
self.project_id = data['project']['id']
print(f' ✅ Project created: ID {self.project_id}')
print(f' ✅ Customer: {self.test_data["customer_name"]}')
self.results['project_id'] = self.project_id
self.results['customer'] = self.test_data
def test_03_add_geometry(self):
"""Test 3: Add Geometry Data"""
print('\n📐 Test 3: Add Geometry Data')
geometry_data = {
'roofType': 'fladt_tag',
'roofWidth': 5.0,
'roofLength': 6.0,
'wallHeight': 2.5,
'roofPitch': 2.0, # Fladt tag har lav hældning
'complexity': 1.0
}
print(f' 📤 Sending payload: {json.dumps(geometry_data, indent=2)}')
response = requests.post(
f'{self.api_url}/api/customer-projects/{self.project_id}/geometry',
json=geometry_data
)
if response.status_code != 200:
print(f' ❌ Error response: {response.text}')
assert response.status_code == 200, f'Add geometry failed: {response.status_code}'
data = response.json()
assert data.get('success'), 'Geometry addition was not successful'
total_area = geometry_data['roofWidth'] * geometry_data['roofLength']
print(f' ✅ Geometry added: {total_area:.1f}')
print(f' ✅ Dimensions: {geometry_data["roofLength"]}m × {geometry_data["roofWidth"]}m')
print(f' ✅ Wall height: {geometry_data["wallHeight"]}m')
self.results['geometry'] = geometry_data
def test_04_get_smart_packages(self):
"""Test 4: Get Available Smart Packages"""
print('\n🔧 Test 4: Get Smart Packages')
response = requests.get(f'{self.api_url}/api/smart-packages')
assert response.status_code == 200, f'Get packages failed: {response.status_code}'
data = response.json()
packages = data.get('packages', [])
print(f' ✅ Found {len(packages)} smart packages')
if packages:
# Show first few packages
for pkg in packages[:3]:
print(f'{pkg.get("name")} ({pkg.get("category")})')
self.results['available_packages'] = len(packages)
def test_05_add_materials(self):
"""Test 5: Add Materials to Project"""
print('\n🧱 Test 5: Add Materials')
# Typical materials for a 30m² terrace - use camelCase for server
materials = [
{
'materialName': 'Douglasgran terrassebrædder 28x145mm',
'quantity': 35,
'unit': 'm',
'unitPrice': 75.50,
'supplier': 'Bygma',
'materialCategory': 'Træ'
},
{
'materialName': 'Terrasseunderkonstruktion 45x95mm',
'quantity': 45,
'unit': 'm',
'unitPrice': 45.00,
'supplier': 'Bygma',
'materialCategory': 'Træ'
},
{
'materialName': 'Rustfrie terrasseskruer 4.5x50mm',
'quantity': 2,
'unit': 'pakke',
'unitPrice': 245.00,
'supplier': 'Bygma',
'materialCategory': 'Beslag'
},
{
'materialName': 'Terrassebeslag og fødder',
'quantity': 24,
'unit': 'stk',
'unitPrice': 15.50,
'supplier': 'Bygma',
'materialCategory': 'Beslag'
},
{
'materialName': 'Træbeskyttelse Douglasgran olie',
'quantity': 2,
'unit': 'liter',
'unitPrice': 185.00,
'supplier': 'Bauhaus',
'materialCategory': 'Maling'
}
]
# Add all materials in one request as server expects array
response = requests.post(
f'{self.api_url}/api/customer-projects/{self.project_id}/materials',
json={'materials': materials}
)
assert response.status_code == 200, f'Add materials failed: {response.status_code}'
data = response.json()
assert data.get('success'), 'Materials addition was not successful'
# Calculate total
total_material_cost = sum(m['quantity'] * m['unitPrice'] for m in materials)
print(f' ✅ Added {len(materials)} materials')
for material in materials:
material_cost = material['quantity'] * material['unitPrice']
print(f'{material["materialName"]}: {material_cost:,.0f} kr')
print(f'\n 💰 Total materials: {len(materials)} items = {total_material_cost:,.0f} kr')
self.results['materials'] = {
'count': len(materials),
'total_cost': total_material_cost,
'items': materials
}
def test_06_calculate_totals(self):
"""Test 6: Calculate Project Totals"""
print('\n💰 Test 6: Calculate Totals')
# Get project with all data
response = requests.get(f'{self.api_url}/api/customer-projects/{self.project_id}')
assert response.status_code == 200, f'Get project failed: {response.status_code}'
data = response.json()
project = data.get('project')
# Calculate from materials
materials = project.get('materials', [])
material_total = sum(
float(m.get('quantity', 0)) * float(m.get('unit_price', 0))
for m in materials
)
# Estimate labor (typical 25-30 hours for 30m² terrace)
estimated_hours = 28
hourly_rate = 500
labor_total = estimated_hours * hourly_rate
# Totals
subtotal = material_total + labor_total
vat = subtotal * 0.25
total_incl_vat = subtotal + vat
print(f' 📦 Materials: {material_total:,.0f} kr ({len(materials)} items)')
print(f' ⏱️ Labor: {labor_total:,.0f} kr ({estimated_hours} timer × {hourly_rate} kr/t)')
print(f' 💵 Subtotal: {subtotal:,.0f} kr')
print(f' 📊 Moms (25%): {vat:,.0f} kr')
print(f' 🎯 Total inkl. moms: {total_incl_vat:,.0f} kr')
self.results['pricing'] = {
'materials': material_total,
'labor_hours': estimated_hours,
'labor_rate': hourly_rate,
'labor_total': labor_total,
'subtotal': subtotal,
'vat': vat,
'total_incl_vat': total_incl_vat
}
def test_07_verify_realism(self):
"""Test 7: Verify Quote Realism"""
print('\n🔍 Test 7: Verify Quote Realism')
pricing = self.results.get('pricing', {})
materials = self.results.get('materials', {})
# Market comparison for 30m² terrace
market_price_per_m2 = 2500 # 2000-3000 kr/m²
market_total = 30 * market_price_per_m2 # ~75,000 kr
our_total = pricing.get('total_incl_vat', 0)
difference_pct = ((our_total - market_total) / market_total) * 100
checks = {
'Has materials': materials.get('count', 0) > 0,
'Material cost reasonable': 15000 < materials.get('total_cost', 0) < 40000,
'Labor hours reasonable': 20 < pricing.get('labor_hours', 0) < 40,
'Total price in market range': 60000 < our_total < 100000,
'Price deviation acceptable': abs(difference_pct) < 30,
'VAT calculated': pricing.get('vat', 0) > 0,
'All components present': all([
pricing.get('materials'),
pricing.get('labor_total'),
pricing.get('vat'),
pricing.get('total_incl_vat')
])
}
passed = sum(checks.values())
total = len(checks)
print(f'\n Realism Check: {passed}/{total} passed\n')
for check, result in checks.items():
print(f' {"" if result else ""} {check}')
print(f'\n 📊 Market comparison:')
print(f' Market price: ~{market_total:,.0f} kr (2500 kr/m²)')
print(f' Our quote: {our_total:,.0f} kr')
print(f' Difference: {difference_pct:+.1f}%')
if abs(difference_pct) < 10:
print(f' ✅ EXCELLENT - Very close to market price')
elif abs(difference_pct) < 20:
print(f' ✅ GOOD - Within reasonable range')
elif abs(difference_pct) < 30:
print(f' ⚠️ ACCEPTABLE - At edge of market range')
else:
print(f' ❌ CONCERN - Significantly different from market')
self.results['realism_check'] = {
'passed': passed,
'total': total,
'checks': checks,
'market_comparison': {
'market_price': market_total,
'our_price': our_total,
'difference_pct': difference_pct
}
}
assert passed >= total * 0.8, f'Only {passed}/{total} realism checks passed'
def test_08_save_results(self):
"""Test 8: Save Test Results"""
print('\n💾 Test 8: Save Results')
results_file = 'test-results/api-quote-results.json'
import os
os.makedirs('test-results', exist_ok=True)
with open(results_file, 'w', encoding='utf-8') as f:
json.dump(self.results, f, indent=2, ensure_ascii=False, default=str)
print(f' ✅ Results saved to: {results_file}')
print(f' ✅ Project ID: {self.project_id}')
print(f' ✅ Total price: {self.results["pricing"]["total_incl_vat"]:,.0f} kr')
if __name__ == '__main__':
test = CarpenterQuoteAPITest()
success = test.run_all_tests()
if success:
print('\n🎉 All tests passed! Quote is realistic and production-ready.\n')
exit(0)
else:
print('\n❌ Some tests failed.\n')
exit(1)