Skip to content

Data Architecture & Relationships

This page explains how the Estate, Department, Employee, and Project models work together in Destinet.

Architecture Overview

Destinet uses a normalized database structure with four separate master entity collections:

┌──────────────────────────┐       ┌──────────────────────────┐
│  Departments Collection  │       │  Employees Collection    │
│  (Meglerkontor)          │◄──────┤  (Meglere)               │
│  Master Entity           │       │  Master Entity           │
└──────────────────────────┘       └──────────────────────────┘
         ↑                                  ↑
         │                                  │
         │ Referenced by                   │ Referenced by
         │                                  │
         ├──────────────────────────────────┴──────────────┐
         │                                                  │
┌──────────────────────────┐       ┌──────────────────────────┐
│  Estates Collection      │       │  Projects Collection     │
│  (Eiendommer)            │       │  (Boligprosjekter)       │
│  References IDs          │       │  References IDs          │
└──────────────────────────┘       └──────────────────────────┘
         ↑                                  │
         │                                  │
         └──────────────────────────────────┘
           (Estates can belong to Projects)

Master Entities (Stored Once, Referenced Many Times)

  • Department - Real estate offices/brokerages
  • Employee - Individual brokers/agents
  • Project - New construction projects (apartment complexes, etc.)
  • Estate - Individual properties (can be standalone or part of Project)

Why Separate Collections?

Benefits

Benefit Explanation
No duplication Each department/employee is stored once, not repeated on every estate
Easy updates Change employee's email once → reflected on all estates
Efficient queries Find all estates for a specific employee or department
Data integrity Single source of truth for each entity
Scalability Supports future features (employee statistics, department dashboards, etc.)

Example: Email Update

With duplicated data (bad):

Employee changes email → Update 150 estate records

With normalized data (good):

Employee changes email → Update 1 employee record
All 150 estates automatically show new email via reference


Shared Image Class

All entities (Estate, Department, Employee, Project, Phase, Building) use a single shared Image class instead of separate PropertyImage, DepartmentImage, EmployeeImage classes.

Benefits of Consolidation

Benefit Explanation
DRY Principle Single source of truth for image handling
Consistency Same behavior across all entity types
Easy maintenance Update image features once, applies everywhere
Future features Add thumbnails, CDN support, lazy loading once for all entities

How It Works

The Category field provides context-specific categorization:

public class Image
{
    public string Id { get; set; }
    public string? OriginalUrl { get; set; }
    public string Filename { get; set; }
    public string FileExtension { get; set; }
    public string? ExternalProviderUrl { get; set; }
    public string? Caption { get; set; }
    public string? AltText { get; set; }
    public string? Category { get; set; }  // Context-dependent!
    public int? Order { get; set; }
    public int? Width { get; set; }
    public int? Height { get; set; }
    public DateTime? LastModifiedOrigin { get; set; }
    public DateTime? LastModifiedLocal { get; set; }
}

Category Examples by Entity

// Estate images
estate.Images.Add(new Image { Category = "Facade" });
estate.Images.Add(new Image { Category = "Living Room" });
estate.Images.Add(new Image { Category = "Floor Plan" });

// Department images
department.Images.Add(new Image { Category = "Office Facade" });
department.Images.Add(new Image { Category = "Team Photo" });

// Employee images
employee.Images.Add(new Image { Category = "Profile Photo" });
employee.Images.Add(new Image { Category = "Signature Image" });

// Project images
project.Images.Add(new Image { Category = "Rendering" });
project.Images.Add(new Image { Category = "Site Plan" });

How Relationships Work

Estate References

public class Estate
{
    // Department/Employee References
    public int? DepartmentId { get; set; }                       // 3005093
    public List<BrokerWithRole> BrokersIdWithRoles { get; set; }

    // Project References (for new builds)
    public string? ProjectId { get; set; }                       // "PROJ-2024-001" (null if standalone)
}

public class BrokerWithRole
{
    public string EmployeeId { get; set; }   // Reference to Employee
    public int BrokerRole { get; set; }      // Role enum (primary, secondary, etc.)
}

Employee References

public class Employee
{
    public List<string>? DepartmentIds { get; set; }  // ["3005093", "3005094"]
    // Note: Employees can belong to multiple departments
}

Department References to Employees

public class Department
{
    public List<string>? ManagingDirectorIds { get; set; }     // ["3006722"]
    public List<string>? DepartmentManagerIds { get; set; }    // ["3006723"]
    public List<string>? ResponsibleBrokerIds { get; set; }    // ["3006708"]
}

Project References

public class Project
{
    public string? DepartmentId { get; set; }                  // "3005093" (Department reference)
    public string? PrimaryAgentId { get; set; }                // "3006722" (Employee reference)
    public List<string>? AdditionalAgentIds { get; set; }      // ["3006723"] (Employee references)

