Roof material engine — formulas, API & integration
This document is for your development team and stakeholders who need to understand how measurements flow through the system, how quantities are derived, and how to integrate the API into a larger CRM, database, or quoting product.
logic/calculator.js) turns that into a structured materials object and
optional pricing. Reports (PDF, Word, Excel) are views of that same data—no
duplicate business logic in the exporters.
1. System architecture
Browser (index.html)
POST /api/calculate → JSON { materials, pricing, input }
POST /api/proposal → PDF stream
POST /api/proposal/docx → Word (.docx) file
POST /api/proposal/xlsx → Excel workbook (material order + pricing sheet)
server.js
→ validation (utils/validation.js)
→ calculateRoofEstimate (logic/calculator.js)
→ calculatePricing (logic/pricing.js)
→ PDF / Word / Excel generators (utils/pdf.js, word.js, excel.js)
2. Request body (what you send)
All POST endpoints use the same JSON shape (after validation):
{
"roof": {
"area_sqft": 2500,
"pitch": 6,
"complexity": "low" | "medium" | "high"
},
"lengths": {
"ridge_lf": 120,
"hip_lf": 80,
"valley_lf": 60,
"eave_lf": 140,
"rake_lf": 100
},
"product": {
"shingle_style": "3tab" | "architectural" | "designer",
"manufacturer": "GAF" | "Owens Corning" | ... | "Other",
"manufacturer_custom": "string (required context when manufacturer is Other)"
},
"pricing_margin_percent": 20
}
product and pricing_margin_percent are optional in the raw request; the server
normalizes defaults (architectural, manufacturer GAF, margin 20).
Shingle style and manufacturer appear on exports for ordering context—they do not change bundle math today
(you can later tie SKUs or waste rules to style in calculator.js).
3. API reference
| Method & path | Response | Purpose |
|---|---|---|
GET /api/health |
JSON | Load balancer / uptime check. |
POST /api/calculate |
JSON | Primary integration: materials + pricing + echoed normalized input. |
POST /api/proposal |
PDF binary | Tabular estimate PDF. |
POST /api/proposal/docx |
Word binary | Same data as PDF in editable Word format. |
POST /api/proposal/xlsx |
Excel binary | “Roofing material order” sheet + “Pricing” sheet (aligned to contractor order workflows). |
Error format (robust handling)
Failed requests return JSON (never a partial file) with a stable shape:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR" | "INVALID_JSON" | "NOT_FOUND" | "INTERNAL_ERROR",
"message": "Human-readable summary",
"details": ["Optional bullet list", "..."]
}
}
Binary export endpoints use the same error JSON on 4xx/5xx so clients can branch on Content-Type: application/json.
4. Core formulas (logic/calculator.js)
4.1 Pitch-adjusted surface area
Plan area is expanded by pitch so shingle coverage matches slope:
pitchMultiplier = sqrt((pitch² + 12²) / 12²)
surfaceArea = area_sqft × pitchMultiplier
Example: 2,500 sq ft plan at 6/12 → multiplier ≈ 1.118 → ~2,795 sq ft surface.
4.2 Waste factor
Base waste 10%, increased by linear features and capped at 25%:
waste = 0.10
+ (valley_lf / 100) × 0.05
+ (hip_lf / 100) × 0.03
+ (ridge_lf / 100) × 0.01
if complexity === "medium" → +0.02
if complexity === "high" → +0.04
waste = min(waste, 0.25)
This drives shingles and underlayment (both use adjusted area).
4.3 Shingles (squares & bundles)
adjustedArea = surfaceArea × (1 + waste)
rawSquares = adjustedArea / 100
squares = ceil(rawSquares) // contractor-style full squares
bundles = squares × 3 // CONSTANTS.bundlesPerSquare
4.4 Starter
lf = eave_lf + rake_lf
bundles = ceil(lf / starterCoverage) // 100 LF per bundle (constant)
4.5 Ridge / hip caps
lf = ridge_lf + hip_lf
bundles = ceil(lf / ridgeCapCoverage) // 20 LF per bundle (constant)
4.6 Underlayment
adjusted = surfaceArea × (1 + waste)
rolls = ceil(adjusted / underlaymentEffectiveCoverage) // 360 sq ft/roll effective
4.7 Drip edge
lf = eave_lf + rake_lf
pieces = ceil(lf / dripEdgeLength) // 10 LF sticks
4.8 Valley membrane
sqft = valley_lf × 3 // 3' width assumption
rolls = ceil(sqft / 200)
4.9 Nails
count = squares × nailsPerSquare // 320 per square (constant)
4.10 Vents
units = ceil(plan_area_sqft / 300)
4.11 Flashing (order estimate)
lf = eave_lf + rake_lf
rolls = ceil(lf / 75)
Flashing is a roll-count proxy for combined step/apron/roll goods—tune constants or replace with SKU logic as needed.
5. Constants (single source of truth)
Defined in export const CONSTANTS inside constants/estimation.js:
| Key | Value | Role |
|---|---|---|
bundlesPerSquare | 3 | Shingle bundles per square |
starterCoverage | 100 LF | Starter per bundle |
ridgeCapCoverage | 20 LF | Ridge/hip cap per bundle |
underlaymentEffectiveCoverage | 360 sq ft | After overlaps |
dripEdgeLength | 10 LF | Stick length |
nailsPerSquare | 320 | Nail estimate per square |
6. Pricing module (logic/pricing.js)
calculatePricing(materials, marginPercent) multiplies each line by placeholder unit costs in
MATERIAL_PRICES, sums a subtotal, applies margin, returns breakdown,
subtotal, marginAmount, total. Swap these numbers for database-
driven or API-driven supplier pricing without changing the calculator.
7. Integration checklist for your devs
- Validate user input client-side for UX; rely on server validation for correctness.
- Store the normalized
input+materialsJSON in your database for audit/reorders. - Replace
MATERIAL_PRICESwith CRM or supplier lookups (keep the calculator pure). - Map Excel columns to your internal SKU table—the workbook is structured for procurement.
- Extend formulas by editing only
calculator.js; exporters read the same output object.
8. Files map
| File | Responsibility |
|---|---|
server.js | Routes, validation pipeline, error handling |
utils/validation.js | Request schema + normalization |
logic/calculator.js | All quantity logic |
logic/pricing.js | Illustrative dollars |
utils/reportHelpers.js | Shared rows/labels for Word & Excel |
utils/pdf.js | PDF layout |
utils/word.js | Word (.docx) layout |
utils/excel.js | Roofing material order workbook |
public/index.html | UI + API consumer example |
public/guide.html | This documentation |