Files
property-match/src/services/matchService.ts
T
Benjamin Sutter 36570c5bdc fix: resolve all TypeScript errors and refactor oversized components
- Fix MUI v9 API: PaperProps/InputLabelProps/inputProps → slotProps in 6 components
- Add ExternalMarketResult alias, PropertyUnit import, OPERATIONS workspace config
- Fix TradeOff.description → .concern, ScoreFactor.label → .criterion
- Make schattenmarktRelease.leadTimeMonths optional, fix mock-data enum values
- Fix useMatchDetailData query typing, weightingService missing WeightProfile keys
- Split Pipeline/Compare/MatchDetail/IntelligenceMatchCard into sub-components
- Fix all test fixtures (CreateNeedInput, CreatePropertyInput, TradeOffInput, etc.)
- Add vercel.json for deployment, zero tsc errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:08:35 +02:00

220 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { MockupMatchProvider } from '../provider/MockupMatchProvider'
import { MockupPropertyProvider } from '../provider/MockupPropertyProvider'
import { MockupNeedProvider } from '../provider/MockupNeedProvider'
import { MockupInquiryProvider } from '../provider/MockupInquiryProvider'
import type { MatchFilters } from '../provider/IMatchProvider'
import type { Match, PropertyNeedMatch } from '../domain/match'
import type { Need } from '../domain/need'
import type { Property } from '../domain/property'
import type { StrongMatchItem } from '../domain/dashboard'
import type { ScoreBreakdown } from '../domain/match'
import type { AdditionalPropertyMatch } from '../domain/additionalMatch'
import type { ListResponse, ItemResponse } from './types'
import { buildFullMatch, computeRankedMatches } from '../features/matching/rankingEngine'
import { ResultType } from '../domain/enums'
import { throwServiceError } from './errors'
const provider = MockupMatchProvider
export const matchService = {
async getAll(filters?: MatchFilters): Promise<ListResponse<Match>> {
try {
const data = await provider.getAll(filters)
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
} catch (err) {
throwServiceError(err)
}
},
async getById(id: string): Promise<ItemResponse<Match | null>> {
try {
const data = await provider.getById(id)
return { data }
} catch (err) {
throwServiceError(err)
}
},
async getByNeed(needId: string): Promise<ListResponse<Match>> {
try {
const data = await provider.getByNeed(needId)
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
} catch (err) {
throwServiceError(err)
}
},
async getByProperty(propertyId: string): Promise<ListResponse<Match>> {
try {
const data = await provider.getByProperty(propertyId)
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
} catch (err) {
throwServiceError(err)
}
},
async approve(id: string, reviewedBy: string): Promise<ItemResponse<Match>> {
try {
const data = await provider.approve(id, reviewedBy)
return { data }
} catch (err) {
throwServiceError(err)
}
},
async getMatchesForProperty(propertyId: string): Promise<ListResponse<Match>> {
try {
const data = await provider.getByProperty(propertyId)
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
} catch (err) {
throwServiceError(err)
}
},
async getMatchDetail(id: string): Promise<ItemResponse<Match | null>> {
try {
const data = await provider.getById(id)
return { data }
} catch (err) {
throwServiceError(err)
}
},
async getScoreBreakdown(matchId: string): Promise<ItemResponse<ScoreBreakdown | null>> {
try {
const match = await provider.getById(matchId)
return { data: match?.scoreBreakdown ?? null }
} catch (err) {
throwServiceError(err)
}
},
// ── Engine-based methods ──────────────────────────────────────────────────
computeMatch(need: Need, property: Property): Match {
return buildFullMatch(need, property)
},
async computeMatchesForNeed(needId: string): Promise<ListResponse<Match>> {
try {
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 } }
} catch (err) {
throwServiceError(err)
}
},
async getNeedMatchesForProperty(
propertyId: string,
opts?: { minScore?: number },
): Promise<PropertyNeedMatch[]> {
try {
const minScore = opts?.minScore ?? 80
const [matches, needs] = await Promise.all([
provider.getByProperty(propertyId),
MockupNeedProvider.getAll(),
])
return matches
.filter(m => m.matchScore >= minScore)
.sort((a, b) => b.matchScore - a.matchScore)
.map(m => {
const need = needs.find(n => n.id === m.needId)
if (!need) return null
return {
matchId: m.id,
score: m.matchScore,
needId: need.id,
needTitle: `${need.companyName}`,
company: need.companyName,
areaRequired: `${need.requiredArea.min}${need.requiredArea.max}`,
budget: `max ${need.budgetRange.maxPerSqm} CHF/m²/Jahr`,
locations: need.preferredLocations,
assetType: need.assetType,
timing: need.timing?.earliestMoveIn ?? '',
mustHaves: need.mustCriteriaText ?? [],
matchHighlights: m.positiveFactors.map(f => f.explanation),
} satisfies PropertyNeedMatch
})
.filter((x): x is PropertyNeedMatch => x !== null)
} catch (err) {
throwServiceError(err)
}
},
async getAdditionalMatchesForInquiry(
inquiryId: string,
opts?: { minScore?: number; excludePropertyId?: string },
): Promise<AdditionalPropertyMatch[]> {
try {
const minScore = opts?.minScore ?? 80
const inquiry = await MockupInquiryProvider.getById(inquiryId)
if (!inquiry) return []
const excludeId = opts?.excludePropertyId ?? inquiry.propertyId
const [allProperties, allMatches] = await Promise.all([
MockupPropertyProvider.getAll(),
provider.getAll(),
])
const portfolioProperties = allProperties.filter(
p => p.resultType === ResultType.VERIFIED_PORTFOLIO && p.id !== excludeId,
)
return portfolioProperties
.map(p => {
const match = allMatches.find(m => m.propertyId === p.id)
const score = match?.matchScore ?? 0
return { property: p, score, match }
})
.filter(({ score }) => score >= minScore)
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(({ property: p, score, match }) => ({
propertyId: p.id,
title: p.title,
location: `${p.location.city}${p.location.district ? `, ${p.location.district}` : ''}`,
areaSqm: p.areaSqm,
rentPricePerSqm: p.rentPricePerSqm,
matchScore: score,
imageUrl: p.images?.[0],
reasons: (match?.positiveFactors ?? []).slice(0, 3).map(f => f.explanation ?? f.criterion ?? '').filter(Boolean),
topUncertainty: (match?.tradeoffs ?? match?.tradeOffs ?? [])[0]?.concern
?? (match?.negativeFactors ?? [])[0]?.explanation
?? undefined,
}))
} catch (err) {
throwServiceError(err)
}
},
async getStrongMatches(minScore = 80): Promise<StrongMatchItem[]> {
try {
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
})
} catch (err) {
throwServiceError(err)
}
},
}