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).
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โElementis 1:1 โ the budget always has exactly one root element; the tree hangs below it.Elementis a self-referencing tree viaParentId/Parent/Children, with unbounded depth.CostItems are the leaves that hold the actual cost numbers (labor, material, equipment, subcontractor + extra cost categories).ExtraCostCategorySnapshotdecouples a budget from later changes to the company'sExtraCostCategoryDefinitions โ values reference the snapshot, not the live definition.Budget.ProjectId/Budget.LeadIdare cross-service references (not enforced FKs).
Quotation (vulcan-be-quotation) โ
The Quote is the aggregate root. There are two parallel structures inside a quote:
- Pricing WBS โ
Activity โ WorkPackage โ CostLine(3 fixed levels). - Document/presentation layer โ
QuoteBlock โ QuoteLineItem/QuotePackage โ QuotePackageLineItem.
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 โ
QuoteLineItemsnapshots the WBS viaSourceActivityId/SourceWorkPackageId/SourceCostLineIdso the printed quote stays stable even if the underlying WBS changes.- A
QuotecarriesSourceBudgetIdlinking it back to the originating Calculations budget. - โน๏ธ The quote's WBS leaf is
CostLine(its cost-kind enum isCostKind) โ the cost breakdown of aWorkPackage, matching the project WBS leaf name (renamed fromResourcein VUL-931). It is distinct from the unifiedResourceregistry (the physical person/machine/material) introduced in the Time Reporting PRD and consumed by the aligned Projects WBS (ยง Proposed direction);Resourcenow 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-projectsto the same WBS shape โProject โ WorkPackage โ CostLineโ moving the budget fromActivityonto theWorkPackageand materialising labourCostLines from the time-reporting service'sTimeEntry. See ยง Proposed direction below, plus WBS Alignment Proposal and Time Reporting PRD.
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" .-> PHow the three models differ โ
| Service | Structure | Depth | Leaf node |
|---|---|---|---|
| Calculations | Budget โ Element โ โฆ โ CostItem | Unbounded (self-referencing Element.ParentId tree) | CostItem |
| Quotation | Quote โ Activity โ WorkPackage โ CostLine | Fixed, 3 levels | CostLine |
| Projects (current) | Project โ Activity + Project โ WorkOrder โ TimeEntry | Flat (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 โ
- A Budget (unbounded-depth tree) is built in Calculations.
- It is transferred to a Quote, flattened into 3 levels via
SourceActivityLevel/SourceWorkPackageLevel. - 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 ownProject โ WorkPackage โ CostLineWBS โ seeded from the quote or the calculation โ with labourCostLines materialising fromTimeEntry.
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:
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:
WorkPackageis the spine and the budget holder. It hangs directly underProject(mandatory parent);ActivityandWorkOrderbecome optional parents (WorkOrderstays a separate execution entity, outside the WBS tree). The budget moves offActivityโ now a pure grouping label โ onto theWorkPackage, which keeps the line-level link to the calculation throughWorkPackageSourceand rolls it up intoCostPriceTotals/SellingPriceTotals.CostLineis the flat leaf (D1:B). One wider entity with conditional fields bycostKind(labour | material | equipment | subcontracting | service | other) โ no per-kind sub-line entities. A line is concept untildate+employeeare set, then realized.- Unified
Resourceregistry (coresetup). The four per-kind references collapse into oneResourceโ the physical thing (person / equipment / material / subcontractor) โ shared by planning, quotation, projects and time reporting. โ ๏ธ Distinct from Quotation'sCostLineline-item entity (the WBS leaf โ same name as the projectCostLine; renamed fromResourcein VUL-931). - Labour
CostLines materialise fromTimeEntry(time-reporting service) on clock-out, carrying immutable snapshot rates (snapCostRate/snapMultiplier/snapSellingRate);PurchaseOrderLinebacks material / equipment / subcontracting actuals. Comparing aWorkPackage's estimate totals against the sum of its realizedCostLines gives estimate-vs-actual at the work-package level.
