Skip to content

Data Model & WBS โ€‹

This document describes the entity structures of the Calculations and Quotation services, and how the Work Breakdown Structure (WBS) flows through the Calculations โ†’ Quotation โ†’ Projects pipeline.

All entities inherit from MasterEntity (Id, CreatedAt, CreatedBy, UpdatedAt, UpdatedBy, soft-delete via IsDeleted). Multi-tenant entities additionally inherit from TenantEntity, which adds CompanyId. These audit/tenant columns are omitted from the diagrams below for readability.

Calculations (vulcan-be-calculations) โ€‹

A calculation is a recursive budget tree: a Budget owns one root Element, every Element is self-referencing (parent/children) and contains CostItems. Cost aggregation is delegated to an ICalculationStrategy (Traditional vs. Elemental).

mermaid
erDiagram
    Budget ||--|| Element : "root element"
    Budget ||--o{ ExtraCostCategorySnapshot : "snapshot at creation"
    Element ||--o{ Element : "parent/children (tree)"
    Element ||--o{ CostItem : "contains"
    CostItem ||--o{ ExtraCostCategoryValue : "values"
    ExtraCostCategorySnapshot ||--o{ ExtraCostCategoryValue : "category"
    CompanyBudgetSettings ||--o{ ExtraCostCategoryDefinition : "definitions (max 10 active)"
    ExtraCostCategoryDefinition ||..o{ ExtraCostCategorySnapshot : "snapshotted from"

    Budget {
        Guid Id PK
        Guid CompanyId FK
        string Name
        Guid ProjectId "cross-service"
        Guid LeadId "cross-service"
        enum Status "Draft|Template"
        bool UniqueResourceCodingEnabled
        decimal Total "computed"
    }
    Element {
        Guid Id PK
        Guid CompanyId FK
        Guid BudgetId FK
        Guid ParentId FK "nullable, self-ref"
        string Code "URC"
        string Description
        string Unit
        decimal Quantity
        enum CalculationType "Traditional|Elemental"
        int Depth "computed"
        decimal Total "via strategy"
    }
    CostItem {
        Guid Id PK
        Guid CompanyId FK
        Guid BudgetId FK
        Guid ElementId FK "nullable"
        string Code "URC"
        string Description
        string Unit
        decimal Quantity
        decimal HourNorm
        decimal HourlyRate
        decimal MaterialPrice
        decimal EquipmentPrice
        decimal SubcontractorPrice
    }
    ExtraCostCategoryValue {
        Guid Id PK
        Guid CompanyId FK
        Guid CostItemId FK
        Guid BudgetId FK
        Guid CostCategoryId FK
        decimal Amount
    }
    ExtraCostCategorySnapshot {
        Guid Id PK
        Guid CompanyId FK
        Guid BudgetId FK
        Guid ExtraCostCategoryDefinitionId FK
        string Name
        int Order
    }
    ExtraCostCategoryDefinition {
        Guid Id PK
        Guid CompanyId FK
        string Name
        int Order
        bool IsActive "soft-delete"
    }
    CompanyBudgetSettings {
        Guid Id PK
        Guid CompanyId FK
    }
    ColumnSelectionPreset {
        Guid Id PK
        string Name
        Guid UserId
        Guid CompanyId
    }

Notes โ€‹

  • Budget โ†’ Element is 1:1 โ€” the budget always has exactly one root element; the tree hangs below it.
  • Element is a self-referencing tree via ParentId/Parent/Children, with unbounded depth.
  • CostItems are the leaves that hold the actual cost numbers (labor, material, equipment, subcontractor + extra cost categories).
  • ExtraCostCategorySnapshot decouples a budget from later changes to the company's ExtraCostCategoryDefinitions โ€” values reference the snapshot, not the live definition.
  • Budget.ProjectId / Budget.LeadId are cross-service references (not enforced FKs).

Quotation (vulcan-be-quotation) โ€‹

The Quote is the aggregate root. There are two parallel structures inside a quote:

  1. Pricing WBS โ€” Activity โ†’ WorkPackage โ†’ CostLine (3 fixed levels).
  2. Document/presentation layer โ€” QuoteBlock โ†’ QuoteLineItem / QuotePackage โ†’ QuotePackageLineItem.
mermaid
erDiagram
    Quote ||--o{ Activity : "WBS level 1"
    Activity ||--o{ WorkPackage : "WBS level 2"
    WorkPackage ||--o{ CostLine : "WBS level 3"
    WorkPackage }o..|| WorkPackageTemplate : "from template"

    Quote ||--o{ QuoteVersion : "versions"
    Quote ||--o| QuoteVersion : "CurrentVersion"
    Quote ||--o{ QuoteBlock : "document layout"
    QuoteBlock ||--o{ QuoteLineItem : "line items"
    QuoteBlock ||--o{ QuotePackage : "packages (optional)"
    QuotePackage ||--o{ QuotePackageLineItem : "line items"

    Quote ||--o| SiteAssessment : "site visit (1:1)"
    SiteAssessment ||--o| FloorPlan : "floor plan"
    Quote ||--o{ QuoteMessage : "communication"
    Quote ||--o{ QuoteStatusHistory : "audit"
    Quote ||--o{ QuoteViewEvent : "views"
    Quote ||--o{ QuoteMergeFieldValue : "merge fields"
    CustomMergeField ||--o{ QuoteMergeFieldValue : "definition"
    Quote ||--o{ QuoteCustomerChange : "customer changes"

    Quote {
        Guid Id PK
        Guid CompanyId FK
        string QuoteNumber "QUO-YYYY-NNNN"
        string Title
        string Status "Draft|Sent|Accepted|..."
        Guid LeadId "cross-service"
        Guid CustomerId "cross-service"
        Guid ContractId "cross-service"
        Guid SourceBudgetId "from Calculations"
        Guid CurrentVersionId FK
        decimal SubTotal
        decimal TotalAmount
    }
    Activity {
        Guid Id PK
        Guid QuoteId FK
        string Code
        string Name
        int SortOrder
        decimal TotalCost
        decimal TotalPrice
    }
    WorkPackage {
        Guid Id PK
        Guid ActivityId FK
        Guid TemplateId FK "nullable"
        string Code "1.1.1"
        string Name
        string Unit
        decimal Quantity
        decimal UnitCost
        decimal UnitPrice
        decimal MarkupPercent
    }
    CostLine {
        Guid Id PK
        Guid WorkPackageId FK
        enum CostKind "Labor|Material|Equipment|Subcontractor"
        string Name
        string Unit
        decimal Quantity
        decimal UnitCost
        decimal TotalCost
    }
    QuoteVersion {
        Guid Id PK
        Guid QuoteId FK
        int VersionNumber
        string Data "JSON snapshot"
    }
    QuoteBlock {
        Guid Id PK
        Guid QuoteId FK
        string BlockType "Text|LineItems|Terms"
        int SortOrder
        string GroupingMode
    }
    QuoteLineItem {
        Guid Id PK
        Guid QuoteId FK
        Guid BlockId FK
        Guid SourceActivityId "WBS snapshot"
        Guid SourceWorkPackageId "WBS snapshot"
        Guid SourceCostLineId "WBS snapshot"
        string Description
        decimal Quantity
        decimal UnitPrice
        decimal VatRate
        bool ReverseChargeApplied
    }
    QuotePackage {
        Guid Id PK
        Guid QuoteId FK
        Guid BlockId FK
        string Name
        bool IsBase
    }
    QuotePackageLineItem {
        Guid Id PK
        Guid QuoteId FK
        Guid PackageId FK
        string Description
        decimal Quantity
        decimal UnitPrice
    }
    SiteAssessment {
        Guid Id PK
        Guid QuoteId FK
        Guid FloorPlanId FK
        DateTimeOffset VisitDate
        string Status
    }
    FloorPlan {
        Guid Id PK
        Guid SiteAssessmentId FK
        string Name
        string Data "JSON"
    }
    QuoteMessage {
        Guid Id PK
        Guid QuoteId FK
        string SenderType "Customer|Contractor"
        string Content
    }
    QuoteStatusHistory {
        Guid Id PK
        Guid QuoteId FK
        string FromStatus
        string ToStatus
        string ActorType
    }
    CustomMergeField {
        Guid Id PK
        Guid CompanyId FK
        string Key
        string FieldType
    }
    QuoteMergeFieldValue {
        Guid Id PK
        Guid QuoteId FK
        Guid CustomMergeFieldId FK
        string Value
    }

Notes โ€‹

  • QuoteLineItem snapshots the WBS via SourceActivityId / SourceWorkPackageId / SourceCostLineId so the printed quote stays stable even if the underlying WBS changes.
  • A Quote carries SourceBudgetId linking it back to the originating Calculations budget.
  • โ„น๏ธ The quote's WBS leaf is CostLine (its cost-kind enum is CostKind) โ€” the cost breakdown of a WorkPackage, matching the project WBS leaf name (renamed from Resource in VUL-931). It is distinct from the unified Resource registry (the physical person/machine/material) introduced in the Time Reporting PRD and consumed by the aligned Projects WBS (ยง Proposed direction); Resource now refers exclusively to that registry.
  • Not drawn for readability (standalone config/log entities): QuoteCanvasTemplate, QuoteCompanyBranding, TextTemplate, QuoteViewEvent, QuoteMessageEmailThrottle.

Work Breakdown Structure (WBS) โ€‹

A key nuance from the code: the hierarchical WBS does not live in vulcan-be-projects โ€” that service is deliberately flat. The real WBS is owned by Calculations and Quotation and flows down the pipeline.

Direction (signed off, June 2026). The flat Projects model below is the current implementation. A signed-off proposal aligns vulcan-be-projects to the same WBS shape โ€” Project โ†’ WorkPackage โ†’ CostLine โ€” moving the budget from Activity onto the WorkPackage and materialising labour CostLines from the time-reporting service's TimeEntry. See ยง Proposed direction below, plus WBS Alignment Proposal and Time Reporting PRD.

mermaid
flowchart TD
    subgraph CALC["Calculations โ€” Budget Tree (recursive)"]
        B[Budget] --> E1[Element root]
        E1 --> E2[Element]
        E2 --> E3[Element ...n deep]
        E2 --> CI1[CostItem]
        E3 --> CI2[CostItem]
    end

    subgraph QUOT["Quotation โ€” WBS, 3 fixed levels"]
        Q[Quote] --> A[Activity]
        A --> WP[WorkPackage]
        WP --> R[CostLine: Labor/Material/Equipment/Subcontractor]
    end

    subgraph PROJ["Projects โ€” FLAT, no nested WBS"]
        P[Project] --> PA[Activity: budget holder]
        P --> WO[WorkOrder]
        PA -.optional.-> WO
        WO --> TE[TimeEntry]
    end

    B -. "SourceBudgetId" .-> Q
    Q -. "ContractId then Project" .-> P

How the three models differ โ€‹

ServiceStructureDepthLeaf node
CalculationsBudget โ†’ Element โ†’ โ€ฆ โ†’ CostItemUnbounded (self-referencing Element.ParentId tree)CostItem
QuotationQuote โ†’ Activity โ†’ WorkPackage โ†’ CostLineFixed, 3 levelsCostLine
Projects (current)Project โ†’ Activity + Project โ†’ WorkOrder โ†’ TimeEntryFlat (no nesting; Activity holds the budget โ€” BudgetCost/ActualCost)TimeEntry
Projects (proposed)Project โ†’ WorkPackage โ†’ CostLine (Activity / WorkOrder optional parents)Fixed, 2 levels (budget on WorkPackage)CostLine

The pipeline โ€‹

  1. A Budget (unbounded-depth tree) is built in Calculations.
  2. It is transferred to a Quote, flattened into 3 levels via SourceActivityLevel / SourceWorkPackageLevel.
  3. On acceptance, the contract spawns a Project. Today that project is flat (activities + work orders, with hours logged as TimeEntry). The signed-off direction (ยง Proposed direction) gives the project its own Project โ†’ WorkPackage โ†’ CostLine WBS โ€” seeded from the quote or the calculation โ€” with labour CostLines materialising from TimeEntry.

Proposed direction โ€” aligned Projects WBS + Time Reporting โ€‹

Status: signed off June 2026. The sections above describe the current code. The companion documents define the target the Projects service is being aligned to โ€” see WBS Alignment Proposal (full ERDs, seeding, concept/realized) and Time Reporting PRD (Resource registry, rule engine, snapshots). This section is a summary; those two documents are canonical.

The Projects service adopts the same WBS shape as Quotation โ€” a WorkPackage spine with a flat CostLine leaf:

mermaid
flowchart TD
    P[Project] ==> WPK[WorkPackage]
    WPK -->|"cost lines (concept โ†’ realized)"| CL[CostLine: labour/material/equipment/subcontracting/service/other]
    ACT[Activity] -. "optional parent" .-> WPK
    WO[WorkOrder] -. "optional parent" .-> WPK
    WPS[WorkPackageSource] -. "estimate ref โ†’ calculation CostItem / quote line" .-> WPK
    CL -. "labour realized from" .-> TE[TimeEntry]
    CL -. "material/equipment/subco from" .-> POL[PurchaseOrderLine]

Key points:

  • WorkPackage is the spine and the budget holder. It hangs directly under Project (mandatory parent); Activity and WorkOrder become optional parents (WorkOrder stays a separate execution entity, outside the WBS tree). The budget moves off Activity โ€” now a pure grouping label โ€” onto the WorkPackage, which keeps the line-level link to the calculation through WorkPackageSource and rolls it up into CostPriceTotals / SellingPriceTotals.
  • CostLine is the flat leaf (D1:B). One wider entity with conditional fields by costKind (labour | material | equipment | subcontracting | service | other) โ€” no per-kind sub-line entities. A line is concept until date + employee are set, then realized.
  • Unified Resource registry (coresetup). The four per-kind references collapse into one Resource โ€” the physical thing (person / equipment / material / subcontractor) โ€” shared by planning, quotation, projects and time reporting. โš ๏ธ Distinct from Quotation's CostLine line-item entity (the WBS leaf โ€” same name as the project CostLine; renamed from Resource in VUL-931).
  • Labour CostLines materialise from TimeEntry (time-reporting service) on clock-out, carrying immutable snapshot rates (snapCostRate / snapMultiplier / snapSellingRate); PurchaseOrderLine backs material / equipment / subcontracting actuals. Comparing a WorkPackage's estimate totals against the sum of its realized CostLines gives estimate-vs-actual at the work-package level.

Built with VitePress | v1.2.0 | ๐Ÿš€ Week One Sprint