3152de004c
domain/scoring.ts: HARD_FILTER thresholds, DATA_QUALITY/CONFIDENCE modifier tables, ScoringWeightProfile type, DEFAULT_SCORING_PROFILES for Office/Retail/Light Industrial/Logistics/Production/Default (all sum to 1.00), HARD/SOFT_CRITERION_KEYS, MatchEngineOutput type. features/matching/scoreCalculator.ts: applyHardFilters (asset type mismatch, area < 85% min, budget > 150%, excluded region → hard exclude; occupied → severe penalty), scoreArea/Location/Budget/Timing as ScoreFactor, scoreSoftFactor for 9 keys mapped to property.softFactors, calcDataQualityModifier/calcConfidenceModifier, calculateScore (resolves profile from need.weightingProfile + default, runs filters, computes normalized hard/soft scores, applies modifiers, classifies positive/negative factors, assembles MatchEngineOutput). features/matching/tradeOffAnalyzer.ts: analyzeTradeOffs (6 patterns: location-vs-budget, area-vs-budget, timing-vs-dataQuality, prestige-vs-flexibility, futureSignal-vs-location, accessibility-vs-commute), analyzeRisks (future signal, data quality, budget, occupied, low confidence, missing critical fields), identifyMissingData (rentPricePerSqm, availabilityDate, softFactors, hardFacts, need.budgetRange). features/matching/rankingEngine.ts: matchStrengthFromScore (>=78 STRONG, >=52 MODERATE, else WEAK), generateNextBestActions (score-based, future signal SCHEDULE, missing data VERIFY), buildFullMatch → full Match entity with ScoreBreakdown + explainabilitySummary + uncertaintyIndicators, rankMatches (score desc → resultType order → confidence desc), computeRankedMatches batch helper. services/matchService.ts: computeMatch(need, property) and computeMatchesForNeed(needId) wired to engine. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
3.6 KiB
TypeScript
85 lines
3.6 KiB
TypeScript
import { MockupMatchProvider } from '../provider/MockupMatchProvider'
|
||
import { MockupPropertyProvider } from '../provider/MockupPropertyProvider'
|
||
import { MockupNeedProvider } from '../provider/MockupNeedProvider'
|
||
import type { MatchFilters } from '../provider/IMatchProvider'
|
||
import type { Match } from '../domain/match'
|
||
import type { Need } from '../domain/need'
|
||
import type { Property } from '../domain/property'
|
||
import type { StrongMatchItem } from '../domain/dashboard'
|
||
import type { ListResponse, ItemResponse } from './types'
|
||
import { buildFullMatch, computeRankedMatches } from '../features/matching/rankingEngine'
|
||
|
||
const provider = MockupMatchProvider
|
||
|
||
export const matchService = {
|
||
async getAll(filters?: MatchFilters): Promise<ListResponse<Match>> {
|
||
const data = await provider.getAll(filters)
|
||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||
},
|
||
async getById(id: string): Promise<ItemResponse<Match | null>> {
|
||
const data = await provider.getById(id)
|
||
return { data }
|
||
},
|
||
async getByNeed(needId: string): Promise<ListResponse<Match>> {
|
||
const data = await provider.getByNeed(needId)
|
||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||
},
|
||
async getByProperty(propertyId: string): Promise<ListResponse<Match>> {
|
||
const data = await provider.getByProperty(propertyId)
|
||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||
},
|
||
async approve(id: string, reviewedBy: string): Promise<ItemResponse<Match>> {
|
||
const data = await provider.approve(id, reviewedBy)
|
||
return { data }
|
||
},
|
||
|
||
async getMatchesForProperty(propertyId: string): Promise<ListResponse<Match>> {
|
||
const data = await provider.getByProperty(propertyId)
|
||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||
},
|
||
|
||
// ── Engine-based methods ──────────────────────────────────────────────────
|
||
|
||
computeMatch(need: Need, property: Property): Match {
|
||
return buildFullMatch(need, property)
|
||
},
|
||
|
||
async computeMatchesForNeed(needId: string): Promise<ListResponse<Match>> {
|
||
const [need, properties] = await Promise.all([
|
||
MockupNeedProvider.getById(needId),
|
||
MockupPropertyProvider.getAll(),
|
||
])
|
||
if (!need) return { data: [], meta: { total: 0, page: 1, pageSize: 0, hasMore: false } }
|
||
const data = computeRankedMatches(need, properties)
|
||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||
},
|
||
|
||
async getStrongMatches(minScore = 80): Promise<StrongMatchItem[]> {
|
||
const [matches, properties] = await Promise.all([
|
||
provider.getAll(),
|
||
MockupPropertyProvider.getAll(),
|
||
])
|
||
return matches
|
||
.filter(m => m.matchScore >= minScore)
|
||
.slice(0, 5)
|
||
.map(m => {
|
||
const prop = properties.find(p => p.id === m.propertyId)
|
||
const topFactor = m.positiveFactors?.[0]
|
||
const firstAction = m.nextBestActions?.[0]
|
||
return {
|
||
matchId: m.id,
|
||
propertyId: m.propertyId,
|
||
propertyTitle: prop?.title ?? 'Unbekanntes Objekt',
|
||
propertyAddress: prop?.address
|
||
? `${prop.address.street} ${prop.address.houseNumber}, ${prop.address.city}`
|
||
: '–',
|
||
needSummary: m.needId,
|
||
matchScore: m.matchScore,
|
||
topReason: topFactor?.explanation ?? topFactor?.criterion ?? '–',
|
||
missingDataCount: m.missingData?.length ?? 0,
|
||
nextBestAction: firstAction?.label ?? '–',
|
||
} satisfies StrongMatchItem
|
||
})
|
||
},
|
||
}
|