/** * Test matematici base modulo Plasticino / Profili. * Esegue in Node (.cjs). * * Copre: * - Guardia porteGarage (contratto M9) * - Validazione config (mancanti, quantità invalida, codice sconosciuto) * - Lookup profilo e calcolo barra intera * - Taglio a misura (+1,50 €/m) * - Lookup accessorio €/cad * - Sconto 44% applicato correttamente * - Markup vendita ×1.4 * - getOpzioniCampo (famiglie filtrate per tipoArticolo, codici per famiglia) * - Edge case: tipoArticolo incoerente, tagliatoAMisura su accessorio * - Verifica alcuni prezzi noti del listino (spot check) */ 'use strict'; const path = require('path'); const Module = require('module'); const fs = require('fs'); // ── Carico listino-data.js: simulo `window` globale prima di eseguirlo ── globalThis.window = globalThis; const LISTINO_DATA_PATH = path.resolve(__dirname, '../shared-database/fornitori/plasticino/profili/listino-data.js'); const listinoSrc = fs.readFileSync(LISTINO_DATA_PATH, 'utf8'); // Sopprimo console.log del listino per pulizia output test const originalLog = console.log; console.log = () => {}; eval(listinoSrc); console.log = originalLog; // ── Carico calcolo.js come CommonJS ── const CALC_PATH = path.resolve(__dirname, '../shared-database/fornitori/plasticino/profili/calcolo.js'); const calcSrc = fs.readFileSync(CALC_PATH, 'utf8'); const m = new Module(CALC_PATH); m.filename = CALC_PATH; m.paths = Module._nodeModulePaths(path.dirname(CALC_PATH)); // Il calcolo.js espone module.exports quando window è undefined. Qui window esiste (l'ho creato), // quindi dobbiamo prendere il modulo da window[EXPORT_NAME]. m._compile(calcSrc, CALC_PATH); const modulo = globalThis.window.FORN_PLASTICINO_PROFILI; if (!modulo) { console.error('❌ Modulo non caricato. window.FORN_PLASTICINO_PROFILI non definito.'); process.exit(1); } let pass = 0, fail = 0; function t(label, cond, extra) { if (cond) { pass++; console.log(' ✅', label); } else { fail++; console.log(' ❌', label, extra != null ? '→ ' + JSON.stringify(extra) : ''); } } function approx(a, b, eps) { return Math.abs(a - b) < (eps || 0.01); } console.log('═══════════════════════════════════════════════════════════'); console.log(' TEST modulo Plasticino / Profili'); console.log('═══════════════════════════════════════════════════════════'); // ═══════════════════════════════════════════════════════════════ // 1 — Metadata e integrità listino // ═══════════════════════════════════════════════════════════════ console.log('\n[1] Integrità listino'); { const lst = modulo.getListino(); t('fornitore = plasticino', lst.fornitore === 'plasticino'); t('categoria = profili', lst.categoria === 'profili'); t('scontoFornitore = 0.44', approx(lst.scontoFornitore, 0.44)); t('markup = 1.4', approx(lst.markupVenditaDefault, 1.4)); t('maggiorazione taglio = 1.50', approx(lst.maggiorazioneTaglioMisuraEurMetro, 1.50)); t('profili presenti', Array.isArray(lst.profili) && lst.profili.length > 100, { n: lst.profili.length }); t('accessori presenti', Array.isArray(lst.accessori) && lst.accessori.length > 100, { n: lst.accessori.length }); t('famiglie presenti', typeof lst.famiglie === 'object' && Object.keys(lst.famiglie).length > 10); } // ═══════════════════════════════════════════════════════════════ // 2 — Guardia porteGarage (contratto M9) // ═══════════════════════════════════════════════════════════════ console.log('\n[2] Guardia porteGarage'); { const r = modulo.calcolaPrezzo( { porteGarage: true }, { tipoArticolo: 'profilo', codice: 'RP95', quantita: 10 } ); t('prezzoAcquisto = 0', r.prezzoAcquisto === 0); t('prezzoVendita = 0', r.prezzoVendita === 0); t('warning porteGarage', r.warnings.some(w => /porteGarage/i.test(w))); } // ═══════════════════════════════════════════════════════════════ // 3 — Validazione config // ═══════════════════════════════════════════════════════════════ console.log('\n[3] Validazione config'); { // Config nulla const r1 = modulo.calcolaPrezzo({}, null); t('config null → 0', r1.prezzoAcquisto === 0); // Codice mancante const r2 = modulo.calcolaPrezzo({}, { tipoArticolo: 'profilo', quantita: 10 }); t('codice mancante → 0', r2.prezzoAcquisto === 0); // Quantità mancante const r3 = modulo.calcolaPrezzo({}, { tipoArticolo: 'profilo', codice: 'RP95' }); t('quantità mancante → 0', r3.prezzoAcquisto === 0); // Quantità = 0 const r4 = modulo.calcolaPrezzo({}, { tipoArticolo: 'profilo', codice: 'RP95', quantita: 0 }); t('quantità 0 → 0', r4.prezzoAcquisto === 0); // Quantità negativa const r5 = modulo.calcolaPrezzo({}, { tipoArticolo: 'profilo', codice: 'RP95', quantita: -5 }); t('quantità negativa → 0', r5.prezzoAcquisto === 0); // Codice sconosciuto const r6 = modulo.calcolaPrezzo({}, { tipoArticolo: 'profilo', codice: 'FARLOCCO99', quantita: 10 }); t('codice sconosciuto → 0', r6.prezzoAcquisto === 0); t('warning codice non in listino', r6.warnings.some(w => /non in listino/.test(w))); } // ═══════════════════════════════════════════════════════════════ // 4 — Calcolo profilo base (barra intera, senza taglio) // ═══════════════════════════════════════════════════════════════ console.log('\n[4] Profilo barra intera — calcolo'); // RP95 = Profilo rettangolare alluminio 125×20×2 a 22,00 €/m (pagina 85/P) // Quantità 10 m, sconto 44%, markup 1.4 // lordo = 22.00 × 10 = 220.00 € // acquisto = 220 × 0.56 = 123.20 € // vendita = 123.20 × 1.4 = 172.48 € { const r = modulo.calcolaPrezzo( {}, { tipoArticolo: 'profilo', codice: 'RP95', quantita: 10 } ); t('prezzoAcquisto ~123.20', approx(r.prezzoAcquisto, 123.20, 0.01), { got: r.prezzoAcquisto }); t('prezzoVendita ~172.48', approx(r.prezzoVendita, 172.48, 0.02), { got: r.prezzoVendita }); t('dettaglio cita RP95', /RP95/.test(r.dettaglio)); t('dettaglio cita sconto 44%', /44%/.test(r.dettaglio)); t('dettaglio cita markup', /Markup.*1\.40/.test(r.dettaglio)); t('no warning', r.warnings.length === 0); } // ═══════════════════════════════════════════════════════════════ // 5 — Calcolo profilo tagliato a misura (+1,50 €/m) // ═══════════════════════════════════════════════════════════════ console.log('\n[5] Profilo tagliato a misura'); // RP95 = 22,00 €/m → con taglio 22 + 1,50 = 23,50 €/m // Quantità 10 m // lordo = 23.50 × 10 = 235.00 € // acquisto = 235 × 0.56 = 131.60 € // vendita = 131.60 × 1.4 = 184.24 € { const r = modulo.calcolaPrezzo( {}, { tipoArticolo: 'profilo', codice: 'RP95', quantita: 10, tagliatoAMisura: true } ); t('prezzoAcquisto ~131.60', approx(r.prezzoAcquisto, 131.60, 0.01), { got: r.prezzoAcquisto }); t('prezzoVendita ~184.24', approx(r.prezzoVendita, 184.24, 0.02), { got: r.prezzoVendita }); t('dettaglio cita taglio', /taglio a misura/i.test(r.dettaglio)); t('dettaglio 23.50 €/m', /23\.50\s*€\/m/.test(r.dettaglio)); } // ═══════════════════════════════════════════════════════════════ // 6 — Calcolo accessorio €/cad // ═══════════════════════════════════════════════════════════════ console.log('\n[6] Accessorio €/cad'); // RM20 = Tappo in PVC 1,30 €/cad // Quantità 4 pz, sconto 44%, markup 1.4 // lordo = 1.30 × 4 = 5.20 € // acquisto = 5.20 × 0.56 = 2.912 € → 2.91 // vendita = 2.91 × 1.4 = 4.074 € → 4.07 { const r = modulo.calcolaPrezzo( {}, { tipoArticolo: 'accessorio', codice: 'RM20', quantita: 4 } ); t('prezzoAcquisto ~2.91', approx(r.prezzoAcquisto, 2.91, 0.02), { got: r.prezzoAcquisto }); t('prezzoVendita ~4.07', approx(r.prezzoVendita, 4.07, 0.02), { got: r.prezzoVendita }); t('dettaglio cita RM20', /RM20/.test(r.dettaglio)); t('dettaglio €/cad', /€\/cad/.test(r.dettaglio)); } // ═══════════════════════════════════════════════════════════════ // 7 — Edge case: tagliatoAMisura su accessorio → warning ignorato // ═══════════════════════════════════════════════════════════════ console.log('\n[7] Edge: tagliatoAMisura su accessorio'); { const r = modulo.calcolaPrezzo( {}, { tipoArticolo: 'accessorio', codice: 'RM20', quantita: 4, tagliatoAMisura: true } ); t('prezzo invariato vs 4 pz normale', approx(r.prezzoAcquisto, 2.91, 0.02), { got: r.prezzoAcquisto }); t('warning tagliatoAMisura ignorato', r.warnings.some(w => /tagliatoAMisura.*ignorato/i.test(w))); } // ═══════════════════════════════════════════════════════════════ // 8 — Edge case: tipoArticolo incoerente con codice // ═══════════════════════════════════════════════════════════════ console.log('\n[8] Edge: tipoArticolo incoerente'); // Dichiaro "profilo" ma passo codice accessorio → il modulo usa il tipo reale e emette warning { const r = modulo.calcolaPrezzo( {}, { tipoArticolo: 'profilo', codice: 'RM20', quantita: 4 } ); t('prezzo calcolato come accessorio', approx(r.prezzoAcquisto, 2.91, 0.02), { got: r.prezzoAcquisto }); t('warning tipo incoerente', r.warnings.some(w => /tipoArticolo.*codice.*accessorio/i.test(w))); } // ═══════════════════════════════════════════════════════════════ // 9 — Spot check prezzi profili noti dal PDF // ═══════════════════════════════════════════════════════════════ console.log('\n[9] Spot check prezzi noti dal PDF'); // Dal listino: // RP02 = Profilo rettangolare PVC 54×44×2 → 5,50 €/m // RP18 = Profilo rettangolare PVC espanso 125×20 → 21,00 €/m // RP95 = Profilo rettangolare Alluminio 125×20×2 → 22,00 €/m // RV01 = Perlina PVC espanso 80×10 → 5,60 €/m // RV12 = Perlina PVC espanso 150×20 → 9,90 €/m { const lst = modulo.getListino(); function trovaProfilo(codice) { return lst.profili.find(p => p.codice === codice); } const rp02 = trovaProfilo('RP02'); t('RP02 esiste + prezzo 5,50', rp02 && approx(rp02.prezzoBarraEurMetro, 5.50), rp02); const rp18 = trovaProfilo('RP18'); t('RP18 esiste + prezzo 21,00', rp18 && approx(rp18.prezzoBarraEurMetro, 21.00), rp18); const rp95 = trovaProfilo('RP95'); t('RP95 esiste + prezzo 22,00', rp95 && approx(rp95.prezzoBarraEurMetro, 22.00), rp95); const rv01 = trovaProfilo('RV01'); t('RV01 esiste + prezzo 5,60', rv01 && approx(rv01.prezzoBarraEurMetro, 5.60), rv01); const rv12 = trovaProfilo('RV12'); t('RV12 esiste + prezzo 9,90', rv12 && approx(rv12.prezzoBarraEurMetro, 9.90), rv12); // Famiglie corrette t('RP02 fam = parapetti-pvc-rigido', rp02 && rp02.famiglia === 'parapetti-pvc-rigido'); t('RP18 fam = parapetti-pvc-espanso', rp18 && rp18.famiglia === 'parapetti-pvc-espanso'); t('RP95 fam = parapetti-alluminio', rp95 && rp95.famiglia === 'parapetti-alluminio'); t('RV01 fam = perline-pvc', rv01 && rv01.famiglia === 'perline-pvc'); } // ═══════════════════════════════════════════════════════════════ // 10 — Spot check prezzi accessori noti // ═══════════════════════════════════════════════════════════════ console.log('\n[10] Spot check prezzi accessori'); // RM20 = Tappo PVC 1,30 € // RM40 = Manicotto PVC 2,60 € // RS23 = Inserto nylon 2,60 € { const lst = modulo.getListino(); function trovaAccessorio(codice) { return lst.accessori.find(a => a.codice === codice); } const rm20 = trovaAccessorio('RM20'); t('RM20 esiste + prezzo 1,30', rm20 && approx(rm20.prezzoEurCad, 1.30), rm20); const rm40 = trovaAccessorio('RM40'); t('RM40 esiste + prezzo 2,60', rm40 && approx(rm40.prezzoEurCad, 2.60), rm40); const rs23 = trovaAccessorio('RS23'); t('RS23 esiste + prezzo 2,60', rs23 && approx(rs23.prezzoEurCad, 2.60), rs23); } // ═══════════════════════════════════════════════════════════════ // 11 — getOpzioniCampo: famiglie filtrate e codici // ═══════════════════════════════════════════════════════════════ console.log('\n[11] getOpzioniCampo'); { const famProf = modulo.getOpzioniCampo('famiglia', { tipoArticolo: 'profilo' }); t('famiglie profilo non vuote', Array.isArray(famProf) && famProf.length > 0); t('famiglia parapetti-alluminio presente', famProf.some(f => f.value === 'parapetti-alluminio')); t('famiglia bulloneria NON presente tra profili', !famProf.some(f => f.value === 'bulloneria')); const famAcc = modulo.getOpzioniCampo('famiglia', { tipoArticolo: 'accessorio' }); t('famiglie accessorio non vuote', Array.isArray(famAcc) && famAcc.length > 0); t('famiglia bulloneria presente tra accessori', famAcc.some(f => f.value === 'bulloneria')); const codici = modulo.getOpzioniCampo('codice', { tipoArticolo: 'profilo', famiglia: 'parapetti-alluminio' }); t('codici parapetti-alluminio non vuoti', codici.length > 5); t('RP95 presente in parapetti-alluminio', codici.some(c => c.value === 'RP95')); t('label contiene prezzo €/m', codici.some(c => /€\/m/.test(c.label))); } // ═══════════════════════════════════════════════════════════════ // 12 — Test matematico "triangolazione" — verifica formula completa // ═══════════════════════════════════════════════════════════════ console.log('\n[12] Triangolazione formula (barra × sconto × markup)'); // Scenario: 6,10 m di RP02 (barra intera PVC rigido) — stessa barra Plasticino // RP02 = 5,50 €/m, quantità = 6,10 m (1 barra) // lordo = 5.50 × 6.10 = 33.55 € // acquisto = 33.55 × (1 - 0.44) = 33.55 × 0.56 = 18.788 → 18.79 // vendita = 18.79 × 1.4 = 26.306 → 26.31 { const r = modulo.calcolaPrezzo( {}, { tipoArticolo: 'profilo', codice: 'RP02', quantita: 6.10 } ); const lordo_atteso = 5.50 * 6.10; const acquisto_atteso = lordo_atteso * 0.56; const vendita_atteso = acquisto_atteso * 1.4; t('lordo interno ≈ 33.55', /33\.55/.test(r.dettaglio)); t('prezzoAcquisto ~18.79', approx(r.prezzoAcquisto, acquisto_atteso, 0.02), { got: r.prezzoAcquisto, exp: acquisto_atteso }); t('prezzoVendita ~26.30', approx(r.prezzoVendita, vendita_atteso, 0.02), { got: r.prezzoVendita, exp: vendita_atteso }); } // ═══════════════════════════════════════════════════════════════ // 13 — Scenario realistico: parapetto terrazzo composito // ═══════════════════════════════════════════════════════════════ console.log('\n[13] Scenario parapetto composto (3 posizioni separate)'); // Esempio realistico parapetto balcone 4 m: // - Posizione A: 4 m profilo RP95 (Al 125×20) tagliato a misura // - Posizione B: 4 × RS23 inserti in nylon // - Posizione C: 2 × RS76V piantana fissaggio { const posA = modulo.calcolaPrezzo( {}, { tipoArticolo: 'profilo', codice: 'RP95', quantita: 4, tagliatoAMisura: true } ); const posB = modulo.calcolaPrezzo( {}, { tipoArticolo: 'accessorio', codice: 'RS23', quantita: 4 } ); const posC = modulo.calcolaPrezzo( {}, { tipoArticolo: 'accessorio', codice: 'RS76V', quantita: 2 } ); // A: (22 + 1.5) × 4 = 94.00 lordo → 52.64 acquisto → 73.70 vendita t('A profilo ~52.64 acquisto', approx(posA.prezzoAcquisto, 52.64, 0.02), { got: posA.prezzoAcquisto }); // B: 2.60 × 4 = 10.40 lordo → 5.824 → 5.82 acquisto t('B 4 inserti ~5.82 acquisto', approx(posB.prezzoAcquisto, 5.82, 0.02), { got: posB.prezzoAcquisto }); // C: RS76V = 137.00 €/cad → 137 × 2 = 274 lordo → 153.44 acquisto t('C 2 piantane ~153.44 acquisto', approx(posC.prezzoAcquisto, 153.44, 0.05), { got: posC.prezzoAcquisto }); const totAcquisto = posA.prezzoAcquisto + posB.prezzoAcquisto + posC.prezzoAcquisto; const totVendita = posA.prezzoVendita + posB.prezzoVendita + posC.prezzoVendita; console.log(` → Totale parapetto acquisto: ${totAcquisto.toFixed(2)} €`); console.log(` → Totale parapetto vendita : ${totVendita.toFixed(2)} €`); t('totale acquisto > 200 €', totAcquisto > 200); t('totale vendita > 200 €', totVendita > 200); } // ═══════════════════════════════════════════════════════════════ // Riepilogo // ═══════════════════════════════════════════════════════════════ console.log('\n═══════════════════════════════════════════════════════════'); console.log(` Risultato: ${pass} pass / ${fail} fail`); if (fail === 0) { console.log(' ✅ TUTTI I TEST PASSATI'); process.exit(0); } else { console.log(' ❌ FALLITI — rivedere'); process.exit(1); }