# Schema Reference — Construction / Field Service

Vertical schema for `businessType: "construction"` — building firms, contractors, and
field-service operators (the SiteDesk brand). This is the **canonical, opinionated** SiteDesk
service-delivery ontology, authored from JobLogic's proven field-service domain so that an
operator's job maps onto shipped labels without inventing the model. JobLogic's term is the
canonical label name for each concept.

This reference extends the base schema. All base node types (Person, Organization, Invoice,
PostalAddress, Event, etc.) remain valid. Load `schema-base.md` first.

When loading this reference, confirm: "Using schema-base + schema-construction".

Property fidelity: properties marked **(JobLogic-exact)** are taken from JobLogic's API field
model; properties marked **(authored)** are modelled from JobLogic's vocabulary plus the
field-service domain where the captured reference does not document the entity at field level.

---

## Top-level node types (operator-entry, natural key)

| Entity | Neo4j Label | Owner | Schema.org Type | Required Properties | Fidelity |
|--------|-------------|-------|-----------------|---------------------|----------|
| Job | `Job` |  | `schema:Project` | `accountId`, `jobId`, `status` | JobLogic-exact |
| Customer | `Customer` |  | `schema:Organization` | `accountId`, `customerId`, `name`, `customerType` | JobLogic-exact |
| Site | `Site` |  | `schema:Place` | `accountId`, `siteId`, `name`, `region` | JobLogic-exact |
| Asset | `Asset` |  | `schema:Product` | `accountId`, `assetId`, `name`, `assetType`, `assetClass`, `condition` | authored |
| Visit | `Visit` | `Job` | `schema:Event` (subtype) | `accountId`, `visitId`, `status`, `startDate`, `purpose` | authored |
| Quote | `Quote` | `Job` | `schema:Order` (estimate) | `accountId`, `ref`, `status`, `totalPaymentDue`, `date` | authored |
| Part | `Part` |  | `schema:Product` | `accountId`, `partId`, `name`, `sku`, `partCategory` | authored |
| Purchase Order | `PurchaseOrder` | `Job` | `schema:Order` | `accountId`, `poNumber`, `status`, `completionStatus`, `dateRaised` | JobLogic-exact |
| Supplier | `Supplier` |  | `schema:Organization` | `accountId`, `supplierId`, `accountNumber` | authored |
| PPM Contract | `PpmContract` |  | `schema:Service` (contract) | `accountId`, `contractId`, `name`, `startDate`, `endDate` | authored |
| WhatsApp Group | `WhatsAppGroup` |  | `cdm:Channel` | `accountId`, `groupId`, `jobId`, `clientName` | n/a (transport) |

`Owner` names the parent entity a label's records are contained by in the graph
ontology. A blank `Owner` is a root entity that owns its own top-level filesystem
bucket. A named `Owner` (here `Job`) is a job-owned entity: its records live under
the owning bucket's folder (`jobs/<jobId>/…`), not a sibling root bucket. The
account SCHEMA.md projection derives a root bucket only for blank-`Owner` labels.

`Supplier` appears in this table so the projection derives its `suppliers/` bucket,
which is where a received supplier invoice is filed. It remains an additional label on
`:Organization` as described below; the row adds a filesystem bucket, not a standalone
node type.

`Job.status` is one of JobLogic's job statuses: `New Job`, `Allocated`, `Attended`,
`Parts To Fit`, `Awaiting Parts`, `Completed`, `Costed`, `Reqs. Invoice`, `Invoiced`, `Paid`,
`Cancelled`, `Recall`. `Job.jobType` is the highest categorisation (Call out, Installation,
Maintenance, Out of Hours, Reactive, Non-Productive); `Job.jobCategory` is the finer trade
detail. `Site.region` is one of `East`, `North`, `South`, `West`. `PurchaseOrder.status` is one
of `InProgress`, `Sent`, `Cancelled`, `NeedsApproval`; `PurchaseOrder.completionStatus` one of
`NotApplicable`, `NotCompleted`, `PartiallyCompleted`, `FullyCompleted`. Never write a sentinel
value for an unknown date — omit the property until the real value is known.

Two distinct invoice documents. The received supplier invoice is `InboundInvoice` (keyed
`(accountId, supplier, confirmationNumber)`), now linked to the `PurchaseOrder` it pays — a
subcontractor company's received invoice uses the same model as any other supplier. The
customer-facing outbound bill is the base `Invoice` (`schema-base.md`), whose billing chain is
the child `InvoiceLine`, `InvoicePayment`, and `Credit`. The two labels carry different required
fields (supplier vs customer), so they stay distinct rather than one label with a direction flag.

## Child node types (write-time MERGE; reached via parent neighbourhood)

