fix(ai): realistic pre-market rent recommendation — annual units + anchor on current price
- getMarketRent now returns CHF/m²/YEAR (stored medians are monthly → ×12); fixes annual-vs-monthly mismatch that also affected MarketPricePanel and NegotiationInsightsPanel - recommendPreMarketRent: anchor on the unit's current rent and adjust within a bounded ±15% by market momentum (vacancy=supply, demand strength, days-on-market, trend) instead of jumping to the city-wide (prime-skewed) median — no more "double the price" suggestions; median only used as a headroom check - tests updated to annual market values Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,28 +2,28 @@ import { describe, it, expect } from 'vitest'
|
||||
import { estimateMarketRent, suggestFutureRent } from '../rentEstimate'
|
||||
|
||||
describe('estimateMarketRent', () => {
|
||||
// Zürich OFFICE median = 42/Monat → 504/Jahr (getMarketRent rechnet ×12)
|
||||
it('flags asking rent clearly above the city median as ABOVE', () => {
|
||||
// Zürich OFFICE median = 42
|
||||
const est = estimateMarketRent('Zürich', 'OFFICE', 60)
|
||||
const est = estimateMarketRent('Zürich', 'OFFICE', 600)
|
||||
expect(est).not.toBeNull()
|
||||
expect(est!.verdict).toBe('ABOVE')
|
||||
expect(est!.deltaPct).toBeGreaterThan(5)
|
||||
expect(est!.fairRentPerSqm).toBe(42)
|
||||
expect(est!.fairRentPerSqm).toBe(504)
|
||||
})
|
||||
|
||||
it('flags asking rent clearly below median as BELOW', () => {
|
||||
const est = estimateMarketRent('Zürich', 'OFFICE', 30)
|
||||
const est = estimateMarketRent('Zürich', 'OFFICE', 400)
|
||||
expect(est!.verdict).toBe('BELOW')
|
||||
expect(est!.deltaPct).toBeLessThan(-5)
|
||||
})
|
||||
|
||||
it('treats near-median asking rent as AT (within ±5%)', () => {
|
||||
const est = estimateMarketRent('Zürich', 'OFFICE', 43)
|
||||
const est = estimateMarketRent('Zürich', 'OFFICE', 510)
|
||||
expect(est!.verdict).toBe('AT')
|
||||
})
|
||||
|
||||
it('returns null for unknown city', () => {
|
||||
expect(estimateMarketRent('Atlantis', 'OFFICE', 40)).toBeNull()
|
||||
expect(estimateMarketRent('Atlantis', 'OFFICE', 480)).toBeNull()
|
||||
})
|
||||
|
||||
it('suggestFutureRent indexes by the city rent trend', () => {
|
||||
|
||||
@@ -6,9 +6,9 @@ export interface CityIntelligence {
|
||||
purchasingPowerIndex: number // Kaufkraft-Index (CH = 100)
|
||||
dominantIndustryClusters: string[]
|
||||
plannedInfrastructure: { project: string; timeline: string; impact: string }[]
|
||||
medianRentOffice: number // CHF/m² für Bürofläche
|
||||
medianRentLogistics: number
|
||||
medianRentRetail: number
|
||||
medianRentOffice: number // CHF/m²/Monat (Bürofläche) — getMarketRent rechnet auf Jahr um
|
||||
medianRentLogistics: number // CHF/m²/Monat
|
||||
medianRentRetail: number // CHF/m²/Monat
|
||||
avgDaysOnMarket: number // Durchschnittliche Tage bis Vermietung
|
||||
demandStrength: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
|
||||
taxIndexCanton: number // Steuerindex 100 = CH-Mittel
|
||||
@@ -137,13 +137,15 @@ export function getCityIntelligence(city: string): CityIntelligence | null {
|
||||
return key ? CITY_INTELLIGENCE[key] : null
|
||||
}
|
||||
|
||||
/** Median-Marktmiete in CHF/m²/**Jahr** (Daten sind monatlich gespeichert → ×12), passend zu property.rentPricePerSqm. */
|
||||
export function getMarketRent(city: string, assetType: string): number | null {
|
||||
const intel = getCityIntelligence(city)
|
||||
if (!intel) return null
|
||||
if (assetType === 'OFFICE') return intel.medianRentOffice
|
||||
if (assetType === 'LOGISTICS' || assetType === 'LIGHT_INDUSTRIAL') return intel.medianRentLogistics
|
||||
if (assetType === 'RETAIL') return intel.medianRentRetail
|
||||
return intel.medianRentOffice
|
||||
const monthly =
|
||||
assetType === 'LOGISTICS' || assetType === 'LIGHT_INDUSTRIAL' ? intel.medianRentLogistics
|
||||
: assetType === 'RETAIL' ? intel.medianRentRetail
|
||||
: intel.medianRentOffice
|
||||
return monthly * 12
|
||||
}
|
||||
|
||||
// ── City coordinates (WGS84) ─────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user