From 66aabde1dc1ed037e452657fbe29c922847a76d5 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Wed, 20 May 2026 19:29:06 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20synthetic=20scoring=20=E2=80=94=20budget?= =?UTF-8?q?=20unit=20mismatch=20+=20PRE-MARKET=20VERIFIED=20in=20demand=20?= =?UTF-8?q?feed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root causes prevented gold and PRE-MARKET VERIFIED cards from showing in wizard-created searches: 1. computeScore compared annual property rates (e.g. 1080 CHF/m²/year) against monthly AI-extracted budget (e.g. 150 CHF/m²/month), causing a constant -8 penalty and capping Bern retail at ~79%. Fix: divide rentPricePerSqm by 12 before comparing. 2. generateSyntheticMatches set resultType: VERIFIED_PORTFOLIO for schattenmarktRelease-enabled properties, which Results.tsx filters out for demand users. Fix: set resultType: FUTURE_AVAILABILITY for these properties so the schattenmarkt signal is resolved and the PRE-MARKET VERIFIED card is rendered. Also adds a score discount (×0.82 for probabilistic FUTURE_AVAILABILITY, ×0.92 for PRE-MARKET) to keep tier distribution realistic. Co-Authored-By: Claude Sonnet 4.6 --- src/provider/MockupNeedProvider.ts | 32 +++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/provider/MockupNeedProvider.ts b/src/provider/MockupNeedProvider.ts index 552205a..6b14cc5 100644 --- a/src/provider/MockupNeedProvider.ts +++ b/src/provider/MockupNeedProvider.ts @@ -35,7 +35,14 @@ function locationScore(propCity: string, preferredLocations: string[]): number { return 0.30 } -function computeScore(prop: { assetType: string; areaSqm: number; rentPricePerSqm: number; location: { city: string } }, need: Need): number | null { +function computeScore( + prop: { + assetType: string; areaSqm: number; rentPricePerSqm: number; + location: { city: string }; resultType?: string; + schattenmarktRelease?: { enabled?: boolean } + }, + need: Need, +): number | null { if (need.assetType && prop.assetType !== need.assetType) return null const locScore = locationScore(prop.location.city, need.preferredLocations ?? []) @@ -51,13 +58,21 @@ function computeScore(prop: { assetType: string; areaSqm: number; rentPricePerSq else if (prop.areaSqm < min * 0.5 || prop.areaSqm > max * 2) score -= 10 } - // Budget fit (+0-10) + // Budget fit (+0-10): property stores annual CHF/m², need budget is monthly CHF/m² if (need.budgetRange?.maxPerSqm && prop.rentPricePerSqm) { - if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm) score += 10 - else if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.2) score += 3 + const monthlyRate = prop.rentPricePerSqm / 12 + if (monthlyRate <= need.budgetRange.maxPerSqm) score += 10 + else if (monthlyRate <= need.budgetRange.maxPerSqm * 1.2) score += 3 else score -= 8 } + // Discount for probabilistic/future signals so they score below confirmed listings + if (prop.resultType === 'FUTURE_AVAILABILITY') { + score = Math.round(score * 0.82) + } else if (prop.schattenmarktRelease?.enabled) { + score = Math.round(score * 0.92) + } + // Small jitter so results look natural score += Math.floor(Math.random() * 6) - 2 @@ -80,12 +95,19 @@ function generateSyntheticMatches(need: Need) { const locS = locationScore(prop.location.city, need.preferredLocations ?? []) const isGoodLoc = locS >= 0.9 + // VERIFIED_PORTFOLIO properties with schattenmarktRelease appear as FUTURE_AVAILABILITY + // in demand search so demand users see the PRE-MARKET VERIFIED card + const effectiveResultType = + prop.resultType === 'VERIFIED_PORTFOLIO' && prop.schattenmarktRelease?.enabled + ? 'FUTURE_AVAILABILITY' + : (prop.resultType ?? 'VERIFIED_PORTFOLIO') + const match: Match = { id: crypto.randomUUID(), propertyId: prop.id, needId: need.id, resultId: prop.id, - resultType: prop.resultType ?? 'VERIFIED_PORTFOLIO', + resultType: effectiveResultType as Match['resultType'], matchScore: score, matchStrength: strengthFromScore(score) as typeof MatchStrength[keyof typeof MatchStrength], status: score >= 75 ? MatchStatus.PENDING_REVIEW : MatchStatus.PENDING_REVIEW,