609a3634bd
All external market properties now surface as VERIFIED_PORTFOLIO. Removes the type from enums, domain types, mock data, matching engine, components, hooks, and tests — zero user-visible distinction remains. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
4.9 KiB
TypeScript
105 lines
4.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import { matchStrengthFromScore, rankMatches, buildFullMatch } from '../rankingEngine'
|
||
import { MatchStrength, ResultType } from '../../../domain/enums'
|
||
import { makeNeed, makeProperty } from './fixtures'
|
||
|
||
// ── matchStrengthFromScore ────────────────────────────────────────────────────
|
||
|
||
describe('matchStrengthFromScore', () => {
|
||
it('classifies score >= 78 as STRONG', () => {
|
||
expect(matchStrengthFromScore(78)).toBe(MatchStrength.STRONG)
|
||
expect(matchStrengthFromScore(100)).toBe(MatchStrength.STRONG)
|
||
expect(matchStrengthFromScore(90)).toBe(MatchStrength.STRONG)
|
||
})
|
||
|
||
it('classifies score 52–77 as MODERATE', () => {
|
||
expect(matchStrengthFromScore(77)).toBe(MatchStrength.MODERATE)
|
||
expect(matchStrengthFromScore(52)).toBe(MatchStrength.MODERATE)
|
||
expect(matchStrengthFromScore(65)).toBe(MatchStrength.MODERATE)
|
||
})
|
||
|
||
it('classifies score < 52 as WEAK', () => {
|
||
expect(matchStrengthFromScore(51)).toBe(MatchStrength.WEAK)
|
||
expect(matchStrengthFromScore(0)).toBe(MatchStrength.WEAK)
|
||
expect(matchStrengthFromScore(30)).toBe(MatchStrength.WEAK)
|
||
})
|
||
})
|
||
|
||
// ── rankMatches ───────────────────────────────────────────────────────────────
|
||
|
||
describe('rankMatches', () => {
|
||
const need = makeNeed()
|
||
|
||
it('sorts matches by matchScore descending', () => {
|
||
const high = buildFullMatch(need, makeProperty({ id: 'high', rentPricePerSqm: 40 }))
|
||
const low = buildFullMatch(need, makeProperty({ id: 'low', rentPricePerSqm: 65, location: { city: 'Basel', country: 'CH' } }))
|
||
const ranked = rankMatches([low, high])
|
||
expect(ranked[0].matchScore).toBeGreaterThanOrEqual(ranked[1].matchScore)
|
||
expect(ranked[0].propertyId).toBe('high')
|
||
})
|
||
|
||
it('breaks ties by result type: VERIFIED_PORTFOLIO and MAISON_WORK ranked above FUTURE_AVAILABILITY', () => {
|
||
// Build one of each type but force identical score by using same property body
|
||
const prop = makeProperty()
|
||
const verified = buildFullMatch(need, { ...prop, id: 'v', resultType: ResultType.VERIFIED_PORTFOLIO })
|
||
const future = buildFullMatch(need, { ...prop, id: 'f', resultType: ResultType.FUTURE_AVAILABILITY, confidenceScore: 0.85 })
|
||
|
||
// Force same score for a clean tiebreak test
|
||
verified.matchScore = 75
|
||
future.matchScore = 75
|
||
|
||
const ranked = rankMatches([future, verified])
|
||
expect(ranked[0].resultType).toBe(ResultType.VERIFIED_PORTFOLIO)
|
||
})
|
||
|
||
it('breaks ties among same type by confidence descending', () => {
|
||
const highConf = buildFullMatch(need, makeProperty({ id: 'hc', confidenceScore: 0.90 }))
|
||
const lowConf = buildFullMatch(need, makeProperty({ id: 'lc', confidenceScore: 0.60 }))
|
||
|
||
// Force same score and same type
|
||
highConf.matchScore = 75
|
||
lowConf.matchScore = 75
|
||
|
||
const ranked = rankMatches([lowConf, highConf])
|
||
expect(ranked[0].confidenceLevel).toBeGreaterThan(ranked[1].confidenceLevel)
|
||
})
|
||
})
|
||
|
||
// ── buildFullMatch ────────────────────────────────────────────────────────────
|
||
|
||
describe('buildFullMatch', () => {
|
||
it('returns a Match with all required explainability fields', () => {
|
||
const match = buildFullMatch(makeNeed(), makeProperty())
|
||
expect(match.id).toContain('match-')
|
||
expect(match.scoreBreakdown).toBeDefined()
|
||
expect(match.scoreBreakdown.hardMatchScore).toBeGreaterThan(0)
|
||
expect(match.scoreBreakdown.softFactorScore).toBeGreaterThan(0)
|
||
expect(match.scoreBreakdown.totalScore).toBe(match.matchScore)
|
||
expect(match.matchStrength).toBeDefined()
|
||
expect(match.explainabilitySummary).toBeTruthy()
|
||
})
|
||
|
||
it('generates shortlist and contact actions for a strong match', () => {
|
||
const match = buildFullMatch(makeNeed(), makeProperty())
|
||
// Perfect fixture should score >= 78 (STRONG) and generate SHORTLIST + CONTACT actions
|
||
if (match.matchScore >= 78) {
|
||
const types = match.nextBestActions?.map(a => a.actionType) ?? []
|
||
expect(types).toContain('SHORTLIST')
|
||
expect(types).toContain('CONTACT')
|
||
}
|
||
})
|
||
|
||
it('adds a SCHEDULE action for future availability properties', () => {
|
||
const prop = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY })
|
||
const match = buildFullMatch(makeNeed(), prop)
|
||
const types = match.nextBestActions?.map(a => a.actionType) ?? []
|
||
expect(types).toContain('SCHEDULE')
|
||
})
|
||
|
||
it('uncertainty indicators include a note for future availability', () => {
|
||
const prop = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY })
|
||
const match = buildFullMatch(makeNeed(), prop)
|
||
expect(match.uncertaintyIndicators?.some(s => s.includes('Signal'))).toBe(true)
|
||
})
|
||
})
|