From adb7e8a17abc29c7b1b10c82568b7fc4d160b9bc Mon Sep 17 00:00:00 2001 From: Alex <14327609+alexpolo1@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:14:57 +0200 Subject: [PATCH] fix: enforce complete scope keyword matches (#29) Co-authored-by: alexpolo1 --- .../src/__tests__/quoteRealismService.test.js | 18 ++++++++++++++++++ backend/src/services/quoteRealismService.js | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/src/__tests__/quoteRealismService.test.js b/backend/src/__tests__/quoteRealismService.test.js index 6004a14..bbf58de 100644 --- a/backend/src/__tests__/quoteRealismService.test.js +++ b/backend/src/__tests__/quoteRealismService.test.js @@ -96,6 +96,24 @@ describe('QuoteRealismService', () => { expect(service.getRoofHistoryRegex({ project_name: 'Nyt tagpap på fladt tag' })).toBe('tagpap|fladt tag|fladtag'); }); + test('matches roof keywords as complete terms and keeps rental precedence', () => { + const service = new QuoteRealismService({ pool: {} }); + expect(service.classifyScope('Forskellige specialarbejder')).toBe('other'); + expect(service.classifyScope('Anden etape')).toBe('other'); + expect(service.classifyScope('Stillads inkl. transport', 'Udlejning')).toBe('rental'); + expect(service.classifyScope('Tape, klæb og membraner')).toBe('roof'); + expect(service.classifyScope('Skel-/naboovergang')).toBe('roof'); + + const analysis = service.buildAnalysis({ + project: { id: 392, project_name: 'Betontag' }, + materials: [{ material_id: 1, total_price: 100 }], + rentals: [{ rental_name: 'Forskellige specialarbejder', quantity: 1, unit: 'sum', unit_price: 1000 }], + historyByScope: { roof: [{ caseNumber: '1', totalHours: 1, materialSalesExVat: 1 }] } + }); + expect(analysis.readyForFixedPrice).toBe(false); + expect(analysis.blockers).toContain('Forskellige specialarbejder kræver manuel afklaring og historik før fast pris'); + }); + test('classifies roof support and logistics reference services without generic errors', () => { const service = new QuoteRealismService({ pool: {} }); expect(service.classifyScope('Tagfod, fuglegitter, ventilation og tætninger')).toBe('roof'); diff --git a/backend/src/services/quoteRealismService.js b/backend/src/services/quoteRealismService.js index b320f5a..d412bb6 100644 --- a/backend/src/services/quoteRealismService.js +++ b/backend/src/services/quoteRealismService.js @@ -115,9 +115,9 @@ class QuoteRealismService { const text = this.normalizeText(`${name} ${category}`); if (text.includes('dør') || text.includes('doer')) return 'doors'; if (/gulv|parket|laminat|sildeben/.test(text)) return 'floor'; - if (/tagfod|fuglegitter|skel|inddækning|tape|klæb|membran|småmaterial/.test(text)) return 'roof'; - if (/container|sortering|bortskaffelse|levering|kranhejs|transport/.test(text)) return 'logistics'; if (/stillads|faldsikring|lift|leje|udlejning/.test(text)) return 'rental'; + if (/(?:^|[^a-zæøå0-9])(?:tagfod|fuglegitter|skel|inddækninger?|tape|klæb|membraner?|småmaterialer?)(?=$|[^a-zæøå0-9])/u.test(text)) return 'roof'; + if (/(?:^|[^a-zæøå0-9])(?:container|sortering|bortskaffelse|levering|kranhejs|transport)(?=$|[^a-zæøå0-9])/u.test(text)) return 'logistics'; return 'other'; }