Skip to content

WBS Alignment Proposal ​

Alignment status (updated June 2026): This proposal has been reviewed against the parallel Time Reporting initiative (time-reporting-prd.md). Both efforts converge on the same project-WBS spine but require coordinated changes β€” primarily a flatter CostLine shape, a unified Resource reference, and an explicit bridge to TimeEntry. The substantive changes are noted inline; a dedicated Β§ Time-reporting alignment section below describes how labour CostLines materialise from clocked time. D1:B signed off (WBS author, June 2026): a single flat CostLine carries all type-specific fields conditional on costKind (no sub-lines) β€” see the D1:B note under Β§ Proposed alternative.

PO review (8 June 2026, Anton Hillestam). The proposal was reviewed against the Rise product track. Four decisions were confirmed β€” (1) the product-canonical name for the WBS node is Work Package; its cost-breakdown leaf line is named CostLine in this document (reserving Resource for the unified registry β€” see Β§ Open questions #1), mapping owned by Pedro; (2) phase is metadata on a work package, not a hierarchy level; (3) MVP seeding priority (see Β§ Quotation β†’ Project); (4) concept labour lines are supported. The per-question outcomes are folded into Β§ Open questions below. Epics: VUL-316 (WBS & Work Packages), VUL-732 (project setup / baseline).

This document compares the current Work Breakdown Structure (WBS) across the Calculations β†’ Quotation β†’ Projects pipeline with a proposed alternative that aligns the Projects service to the same WBS shape as Calculations and Quotation.

See also: Data Model & WBS.

Current state ​

The hierarchical WBS lives in Calculations and Quotation. The Projects service is deliberately flat β€” there is no nested WBS. Today Activity is the budget holder: the Activity entity carries BudgetHours / BudgetCost / ActualHours / ActualCost directly and groups a flat list of WorkOrders, with no line-level link back to the calculation it was budgeted from.

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[Resource: 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

Proposed alternative ​

The Projects column adopts the same WBS as Quotation, with the WorkPackage as the spine: a WorkPackage hangs directly under Project (mandatory parent) and breaks down into CostLines. Activity and WorkOrder become optional parents of a WorkPackage β€” extra grouping layers that can slot in where useful, without being required.

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 β€” same WBS as Quotation"]
        P[Project] ==> PWP[WorkPackage]
        PWP -->|"cost lines (concept β†’ realized)"| PR[CostLine: Labor/Material/Equipment/Subcontractor]
        PR -. "optionally detailed by" .-> TE[TimeEntry]
        PR -. "optionally detailed by" .-> POL[PurchaseOrderLine]
        PA[Activity] -. "optional parent" .-> PWP
        WO[WorkOrder] -. "optional parent" .-> PWP
    end

    B -. "SourceBudgetId" .-> Q
    Q -. "ContractId then Project" .-> P
    CI1 -. "estimate ref" .-> PWP
    R -. "estimate ref" .-> PWP

WorkPackage parentage ​

In the proposed model a WorkPackage has one mandatory parent and two optional ones. Note that WorkOrder is a separate execution entity, not a node inside the project WBS β€” it's an optional grouper for WorkPackages under a single execution unit.

ParentCardinalityMeaning
ProjectMandatory (1)Every work package belongs to exactly one project.
ActivityOptional (0..1)A work package may be grouped under an activity. In the proposed model Activity is a pure grouping label β€” it no longer holds the budget (see below).
WorkOrderOptional (0..1)A work package may be grouped under a work order for execution-level breakdown. WorkOrder lives outside the project WBS as its own entity.
mermaid
erDiagram
    Project ||--o{ WorkPackage : "mandatory parent"
    Activity ||--o{ WorkPackage : "optional parent"
    WorkOrder ||--o{ WorkPackage : "optional parent"
    WorkPackage ||--o{ WorkPackageSource : "estimate references"
    WorkPackage ||--o{ CostLine : "cost lines (concept/realized)"
    Project ||--o{ Activity : "groups"
    Project ||--o{ WorkOrder : "execution units"
    Resource ||--o{ CostLine : "physical thing used"
    TimeEntry ||--o{ CostLine : "realized labour bridge (D4)"
    Article ||--o{ CostLine : "billable line (labour)"
    SalaryType ||--o{ CostLine : "tier (labour)"

    WorkPackage {
        Guid Id PK
        Guid ProjectId FK "mandatory"
        Guid ActivityId FK "optional, nullable"
        Guid WorkOrderId FK "optional, nullable"
        string BudgetCode 
        string Name
        Guid InstallationId FK "optional, nullable"
        string WorkDescription
        Guid ChecklistId[] FK "optional, nullable"
        enum PriceDetermination "Fixed|Billable + flags(warranty,additional)"
        Guid SellingCondition FK "mandatory"
        Totals CostPriceTotals
        Totals SellingPriceTotals
    }
    WorkPackageSource {
        Guid Id PK
        Guid WorkPackageId FK
        Guid CalculationCostItemId FK "calculation, nullable"
        Guid QuotationCostLineId FK "quotation, nullable"
    }
    CostLine {
        Guid Id PK
        Guid WorkPackageId FK
        enum CostKind "labour|material|equipment|subcontracting|service|other"
        Guid ResourceId FK "β†’ Resource (type matches costKind) β€” replaces per-kind FKs"
        string Name
        decimal Quantity
        string UnitOfMeasurement
        Price CostPrice
        decimal MarkupPercentage
        enum PriceStrategy "gross|net|margin"
        Price SellingPrice
        decimal CostPriceAmount "pre-computed β€” keeps the common aggregate a single-table SUM"
        decimal SellingPriceAmount "pre-computed β€” keeps the common aggregate a single-table SUM"
        date Date "set when realized"
        Guid EmployeeId FK "set when realized (when costKind=labour, redundant with Resource)"
        Guid OriginCalcLineId FK "optional, links realized β†’ concept line"

        Guid TimeEntryId FK "labour only β€” backs to TimeEntry (D4)"
        Guid AppointedSubtypeId FK "labour only β€” ResourceSubtype actually worked"
        Guid SalaryTypeId FK "labour only β€” tier (NORMAL/OB1/OB2/OT)"
        Guid ArticleId FK "labour only β€” billable Article via Agreement"
        decimal SnapCostRate "labour only β€” snapshot at materialization (D3)"
        decimal SnapMultiplier "labour only β€” snapshot at materialization (D3)"
        decimal SnapSellingRate "labour only β€” snapshot at materialization (D3)"

        TradeRef TradeItem "material only"
        decimal ArticleIdRef "material only"
        PurchaseOrderRef[] PurchaseOrderReferences "material/equipment/subco"
        WarehouseIssueRef[] WarehouseIssueReferences "material only"
    }
    TimeEntry {
        Guid Id PK
        Guid ResourceId FK "β†’ Resource (labor)"
        Guid AppointedSubtypeId FK
        string EntityType "work_package|work_order|absence"
        Guid EntityId "polymorphic"
        timestamp StartTime
        timestamp EndTime
        int BreakMinutes
        enum Kind "work|absence:*"
        enum Status "draft|submitted|approved"
    }
    Resource {
        Guid Id PK
        Guid ResourceTypeId FK "LABOR|EQUIPMENT|MATERIAL|SUBCONTRACTOR|SERVICE|OTHER"
        Guid ResourceSubtypeId FK "Plumber, Excavator 14-18T, …"
        string Number
        string Name
        decimal HourlyCost "labour only, nullable"
        Guid RulesetId FK "labour only, nullable"
    }
    Price {
        decimal GrossPrice 
        decimal discountPercentage
        decimal NetPrice
    }
    Totals {
        decimal Labour
        decimal Hours
        decimal Materials
        decimal Subcontracting
        decimal Equipment
        decimal Service
        decimal Other
    }

D1:B β€” flat CostLine (WBS author sign-off, June 2026). A cost line is not split into type-specific sub-line entities; the model uses one wider CostLine with conditional fields by costKind. Material fields (tradeItem, purchaseOrderReferences, warehouseIssueReferences), equipment/subcontractor refs, and the labour substantiation (appointed subtype, salary tier, billable Article, the TimeEntry back-reference and the D3 snapshot rates) all live directly on CostLine, populated only when the matching costKind applies. Pre-computed CostPriceAmount / SellingPriceAmount keep the common aggregate query a single-table SUM. This is exactly the flat shape the time-reporting PRD specifies (D1); the VUL-918 engine materialises labour rows straight onto CostLine.

D1+D2 β€” unified Resource registry. All four type-specific references (employee, equipment, subcontractor, tradeItem) collapse into one Resource reference. Resource lives in coresetup as a shared registry β€” same row whether referenced from the project WBS, planning availability, quotation line items, or the time-reporting clock-in. Resource carries type (LABOR/EQUIPMENT/MATERIAL/SUBCONTRACTOR/SERVICE/OTHER) + subtype (Plumber/Excavator 14-18T/...). Labour-specific fields (hourly_cost, ruleset, competencies) are nullable on Resource β€” populated only when type=LABOR.

Budget ownership: from Activity to WorkPackage ​

This is the core structural shift, and it changes where the budget lives.

In the current Projects service the budget sits on Activity. The Activity entity carries BudgetHours, BudgetCost, ActualHours and ActualCost directly, and WorkOrders hang underneath it β€” but nothing links those figures back to the calculation line items they were derived from. The budget is a number typed onto a grouping node.

In the proposed model the budget moves down to the WorkPackage, because that is where the link to the originating calculation is kept:

  • A WorkPackage collects the relevant calculation CostItems (and/or quote CostLines) through WorkPackageSource, and rolls them up into its CostPriceTotals / SellingPriceTotals. That roll-up is the work package's budget β€” and it stays traceable line-by-line back to the calculation, instead of being a standalone typed-in figure.
  • Activity is demoted to a pure grouping label β€” name, code, status, dates, sort order. It no longer owns BudgetCost / BudgetHours / ActualCost / ActualHours; any activity-level figure shown in the UI is a roll-up of its child work packages, not stored budget.
  • Actuals live on the work package's CostLines. Comparing CostPriceTotals (estimate) against the sum of realized CostLines gives estimate-vs-actual at the work-package level (see below), which the flat Activity.BudgetCost/ActualCost pair could only ever do per grouping.

In short: the budget is held where the calculation link is held β€” the WorkPackage β€” not on the grouping node.

Estimate vs. actuals ​

The project side separates the estimate (what was planned/sold) from the actuals (what was really spent):

  • WorkPackage carries the estimate. It holds a set of source references (WorkPackageSource) back to the originating CostItems of the calculation and/or CostLines of the quotation, and rolls those up into its CostPriceTotals / SellingPriceTotals. This is where the planned numbers live.
  • CostLines break the work down for execution. A CostLine is the leaf under a WorkPackage (per costKind: Labor / Material / Equipment / Subcontractor), with its own cost price, markup and selling price. a WorkPackage can be pre-filled with planned (concept) cost lines long before any cost is incurred, and a line becomes realized once the work is actually done (see Concept vs. realized cost lines).

TimeEntry and PurchaseOrderLine are optional detailings of a cost line, not a prerequisite for it β€” they back a realized line with its underlying registration:

DetailingBacksMeaning
TimeEntryLabor CostLinesActual hours logged against the cost line.
PurchaseOrderLineMaterial / Equipment / Subcontractor CostLinesActual procurement booked against the cost line.

Both are optional (0..many) β€” a cost line is valid without any detailing. Comparing a WorkPackage's estimate totals against the sum of its cost lines gives a direct estimate-vs-actual view.

Why align the models ​

  • Consistency β€” a near-identical leaf shape (WorkPackage β†’ CostLine on the quote, WorkPackage β†’ CostLine on the project) means budget/quote line items map cleanly onto project work, removing the flattening step that currently loses structure.
  • Traceability β€” cost lines estimated in the quote can be tracked directly against the project WBS, with TimeEntry and PurchaseOrderLine providing actuals-vs-estimate at the cost line leaf.
  • Flexible execution β€” keeping Activity and WorkOrder as optional parents of the WorkPackage preserves the operational/grouping layers while letting work packages sit directly under the project when no extra grouping is needed.

Translating the WBS across the pipeline ​

Work flows Calculation β†’ Quote β†’ Project, but the WBS is re-derived at every step rather than copied verbatim. Each service owns its own tree, and the Project may deliberately diverge from both the calculation and the quote.

The base mapping between levels is (the project renames the levels but keeps the same shape):

CalculationQuoteProjectNotes
ElementActivityActivityGrouping level.
CostItemWorkPackageWorkPackageThe work package carrying quantity, cost price and selling price.
CostItem cost component (Labor / Material / Equipment / Subcontractor)CostLineCostLineThe cost breakdown of a cost item becomes the work package's cost lines.

The quotation service implements this leaf as its own Resource line item; In this document the WBS leaf is called CostLine β€” distinct from the unified Resource registry it points at.

Calculation β†’ Quotation ​

Element β†’ Activity, CostItem β†’ WorkPackage. The catch: a budget is an unbounded Element tree, while the quote has a flat activity level. So the tree must be flattened to two levels before it maps.

mermaid
flowchart TD
    subgraph BUD["Calculation β€” Budget tree (unbounded depth)"]
        E0[Element root] --> E1[Element A]
        E0 --> E2[Element B]
        E1 --> CI1[CostItem a1]
        E1 --> CI2[CostItem a2]
        E2 --> CI3[CostItem b1]
    end
    subgraph QWBS["Quote β€” flattened to 2 levels"]
        A1[Activity A] --> W1[WorkPackage a1]
        A1 --> W2[WorkPackage a2]
        A2[Activity B] --> W3[WorkPackage b1]
    end
    E1 -->|"Element β†’ Activity"| A1
    E2 -->|"Element β†’ Activity"| A2
    CI1 -->|"CostItem β†’ WorkPackage"| W1
    CI2 --> W2
    CI3 --> W3

Flattening method. Walk the budget tree depth-first:

  1. Every CostItem becomes one WorkPackage (carrying its quantity, unit cost and unit price).
  2. The WorkPackage's parent Activity is derived by collapsing the owning element path. Which element becomes the activity is controlled by the existing depth markers Quote.SourceActivityLevel and Quote.SourceWorkPackageLevel:
    • Elements at the activity level become Activitys (intermediate ancestor codes/names joined into a path label so the activity stays uniquely identifiable).
    • Elements between the activity level and the cost items are folded into the WorkPackage.
  3. Each CostItem's non-zero cost components map to the work package's CostLines.

Provenance is preserved: Quote.SourceBudgetId points back to the budget, and each QuoteLineItem keeps SourceActivityId / SourceWorkPackageId / SourceResourceId.

Quotation β†’ Project (on approval) ​

When a quote is approved, the project gets its own WBS. It is not automatically the quote's WBS β€” the way work is broken down for execution is often completely different from how it was structured for sales. There are three ways to seed the project WBS:

mermaid
flowchart TD
    CALC["Calculation / Budget<br/>(Project.CalculationId)"]
    QUOTE["Approved Quote<br/>Activity β†’ WorkPackage"]
    PROJ["Project WBS<br/>Project β†’ WorkPackage β†’ CostLine"]

    CALC -->|"1. cost-kind-based<br/>(group by per cost-kind)"| PROJ
    CALC -->|"2. code-based<br/>(group by CostItem unique code / URC)"| PROJ
    QUOTE -->|"3. Quotation<br/>(use WBR of quote)"| PROJ
  1. From calculation, cost-kind-based β€” create a work package for every cost-kind used in the calculation. So a work package for Labour, materials, subcontracting etc.
  2. From calculation, code-based β€” structure by the unique code (URC) of the CostItems instead of the element tree. Cost items are grouped/aggregated by their Code regardless of where they sit in the element tree, giving a trade/resource-oriented breakdown (e.g. all items with code 21.* under one grouping). Also re-derivable via Project.CalculationId.
  3. From quote β€” Use the same WBS as the quotation Activity β†’ WorkPackage structure (the project keeps the same two levels: Activity β†’ WorkPackage).
StrategySourceDriven byWhen to use
Cost-kind-basedCalculationCost-kindA work package per cost-kind
Code-basedCalculationCostItem.Code (URC)Trade/resource-oriented breakdown, ignoring element nesting.
QuotationQuoteQuote Activity/WorkPackageExecution mirrors the estimate structure.

PO decision (8 June 2026) β€” MVP seeding priority. For MVP the Quotation strategy is the default whenever a linked quote exists; cost-kind-based is the automatic fallback when there is no linked quote (manual project creation). Code-based (URC) seeding is deferred to phase 2 β€” not built or exposed in the UI for MVP. Seeded work packages carry a SourceQuoteResourceId back-reference, cannot be deleted, and cannot have their sellingPriceTotals changed.

In all three cases the resulting Project WBS is independent and editable β€” once seeded, it evolves on its own (with WorkPackage directly under Project and optional Activity / WorkOrder parents as described above). Work packages can be added for Time&Material work in a project, or for additional work (deviations). Work packages seeded by this logic can only be edited in some ways. They cannot be deleted, changed in a way that would change the sellingPriceTotals.

Concept vs. realized cost lines ​

A cost line's date and employee are only filled in once the work has actually been done β€” together they signal that the line is realized.

When you edit the work packages and cost lines under a project or work order, the status of the work order decides how newly created lines are treated (and when a work package has no work order, the project's status is used instead):

  • "New" or "needs revisit" β€” new lines get no date or employee. You are pre-filling the work packages with suggestions or planned work, i.e. concept cost lines.
  • "In progress" β€” the mechanic is actively working on the order, so any cost line they enter automatically gets today's date and the mechanic assigned as employee.
  • "Done" β€” lines added now are assumed to register the work actually done, so the date and employee are filled in automatically as well.

When the work order is moved to "done" while some cost lines still have no date or employee, the mechanic is prompted to enter the actual quantities used for every such line; those lines are then stamped with the current date and the mechanic, with the option to adjust either value.

So a cost line without a date and employee is a concept line, and one with a date and employee is a realized line.

Realized cost lines ​

A realized cost line carries a reference to its underlying registration, depending on its type:

  • Material β€” a purchase-order line or a dispatch line.
  • Labour β€” an employee, an hourly rate, and an hour-registration line.
  • Equipment β€” an equipment id.
  • Subcontracting β€” a subcontractor, plus either a purchase order or an assigned "external employee".
  • Realized other lines do not have restrictions

Time-reporting alignment ​

This section describes how the project WBS integrates with the parallel Time Reporting initiative (time-reporting-prd.md). The two efforts share data β€” labour CostLines under a project's WorkPackage materialise from clocked time captured via the time-reporting service.

Two entry surfaces for labour CostLines (D7) ​

A labour CostLine can become realized through either of two flows:

Time-first (mobile clock-in flow):

  1. The worker (Resource of type LABOR) clocks in on the mobile app against a project + activity β†’ a TimeEntry is created in the time service with entity_type="work_package" and entity_id=<workPackage.id>
  2. The worker clocks out β†’ TimeEntry.status transitions to submitted
  3. The Rule Engine immediately materialises labour CostLines under the parent WorkPackage β€” one CostLine per SalaryType segment (NORMAL, OB1, OB2, OT) per Article (D5: write-through on clock-out)
  4. Materialised CostLines are editable until TimeEntry.status=approved (D5: PM weekly approval is the lock point)

Work-first (project execution UI flow):

  1. PM pre-fills WorkPackage with concept labour CostLines (planned hours per Article)
  2. Mechanic working through the WorkPackage edits a concept line β€” fills in date and employee (or these auto-fill based on parent WorkOrder.status)
  3. The line is now realized β€” but no corresponding TimeEntry is created (this flow bypasses the time service)

Both flows produce the same shape of realized labour CostLine. The time-first flow additionally creates a TimeEntry with a back-reference; the work-first flow leaves TimeEntry empty on the line.

mermaid
flowchart LR
    subgraph T["Time-first (mobile clock-in)"]
        TE["TimeEntry<br/>(time service)"] --> ENG["Rule Engine<br/>derivation"]
        ENG --> WL1["realized<br/>Labour CostLine(s)"]
        WL1 -.back-ref.-> TE
    end
    subgraph W["Work-first (project UI)"]
        CL["concept<br/>Labour CostLine"] --> WL2["realized<br/>Labour CostLine"]
    end
    WL1 --> WI["WorkPackage<br/>(under Project)"]
    WL2 --> WI

The engine produces multiple CostLines per shift (D3, D4) ​

A single 11-hour shift typically materialises as two or more labour CostLines β€” not one. The Rule Engine splits the shift along SalaryType boundaries (per the worker's assigned Ruleset), and each tier-segment becomes its own CostLine keyed to its billable Article:

HoursSalaryTypeArticle→ produces
8hNORMALElectrician hour (90-100)one CostLine, qty 8, articleRef = 90-100, salaryTypeRef = NORMAL
2hOB1Electrician OT (90-101)merged with the OB2 row β†’ one CostLine, qty 3 (2h OB1 + 1h OB2), articleRef = 90-101, salaryTypeRef = OB1/OB2
1hOB2Electrician OT (90-101)↑ merged into the OB1 row above (same Article)

The Article routing happens through the Agreement resolution chain (Project β†’ Contract β†’ Framework β†’ Company default). The number of CostLines materialised equals the number of distinct Articles the shift segments route to.

Snapshot fields on materialised CostLines (D3) ​

When the engine materialises a labour CostLine, it writes both the FK references AND a snapshot of the resolved values at that moment:

CostLine (labour, realized)
    resourceRef              β†’ Resource (the worker)
    appointedSubtypeRef      β†’ ResourceSubtype (role worked β€” may differ from default)
    salaryTypeRef            β†’ SalaryType (which tier)
    articleRef               β†’ Article (billable line)
    timeEntryRef             β†’ TimeEntry (back-reference for audit)
    snapCostRate             decimal β€” Resource.hourly_cost at time of materialisation
    snapMultiplier           decimal β€” SalaryType.multiplier at time of materialisation
    snapSellingRate          decimal β€” Article rate (via Agreement) at time of materialisation
    quantity                 decimal β€” hours in this tier-segment
    costPriceAmount          (derived) β€” qty Γ— snapCostRate Γ— snapMultiplier
    sellingPriceAmount       (derived) β€” qty Γ— snapSellingRate

The engine never silently rewrites historical CostLines. A change to Resource.hourly_cost, Article.default_billable_rate, or Agreement.billing_rules only affects CostLines materialised after the change. To re-derive historical lines, an admin must explicitly trigger "Recompute since date X" β€” confirmation-gated. This prevents a typo correction on an article rate from retroactively changing last quarter's invoiced revenue.

Fixed-price WorkPackages (D11) ​

When WorkPackage.priceDetermination = Fixed, the customer invoice is driven by the WorkPackage's fixed sellingPriceTotals, not by individual CostLine selling prices. By default the engine still produces labour billable rows for fixed-price work β€” they are informational, used for margin tracking and "have we burned through the fixed price?" visibility.

A company-level toggle (derive_billable_on_fixed, default ON) controls this behaviour. Companies that don't care about labour margin visibility on fixed-price work can disable it to reduce engine load.

Naming alignment (D6) ​

The time service's TimeEntry.entity_type enum includes work_package to point at this WBS proposal's WorkPackage. (Open question #1 is now resolved: the WBS node is named Work Package and its leaf line CostLine. The time-service contract keys off a stable entity_type string, not the entity's display name.)

Open questions ​

Status legend. βœ“ Resolved Β· β—‘ Direction agreed, detail open Β· β—‹ Open. Outcomes from the PO review of 8 June 2026 (Anton Hillestam) are folded in inline, with owners and cross-references where given.

  1. βœ“ Naming alignment across contexts. Resolved (D6 + PO decision 1, refined): the PO confirmed the product-canonical name Work Package for the WBS node. To avoid a clash between the WBS leaf line and the unified Resource registry (the physical thing β€” person / equipment / material / subcontractor), this document names the WBS leaf line CostLine (formerly WorkLine), reserving Resource exclusively for the registry. So: WorkPackage = WBS node, CostLine = its cost-breakdown leaf, Resource = the registry entity the leaf points at.
  2. βœ“ Checklist structure on a work package. Resolved (PO): a work package supports multiple checklists β€” the ChecklistId[] array is the confirmed model. Sub-questions still open before checklist stories are written: how work-package checklists relate to the project-level Control Plan (kontrollplan.md / VUL-733) β€” same engine or separate β€” and whether the Form.io / TBR checklist engine serves both. Owner: Ivo Huizinga (Checklist Engine phase).
  3. β—‹ Cost typing at cost-line level. Only CostLine.costKind is modelled. The full chain cost group β†’ cost kind β†’ general-ledger account, plus the import flow from the accounting integration, is unresolved. Open: how a cost line maps to a GL account in Exact Online (NL) and Fortnox (SE), and whether that mapping is configurable per company or fixed per cost kind. Owner: Pedro (VIL / accounting layer) + Niels (Exact Online). Affects VUL-734 (material & cost reporting) and the Exact Online integration.
  4. β—‘ Change-order ("additional work") handling. The additional price-determination type exists, but the contractual process is not yet designed. Agreed direction (PO): distinguish an internal deviation (affects actualCost only, never customer-visible) from an external change order (affects contractValue + wbsBudgetCost, requires customer approval). project-schema-gaps.md proposes adding visibility, priceImpact and contractAmendmentId fields β€” not yet in Jira. Owner: Anton + Mladjan; relates to VUL-321 (deviation management) and VUL-365 (convert to change order). Needs a schema design session with Pedro.
  5. β—‹ Country-specific checklist regulations. Regulatory checklist requirements per country (NL vs. SE β€” fire safety, excavation, asbestos, …) are not inventoried. Open: which checklist types are legally required in NL for our ICP (residential trade contractors), and whether the Form.io / TBR database covers them or a separate regulatory library is needed. Owner: Niels Poort (NL) + Ivo; input from NL pilot customers during Phase 2 discovery.
  6. βœ“ Labour as concept lines. Resolved (D8 + PO decision 4): concept labour CostLines exist β€” a PM can plan "8h of Electrician work" before any clock-in. Realised lines later materialise from TimeEntry and link via originCalcLine, enabling planned-vs-actual at the line level. Flagged to the time-reporting team (VUL-896) so the L1 model treats a concept labour line as distinct from a time entry.
  7. ○ Phased concept → realized for materials. When a material quantity is consumed in steps (e.g. 100 units over several weeks), the concept→realized mechanic is undefined. Two candidates: split the concept line (a new realized line per partial delivery; the concept line keeps the remainder) vs. status per period (one line, status/quantity updated incrementally). Owner: Kick / Enk (Work Order microservice owns material realization); affects VUL-734 and purchase-order reconciliation.
  8. β—‹ Payment-plan coupling. How a work package links to progress billing is unresolved. Two candidate models: fixed price with progress % (bill at a defined completion percentage) vs. milestones (billing tied to discrete events). The contract already captures payment milestones (VUL-525) but the link from those to work-package progress is not modelled. Owner: Anton + Mladjan (project-based invoicing); relates to VUL-318. NL residential billing often uses fixed milestones (deposit / midpoint / completion).
  9. β—‹ priceDetermination simplification (suggested by time-reporting alignment, D12). (Not reviewed in the 8 June PO session.) Optional refactor: replace the four-value enum (fixed | T&M | warranty | additional) with Fixed | Billable + orthogonal flags (isWarranty, isAdditional, tmCap?). Cleaner separation of "how is it priced" from "what kind of work is it". Adopt only if the refactor cost is low.

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