    // Nested entities (stored within Project)
    public List<Phase>? Phases { get; set; }
    public List<Building>? Buildings { get; set; }
}

Looking Up Full Data

// 1. Get estate
var estate = await estateRepository.GetById("3221523");

// 2. Get related department
var department = await departmentRepository.GetById(estate.DepartmentId.ToString());

// 3. Get related employees
var employees = new List<Employee>();
foreach (var broker in estate.BrokersIdWithRoles)
{
    var employee = await employeeRepository.GetById(broker.EmployeeId);
    employees.Add(employee);
}

// 4. Present to user
return new EstateDetailsViewModel
{
    Estate = estate,
    Department = department,
    Employees = employees
};

Vitec Integration: Upsert Strategy

When Vitec pushes updates, we extract and update each entity separately.

Vitec XML Structure

Vitec sends all data in one XML per estate:

<eneiendom>
  <!-- Estate data -->
  <felt navn="id">3221523</felt>
  <felt navn="adresse">Storgata 10</felt>

  <!-- Department data (embedded) -->
  <felt navn="avdeling_id">3005093</felt>
  <felt navn="avdeling_navn">Proaktiv Næring</felt>
  <felt navn="avdeling_email">borg@proaktiv.no</felt>

  <!-- Employee data (embedded) -->
  <felt navn="avdeling_fagansvarlig_id">3006722</felt>
  <felt navn="avdeling_fagansvarlig_navn">Karianne Jensen</felt>
  <felt navn="avdeling_fagansvarlig_email">karianne@proaktiv.no</felt>
</eneiendom>

Processing Strategy

public async Task ProcessVitecEstate(VitecXml vitecData)
{
    // 1. Extract and upsert Department
    var department = new Department
    {
        Id = vitecData.AvdelingId.ToString(),
        Name = vitecData.AvdelingNavn,
        Email = vitecData.AvdelingEmail,
        LastModifiedLocal = DateTime.UtcNow,
        Origin = "Vitec"
    };
    await departmentRepository.Upsert(department);

    // 2. Extract and upsert Primary Employee
    var employee = new Employee
    {
        Id = vitecData.FagansvarligId,
        Name = vitecData.FagansvarligNavn,
        Email = vitecData.FagansvarligEmail,
        DepartmentIds = new List<string> { vitecData.AvdelingId.ToString() },
        LastModifiedLocal = DateTime.UtcNow,
        Origin = "Vitec"
    };
    await employeeRepository.Upsert(employee);

    // 3. Extract and upsert Additional Employees
    foreach (var ansatt in vitecData.Ansatte)
    {
        var additionalEmployee = new Employee
        {
            Id = ansatt.Id,
            Name = ansatt.Navn,
            Email = ansatt.Email,
            DepartmentIds = new List<string> { vitecData.AvdelingId.ToString() },
            LastModifiedLocal = DateTime.UtcNow,
            Origin = "Vitec"
        };
        await employeeRepository.Upsert(additionalEmployee);
    }

    // 4. Create/update Estate with ONLY references
    var estate = new Estate
    {
        Id = vitecData.Id.ToString(),
        DepartmentId = vitecData.AvdelingId,
        BrokersIdWithRoles = new List<BrokerWithRole>
        {
            new BrokerWithRole
            {
                EmployeeId = vitecData.FagansvarligId,
                BrokerRole = 1  // Primary broker
            }
        },
        // Add additional brokers
        // BrokersIdWithRoles.AddRange(vitecData.Ansatte.Select(a =>
        //     new BrokerWithRole { EmployeeId = a.Id, BrokerRole = 2 })),

        // ... other estate properties
        Origin = "Vitec"
    };
    await estateRepository.Upsert(estate);
}

Update Scenarios

Scenario 1: New Estate from Vitec

{
  "eventType": "estate.created",
  "estateId": "3221523",
  "departmentId": "3005093",
  "employeeId": "3006722"
}

Destinet actions: 1. Check if Department 3005093 exists → No → Create from Vitec data 2. Check if Employee 3006722 exists → No → Create from Vitec data 3. Create Estate with references

Result: 3 new records (1 department, 1 employee, 1 estate)


Scenario 2: Employee Email Update

{
  "eventType": "employee.updated",
  "employeeId": "3006722",
  "changes": {
    "email": "new.email@proaktiv.no"
  }
}

Destinet actions: 1. Update Employee 3006722 email field 2. Done!

Result: 1 updated record. All estates automatically reflect new email.


Scenario 3: Estate Updated (same department/employee)

{
  "eventType": "estate.updated",
  "estateId": "3221523",
  "changes": {
    "priceSuggestion": 5000000
  }
}

