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.
All /api/admin/service-definitions endpoints require ADMIN role. Read-only endpoints also allow PM and ESTIMATOR.
Service Definitions
List Service Definitions
- Request
- Response
GET /api/admin/service-definitions
Cookie: sAccessToken=...; sRefreshToken=...
[
{
"id": "uuid",
"name": "Joint Saw - Green",
"label": "Joint Sawing (Green)",
"computeKey": "joint_saw_green",
"isActive": true,
"sortOrder": 0,
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z",
"_count": { "fields": 4 }
},
...
]
Roles: ADMIN, PM, ESTIMATOR
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
isActive | boolean | Filter by active status (true or false) |
Results are ordered by sortOrder ascending.
Get Service Definition
- Request
- Response
GET /api/admin/service-definitions/:id
Cookie: sAccessToken=...; sRefreshToken=...
{
"id": "uuid",
"name": "Joint Saw - Green",
"label": "Joint Sawing (Green)",
"computeKey": "joint_saw_green",
"isActive": true,
"sortOrder": 0,
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z",
"fields": [
{
"id": "uuid",
"key": "linearFeet",
"label": "Linear Feet",
"role": "input",
"fieldType": "number",
"unit": "LF",
"sortOrder": 0,
"isActive": true
},
{
"id": "uuid",
"key": "depthCategory",
"label": "Cut Depth",
"role": "input",
"fieldType": "select",
"options": [
{ "value": "THIN", "label": "0.125\" - 0.25\"" },
{ "value": "THICK", "label": "0.26\" - 2\"" }
],
"sortOrder": 1,
"isActive": true
}
]
}
Roles: ADMIN, PM, ESTIMATOR
Create Service Definition
- Request
- Response 201
POST /api/admin/service-definitions
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json
{
"name": "Hydro Excavation",
"label": "Hydro Excavation",
"computeKey": "hydro_excavation",
"sortOrder": 10
}
{
"id": "uuid",
"name": "Hydro Excavation",
"label": "Hydro Excavation",
"computeKey": "hydro_excavation",
"isActive": true,
"sortOrder": 10,
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}
Roles: ADMIN
Required Fields:
| Field | Type | Constraints |
|---|---|---|
name | string | Unique; matches service key used in subcontractor items |
label | string | Display label shown to estimators |
computeKey | string | Must be a valid key from the Compute Registry |
Optional Fields:
| Field | Type | Default | Description |
|---|---|---|---|
sortOrder | number | 0 | Display ordering |
Error Responses:
400— Missing required field409— Duplicatename400— InvalidcomputeKey(lists valid keys in error message)
Update Service Definition
- Request
- Response
PUT /api/admin/service-definitions/:id
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json
{
"label": "Hydro Excavation (LF/LS)",
"isActive": true,
"sortOrder": 5
}
{
"id": "uuid",
"name": "Hydro Excavation",
"label": "Hydro Excavation (LF/LS)",
"computeKey": "hydro_excavation",
"isActive": true,
"sortOrder": 5,
"updatedAt": "2025-01-16T10:00:00.000Z"
}
Roles: ADMIN
All fields are optional. Send only the fields you want to change.
Delete Service Definition
- Request
- Response (soft-delete)
DELETE /api/admin/service-definitions/:id
Cookie: sAccessToken=...; sRefreshToken=...
{
"id": "uuid",
"name": "Joint Saw - Green",
"isActive": false,
"warning": "3 active SubcontractorItem(s) reference service \"Joint Saw - Green\""
}
Roles: ADMIN
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
- Request
- Response
PUT /api/admin/service-definitions/bulk
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json
{
"ids": ["uuid-1", "uuid-2"],
"updates": {
"isActive": false
}
}
{
"message": "Successfully updated 2 service definitions",
"count": 2
}
Roles: ADMIN
Updatable Fields via Bulk: label, computeKey, isActive, sortOrder
Bulk Delete Service Definitions
- Request
- Response
DELETE /api/admin/service-definitions/bulk
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json
{
"ids": ["uuid-1", "uuid-2"]
}
{
"message": "Successfully soft-deleted 2 service definitions",
"count": 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
- Request
- Response
GET /api/admin/service-definitions/:id/fields
Cookie: sAccessToken=...; sRefreshToken=...
[
{
"id": "uuid",
"serviceDefinitionId": "uuid",
"key": "linearFeet",
"label": "Linear Feet",
"role": "input",
"fieldType": "number",
"defaultValue": null,
"unit": "LF",
"options": null,
"meta": null,
"min": 0,
"step": 1,
"sortOrder": 0,
"isActive": true
}
]
Roles: ADMIN, PM, ESTIMATOR
Create Service Field
- Request
- Response 201
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
}
{
"id": "uuid",
"serviceDefinitionId": "uuid",
"key": "addSlurry",
"label": "Add Slurry",
"role": "input",
"fieldType": "checkbox",
"defaultValue": null,
"unit": null,
"options": null,
"meta": null,
"min": null,
"step": null,
"sortOrder": 3,
"isActive": true
}
Roles: ADMIN
Required Fields:
| Field | Type | Valid Values |
|---|---|---|
key | string | Unique within the service definition |
label | string | Display label |
role | string | "input" or "rate" |
fieldType | string | "number", "select", "checkbox", "text" |
Optional Fields:
| Field | Type | Description |
|---|---|---|
defaultValue | string | Stored as string; parsed by fieldType |
unit | string | Display unit (e.g., "LF", "$/LF") |
options | array | For select type: [{"value": "KEY", "label": "Display"}] |
meta | object | visibleWhen rules, prefill hints |
min | number | Minimum value for number fields |
step | number | Step increment for number fields |
sortOrder | number | Display order within the service |
Update Service Field
- Request
- Response
PUT /api/admin/service-definitions/:id/fields/:fieldId
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json
{
"label": "Add Slurry ($200 flat)",
"sortOrder": 4
}
{
"id": "uuid",
"key": "addSlurry",
"label": "Add Slurry ($200 flat)",
"role": "input",
"fieldType": "checkbox",
"sortOrder": 4,
"isActive": true
}
Roles: ADMIN
Delete Service Field
- Request
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
- Request
- Response
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
}
}
{
"message": "Successfully updated 2 fields",
"count": 2
}
Roles: ADMIN
Bulk Delete Service Fields
- Request
- Response
DELETE /api/admin/service-definitions/:id/fields/bulk
Cookie: sAccessToken=...; sRefreshToken=...
Content-Type: application/json
{
"ids": ["field-uuid-1", "field-uuid-2"]
}
{
"message": "Successfully deleted 2 fields",
"count": 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
- Request
- Response
GET /api/admin/service-definitions/compute-keys
Cookie: sAccessToken=...; sRefreshToken=...
[
"simple",
"joint_saw_green",
"joint_saw_demo",
"place_and_finish",
"pumping",
"rodbusting",
"pier_drilling",
"lump_sum",
"hydro_excavation",
"extruded_curb",
"monolithic_curb"
]
Roles: ADMIN
Compute Key Reference
| Key | Service | Inputs | Rate Fields |
|---|---|---|---|
simple | Generic quantity × rate | First number-type input field | First rate field |
joint_saw_green | Green joint sawing | linearFeet, depthCategory (THIN/THICK), addElectricSurcharge, addSlurry, overrideMinimumCost, overrideBaseRate | (base rates hardcoded by depth) |
joint_saw_demo | Demo/removal sawing | linearFeet, cutDepth (SIX/SEVEN/EIGHT), addElectricSurcharge, overrideMinimumCost, overrideBaseRate | (base rates hardcoded by depth) |
place_and_finish | Place & finish labor | squareFeet, complexity | unitRate |
pumping | Concrete pumping | hours, volume, vendor, pump, overrideMinimumHours | hourRate, volumeRate, travelFee, minimumHours, fuelSurchargePercent |
rodbusting | Rebar installation | quantity, unitOfMeasure (LB/SQFT), wastePercent | rodRateLb, rodRateSqft |
pier_drilling | Drilled piers | unitType (EA/DAY/LS), pierCount, drillDays, lumpSumAmount | perPierRate, perDayRate |
lump_sum | Any lump sum | lumpSum | (none) |
hydro_excavation | Hydrovac excavation | unitType (LF/LS), linearFeet, lumpSumAmount | unitRate |
extruded_curb | Extruded curb | unitType (LF/DAY), quantity | ratePerLF, ratePerDay |
monolithic_curb | Monolithic curb | unitType (LF/DAY), quantity | ratePerLF, 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 ×perPierRateDAY— drill days ×perDayRateLS— 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:
| Action | Entity Type | Details |
|---|---|---|
| Create | SERVICE_DEFINITION | name, label, computeKey |
| Update | SERVICE_DEFINITION | Changed fields (delta) |
| Delete | SERVICE_DEFINITION | Full snapshot of deleted record |
| Bulk Update | SERVICE_DEFINITION | IDs, updated field names, count |
| Bulk Delete | SERVICE_DEFINITION | IDs, name list, count |
| Create Field | SERVICE_FIELD_DEF | key, role, fieldType, parent service |
| Update Field | SERVICE_FIELD_DEF | Updated field names |
| Delete Field | SERVICE_FIELD_DEF | Full snapshot |
Related Endpoints
- Subcontractor Items — estimator-facing subcontractor item CRUD that uses service definitions
- Admin Panel — user management and other admin operations
- Audit Log — view all logged changes