| Entity | Neo4j Label | Required Properties | Fidelity |
|--------|-------------|---------------------|----------|
| Quote line | `QuoteLine` | `accountId`, `jobId`, `trade`, `description`, `contractValue` | authored |
| Valuation | `Valuation` | `accountId`, `jobId`, `valuationNumber`, `date`, `totalValue` | authored |
| Milestone | `Milestone` | `accountId`, `jobId`, `name`, `completedDate` | authored |
| Variation | `VariationNote` | `accountId`, `jobId`, `variationNumber`, `description`, `totalPaymentDue`, `status` | authored |
| Job cost | `JobCost` | `accountId`, `costType` | JobLogic-exact |
| Contact | `Contact` | `accountId`, `contactId`, `name` | JobLogic-exact |
| PO line | `PurchaseOrderLine` | `accountId`, `poNumber`, `lineId`, `description`, `quantity`, `pricePerUnit` | JobLogic-exact |
| Job note | `Note` | `accountId`, `noteId`, `text`, `author`, `createdAt` | homegrown |

`JobCost.costType` is one of `labour`, `material`, `travel`, `mileage`, `expense`,
`subcontractor`, `scheduleOfRates` — the JobLogic cost-line taxonomy. A logged labour day (the
old check-in) is a `JobCost` with `costType:'labour'`. `JobCost.priceCalculationType` is `0`
(Non-Chargeable), `1` (Calculated Price), or `2` (Fixed Price), JobLogic's enum.

`QuoteLine` is one priced row of the `Quote` contract schedule — the line a `Valuation` measures
percent-complete against. It is **not** a `JobCost`: the quote line is the priced contract value,
the job cost is the incurred cost. Per-line percent-complete a valuation records is carried
valuation state, not a property of the line or the edge.

## Additional labels on a base node

`Engineer` is an additional label on the field engineer's `:Person` (`accountId`, `engineerId`,
`name`, `engagement` (employee/cis), `dayRatePence`, `showCisDeduction`, `invoiceEmail`); there
is no standalone `:Engineer`. `Supplier` is an additional label on a supplier's `:Organization`
(`accountId`, `supplierId`, `accountNumber`); there is no standalone `:Supplier`. Both are set
via `memory-update`, mirroring how the field worker was modelled before the JobLogic adoption.

---

## Idempotency contracts

MERGE keys per label:

- `:Job` — `(accountId, jobId)`
- `:Customer` — `(accountId, customerId)`
- `:Site` — `(accountId, siteId)`
- `:Asset` — `(accountId, assetId)`
- `:Visit` — `(accountId, visitId)`
- `:Quote` — `(accountId, ref)`
- `:Invoice` — `(accountId, confirmationNumber)` — the base outbound customer invoice (schema-base.md)
- `:Part` — `(accountId, partId)`
- `:PurchaseOrder` — `(accountId, poNumber)`
- `:PpmContract` — `(accountId, contractId)`
- `:WhatsAppGroup` — `(accountId, groupId)`
- `:QuoteLine` — `(accountId, jobId, trade, description)` — child of `:Quote`, write-time MERGE
- `:Valuation` — `(accountId, jobId, valuationNumber)`
- `:Milestone` — `(accountId, jobId, name)` — child of `:Job`, write-time MERGE
- `:VariationNote` — `(accountId, jobId, variationNumber)`
- `:JobCost` — `(accountId, costId)` — write-time MERGE; `jobId` is set when the cost is job-scoped, omitted for a worker-day labour roll-up
- `:Contact` — `(accountId, contactId)` — write-time MERGE
- `:PurchaseOrderLine` — `(accountId, poNumber, lineId)` — child of `:PurchaseOrder`
- `:Note` — `(accountId, noteId)` — child of `:Job` via `(:Note)-[:ABOUT]->(:Job)`, append-only CREATE (a fresh `noteId` per note, not a content MERGE), so a second note never overwrites the first
- `:Engineer` — `(accountId, personId)` — additional label on `:Person`, set via `memory-update`
- `:Supplier` — `(accountId, organizationId)` — additional label on `:Organization`

---

## Relationship Patterns

```
(:Job)-[:FOR_CUSTOMER]->(:Customer)
(:Customer)-[:HAS_SITE]->(:Site)
(:Job)-[:AT_SITE]->(:Site)
(:Site)-[:HAS_ASSET]->(:Asset)
(:Job)-[:HAS_VISIT]->(:Visit)
(:Visit)-[:ATTENDED_BY]->(:Engineer)
(:Job)-[:HAS_QUOTE]->(:Quote)
(:Quote)-[:HAS_LINE]->(:QuoteLine)
(:Job)-[:HAS_VALUATION]->(:Valuation)
(:Valuation)-[:VALUES]->(:QuoteLine)
(:Job)-[:HAS_VARIATION]->(:VariationNote)
(:Job)-[:HAS_MILESTONE]->(:Milestone)
(:Note)-[:ABOUT]->(:Job)
(:Job)-[:HAS_COST]->(:JobCost)
(:JobCost)-[:INCURRED_BY]->(:Engineer)
(:PurchaseOrder)-[:HAS_PO_LINE]->(:PurchaseOrderLine)
(:PurchaseOrderLine)-[:FOR_PART]->(:Part)
(:PurchaseOrder)-[:FROM_SUPPLIER]->(:Supplier)
(:InboundInvoice)-[:ON_PURCHASE_ORDER]->(:PurchaseOrder)
(:PpmContract)-[:FOR_CUSTOMER]->(:Customer)
(:PpmContract)-[:COVERS_SITE]->(:Site)
(:Contact)-[:CONTACT_FOR]->(:Customer)
(:WhatsAppGroup)-[:MAPPED_TO]->(:Job)
```

