Files
tilbudgivern/backend/scripts/import-bygma-prices.js

219 lines
7.2 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Import prices from bygma_products to material_prices
* Matches materials by name similarity and imports current_netto_pris
*/
const mysql = require('mysql2/promise');
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
};
// Calculate simple similarity score between two strings
function similarity(s1, s2) {
const longer = s1.length > s2.length ? s1 : s2;
const shorter = s1.length > s2.length ? s2 : s1;
if (longer.length === 0) return 1.0;
const editDistance = levenshteinDistance(longer.toLowerCase(), shorter.toLowerCase());
return (longer.length - editDistance) / longer.length;
}
// Levenshtein distance algorithm
function levenshteinDistance(str1, str2) {
const costs = [];
for (let i = 0; i <= str1.length; i++) {
let lastValue = i;
for (let j = 0; j <= str2.length; j++) {
if (i === 0) {
costs[j] = j;
} else {
if (j > 0) {
let newValue = costs[j - 1];
if (str1.charAt(i - 1) !== str2.charAt(j - 1)) {
newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
}
costs[j - 1] = lastValue;
lastValue = newValue;
}
}
}
if (i > 0) costs[str2.length] = lastValue;
}
return costs[str2.length];
}
// Check if material name contains key words from bygma product
function containsKeyWords(materialName, bygmaText) {
const materialWords = materialName.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const bygmaWords = bygmaText.toLowerCase().split(/\s+/);
if (materialWords.length === 0) return 0;
let matchCount = 0;
for (const mWord of materialWords) {
if (bygmaWords.some(bWord => bWord.includes(mWord) || mWord.includes(bWord))) {
matchCount++;
}
}
return matchCount / materialWords.length;
}
async function main() {
const connection = await mysql.createConnection(dbConfig);
try {
console.log('🔍 Finding materials without prices...\n');
// Get materials without prices
const [materials] = await connection.execute(`
SELECT m.id, m.name, m.unit, m.category
FROM materials m
LEFT JOIN material_prices mp ON m.id = mp.material_id AND mp.is_active = 1
WHERE mp.id IS NULL
ORDER BY m.id
`);
console.log(`Found ${materials.length} materials without prices\n`);
if (materials.length === 0) {
console.log('✅ All materials already have prices!');
await connection.end();
return;
}
// Get all active bygma products with prices
const [bygmaProducts] = await connection.execute(`
SELECT id, tekst, enhed, current_netto_pris, category_name
FROM bygma_products
WHERE is_active = 1 AND current_netto_pris IS NOT NULL
ORDER BY tekst
`);
console.log(`Loaded ${bygmaProducts.length} Bygma products with prices\n`);
console.log('─'.repeat(80));
const matches = [];
let autoImportCount = 0;
let manualReviewCount = 0;
// Match each material to best bygma product
for (const material of materials) {
let bestMatch = null;
let bestScore = 0;
for (const bygma of bygmaProducts) {
// Calculate similarity score
const nameSimilarity = similarity(material.name, bygma.tekst);
const keywordScore = containsKeyWords(material.name, bygma.tekst);
// Bonus if units match (normalize units)
const materialUnit = (material.unit || '').toLowerCase().replace(/[^a-z0-9]/g, '');
const bygmaUnit = (bygma.enhed || '').toLowerCase().replace(/[^a-z0-9]/g, '');
const unitBonus = materialUnit === bygmaUnit ? 0.2 : 0;
// Bonus if categories match
const categoryBonus = material.category && bygma.category_name &&
material.category.toLowerCase().includes(bygma.category_name.toLowerCase()) ? 0.1 : 0;
const totalScore = (nameSimilarity * 0.5) + (keywordScore * 0.3) + unitBonus + categoryBonus;
if (totalScore > bestScore) {
bestScore = totalScore;
bestMatch = bygma;
}
}
if (bestMatch && bestScore >= 0.3) {
matches.push({
material,
bygma: bestMatch,
score: bestScore,
autoImport: bestScore >= 0.6 // High confidence threshold
});
if (bestScore >= 0.6) {
autoImportCount++;
} else {
manualReviewCount++;
}
}
}
console.log(`\n📊 Match Results:`);
console.log(` High confidence (auto-import): ${autoImportCount}`);
console.log(` Medium confidence (review): ${manualReviewCount}`);
console.log(` No match found: ${materials.length - matches.length}\n`);
console.log('─'.repeat(80));
// Show matches and ask for confirmation
console.log('\n🎯 Matches found:\n');
for (const match of matches) {
const confidence = match.autoImport ? '✅ AUTO' : '⚠️ REVIEW';
const scorePercent = (match.score * 100).toFixed(0);
console.log(`${confidence} [${scorePercent}%] Material: "${match.material.name}" (${match.material.unit})`);
console.log(` → Bygma: "${match.bygma.tekst}" (${match.bygma.enhed})`);
console.log(` → Price: ${parseFloat(match.bygma.current_netto_pris).toFixed(2)} DKK`);
console.log('');
}
console.log('─'.repeat(80));
console.log(`\n💾 Importing ${autoImportCount} high-confidence matches...\n`);
// Import high-confidence matches
let importedCount = 0;
for (const match of matches.filter(m => m.autoImport)) {
try {
await connection.execute(`
INSERT INTO material_prices
(name, material_id, price, currency, valid_from, is_active, unit, supplier_name, category)
VALUES (?, ?, ?, 'DKK', CURDATE(), 1, ?, 'Bygma', ?)
`, [
match.bygma.tekst,
match.material.id,
match.bygma.current_netto_pris,
match.bygma.enhed,
match.material.category
]);
importedCount++;
console.log(`✅ Imported: ${match.material.name}${parseFloat(match.bygma.current_netto_pris).toFixed(2)} DKK`);
} catch (error) {
console.error(`❌ Error importing ${match.material.name}: ${error.message}`);
}
}
console.log(`\n✨ Successfully imported ${importedCount} prices!`);
if (manualReviewCount > 0) {
console.log(`\n⚠️ ${manualReviewCount} matches need manual review (score < 60%)`);
console.log(' Review the medium-confidence matches above and import manually if correct.');
}
if (materials.length - matches.length > 0) {
console.log(`\n${materials.length - matches.length} materials had no suitable Bygma match`);
console.log(' Consider adding prices manually or improving product descriptions.');
}
} catch (error) {
console.error('❌ Error:', error);
throw error;
} finally {
await connection.end();
}
}
// Run the script
main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});