Destinet actions: 1. Check if Department still exists → Yes → Skip 2. Check if Employee still exists → Yes → Skip 3. Update Estate price

Result: 1 updated record (estate only)


Database Schema

Departments Collection

Field Type Description
Id string Unique identifier (from Vitec: avdeling_id)
Name string Department name
RegistrationNumber string? Organization number
IsRegion bool? Whether this is a regional office
IsPartOfFranchise bool? Whether this is part of a franchise
SubDepartments List? Sub departments as enum list
Address Address? Office address (Street, City, PostalCode, etc.)
Phone string? Contact phone
Email string? Contact email
LeadsEmail string? Email for leads
Website string? Department website
Description string? Description/biography of the department
ShortDescription string? Short description of the department
LogoUrl string? Logo image URL
VideoUrl string? URL to department video presentation
HeaderImageUrl string? URL to header/banner image
PriceListUrl string? URL to price list document
ManagingDirectorIds List? References to managing directors
DepartmentManagerIds List? References to department managers
ResponsibleBrokerIds List? References to responsible brokers
Images ImageCollection? Office photos (facade, interior, etc.)
ImageTimestamp DateTime? Image last modified timestamp
IsActive bool? Whether this department is currently active
LastModifiedOrigin DateTime? Last update timestamp in origin system
LastModifiedLocal DateTime? Last update timestamp locally
OriginalDepartmentId string? Old department IDs if needing to match new Id with old
Origin string? Source (e.g., "Vitec")
CustomData Dictionary? System-specific metadata

Employees Collection

Field Type Description
Id string Unique identifier (from Vitec: fagansvarlig_id, ansatte_id)
Name string? Employee name
Title string? Job title
Email string? Contact email
MobilePhone string? Mobile number
Phone string? Office phone
PhotoUrl string? Profile photo URL
VideoUrl string? URL to employee's video presentation
Description string? Biography/description
ShortDescription string? Short biography
Qualifications string? Certifications and qualifications
IsProjectBroker bool? Specializes in new build projects
IsRentalBroker bool? Specializes in rentals
IsActive bool? Active status
DepartmentIds List? References to Departments (plural - can belong to multiple)
SoldProperties List? List of sold property addresses
Images ImageCollection? Profile photos, signatures, etc.
ImageTimestamp DateTime? Image last modified timestamp
LastModifiedOrigin DateTime? Last update timestamp in origin system
LastModifiedLocal DateTime? Last update timestamp locally
OriginalEmployeeId string? Old employee IDs if needing to match new Id with old
Origin string? Source (e.g., "Vitec")
CustomData Dictionary? System-specific metadata

Estates Collection

Field Type Description
Id string Unique identifier
AssignmentNum string? Assignment number / Oppdragsnummer
Heading string? Marketing headline
DepartmentId int? Reference to Department
BrokersIdWithRoles List? References to Employees with roles
EstateBaseType int? Base type enum from Vitec (0=NotSet, 1=Detached, etc.)
AssignmentTypeGroup int? Assignment type enum from Vitec (0=NotSet, 1=Sale, etc.)
Ownership int? Ownership type enum from Vitec (0=Owned, 1=Cooperative, etc.)
Status int Status enum from Vitec (0=Request, 1=Preparation, 2=ForSale, etc.)
Address Address? Property address
GeoCoordinates GeoCoordinates? Latitude and longitude
Matrikkel List? Norwegian cadastral information
EstateSize EstateSize? Area measurements (primary room area, gross area, etc.)
EstatePrice EstatePrice? Pricing information (asking price, costs, fees, etc.)
PartOwnership PartOwnership? Cooperative/condominium details
Images ImageCollection? Property photos (facade, interior, floor plans)
Documents DocumentCollection? PDFs, prospectuses, etc.
Links List? External links (virtual tours, videos)
Showings List? Scheduled viewings
DescriptionSections List? Structured descriptions
ProjectId string? Reference to Project (null if standalone)
Origin string? Source (e.g., "Vitec")
CustomData Dictionary? System-specific metadata

Projects Collection

Field Type Description
Id string Unique identifier
ParentProjectId string? Reference to parent project if this is a sub-project
Name string Project name
Developer string? Developer company
DeveloperWebsite string? Developer company website
SellerId string? Seller identifier
Address Address? Project address
Coordinates GeoCoordinates? Latitude and longitude
TotalUnits int? Total number of units in project
Phases List? Construction/sales phases (nested)
Buildings List? Individual buildings (nested)
Images ImageCollection? Project renderings, site plans
Documents DocumentCollection? Brochures, legal documents
DescriptionSections List? Structured descriptions
ExternalPlatformUrls ExternalPlatformUrls? External platform URLs
DepartmentId string? Reference to Department handling project
PrimaryAgentId string? Reference to primary Employee
AdditionalAgentIds List? References to additional Employees
LastModifiedOrigin DateTime? Last update timestamp in origin system
LastModifiedLocal DateTime? Last update timestamp locally
Origin string? Source (e.g., "Vitec")
CustomData Dictionary? System-specific metadata