Every `:Job` is the hub for its visits, quotes, valuations, variations, milestones, notes, and costs,
satisfying the ≥1-adjacency write-gate doctrine. Job notes are the operator's free-text remarks
about a job, captured append-only by the `sitedesk-job` `note-capture` tool — one `:Note` per note,
never an overwritten scalar. Quote lines are reached from the job through
the quote — `(:Job)-[:HAS_QUOTE]->(:Quote)-[:HAS_LINE]->(:QuoteLine)` — and each valuation links
the lines it measures with `(:Valuation)-[:VALUES]->(:QuoteLine)`. A job links to its customer
and site; an asset hangs off the site it lives at. Costs link to the engineer who incurred them.
Procurement is a `:PurchaseOrder` of `:PurchaseOrderLine`s for `:Part`s from a `:Supplier`; the
received supplier invoice is an `:InboundInvoice` on that PO. A `:PpmContract` is a
planned-maintenance agreement for a customer/site, distinct from a reactive `:Job`.

---

## Filesystem ↔ graph (base template)

The graph is the canonical ontology; the operator's job folder is a conforming projection of it.
Every node corresponds to a filesystem artefact, and the edges follow the folder containment.
This mapping is the **base template** — the default a standard field-service job folder projects
into. It is shipped-generic; the folder names below are the common default, not any operator's
data. On any divergence, the graph is canonical.

A standard job folder, `{jobId} - {name}, {address} ({client})/`, projects as:

| Folder / artefact | Node | Edge from the job |
|---|---|---|
| the job folder itself | `:Job` | — (the hub); `(:Job)-[:FOR_CUSTOMER]->(:Customer)`, `(:Job)-[:AT_SITE]->(:Site)` |
| `Quotations/` — the client quote and its priced schedule | `:Quote`; its priced rows → `:QuoteLine` | `(:Job)-[:HAS_QUOTE]->(:Quote)-[:HAS_LINE]->(:QuoteLine)` |
| `Finances/Valuations/` — one interim valuation per cycle | `:Valuation`; each measures the priced lines | `(:Job)-[:HAS_VALUATION]->(:Valuation)-[:VALUES]->(:QuoteLine)` |
| the variations register | `:VariationNote` | `(:Job)-[:HAS_VARIATION]->(:VariationNote)` |
| `Costs/` — labour days, materials, travel, mileage, expenses, subcontractor | `:JobCost` (by `costType`) | `(:Job)-[:HAS_COST]->(:JobCost)-[:INCURRED_BY]->(:Engineer)` |
| `Visits/` — attendance records | `:Visit` | `(:Job)-[:HAS_VISIT]->(:Visit)-[:ATTENDED_BY]->(:Engineer)` |
| `Suppliers/<Supplier>/{Quotes,POs,Invoices}` | `:Organization:Supplier`; POs → `:PurchaseOrder`+`:PurchaseOrderLine`; received invoices → `:InboundInvoice` | `(:PurchaseOrder)-[:FROM_SUPPLIER]->(:Supplier)`, `(:InboundInvoice)-[:ON_PURCHASE_ORDER]->(:PurchaseOrder)` |
| `Assets/` — serviced plant on the site | `:Asset` | `(:Site)-[:HAS_ASSET]->(:Asset)` |
| `Drawings/`, `Health & Safety/`, `Emails/`, `Snagging/`, `Admin/` | ingested documents (`:KnowledgeDocument` + `:Section`) | linked to the `:Job` |

A contractor who keeps the whole job in one master workbook holds the same model in sheets
rather than folders: a cover/calculation sheet is the priced schedule (`:Quote` + `:QuoteLine`s),
one sheet per valuation cycle is a `:Valuation`, a variations sheet is the `:VariationNote`
register, a costs sheet is the `:JobCost` lines, and per-supplier sheets are the procurement
packages. Folder and workbook are two views of one job; both project into this same graph.

The `Suppliers/` row above is the per-job procurement package, held inside one job folder.
Separately, the account root carries a projected `suppliers/` bucket, one folder per
`Supplier` record, holding that supplier's artefacts across every job. A received supplier
invoice is filed at `suppliers/<supplier>/Invoices/`. It is never filed to a top-level
folder named after the `InboundInvoice` label; that label describes the node, not the
folder.

Operators vary this template — they rename folders, add trades, restructure their workbook. This
template is the shipped-generic default the agent projects against; the graph follows this one
canonical ontology, not a per-operator schema. Where a real folder does not fit the default, the
agent maps it to the nearest canonical node rather than inventing a node or edge type absent from
this reference.
