Skip to main content

Service Definitions

Overview

Service Definitions are admin-managed templates that define how subcontractor services are structured and calculated. Each definition maps a service (e.g., "Joint Saw - Green") to:

  • A compute key linking it to a cost-calculation function in the Compute Registry
  • A set of field definitions describing the inputs and rate fields estimators fill in
  • Display metadata (label, sort order, active status)

When estimators add subcontractor items to a scope, the service definition determines what fields appear and how the cost is computed.

info

All /api/admin/service-definitions endpoints require ADMIN role. Read-only endpoints also allow PM and ESTIMATOR.


Service Definitions

List Service Definitions

GET /api/admin/service-definitions
Cookie: sAccessToken=...; sRefreshToken=...

Roles: ADMIN, PM, ESTIMATOR

Query Parameters:

ParameterTypeDescription
isActivebooleanFilter by active status (true or false)

Results are ordered by sortOrder ascending.


Get Service Definition

GET /api/admin/service-definitions/:id
Cookie: sAccessToken=...; sRefreshToken=...

Roles: ADMIN, PM, ESTIMATOR


Create Service Definition

POST /api/admin/service-definitions
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"name": "Hydro Excavation",
"label": "Hydro Excavation",
"computeKey": "hydro_excavation",
"sortOrder": 10
}

Roles: ADMIN

Required Fields:

FieldTypeConstraints
namestringUnique; matches service key used in subcontractor items
labelstringDisplay label shown to estimators
computeKeystringMust be a valid key from the Compute Registry

Optional Fields:

FieldTypeDefaultDescription
sortOrdernumber0Display ordering

Error Responses:

  • 400 — Missing required field
  • 409 — Duplicate name
  • 400 — Invalid computeKey (lists valid keys in error message)

Update Service Definition

PUT /api/admin/service-definitions/:id
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"label": "Hydro Excavation (LF/LS)",
"isActive": true,
"sortOrder": 5
}

Roles: ADMIN

All fields are optional. Send only the fields you want to change.


Delete Service Definition

DELETE /api/admin/service-definitions/:id
Cookie: sAccessToken=...; sRefreshToken=...

Roles: ADMIN

warning

Delete is a soft delete — the record is marked isActive: false, not removed. Existing SubcontractorItem rows referencing the service by name continue to function. A warning field is included in the response if active references exist.


Bulk Update Service Definitions

PUT /api/admin/service-definitions/bulk
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"ids": ["uuid-1", "uuid-2"],
"updates": {
"isActive": false
}
}

Roles: ADMIN

Updatable Fields via Bulk: label, computeKey, isActive, sortOrder


Bulk Delete Service Definitions

DELETE /api/admin/service-definitions/bulk
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"ids": ["uuid-1", "uuid-2"]
}

Roles: ADMIN


Service Fields

Each service definition has a set of field definitions (ServiceFieldDef) that describe the form inputs estimators complete when adding a subcontractor item. Fields have two roles:

  • input — Quantities or toggles the estimator enters (e.g., linear feet, depth category)
  • rate — Lookup values sourced from the pricing database or default variables (e.g., unitRate)

List Service Fields

GET /api/admin/service-definitions/:id/fields
Cookie: sAccessToken=...; sRefreshToken=...

Roles: ADMIN, PM, ESTIMATOR


Create Service Field

POST /api/admin/service-definitions/:id/fields
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"key": "addSlurry",
"label": "Add Slurry",
"role": "input",
"fieldType": "checkbox",
"sortOrder": 3
}

Roles: ADMIN

Required Fields:

FieldTypeValid Values
keystringUnique within the service definition
labelstringDisplay label
rolestring"input" or "rate"
fieldTypestring"number", "select", "checkbox", "text"

Optional Fields:

FieldTypeDescription
defaultValuestringStored as string; parsed by fieldType
unitstringDisplay unit (e.g., "LF", "$/LF")
optionsarrayFor select type: [{"value": "KEY", "label": "Display"}]
metaobjectvisibleWhen rules, prefill hints
minnumberMinimum value for number fields
stepnumberStep increment for number fields
sortOrdernumberDisplay order within the service

Update Service Field

PUT /api/admin/service-definitions/:id/fields/:fieldId
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"label": "Add Slurry ($200 flat)",
"sortOrder": 4
}

Roles: ADMIN


Delete Service Field

DELETE /api/admin/service-definitions/:id/fields/:fieldId
Cookie: sAccessToken=...; sRefreshToken=...

Roles: ADMIN

Returns 204 No Content. This is a hard delete — field definition config data is removed permanently. Existing SubcontractorItem records are not affected.


Bulk Update Service Fields

PUT /api/admin/service-definitions/:id/fields/bulk
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"ids": ["field-uuid-1", "field-uuid-2"],
"updates": {
"isActive": false
}
}

Roles: ADMIN


Bulk Delete Service Fields

DELETE /api/admin/service-definitions/:id/fields/bulk
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json

