feat(domain): F004 – expand domain model layer

- need.ts: add SizeRange, MustHaveCriterion, WeightedPreference types; extend Need with desiredLocation, sizeRange, mustHaveCriteria, weightedPreferences, status
- futureSignal.ts: add SignalEvidence type; extend FutureSignal with title, evidence, matchabilityScore, reviewStatus
- aiOutput.ts: new AIOutput entity with type, inputHash, outputJson, provider/model/promptVersion/schemaVersion, reviewStatus
- activityEvent.ts: new ActivityEvent entity with actorId, action, entityType, entityId, timestamp, metadata
- unifiedResult.ts: discriminated union (VerifiedPortfolioResult | ExternalMarketResult | FutureAvailabilityResult)
- index.ts: export all three new files
- Properties.tsx: add LIGHT_INDUSTRIAL + UNKNOWN cases to exhaustive switches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-15 10:58:27 +02:00
parent 3b45b0c121
commit 0b874865ba
10 changed files with 399 additions and 40 deletions
+34
View File
@@ -0,0 +1,34 @@
export const ActivityAction = {
CREATED: 'CREATED',
UPDATED: 'UPDATED',
DELETED: 'DELETED',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
SHORTLISTED: 'SHORTLISTED',
REVIEWED: 'REVIEWED',
VERIFIED: 'VERIFIED',
EXPORTED: 'EXPORTED',
SHARED: 'SHARED',
} as const
export type ActivityAction = typeof ActivityAction[keyof typeof ActivityAction]
export const ActivityEntityType = {
PROPERTY: 'PROPERTY',
NEED: 'NEED',
MATCH: 'MATCH',
FUTURE_SIGNAL: 'FUTURE_SIGNAL',
AI_OUTPUT: 'AI_OUTPUT',
SHORTLIST: 'SHORTLIST',
USER: 'USER',
} as const
export type ActivityEntityType = typeof ActivityEntityType[keyof typeof ActivityEntityType]
export interface ActivityEvent {
id: string
actorId: string // user or system ID that triggered the event
action: ActivityAction
entityType: ActivityEntityType
entityId: string
timestamp: string
metadata?: Record<string, unknown>
}
+25
View File
@@ -0,0 +1,25 @@
import type { ReviewStatus } from './enums'
export const AIOutputType = {
MATCH_SCORE: 'MATCH_SCORE',
EXPLAINABILITY: 'EXPLAINABILITY',
SIGNAL_EXTRACTION: 'SIGNAL_EXTRACTION',
NEED_PARSING: 'NEED_PARSING',
SUMMARY: 'SUMMARY',
RECOMMENDATION: 'RECOMMENDATION',
} as const
export type AIOutputType = typeof AIOutputType[keyof typeof AIOutputType]
export interface AIOutput {
id: string
type: AIOutputType
inputHash: string // hash of the input for cache/dedup
outputJson: unknown // raw output — typed per consumer
provider: string // e.g. "openai", "anthropic"
model: string // e.g. "gpt-4o", "claude-3-5-sonnet"
promptVersion: string // semver of the prompt template used
schemaVersion: string // semver of expected output schema
createdAt: string
reviewedBy?: string
reviewStatus?: ReviewStatus
}
+101 -6
View File
@@ -1,13 +1,17 @@
// ── Asset Types ───────────────────────────────────────────────────────────────
export const AssetType = {
OFFICE: 'OFFICE',
RETAIL: 'RETAIL',
GASTRO: 'GASTRO',
GASTRO: 'GASTRO', // legacy kept for mock-data compatibility
LIGHT_INDUSTRIAL: 'LIGHT_INDUSTRIAL',
LOGISTICS: 'LOGISTICS',
PRODUCTION: 'PRODUCTION',
MIXED: 'MIXED',
UNKNOWN: 'UNKNOWN',
} as const
export type AssetType = typeof AssetType[keyof typeof AssetType]
// ── Result Types ──────────────────────────────────────────────────────────────
export const ResultType = {
VERIFIED_PORTFOLIO: 'VERIFIED_PORTFOLIO',
EXTERNAL_MARKET: 'EXTERNAL_MARKET',
@@ -15,6 +19,7 @@ export const ResultType = {
} as const
export type ResultType = typeof ResultType[keyof typeof ResultType]
// ── Match Strength ─────────────────────────────────────────────────────────────
export const MatchStrength = {
STRONG: 'STRONG',
MODERATE: 'MODERATE',
@@ -22,6 +27,17 @@ export const MatchStrength = {
} as const
export type MatchStrength = typeof MatchStrength[keyof typeof MatchStrength]
// ── Match Status ──────────────────────────────────────────────────────────────
export const MatchStatus = {
PENDING_REVIEW: 'PENDING_REVIEW',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
SHORTLISTED: 'SHORTLISTED',
ARCHIVED: 'ARCHIVED',
} as const
export type MatchStatus = typeof MatchStatus[keyof typeof MatchStatus]
// ── Risk Level ────────────────────────────────────────────────────────────────
export const RiskLevel = {
LOW: 'LOW',
MEDIUM: 'MEDIUM',
@@ -30,6 +46,26 @@ export const RiskLevel = {
} as const
export type RiskLevel = typeof RiskLevel[keyof typeof RiskLevel]
// ── Confidence Level (qualitative) ───────────────────────────────────────────
export const ConfidenceLevel = {
VERY_HIGH: 'VERY_HIGH', // >= 0.90
HIGH: 'HIGH', // >= 0.75
MEDIUM: 'MEDIUM', // >= 0.55
LOW: 'LOW', // >= 0.35
VERY_LOW: 'VERY_LOW', // < 0.35
} as const
export type ConfidenceLevel = typeof ConfidenceLevel[keyof typeof ConfidenceLevel]
// ── Data Quality Level (qualitative) ─────────────────────────────────────────
export const DataQualityLevel = {
HIGH: 'HIGH', // >= 0.80
MEDIUM: 'MEDIUM', // >= 0.60
LOW: 'LOW', // < 0.60
INCOMPLETE: 'INCOMPLETE', // missing critical fields
} as const
export type DataQualityLevel = typeof DataQualityLevel[keyof typeof DataQualityLevel]
// ── Availability Status ───────────────────────────────────────────────────────
export const AvailabilityStatus = {
AVAILABLE_NOW: 'AVAILABLE_NOW',
AVAILABLE_SOON: 'AVAILABLE_SOON',
@@ -39,13 +75,70 @@ export const AvailabilityStatus = {
} as const
export type AvailabilityStatus = typeof AvailabilityStatus[keyof typeof AvailabilityStatus]
export const DataFreshness = {
FRESH: 'FRESH',
STALE: 'STALE',
OUTDATED: 'OUTDATED',
// ── Availability Type (structural distinction) ────────────────────────────────
export const AvailabilityType = {
CONFIRMED: 'CONFIRMED', // verified, date known
INDICATIVE: 'INDICATIVE', // external listing, unconfirmed
PROBABILISTIC: 'PROBABILISTIC', // AI signal, no confirmed date
} as const
export type DataFreshness = typeof DataFreshness[keyof typeof DataFreshness]
export type AvailabilityType = typeof AvailabilityType[keyof typeof AvailabilityType]
// ── Source Type ───────────────────────────────────────────────────────────────
export const SourceType = {
ERP_IMPORT: 'ERP_IMPORT',
MANUAL_ENTRY: 'MANUAL_ENTRY',
IMMOSCOUT_SCRAPE: 'IMMOSCOUT_SCRAPE',
HOMEGATE_SCRAPE: 'HOMEGATE_SCRAPE',
NEWHOME_SCRAPE: 'NEWHOME_SCRAPE',
MATCHOFFICE_SCRAPE: 'MATCHOFFICE_SCRAPE',
MAISON_WORK_SCRAPE: 'MAISON_WORK_SCRAPE',
AI_SIGNAL: 'AI_SIGNAL',
PARTNER_FEED: 'PARTNER_FEED',
UNKNOWN: 'UNKNOWN',
} as const
export type SourceType = typeof SourceType[keyof typeof SourceType]
// ── Freshness Status ──────────────────────────────────────────────────────────
export const FreshnessStatus = {
FRESH: 'FRESH', // updated within 48h
STALE: 'STALE', // 214 days old
OUTDATED: 'OUTDATED', // > 14 days old
} as const
export type FreshnessStatus = typeof FreshnessStatus[keyof typeof FreshnessStatus]
/** @deprecated Use FreshnessStatus — kept for mock-data backward compatibility */
export const DataFreshness = FreshnessStatus
export type DataFreshness = FreshnessStatus
// ── Review Status ─────────────────────────────────────────────────────────────
export const ReviewStatus = {
UNREVIEWED: 'UNREVIEWED',
IN_REVIEW: 'IN_REVIEW',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
FLAGGED: 'FLAGGED',
} as const
export type ReviewStatus = typeof ReviewStatus[keyof typeof ReviewStatus]
// ── Sensitivity Level ─────────────────────────────────────────────────────────
export const SensitivityLevel = {
PUBLIC: 'PUBLIC',
INTERNAL: 'INTERNAL',
CONFIDENTIAL: 'CONFIDENTIAL',
RESTRICTED: 'RESTRICTED',
} as const
export type SensitivityLevel = typeof SensitivityLevel[keyof typeof SensitivityLevel]
// ── Shortlist Status ──────────────────────────────────────────────────────────
export const ShortlistStatus = {
ACTIVE: 'ACTIVE',
SHARED: 'SHARED',
ARCHIVED: 'ARCHIVED',
CONVERTED: 'CONVERTED',
} as const
export type ShortlistStatus = typeof ShortlistStatus[keyof typeof ShortlistStatus]
// ── User Role ─────────────────────────────────────────────────────────────────
export const UserRole = {
SUPER_ADMIN: 'SUPER_ADMIN',
ORGANIZATION_ADMIN: 'ORGANIZATION_ADMIN',
@@ -56,6 +149,7 @@ export const UserRole = {
} as const
export type UserRole = typeof UserRole[keyof typeof UserRole]
// ── Signal Type ───────────────────────────────────────────────────────────────
export const SignalType = {
EXPANSION: 'EXPANSION',
POSSIBLE_MOVE_OUT: 'POSSIBLE_MOVE_OUT',
@@ -66,6 +160,7 @@ export const SignalType = {
} as const
export type SignalType = typeof SignalType[keyof typeof SignalType]
// ── Workspace ─────────────────────────────────────────────────────────────────
export const WorkspaceType = {
SUPPLY: 'SUPPLY',
DEMAND: 'DEMAND',
+13 -1
View File
@@ -1,4 +1,4 @@
import type { SignalType, RiskLevel } from './enums'
import type { SignalType, RiskLevel, ReviewStatus } from './enums'
export interface SignalSource {
type: 'PRESS' | 'CONSTRUCTION_PERMIT' | 'JOB_POSTING' | 'COMPANY_REPORT' | 'MARKET_DATA' | 'MANUAL'
@@ -7,6 +7,12 @@ export interface SignalSource {
credibility: 'LOW' | 'MEDIUM' | 'HIGH'
}
export interface SignalEvidence {
summary: string
sourceUrls?: string[]
extractedAt?: string
}
export interface FutureSignal {
id: string
signalType: SignalType
@@ -30,4 +36,10 @@ export interface FutureSignal {
organizationId?: string
createdAt: string
updatedAt: string
// F004 additions
title?: string // short human-readable headline
evidence?: SignalEvidence // structured evidence block
matchabilityScore?: number // 0100: how well this signal can be matched to needs
reviewStatus?: ReviewStatus
}
+3
View File
@@ -3,3 +3,6 @@ export * from './property'
export * from './need'
export * from './match'
export * from './futureSignal'
export * from './aiOutput'
export * from './activityEvent'
export * from './unifiedResult'
+71 -15
View File
@@ -1,52 +1,108 @@
import type { MatchStrength, RiskLevel } from './enums'
import type { MatchStrength, MatchStatus, RiskLevel, ResultType, ConfidenceLevel } from './enums'
// ── Score Building Blocks ─────────────────────────────────────────────────────
export interface ScoreBreakdown {
hardMatchScore: number
softFactorScore: number
confidenceModifier: number
dataQualityModifier: number
totalScore: number
hardMatchScore: number // 0100 hard criteria score
softFactorScore: number // 0100 soft factors score
confidenceModifier: number // -20 to +5 adjustment
dataQualityModifier: number // -15 to 0 adjustment
totalScore: number // final 0100
}
export interface ScoreFactor {
criterion: string
weight: number
score: number
contribution: number
weight: number // 01 relative weight
score: number // 0100
contribution: number // weighted points added to total
explanation: string
}
export interface Tradeoff {
// ── Explainability Types ──────────────────────────────────────────────────────
/** Canonical tradeoff type (F004) */
export interface TradeOff {
criterion: string
concern: string
severity: 'LOW' | 'MEDIUM' | 'HIGH'
mitigation?: string
impactOnScore?: number
}
/** @deprecated Use TradeOff — kept for backward compatibility */
export type Tradeoff = TradeOff
export interface Risk {
category: string // e.g. "Datenverfügbarkeit", "Standortrisiko"
description: string
level: RiskLevel
mitigation?: string
}
export interface MissingDataItem {
field: string
importance: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
description: string
impact: string // how it affects the match score
}
export interface NextBestAction {
label: string // e.g. "Besichtigung anfragen"
description?: string
priority: 'HIGH' | 'MEDIUM' | 'LOW'
actionType: 'CONTACT' | 'VERIFY' | 'REVIEW' | 'SHORTLIST' | 'COMPARE' | 'SCHEDULE'
externalUrl?: string
}
export interface AlternativeStrategy {
title: string
description: string
expectedScore?: number
reasoning?: string
}
// ── Match (core entity) ───────────────────────────────────────────────────────
export interface Match {
id: string
propertyId: string
needId: string
matchScore: number
// Result reference — works for all three result types
resultId?: string // preferred: generic result ID
resultType?: ResultType
propertyId: string // legacy alias for resultId (VERIFIED_PORTFOLIO)
// Scoring
matchScore: number // 0100
matchStrength: MatchStrength
scoreBreakdown: ScoreBreakdown
confidenceLevel: number // 01 numeric
confidenceLevelLabel?: ConfidenceLevel
dataConfidenceScore?: number // 01 data quality contribution
// Explainability
positiveFactors: ScoreFactor[]
negativeFactors: ScoreFactor[]
tradeoffs: Tradeoff[]
tradeoffs: TradeOff[]
tradeOffs?: TradeOff[] // alias for F004 naming convention
risks?: Risk[]
missingData?: MissingDataItem[]
nextBestActions?: NextBestAction[]
explainabilitySummary: string
confidenceLevel: number
explanation?: string // alias / longer form
// Risk
riskLevel: RiskLevel
uncertaintyIndicators: string[]
// Alternatives
alternativeStrategies?: AlternativeStrategy[]
// Review / lifecycle
status?: MatchStatus
isApproved?: boolean // legacy — prefer status
reviewedBy?: string
reviewedAt?: string
isApproved?: boolean
organizationId?: string
createdAt: string
updatedAt: string
+28 -2
View File
@@ -1,4 +1,4 @@
import type { AssetType } from './enums'
import type { AssetType, MatchStatus } from './enums'
export interface AreaRange {
min: number
@@ -40,17 +40,43 @@ export interface SoftFactorPreferences {
requireHighVisibility?: boolean
}
export interface SizeRange {
minSqm: number
maxSqm: number
}
export interface MustHaveCriterion {
criterion: string
description?: string
weight?: number // 01, how much a miss hurts the score
}
export interface WeightedPreference {
criterion: string
weight: number // 01
idealValue?: string | number
description?: string
}
export interface Need {
id: string
companyName: string
contactName?: string
assetType: AssetType
requiredArea: AreaRange
// F004 additions
desiredLocation?: string[] // preferred city/district list
sizeRange?: SizeRange // structured alias for requiredArea
mustHaveCriteria?: MustHaveCriterion[]
weightedPreferences?: WeightedPreference[]
status?: MatchStatus | 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'CLOSED'
preferredLocations: string[]
excludedLocations?: string[]
budgetRange: BudgetRange
timing: Timing
mustCriteriaText?: string[]
mustCriteriaText?: string[] // legacy — prefer mustHaveCriteria
softFactors?: SoftFactorPreferences
weightingProfile: WeightingProfile
confidenceInCriteria: number
+82 -16
View File
@@ -1,14 +1,17 @@
import type { AssetType, ResultType, AvailabilityStatus, DataFreshness, RiskLevel } from './enums'
import type {
AssetType, ResultType, AvailabilityStatus, AvailabilityType,
FreshnessStatus, RiskLevel, SourceType, DataQualityLevel,
} from './enums'
// ── Location / Address ────────────────────────────────────────────────────────
export interface Location {
city: string
district?: string
canton?: string
region?: string
country: string
coordinates?: {
lat: number
lng: number
}
coordinates?: { lat: number; lng: number }
}
export interface Address {
@@ -19,10 +22,58 @@ export interface Address {
country: string
}
// ── Source Metadata ───────────────────────────────────────────────────────────
export interface SourceMeta {
sourceType: SourceType | string
sourceLabel?: string
sourceUrl?: string
sourceUpdatedAt?: string
externalId?: string
}
// ── Data Quality ──────────────────────────────────────────────────────────────
export interface DataQuality {
score: number
qualityLevel?: DataQualityLevel
missingCriticalFields: string[]
missingOptionalFields: string[]
lastVerifiedAt?: string
freshness: FreshnessStatus
warnings: string[]
}
// ── Hard Facts ────────────────────────────────────────────────────────────────
export interface PropertyHardFacts {
floor?: number
fitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM'
parking?: number
publicTransportScore?: number
usageType?: string
ceilingHeightM?: number
loadingDocksCount?: number
powerSupplyKva?: number
hasServerRoom?: boolean
isBarrierFree?: boolean
}
// ── Soft Factors ──────────────────────────────────────────────────────────────
export interface SoftFactors {
prestigeScore?: number
visibilityScore?: number
footfallScore?: number
commuterAccessScore?: number
talentAccessScore?: number
esgScore?: number
flexibilityScore?: number
expansionPotentialScore?: number
taxEnvironmentScore?: number
// Legacy aliases kept for mock-data backward compatibility
prestige?: number
accessibility?: number
visibilityScore?: number
talentAccess?: number
esgRating?: string
passerbyFrequency?: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
@@ -31,40 +82,55 @@ export interface SoftFactors {
infrastructureNotes?: string
}
export interface DataQuality {
score: number
missingCriticalFields: string[]
missingOptionalFields: string[]
lastVerifiedAt?: string
freshness: DataFreshness
warnings: string[]
}
// ── Property ──────────────────────────────────────────────────────────────────
export interface Property {
id: string
organizationId?: string
title: string
assetType: AssetType
resultType: ResultType
location: Location
address: Address
areaSqm: number
areaSqmMin?: number
areaSqmMax?: number
rentPricePerSqm: number
rentChfSqmYear?: number
totalRentMonthly?: number
ancillaryCosts?: number
availabilityDate: string
availabilityStatus: AvailabilityStatus
availabilityType?: AvailabilityType
sourceType: string
sourceLabel?: string
sourceUrl?: string
sourceUpdatedAt?: string
sourceMeta?: SourceMeta
confidenceScore: number
dataQuality: DataQuality
softFactors?: SoftFactors
hardFacts?: PropertyHardFacts
// Legacy fields — kept for backward compat
floorLevel?: number
expansionPotentialSqm?: number
contractDurationMonths?: number
ancillaryCosts?: number
riskLevel?: RiskLevel
description?: string
images?: string[]
organizationId?: string
status?: 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'ARCHIVED'
lastReviewedAt?: string
createdAt: string
updatedAt: string
}
+38
View File
@@ -0,0 +1,38 @@
import type { ResultType } from './enums'
import type { Property } from './property'
import type { FutureSignal } from './futureSignal'
import type { Match } from './match'
// ── Unified Match Result (discriminated union) ────────────────────────────────
// Wraps the three result types so UI components can handle all three paths
// without type confusion. Discriminate on `resultType`.
interface UnifiedResultBase {
matchId: string
needId: string
matchScore: number
resultType: ResultType
}
export interface VerifiedPortfolioResult extends UnifiedResultBase {
resultType: 'VERIFIED_PORTFOLIO'
property: Property
match: Match
}
export interface ExternalMarketResult extends UnifiedResultBase {
resultType: 'EXTERNAL_MARKET'
property: Property
match: Match
}
export interface FutureAvailabilityResult extends UnifiedResultBase {
resultType: 'FUTURE_AVAILABILITY'
signal: FutureSignal
match: Match
}
export type UnifiedMatchResult =
| VerifiedPortfolioResult
| ExternalMarketResult
| FutureAvailabilityResult
+4
View File
@@ -30,6 +30,8 @@ function getAssetTypeLabel(type: AssetType): string {
case AssetType.GASTRO: return 'Gastro'
case AssetType.PRODUCTION: return 'Produktion'
case AssetType.MIXED: return 'Gemischt'
case AssetType.LIGHT_INDUSTRIAL: return 'Leichtindustrie'
case AssetType.UNKNOWN: return 'Unbekannt'
}
}
@@ -41,6 +43,8 @@ function getAssetTypeColor(type: AssetType): string {
case AssetType.GASTRO: return '#0d9488'
case AssetType.PRODUCTION: return '#92400e'
case AssetType.MIXED: return '#6b7280'
case AssetType.LIGHT_INDUSTRIAL: return '#b45309'
case AssetType.UNKNOWN: return '#9ca3af'
}
}