Files
tilbudgivern/backend/src/services/packageService.js
T
alex 9f74ec8949 Add initial test suite configuration with Jest and Supertest
- Created package.json for the test suite "tilbudgivern-tests"
- Added scripts for running tests, watching tests, and generating coverage reports
- Included Jest and Supertest as development dependencies
- Configured Jest with a test environment, test match pattern, and coverage collection settings
- Specified coverage output directory
2025-09-26 00:14:38 +02:00

879 lines
28 KiB
JavaScript

const logger = require('../utils/logger');
/**
* PackageService - Håndterer pakker til forskellige tagtyper
* Pakker indeholder materialelister, priser og fortjenestemarginaler
*/
class PackageService {
constructor(databaseService) {
this.db = databaseService;
}
/**
* Find real materials from database based on search terms
* @param {string} searchTerm - Search term for material name
* @param {string} category - Optional category filter
* @returns {Object|null} Material with price info
*/
async findRealMaterial(searchTerm, category = null) {
try {
// First try Bygma products (most comprehensive)
let bygmaQuery = `
SELECT
bp.vareNr as varenummer,
bp.tekst as name,
bp.enhed as unit,
bp.current_netto_pris as unitPrice,
bp.varegrp as category,
'bygma' as source
FROM bygma_products bp
WHERE bp.is_active = 1
AND (bp.tekst LIKE ? OR bp.vareNr LIKE ?)
`;
const bygmaParams = [`%${searchTerm}%`, `%${searchTerm}%`];
if (category) {
bygmaQuery += ` AND bp.varegrp = ?`;
bygmaParams.push(category);
}
bygmaQuery += ` ORDER BY bp.current_netto_pris ASC LIMIT 1`;
const bygmaMaterials = await this.db.query(bygmaQuery, bygmaParams);
if (bygmaMaterials && bygmaMaterials.length > 0) {
const material = bygmaMaterials[0];
return {
name: material.name,
varenummer: material.varenummer,
unit: material.unit || 'stk',
unitPrice: parseFloat(material.unitPrice) || 0,
category: material.category || 'diverse',
source: 'bygma'
};
}
// Fallback to built-in materials
let builtInQuery = `
SELECT
CONCAT('MP', mp.id) as varenummer,
mp.name,
mp.unit,
mp.price as unitPrice,
mp.category,
'built_in' as source
FROM material_prices mp
WHERE mp.is_active = 1
AND mp.name LIKE ?
`;
const builtInParams = [`%${searchTerm}%`];
if (category) {
builtInQuery += ` AND mp.category = ?`;
builtInParams.push(category);
}
builtInQuery += ` ORDER BY mp.price ASC LIMIT 1`;
const builtInMaterials = await this.db.query(builtInQuery, builtInParams);
if (builtInMaterials && builtInMaterials.length > 0) {
const material = builtInMaterials[0];
return {
name: material.name,
varenummer: material.varenummer,
unit: material.unit || 'stk',
unitPrice: parseFloat(material.unitPrice) || 0,
category: material.category || 'diverse',
source: 'built_in'
};
}
return null;
} catch (error) {
logger.error('Error finding real material:', error);
return null;
}
}
/**
* Få pakker for en specifik tagtype
* @param {string} roofType - Tag type (Betontegl, B7, B6, Vingetegl, etc.)
* @param {number} area - Areal i m² for mængdeberegning
*/
async getPackagesForRoofType(roofType, area = 100) {
try {
// Hent pakker fra database eller brug standard pakker
const packages = await this.getStandardPackages(roofType, area);
// Beregn priser for alle pakker
const packagesWithPricing = packages.map(pkg => ({
...pkg,
pricing: this.calculatePackagePrice(pkg),
geometryArea: area
}));
logger.info('Retrieved packages for roof type', {
roofType,
area,
packageCount: packagesWithPricing.length
});
return packagesWithPricing;
} catch (error) {
logger.error('Error getting packages for roof type:', error);
throw error;
}
}
/**
* Standard pakkedefinitioner for forskellige tagtyper
*/
async getStandardPackages(roofType, area) {
const packages = [];
// Først standard pakker
switch (roofType.toLowerCase()) {
case 'betontegl':
packages.push(...await this.getBetonteglPackages(area));
break;
case 'b7':
packages.push(...await this.getB7Packages(area));
break;
case 'b6':
packages.push(...await this.getB6Packages(area));
break;
case 'vingetegl':
packages.push(...await this.getVingeteglPackages(area));
break;
case 'røde_teglsten':
packages.push(...await this.getRødeTeglstenPackages(area));
break;
default:
packages.push(...await this.getGenericRoofPackages(area));
}
// Tilføj custom packages for denne tagtype
try {
const customResult = await this.getCustomPackages();
if (customResult.success) {
const relevantCustomPackages = customResult.packages
.filter(pkg => pkg.roof_type.toLowerCase() === roofType.toLowerCase())
.map(pkg => ({
id: `custom_${pkg.id}`,
name: pkg.name,
description: pkg.description,
roofType: pkg.roof_type,
unit: pkg.unit,
materials: pkg.materials,
profitMargin: pkg.pricing.profitMargin,
isCustom: true,
pricing: pkg.pricing
}));
packages.push(...relevantCustomPackages);
}
} catch (error) {
logger.error('Error loading custom packages:', error);
// Continue without custom packages if there's an error
}
return packages;
}
/**
* Betontegl pakker - nu med rigtige materialer fra databasen
*/
async getBetonteglPackages(area) {
// Find rigtige materialer fra databasen
const betontegl = await this.findRealMaterial('betontegl', '4840') ||
await this.findRealMaterial('tegl') ||
{ name: 'Betonteglsten (ikke fundet)', varenummer: 'FALLBACK_BT', unitPrice: 12.50, unit: 'stk', category: 'Tagbeklædning' };
const tagcentraler = await this.findRealMaterial('tagcentral', '5160') ||
await this.findRealMaterial('beslag') ||
{ name: 'Tagcentraler (ikke fundet)', varenummer: 'FALLBACK_TC', unitPrice: 85.00, unit: 'stk', category: 'Beslag' };
const undertag = await this.findRealMaterial('undertag', '4860') ||
await this.findRealMaterial('membran') ||
{ name: 'Undertag diffusionsåben (ikke fundet)', varenummer: 'FALLBACK_UT', unitPrice: 45.00, unit: 'm²', category: 'Undertag' };
const vindskeder = await this.findRealMaterial('vindskede', '1000') ||
await this.findRealMaterial('brædder') ||
{ name: 'Vindskeder 25x200mm (ikke fundet)', varenummer: 'FALLBACK_VS', unitPrice: 95.00, unit: 'løbm', category: 'Træ' };
const isolering = await this.findRealMaterial('mineraluld', '1045') ||
await this.findRealMaterial('isolering') ||
{ name: 'Mineraluld 200mm (ikke fundet)', varenummer: 'FALLBACK_IS', unitPrice: 75.00, unit: 'm²', category: 'Isolering' };
return [
{
id: 'betontegl_basic',
name: 'Betontegl Basis Pakke',
description: 'Standard betontegl installation med grundmaterialer - opdateret med rigtige materialer',
roofType: 'betontegl',
unit: 'm²',
materials: [
{
name: betontegl.name,
varenummer: betontegl.varenummer,
quantity: Math.ceil(area * 11), // 11 stk/m²
unit: betontegl.unit,
unitPrice: betontegl.unitPrice,
category: betontegl.category || 'Tagbeklædning',
source: betontegl.source || 'database'
},
{
name: tagcentraler.name,
varenummer: tagcentraler.varenummer,
quantity: Math.ceil(area * 0.8), // 0.8 stk/m²
unit: tagcentraler.unit,
unitPrice: tagcentraler.unitPrice,
category: tagcentraler.category || 'Beslag',
source: tagcentraler.source || 'database'
},
{
name: undertag.name,
varenummer: undertag.varenummer,
quantity: Math.ceil(area * 1.05), // 5% spild
unit: undertag.unit,
unitPrice: undertag.unitPrice,
category: undertag.category || 'Undertag',
source: undertag.source || 'database'
},
{
name: vindskeder.name,
varenummer: vindskeder.varenummer,
quantity: this.calculateWindboardLength(area),
unit: vindskeder.unit,
unitPrice: vindskeder.unitPrice,
category: vindskeder.category || 'Træ',
source: vindskeder.source || 'database'
}
],
profitMargin: 25, // 25% fortjeneste
complexity: 1.0
},
{
id: 'betontegl_premium',
name: 'Betontegl Premium Pakke',
description: 'Premium betontegl med ekstra isolering og kvalitetsmaterialer - opdateret med rigtige materialer',
roofType: 'betontegl',
unit: 'm²',
materials: [
{
name: betontegl.name + ' (Premium)',
varenummer: betontegl.varenummer,
quantity: Math.ceil(area * 11),
unit: betontegl.unit,
unitPrice: betontegl.unitPrice * 1.5, // Premium pris
category: betontegl.category || 'Tagbeklædning',
source: betontegl.source || 'database'
},
{
name: tagcentraler.name + ' (Rustfri)',
varenummer: tagcentraler.varenummer,
quantity: Math.ceil(area * 0.8),
unit: tagcentraler.unit,
unitPrice: tagcentraler.unitPrice * 1.4, // Premium pris
category: tagcentraler.category || 'Beslag',
source: tagcentraler.source || 'database'
},
{
name: undertag.name + ' (Premium)',
varenummer: undertag.varenummer,
quantity: Math.ceil(area * 1.05),
unit: undertag.unit,
unitPrice: undertag.unitPrice * 1.3, // Premium pris
category: undertag.category || 'Undertag',
source: undertag.source || 'database'
},
{
name: isolering.name,
varenummer: isolering.varenummer,
quantity: Math.ceil(area * 1.02),
unit: isolering.unit,
unitPrice: isolering.unitPrice,
category: isolering.category || 'Isolering',
source: isolering.source || 'database'
}
],
profitMargin: 30, // 30% fortjeneste for premium
complexity: 1.2
}
];
}
/**
* B7 tagplader pakker - nu med rigtige materialer fra databasen
*/
async getB7Packages(area) {
// Find rigtige B7 materialer fra databasen
const b7Plader = await this.findRealMaterial('B7', '4840') ||
await this.findRealMaterial('tagplade') ||
await this.findRealMaterial('eternit') ||
{ name: 'B7 tagplader 1100x2500mm (ikke fundet)', varenummer: 'FALLBACK_B7', unitPrice: 285.00, unit: 'stk', category: 'Tagbeklædning' };
const skruer = await this.findRealMaterial('skruer', '5160') ||
await this.findRealMaterial('tagskrue') ||
{ name: 'B7 Skruer 6.3x150mm (ikke fundet)', varenummer: 'FALLBACK_BS', unitPrice: 4.50, unit: 'stk', category: 'Beslag' };
const ryggesten = await this.findRealMaterial('ryggesten', '4840') ||
await this.findRealMaterial('tagryg') ||
{ name: 'Tagryggesten til B7 (ikke fundet)', varenummer: 'FALLBACK_RS', unitPrice: 145.00, unit: 'løbm', category: 'Ryg/Gavl' };
const undertag = await this.findRealMaterial('undertag', '4860') ||
await this.findRealMaterial('membran') ||
{ name: 'Undertag til B7 (ikke fundet)', varenummer: 'FALLBACK_UT2', unitPrice: 42.00, unit: 'm²', category: 'Undertag' };
const dampspærre = await this.findRealMaterial('dampspærre', '4860') ||
await this.findRealMaterial('plastfolie') ||
{ name: 'Dampspærre folie (ikke fundet)', varenummer: 'FALLBACK_DS', unitPrice: 25.50, unit: 'm²', category: 'Dampspærre' };
return [
{
id: 'b7_basic',
name: 'B7 Tagplader Basis',
description: 'Standard B7 tagpladepakke til skråtag - opdateret med rigtige materialer',
roofType: 'b7',
unit: 'm²',
materials: [
{
name: b7Plader.name,
varenummer: b7Plader.varenummer,
quantity: Math.ceil(area / 2.75), // pr. plade dækker ~2.75m²
unit: b7Plader.unit,
unitPrice: b7Plader.unitPrice,
category: b7Plader.category || 'Tagbeklædning',
source: b7Plader.source || 'database'
},
{
name: skruer.name,
varenummer: skruer.varenummer,
quantity: Math.ceil(area * 8), // 8 skruer/m²
unit: skruer.unit,
unitPrice: skruer.unitPrice,
category: skruer.category || 'Beslag',
source: skruer.source || 'database'
},
{
name: ryggesten.name,
varenummer: ryggesten.varenummer,
quantity: this.calculateRidgeLength(area),
unit: ryggesten.unit,
unitPrice: ryggesten.unitPrice,
category: ryggesten.category || 'Ryg/Gavl',
source: ryggesten.source || 'database'
},
{
name: undertag.name,
varenummer: undertag.varenummer,
quantity: Math.ceil(area * 1.05),
unit: undertag.unit,
unitPrice: undertag.unitPrice,
category: undertag.category || 'Undertag',
source: undertag.source || 'database'
}
],
profitMargin: 22, // 22% fortjeneste
complexity: 0.9
},
{
id: 'b7_insulated',
name: 'B7 Isoleret Løsning',
description: 'B7 tagplader med integreret isolering og dampspærre - opdateret med rigtige materialer',
roofType: 'b7',
unit: 'm²',
materials: [
{
name: b7Plader.name + ' (Isoleret)',
varenummer: b7Plader.varenummer,
quantity: Math.ceil(area / 2.75),
unit: b7Plader.unit,
unitPrice: b7Plader.unitPrice * 1.5, // Isoleret version er dyrere
category: b7Plader.category || 'Tagbeklædning',
source: b7Plader.source || 'database'
},
{
name: skruer.name + ' (Termo)',
varenummer: skruer.varenummer,
quantity: Math.ceil(area * 6),
unit: skruer.unit,
unitPrice: skruer.unitPrice * 1.8, // Termoskruer er dyrere
category: skruer.category || 'Beslag',
source: skruer.source || 'database'
},
{
name: dampspærre.name,
varenummer: dampspærre.varenummer,
quantity: Math.ceil(area * 1.1),
unit: dampspærre.unit,
unitPrice: dampspærre.unitPrice,
category: dampspærre.category || 'Dampspærre',
source: dampspærre.source || 'database'
}
],
profitMargin: 28,
complexity: 1.1
}
];
}
/**
* B6 tagplader pakker
*/
async getB6Packages(area) {
return [
{
id: 'b6_basic',
name: 'B6 Tagplader Standard',
description: 'Standard B6 tagpladepakke til mindre projekter',
roofType: 'b6',
unit: 'm²',
materials: [
{
name: 'B6 tagplader 900x2000mm',
varenummer: 'B6001',
quantity: Math.ceil(area / 1.8), // pr. plade dækker ~1.8m²
unit: 'stk',
unitPrice: 195.00,
category: 'Tagbeklædning'
},
{
name: 'B6 Skruer 5.5x120mm',
varenummer: 'B6002',
quantity: Math.ceil(area * 10), // flere skruer pga. mindre plader
unit: 'stk',
unitPrice: 3.75,
category: 'Beslag'
},
{
name: 'Tagryggesten til B6',
varenummer: 'B6003',
quantity: this.calculateRidgeLength(area),
unit: 'løbm',
unitPrice: 125.00,
category: 'Ryg/Gavl'
}
],
profitMargin: 20,
complexity: 0.8
}
];
}
/**
* Vingetegl pakker - nu med rigtige materialer fra databasen
*/
async getVingeteglPackages(area) {
// Find rigtige vingetegl materialer fra databasen
const vingetegl = await this.findRealMaterial('vingetegl', '4840') ||
await this.findRealMaterial('tegl') ||
{ name: 'Vingetegl klassisk (ikke fundet)', varenummer: 'FALLBACK_VT', unitPrice: 16.50, unit: 'stk', category: 'Tagbeklædning' };
const tagcentraler = await this.findRealMaterial('tagcentral', '5160') ||
await this.findRealMaterial('beslag') ||
{ name: 'Tagcentraler til vingetegl (ikke fundet)', varenummer: 'FALLBACK_TC', unitPrice: 95.00, unit: 'stk', category: 'Beslag' };
const mørtel = await this.findRealMaterial('mørtel', '7020') ||
await this.findRealMaterial('rygningsmørtel') ||
{ name: 'Rygningsmørtel (ikke fundet)', varenummer: 'FALLBACK_RM', unitPrice: 15.00, unit: 'kg', category: 'Mørtel' };
return [
{
id: 'vingetegl_classic',
name: 'Vingetegl Klassisk',
description: 'Traditionel vingetegl med klassisk udseende - opdateret med rigtige materialer',
roofType: 'vingetegl',
unit: 'm²',
materials: [
{
name: vingetegl.name,
varenummer: vingetegl.varenummer,
quantity: Math.ceil(area * 14), // 14 stk/m²
unit: vingetegl.unit,
unitPrice: vingetegl.unitPrice,
category: vingetegl.category || 'Tagbeklædning',
source: vingetegl.source || 'database'
},
{
name: tagcentraler.name,
varenummer: tagcentraler.varenummer,
quantity: Math.ceil(area * 0.7),
unit: tagcentraler.unit,
unitPrice: tagcentraler.unitPrice,
category: tagcentraler.category || 'Beslag',
source: tagcentraler.source || 'database'
},
{
name: mørtel.name,
varenummer: mørtel.varenummer,
quantity: Math.ceil(this.calculateRidgeLength(area) * 2), // kg pr. løbm
unit: mørtel.unit,
unitPrice: mørtel.unitPrice,
category: mørtel.category || 'Mørtel',
source: mørtel.source || 'database'
}
],
profitMargin: 26,
complexity: 1.3
}
];
}
/**
* Røde teglsten pakker
*/
async getRødeTeglstenPackages(area) {
return [
{
id: 'roed_tegl_premium',
name: 'Røde Teglsten Premium',
description: 'Klassiske røde teglsten af høj kvalitet',
roofType: 'røde_teglsten',
unit: 'm²',
materials: [
{
name: 'Røde tagsten premium',
varenummer: 'RT001',
quantity: Math.ceil(area * 12),
unit: 'stk',
unitPrice: 22.00,
category: 'Tagbeklædning'
},
{
name: 'Traditionelle tagcentraler',
varenummer: 'RT002',
quantity: Math.ceil(area * 0.9),
unit: 'stk',
unitPrice: 115.00,
category: 'Beslag'
}
],
profitMargin: 32, // Højere fortjeneste på premium produkter
complexity: 1.4
}
];
}
/**
* Generiske tag pakker
*/
async getGenericRoofPackages(area) {
return [
{
id: 'generic_roof',
name: 'Standard Tag Pakke',
description: 'Generel tagpakke til forskellige tagtyper',
roofType: 'generisk',
unit: 'm²',
materials: [
{
name: 'Tagmateriale standard',
varenummer: 'GEN001',
quantity: area,
unit: 'm²',
unitPrice: 125.00,
category: 'Tagbeklædning'
},
{
name: 'Undertag standard',
varenummer: 'GEN002',
quantity: Math.ceil(area * 1.05),
unit: 'm²',
unitPrice: 45.00,
category: 'Undertag'
}
],
profitMargin: 20,
complexity: 1.0
}
];
}
/**
* Beregn pakkepriser med fortjeneste
*/
calculatePackagePrice(packageData) {
let totalCostPrice = 0;
packageData.materials.forEach(material => {
totalCostPrice += material.quantity * material.unitPrice;
});
const profitAmount = totalCostPrice * (packageData.profitMargin / 100);
const listPrice = totalCostPrice + profitAmount;
return {
costPrice: Math.round(totalCostPrice * 100) / 100,
profitAmount: Math.round(profitAmount * 100) / 100,
listPrice: Math.round(listPrice * 100) / 100,
profitMargin: packageData.profitMargin,
currency: 'DKK'
};
}
/**
* Søg i pakker
*/
async searchPackages(searchTerm, roofType = null) {
try {
const allPackages = [];
// Hvis roofType er specificeret, søg kun i den type
if (roofType) {
const packages = await this.getPackagesForRoofType(roofType, 100);
allPackages.push(...packages);
} else {
// Søg i alle tagtyper
const roofTypes = ['betontegl', 'b7', 'b6', 'vingetegl', 'røde_teglsten'];
for (const type of roofTypes) {
const packages = await this.getPackagesForRoofType(type, 100);
allPackages.push(...packages);
}
}
// Filtrer baseret på søgeterm
const filteredPackages = allPackages.filter(pkg =>
pkg.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
pkg.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
pkg.materials.some(material =>
material.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
material.varenummer.toLowerCase().includes(searchTerm.toLowerCase())
)
);
return filteredPackages;
} catch (error) {
logger.error('Error searching packages:', error);
throw error;
}
}
/**
* Tilføj pakke til projekt
*/
async addPackageToProject(projectId, packageId, customArea = null) {
try {
// Hent projekt geometri for at beregne areal
const [geometryRows] = await this.db.pool.execute(
'SELECT total_area FROM roof_geometry WHERE project_id = ?',
[projectId]
);
const area = customArea || (geometryRows[0] ? geometryRows[0].total_area : 100);
// Find pakken
const packages = await this.searchPackages('', null);
const packageData = packages.find(pkg => pkg.id === packageId);
if (!packageData) {
throw new Error(`Package not found: ${packageId}`);
}
// Tilføj alle materialer fra pakken til projektet
for (const material of packageData.materials) {
await this.db.pool.execute(`
INSERT INTO project_materials (
project_id, name, material_category, unit,
quantity, unit_price, total_price, varenummer, source
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
projectId,
material.name,
material.category,
material.unit,
material.quantity,
material.unitPrice,
material.quantity * material.unitPrice,
material.varenummer,
`package:${packageId}`
]);
}
logger.info('Package added to project', {
projectId,
packageId,
materialCount: packageData.materials.length,
area
});
return {
success: true,
package: packageData,
materialsAdded: packageData.materials.length,
totalCost: this.calculatePackagePrice(packageData).listPrice
};
} catch (error) {
logger.error('Error adding package to project:', error);
throw error;
}
}
/**
* Hjælpefunktioner til geometriberegninger
*/
calculateWindboardLength(area) {
// Antag kvadratisk tag og beregn omkreds
const sideLength = Math.sqrt(area);
return Math.ceil(sideLength * 4); // løbm
}
calculateRidgeLength(area) {
// Antag at ryglængde er ca. 1/3 af kvadratrodens længde
const sideLength = Math.sqrt(area);
return Math.ceil(sideLength * 1.2); // løbm
}
/**
* Gem custom pakke i database
*/
async saveCustomPackage(packageData) {
try {
// Calculate cost price from materials
const costPrice = packageData.materials ?
packageData.materials.reduce((sum, material) =>
sum + (parseFloat(material.quantity || 0) * parseFloat(material.unitPrice || 0)), 0) : 0;
const [result] = await this.db.pool.execute(`
INSERT INTO custom_packages (
name, description, roof_type, materials, unit,
cost_price, profit_margin
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
packageData.name,
packageData.description || '',
packageData.roofType,
JSON.stringify(packageData.materials || []),
packageData.unit || 'm2',
costPrice,
packageData.profitMargin || 20.00
]);
logger.info('Custom package saved', {
packageId: result.insertId,
name: packageData.name,
costPrice: costPrice,
unit: packageData.unit
});
return {
success: true,
id: result.insertId,
message: 'Pakke gemt succesfuldt'
};
} catch (error) {
logger.error('Error saving custom package:', error);
throw error;
}
}
async getCustomPackages() {
try {
const [rows] = await this.db.pool.execute(`
SELECT id, name, description, roof_type, materials, unit,
cost_price, list_price, profit_margin, quantity_from_geometry,
created_at, updated_at
FROM custom_packages
WHERE is_active = 1
ORDER BY created_at DESC
`);
const packages = rows.map(pkg => ({
...pkg,
materials: JSON.parse(pkg.materials || '[]'),
pricing: {
costPrice: parseFloat(pkg.cost_price || 0),
listPrice: parseFloat(pkg.list_price || 0),
profitMargin: parseFloat(pkg.profit_margin || 0)
}
}));
return {
success: true,
packages: packages
};
} catch (error) {
logger.error('Error fetching custom packages:', error);
throw error;
}
}
async updateCustomPackage(packageId, packageData) {
try {
// Calculate cost price from materials
const costPrice = packageData.materials ?
packageData.materials.reduce((sum, material) =>
sum + (parseFloat(material.quantity || 0) * parseFloat(material.unitPrice || 0)), 0) : 0;
const [result] = await this.db.pool.execute(`
UPDATE custom_packages SET
name = ?, description = ?, roof_type = ?,
materials = ?, unit = ?, cost_price = ?, profit_margin = ?,
updated_at = NOW()
WHERE id = ? AND is_active = 1
`, [
packageData.name,
packageData.description || '',
packageData.roofType,
JSON.stringify(packageData.materials || []),
packageData.unit || 'm2',
costPrice,
packageData.profitMargin || 20.00,
packageId
]);
if (result.affectedRows === 0) {
return {
success: false,
message: 'Pakke ikke fundet eller ikke aktiv'
};
}
logger.info('Custom package updated', {
packageId: packageId,
name: packageData.name,
costPrice: costPrice,
unit: packageData.unit
});
return {
success: true,
message: 'Pakke opdateret succesfuldt'
};
} catch (error) {
logger.error('Error updating custom package:', error);
throw error;
}
}
async deleteCustomPackage(packageId) {
try {
// Soft delete - set is_active = 0
const [result] = await this.db.pool.execute(`
UPDATE custom_packages SET is_active = 0, updated_at = NOW()
WHERE id = ? AND is_active = 1
`, [packageId]);
if (result.affectedRows === 0) {
return {
success: false,
message: 'Pakke ikke fundet eller allerede slettet'
};
}
logger.info('Custom package deleted', { packageId: packageId });
return {
success: true,
message: 'Pakke slettet succesfuldt'
};
} catch (error) {
logger.error('Error deleting custom package:', error);
throw error;
}
}
}
module.exports = PackageService;