{
"ids": ["field-uuid-1", "field-uuid-2"]
}

Roles: ADMIN

This is a hard delete — field definitions are permanently removed.


Compute Registry

The compute registry is a server-side map of computeKey → function. Every service definition must reference a valid key. The registered keys are stable identifiers; adding new compute logic requires a backend deployment.

List Compute Keys

GET /api/admin/service-definitions/compute-keys
Cookie: sAccessToken=...; sRefreshToken=...

Roles: ADMIN

Compute Key Reference

KeyServiceInputsRate Fields
simpleGeneric quantity × rateFirst number-type input fieldFirst rate field
joint_saw_greenGreen joint sawinglinearFeet, depthCategory (THIN/THICK), addElectricSurcharge, addSlurry, overrideMinimumCost, overrideBaseRate(base rates hardcoded by depth)
joint_saw_demoDemo/removal sawinglinearFeet, cutDepth (SIX/SEVEN/EIGHT), addElectricSurcharge, overrideMinimumCost, overrideBaseRate(base rates hardcoded by depth)
place_and_finishPlace & finish laborsquareFeet, complexityunitRate
pumpingConcrete pumpinghours, volume, vendor, pump, overrideMinimumHourshourRate, volumeRate, travelFee, minimumHours, fuelSurchargePercent
rodbustingRebar installationquantity, unitOfMeasure (LB/SQFT), wastePercentrodRateLb, rodRateSqft
pier_drillingDrilled piersunitType (EA/DAY/LS), pierCount, drillDays, lumpSumAmountperPierRate, perDayRate
lump_sumAny lump sumlumpSum(none)
hydro_excavationHydrovac excavationunitType (LF/LS), linearFeet, lumpSumAmountunitRate
extruded_curbExtruded curbunitType (LF/DAY), quantityratePerLF, ratePerDay
monolithic_curbMonolithic curbunitType (LF/DAY), quantityratePerLF, ratePerDay

Compute Result Shape

Every compute function returns the same shape:

interface ComputeResult {
quantity: number // Primary quantity used
unit: string // Unit of measure (LF, CY, SF, EA, LS, etc.)
ratePerUnit: number // Effective rate per unit
adjustedQuantity: number // Quantity after waste/minimum adjustments
wastePercent: number // Waste factor applied (0 if none)
hardCost: number // Base cost before surcharges
totalCost: number // Final cost (may include fuel surcharge for pumping)
breakdown: Array<{label: string, amount: number}> // Line-item detail
summary: string // Human-readable one-liner
details?: object // Service-specific extra data (pumping, sawing)
}

Special Cases

Sawing minimums: Both joint_saw_green and joint_saw_demo enforce a $550 minimum unless overrideMinimumCost is true. When the minimum triggers, a "Minimum Adjustment" line is added to breakdown.

Pumping fuel surcharge: pumping applies a fuel surcharge percentage (default 15%) on top of base cost. The surcharge is added to totalCost but not hardCost. This means the subcontractor module markup applies to hardCost only.

Pier drilling unit types:

  • EA — per pier count × perPierRate
  • DAY — drill days × perDayRate
  • LS — lump sum amount (rate fields ignored)

Data Models

ServiceDefinition

interface ServiceDefinition {
id: string // UUID
name: string // Unique service key (e.g., "Joint Saw - Green")
label: string // Display label (e.g., "Joint Sawing (Green)")
computeKey: string // Registry key for cost calculation
isActive: boolean // Soft-delete flag
sortOrder: number // Display ordering
createdAt: string
updatedAt: string

// Included in GET /:id
fields: ServiceFieldDef[]

// Included in list responses
_count?: { fields: number }
}

ServiceFieldDef

interface ServiceFieldDef {
id: string
serviceDefinitionId: string
key: string // Field key (e.g., "linearFeet") — unique per service
label: string // Display label
role: "input" | "rate"
fieldType: "number" | "select" | "checkbox" | "text"
defaultValue: string | null // Always stored as string; parsed by fieldType
unit: string | null // Display unit (e.g., "LF", "$/LF", "%")
options: Array<{value: string, label: string}> | null // For select fields
meta: object | null // visibleWhen rules, prefill hints
min: number | null // Minimum for number fields
step: number | null // Step increment for number fields
sortOrder: number
isActive: boolean
createdAt: string
updatedAt: string
}

Audit Logging

All write operations are logged to the audit trail:

ActionEntity TypeDetails
CreateSERVICE_DEFINITIONname, label, computeKey
UpdateSERVICE_DEFINITIONChanged fields (delta)
DeleteSERVICE_DEFINITIONFull snapshot of deleted record
Bulk UpdateSERVICE_DEFINITIONIDs, updated field names, count
Bulk DeleteSERVICE_DEFINITIONIDs, name list, count
Create FieldSERVICE_FIELD_DEFkey, role, fieldType, parent service
Update FieldSERVICE_FIELD_DEFUpdated field names
Delete FieldSERVICE_FIELD_DEFFull snapshot