Shared Classes Used Across All Models

Class Used By Description
Address All entities Street, City, PostalCode, Municipality, County, etc.
GeoCoordinates Estate, Project Latitude, Longitude for map display
Image All entities Generic image class (facade, interior, profile photos, etc.)
PropertyDocument Estate, Project PDFs, floor plans, brochures
PropertyLink Estate External links (virtual tours, videos)
Showing Estate Scheduled open houses
DescriptionSection Estate, Project Structured description sections
ExternalPlatformUrls Estate, Project External platform URLs
Matrikkel Estate Norwegian cadastral information

Estate-Specific Classes

Class Description
BrokerWithRole Employee reference with role assignment
EstateSize Area measurements (primary room area, gross area, usable area, etc.)
EstatePrice Comprehensive pricing (asking price, costs, fees, collective debt, etc.)
PartOwnership Cooperative/condominium details (for Borettslag)
Plot Plot/land information
EnergyRating Energy efficiency rating (A-G)

Project-Specific Classes

Class Description
Phase Construction/sales phases with timeline and unit counts
Building Individual buildings within a project

Query Examples

Get All Estates for an Employee

var estates = await estateRepository.Find(e =>
    e.BrokersIdWithRoles.Any(b => b.EmployeeId == "3006722")
);

Get All Estates for a Department

var estates = await estateRepository.Find(e =>
    e.DepartmentId == 3005093
);

Get Estate with Full Details

// Get estate
var estate = await estateRepository.GetById("3221523");

// Enrich with related data
var estateDetails = new EstateDetailsViewModel
{
    Estate = estate,
    Department = await departmentRepository.GetById(estate.DepartmentId.ToString()),
    Employees = await GetEmployeesForEstate(estate)
};

async Task<List<Employee>> GetEmployeesForEstate(Estate estate)
{
    var employees = new List<Employee>();
    foreach (var broker in estate.BrokersIdWithRoles)
    {
        var employee = await employeeRepository.GetById(broker.EmployeeId);
        employees.Add(employee);
    }
    return employees;
}

Get Employee Statistics

// Find all estates for employee
var employeeEstates = await estateRepository.Find(e =>
    e.BrokersIdWithRoles.Any(b => b.EmployeeId == employeeId)
);

// Calculate statistics
var stats = new EmployeeStatistics
{
    TotalListings = employeeEstates.Count,
    ActiveListings = employeeEstates.Count(e => e.Status == 2), // ForSale
    SoldListings = employeeEstates.Count(e => e.Status == 3),   // Sold
    TotalValue = employeeEstates.Sum(e => e.EstatePrice?.PriceSuggestion ?? 0)
};

Get All Estates in a Project

// Get the project
var project = await projectRepository.GetById("PROJ-2024-001");

// Get all estates belonging to this project
var projectEstates = await estateRepository.Find(e =>
    e.ProjectId == "PROJ-2024-001"
);

// Count sold vs available
var soldCount = projectEstates.Count(e => e.Status == 3); // Sold
var availableCount = projectEstates.Count(e => e.Status == 2); // ForSale

Get Estates in a Specific Building

// Get project first
var project = await projectRepository.GetById("PROJ-2024-001");
var building = project.Buildings.FirstOrDefault(b => b.Id == "BYGG-A");

// Get estates in this project - would need additional filtering logic
var projectEstates = await estateRepository.Find(e =>
    e.ProjectId == "PROJ-2024-001"
    // Note: Building linkage would need additional implementation
);

Get All Departments Serving a Postal Code

var departments = await departmentRepository.Find(d =>
    d.ServiceAreaPostalCodes.Contains("7011")
);

Get All Employees Working at Multiple Departments

var multiDepartmentEmployees = await employeeRepository.Find(e =>
    e.DepartmentIds.Count > 1
);

Best Practices

DO:

  • Always use IDs for references between entities
  • Upsert departments and employees before creating/updating estates
  • Query by ID when looking up related entities
  • Cache frequently accessed department/employee data if needed for performance
  • Index DepartmentId and BrokersIdWithRoles.EmployeeId fields in Estate collection
  • Store integer enums directly from source system (Vitec) - use mapping tables for display

DON'T:

  • Store full Department/Employee objects in Estate
  • Duplicate department/employee data across estates
  • Skip upserting department/employee when processing estate updates
  • Assume department/employee exists without checking
  • Convert enums to strings during storage - keep as integers