feat: unit-level matching architecture — match against Einheiten, not Objekte

- domain/property: add getEffectiveUnits() — returns explicit units or synthesises
  one from property-level data so all properties work uniformly at unit level
- domain/match: add unitId? field — every Match references a specific unit
- domain/unifiedResult: add unit? to VerifiedPortfolioResult + ExternalMarketResult
- MockupNeedProvider: generateSyntheticMatches iterates getEffectiveUnits(prop);
  computeScore uses unit-level areaSqm + rentPricePerSqm; resultId for PRE-MARKET
  signals matches useSchattenmarktSignals signal ID format
- useUnifiedResults: resolve unit from match.unitId for VERIFIED_PORTFOLIO/EXTERNAL_MARKET
- matchCardAdapter: unit now extracted for all result types; unitId from match.unitId
- IntelligenceMatchCard: regular cards show unit chip (floor + label + m²) when a
  named unit is known — only adds context, never shows for synthetic/whole-property units

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-20 20:03:54 +02:00
parent d902feb6c0
commit 99e99d66c5
7 changed files with 128 additions and 60 deletions
+25
View File
@@ -2,6 +2,7 @@ import type {
AssetType, ResultType, AvailabilityStatus, AvailabilityType,
FreshnessStatus, RiskLevel, SourceType, DataQualityLevel,
} from './enums'
import { AvailabilityStatus as AS } from './enums'
// ── Location / Address ────────────────────────────────────────────────────────
@@ -196,3 +197,27 @@ export interface Property {
export type CreatePropertyInput = Omit<Property, 'id' | 'createdAt' | 'updatedAt'>
export type UpdatePropertyInput = Partial<CreatePropertyInput>
/**
* Returns the rentable units of a property.
* For properties without explicit units, synthesises one unit from property-level data
* so that all matching logic can operate uniformly at unit level.
*/
export function getEffectiveUnits(p: Property): PropertyUnit[] {
if (p.units && p.units.length > 0) return p.units
return [{
id: p.id,
propertyId: p.id,
floorLevel: p.hardFacts?.floor ?? p.floorLevel ?? 0,
unitLabel: undefined,
areaSqm: p.areaSqm,
available: p.availabilityStatus === AS.AVAILABLE_NOW || p.availabilityStatus === AS.AVAILABLE_SOON,
rentPricePerSqm: p.rentPricePerSqm,
currentTenant: p.currentTenant,
leaseTerm: p.leaseTerm,
leaseEndDate: p.leaseEndDate,
schattenmarktRelease: p.schattenmarktRelease?.enabled
? { enabled: true, availableFrom: p.leaseEndDate }
: undefined,
}]
}