Cost Calculations
Overview
ForgeX uses a sophisticated cost rollup system that automatically calculates costs from individual items → scopes → bids with module-specific markups applied at each level.
See the Cost Rollup Diagram for a visual representation.
Calculation Flow
Each module calculates its own item costs with module-specific logic (cubic yards, soft costs, waste factors, etc.)
Sum all item costs by module within a scope, then apply scope multiplier
Sum all scope costs by module, apply module markups (WC, overhead, profit, GL pollution)
Calculate subtotal, apply bid-level overhead and profit
Item-Level Calculations
Concrete Items
// Cubic yards
sf = length * width
cubicYards = (length * width * depth) / 27
// Rebar
rebarPoundsPerSF = lookupRebarRate(slabRebarSize, slabRebarOC)
totalRebarPounds = sf * rebarPoundsPerSF
rebarPoundsWithWaste = totalRebarPounds * 1.10 // 10% waste
// Costs
concreteBaseCost = cubicYards * mixPricePerCY
concreteTaxAmount = taxExempt ? 0 : concreteBaseCost * concreteTaxRate
concreteCost = concreteBaseCost + concreteTaxAmount
rebarBaseCost = rebarPoundsWithWaste * rebarPricePerPound
rebarTaxAmount = taxExempt ? 0 : rebarBaseCost * rebarTaxRate
rebarCost = rebarBaseCost + rebarTaxAmount
totalCost = concreteCost + rebarCost
Labor Items
// Base cost
baseCost = quantity * hoursPerDay * days * ratePerHour
// Soft costs (payroll taxes)
ficaAmount = baseCost * 0.0765 // 7.65%
futaAmount = baseCost * 0.006 // 0.6%
sutaAmount = baseCost * 0.0175 // 1.75%
// Auto costs (vehicles)
autoDepreciation = baseCost * 0.03 // 3%
autoFuel = baseCost * 0.02 // 2%
autoInsurance = baseCost * 0.01 // 1%
totalCost = baseCost + fica + futa + suta + autoCosts
Equipment Items
// Base rental
baseRentalCost = quantity * duration * ratePerUnit
rentalTaxAmount = taxExempt ? 0 : baseRentalCost * rentalTaxRate
rentalCost = baseRentalCost + rentalTaxAmount
// Additional charges
fuelCharge = quantity * days * fuelChargePerDay
deliveryFee = onPrevious ? 0 : deliveryFee
trucksCost = truckCount * truckHaulCost
totalCost = rentalCost + fuelCharge + deliveryFee + trucksCost
Material Items
// Waste factor
adjustedQuantity = quantity * (1 + wastePercent/100)
// Cost
baseCost = adjustedQuantity * unitCost
taxAmount = taxExempt ? 0 : baseCost * taxRate
totalCost = baseCost + taxAmount
Subcontractor Items
// Waste factor
adjustedQuantity = quantity * (1 + wastePercent/100)
// Base cost
subtotal = adjustedQuantity * ratePerUnit
// Buffer markup
totalCost = subtotal * (1 + buffer/100)
View Subcontractor Endpoints →
Miscellaneous Items
totalCost = quantity * ratePerUnit
Scope-Level Rollup
// Sum all items by module
scope.concreteCost = Σ ConcreteItem.totalCost
scope.laborCost = Σ LaborItem.totalCost
scope.equipmentCost = Σ EquipmentItem.totalCost
// Material cost EXCLUDES items linked to concrete (prevents double-counting)
// Items with sourceConcreteItemId are already counted in concreteCost
scope.materialCost = Σ MaterialItem.totalCost
WHERE sourceConcreteItemId IS NULL
scope.subcontractorCost = Σ SubcontractorItem.totalCost
scope.miscCost = Σ MiscItem.totalCost
// Apply scope multiplier
moduleTotals = concreteCost + laborCost + equipmentCost +
materialCost + subcontractorCost + miscCost
scope.totalCost = moduleTotals * multiplier
BM-32: Material items with sourceConcreteItemId are excluded from cost rollup to prevent double-counting concrete-generated materials.
Bid-Level Rollup
Step 1: Sum Scopes by Module
bid.concreteCost = Σ Scope.concreteCost
bid.laborCost = Σ Scope.laborCost
bid.equipmentCost = Σ Scope.equipmentCost
bid.materialCost = Σ Scope.materialCost
bid.subcontractorCost = Σ Scope.subcontractorCost
bid.miscCost = Σ Scope.miscCost
Step 2: Apply Module Markups
Each module applies:
- Workers Compensation (WC) - Percentage of hard cost
- Overhead - Percentage of hard cost
- Profit - Percentage of (hard cost + WC + overhead)
- GL Pollution - Percentage of (hard cost + WC + overhead + profit)
- Rental Insurance - Equipment only, percentage of base rental cost
function calculateModuleTotal(moduleKey, hardCost, bid) {
// Get module-specific rates from global variables
const wcPercent = getModuleVariable(moduleKey, 'wc_percentage')
const overheadPercent = getModuleVariable(moduleKey, 'overhead_percentage')
const profitPercent = getModuleVariable(moduleKey, 'profit_percentage')
const glPollutionPercent = getModuleVariable(moduleKey, 'gl_pollution_percentage')
// Calculate markups
const wcAmount = hardCost * wcPercent
const overheadAmount = hardCost * overheadPercent
const profitBase = hardCost + wcAmount + overheadAmount
const profitAmount = profitBase * profitPercent
const glPollutionBase = profitBase + profitAmount
const glPollutionAmount = glPollutionBase * glPollutionPercent
// Equipment only: add rental insurance
let rentalInsuranceAmount = 0
if (moduleKey === 'equipment') {
const rentalInsurancePercent = getVariable('rental_insurance')
rentalInsuranceAmount = baseRentalCost * rentalInsurancePercent
}
return hardCost + wcAmount + overheadAmount + profitAmount +
glPollutionAmount + rentalInsuranceAmount
}
// Apply to each module
const concreteWithMarkups = calculateModuleTotal('concrete', bid.concreteCost, bid)
const laborWithMarkups = calculateModuleTotal('labor', bid.laborCost, bid)
const equipmentWithMarkups = calculateModuleTotal('equipment', bid.equipmentCost, bid)
const materialWithMarkups = calculateModuleTotal('material', bid.materialCost, bid)
const subcontractorWithMarkups = calculateModuleTotal('subcontractor', bid.subcontractorCost, bid)
const miscWithMarkups = calculateModuleTotal('misc', bid.miscCost, bid)
Step 3: Calculate Subtotal
bid.subtotalCost = concreteWithMarkups + laborWithMarkups +
equipmentWithMarkups + materialWithMarkups +
subcontractorWithMarkups + miscWithMarkups
Step 4: Apply Bid-Level Overhead and Profit
// Overhead
bid.overheadAmount = subtotalCost * bid.overheadPercentage
// Profit (applied to subtotal + overhead)
profitBase = subtotalCost + overheadAmount
bid.profitAmount = profitBase * bid.profitPercentage
// Total
bid.totalCost = subtotalCost + overheadAmount + profitAmount
Example Calculation
Bid with Single Scope
Items:
- Concrete: $10,000
- Labor: $5,000
- Equipment: $2,000
Scope Multiplier: 1.0 (no scaling)
Module Markups:
- WC: 12%
- Overhead: 10%
- Profit: 15%
- GL Pollution: 2%
Bid-Level:
- Overhead: 10%
- Profit: 15%
Step-by-Step
// 1. Scope totals (no multiplier)
scope.concreteCost = $10,000
scope.laborCost = $5,000
scope.equipmentCost = $2,000
scope.totalCost = $17,000
// 2. Bid module totals (sum of scopes)
bid.concreteCost = $10,000
bid.laborCost = $5,000
bid.equipmentCost = $2,000
// 3. Module markups (example for concrete)
concreteWC = $10,000 * 0.12 = $1,200
concreteOverhead = $10,000 * 0.10 = $1,000
concreteBase = $10,000 + $1,200 + $1,000 = $12,200
concreteProfit = $12,200 * 0.15 = $1,830
concreteGLBase = $12,200 + $1,830 = $14,030
concreteGL = $14,030 * 0.02 = $280.60
concreteTotal = $14,310.60
// (Repeat for labor and equipment)
// 4. Subtotal
bid.subtotalCost = $14,310.60 + $7,155.30 + $2,862.12 = $24,328.02
// 5. Bid overhead
bid.overheadAmount = $24,328.02 * 0.10 = $2,432.80
// 6. Bid profit
profitBase = $24,328.02 + $2,432.80 = $26,760.82
bid.profitAmount = $26,760.82 * 0.15 = $4,014.12
// 7. Final total
bid.totalCost = $24,328.02 + $2,432.80 + $4,014.12 = $30,774.94
Automatic Recalculation
Cost rollup is automatically triggered when:
- ✅ Item created/updated/deleted
- ✅ Scope created/updated/deleted (affects multiplier)
- ✅ Bid variables updated (overhead, profit)
- ✅ Pricing items updated (affects item costs)
- ✅ Global variables updated (affects markups)
API Endpoints
Trigger Manual Recalculation
# Recalculate single scope
POST /api/costs/scope/:scopeId
# Recalculate scope and propagate to bid
POST /api/costs/scope/:scopeId/propagate
# Recalculate entire bid (all scopes)
POST /api/costs/bid/:bidId/full
# Recalculate bid totals only (no scope changes)
POST /api/costs/bid/:bidId
Manual recalculation is rarely needed - the system auto-calculates on all relevant changes.
Performance
- Async Processing: Cost rollup runs asynchronously to avoid blocking API responses
- Batch Updates: Multiple item changes trigger single recalculation
- Caching: Scope and bid totals cached in database fields
- Incremental Updates: Only affected scopes/bids recalculated