Pricing Engine
The pricing engine is the heart of SignQuote. It lives in a single isolated module,
src/pricing/engine.ts, and exposes one function:
export function computePricing(config: SignConfig, settings: Settings): PricingResultIt is pure, synchronous, and fully deterministic — no Date, no randomness, no I/O. The
same config (Table A — the job) and settings (Table B — the shop’s constants) always yield
the same PricingResult. That property is what lets the test suite pin the seeded
quotes to exact dollar figures (AC-8), and it is why every formula and constant below can be
reproduced by hand.
computePricing branches on config.productCategory:
return config.productCategory === 'channel_letter'
? priceChannelLetters(config, settings)
: priceDimensionalLetters(config, settings)The most important idea on this page: per-inch is the price; cost-plus is a check. For
channel letters the engine runs two paths. Path B (per-inch) produces fabPerInch — the
number the customer sees. Path A (cost-plus) runs only in the background to compute a
costSubtotal “floor”; its sole job is to drive the margin badge and the “cost-plus
reference” figure. Changing a material cost moves the badge, never the quoted price. This also
enforces the role boundary (AC-10): the salesperson sees prices and a badge,
never costs.
Channel letters
Inputs come from the ChannelLetterConfig: letterHeight (H), letterCount
(n), illuminationType, mountType, returnDepth, faceMaterial, ledProductId, plus the
install and ancillary flags.
1. Derived geometry (§4.2.1)
The demo has no DXF, so per-letter geometry is estimated from height H using shape factors back-derived from the taxonomy’s worked Example 1 (an 18” letter → 48” perimeter, 1.6 sqft bounding box, 3.2 ft stroke run):
const perimeterFt = (2.67 * H) / 12
const faceAreaSqft = (0.711 * H * H) / 144
const strokeLengthFt = (2.13 * H) / 12
const letterWidthIn = 0.711 * H // for raceway-length estimate
const estStrokeWidthIn = H / 5 // typical block letter [A9]| Factor | Value | Meaning |
|---|---|---|
| Perimeter | 2.67 × H / 12 | feeds coil + trim-cap material |
| Face area | 0.711 × H² / 144 | feeds face + backing material |
| Stroke length | 2.13 × H / 12 | feeds LED module count |
| Letter width | 0.711 × H | feeds raceway length |
| Est. stroke width | H / 5 | feeds the LED-rows bucket |
Documented limitation (not a bug). Real letters vary roughly 3× in material by shape (“I” vs “M”/“W”). These factors model an average letter. Because geometry feeds only the background cost-plus path, the displayed per-inch price is completely unaffected — see Assumptions A8/A9.
2. LED & power-supply derivation (§4.2.2)
The selected LED product (or the first product as a fallback) provides densityPerFt,
costPerModule, and wattsPerModule. The seeded default sloan_prism12 has
densityPerFt = 1.5, wattsPerModule = 1, costPerModule = 2.
let modPerFt = led.densityPerFt
if (config.returnDepth <= 3) modPerFt = Math.max(modPerFt, 4.0) // shallow-can rule
const rows =
estStrokeWidthIn <= 4 ? 1 :
estStrokeWidthIn <= 6 ? 2 :
estStrokeWidthIn <= 8 ? 3 : 4
const modsPerLetter = Math.ceil(strokeLengthFt * modPerFt) * rows
const jobModules = modsPerLetter * n
const totalWatts = jobModules * led.wattsPerModule
const usableWatts = c.ps_max_watts * c.ps_derate // 60 × 0.80 = 48
const psCount = usableWatts > 0 ? Math.ceil(totalWatts / usableWatts) : 0Two details worth stressing:
ps_derateis0.80, not0.9. This is a deliberate taxonomy decision (#1); see the inline note insrc/data/defaults.ts(“do NOT ‘fix’ to 0.9”). Usable wattage per supply isps_max_watts × ps_derate = 60 × 0.80 = 48 W.- The divisor is guarded. If an owner clears
ps_max_wattsorps_derateto 0 on the Settings screen,psCountis forced to0rather thanInfinity.
rows_by_stroke buckets: 1 (≤4”), 2 (>4–6”), 3 (>6–8”), 4 (>8”). At storefront heights the
H/5 stroke estimate keeps most letters in the single-row bucket.
3. Path B — per-inch (the displayed price, §4.2.3)
This is the number the customer sees. Rate selection follows a strict precedence — halo beats raceway beats front-lit — then multiplies by total upright inches:
const rate =
config.illuminationType === 'halo_reverse' ? c.price_per_inch_halo // $22
: config.mountType === 'raceway' ? c.price_per_inch_raceway // $21
: c.price_per_inch_frontlit // $19
const fabPerInch = n * H * rate| Condition | Constant | Seeded rate ($/upright inch) |
|---|---|---|
illuminationType === 'halo_reverse' | price_per_inch_halo | $22 |
else if mountType === 'raceway' | price_per_inch_raceway | $21 |
| otherwise (front-lit) | price_per_inch_frontlit | $19 |
4. Path A — cost-plus (background only, §4.2.3)
Path A reconstructs an estimated cost “floor”. It never appears in the quote or PDF — it exists
to compute the margin badge and a reference price. The full decomposition is the
ClCostBreakdown and is rendered only on the Settings preview panel.
Per-letter materials. The face-material rate switches to colored acrylic only for front-lit colored faces; otherwise it uses the white-acrylic rate:
const facePerSqft =
config.illuminationType === 'front_lit' && config.faceMaterial === 'colored_acrylic'
? c.acrylic_colored_per_sqft // $2.19
: c.acrylic_white_per_sqft // $1.88
const matCoil = perimeterFt * c.coil_cost_per_ft // $2.00 / ft
const matFace = faceAreaSqft * facePerSqft
const matBack = faceAreaSqft * c.acm_backing_per_sqft // $1.31 / sqft
const matTrim = perimeterFt * c.trim_cap_per_ft // $0.41 / ft
const matLed = modsPerLetter * led.costPerModule
const matMisc = c.wire_studs_per_letter // $5 flat [A11]
const perLetterMaterial = matCoil + matFace + matBack + matTrim + matLed + matMiscJob materials. Power supplies, raceway (only for raceway mount), then a flat +7% waste allowance:
const matPs = psCount * c.power_supply_cost // $42.95 each
// Raceway spans the letter run + 15% spacing allowance [A12]
const racewayLenFt = (letterWidthIn * n * 1.15) / 12
const matRaceway = config.mountType === 'raceway' ? racewayLenFt * c.raceway_cost_per_ft : 0 // $25/ft
const materialBase = perLetterMaterial * n + matPs + matRaceway
const materialTotal = materialBase * (1 + c.material_indeterminates_pct) // × 1.07Labor — task-rate model. Hours scale with a height-based sizeFactor clamped to [0.5, 2],
calibrated to reproduce both taxonomy labor anchors. Design is not in labor — it is a flat
ancillary fee (decision D1), which avoids double-counting:
const sizeFactor = clamp(H / 24, 0.5, 2.0)
const cncBendHours = 0.25 * sizeFactor * n
const assemblyHours = 0.75 * sizeFactor * n
const cncBendCost = cncBendHours * c.cnc_bend_rate // $65 / hr
const assemblyCost = assemblyHours * c.assembly_rate // $45 / hr
const burden = (cncBendCost + assemblyCost) * c.labor_burden_pct // × 0.15
const laborCost = cncBendCost + assemblyCost + burden
const costSubtotal = materialTotal + laborCost // the "floor"
const fabCostPlus = costSubtotal * config.markupMultiplier // reference priceHalo cost-plus is an approximation. Halo letters reuse the front-lit material model (there
are no sourced halo-specific material prices). This affects only the badge — the displayed
halo price always comes from price_per_inch_halo. See A14.
5. Margin badge
The badge is the reconciliation between the two paths. It compares the quoted per-inch price against the estimated cost floor:
const marginPct = fabPerInch > 0 ? (fabPerInch - costSubtotal) / fabPerInch : 0
const badge =
marginPct < c.margin_red_below ? 'red' : // < 0.30
marginPct < c.margin_amber_below ? 'amber' : // < 0.45
'green'| Badge | Condition | Seeded threshold |
|---|---|---|
| red | marginPct < margin_red_below | < 0.30 |
| amber | marginPct < margin_amber_below | < 0.45 |
| green | otherwise | ≥ 0.45 |
The thresholds are configurable constants. A red badge carries the warning text: “Below margin floor — this price approaches material + labor cost.” (Forced red is exercised by AC-5.)
6. Install heuristic (§4.2.3)
Install hours scale with letter count and are scaled by access and surface multipliers, then
floored by a minimum fee, with a crane fee added when access is crane:
const installHours =
(2 + 0.5 * n) * ACCESS_MULT[config.installHeightAccess] * SURFACE_MULT[config.wallSurface]
const install =
Math.max(c.install_min_fee, installHours * c.install_rate) +
(config.installHeightAccess === 'crane' ? c.crane_fee : 0)installHeightAccess | Multiplier |
|---|---|
ground_ladder | 1.0 |
bucket | 1.25 |
lift | 1.5 |
crane | 2.0 |
Seeded fees: install_rate $100/hr, install_min_fee $175, crane_fee $2,500.
7. Ancillary fees & TOTAL
Permit and engineering lines appear only when their flags are set; design auto-applies to channel letters (decision D1):
const ancillary =
(config.permitRequired ? c.permit_fee : 0) + // $300
(config.engineeringRequired ? c.pe_stamp_fee : 0) + // $600 (PE stamp)
c.design_fee // $150, CL only
const total = subtotal + ancillaryThe TOTAL is composed as fabPerInch + install + ancillary (with the minimum-job adjustment
described below applied to fabPerInch + install first).
Dimensional letters (§4.2.4)
Dimensional letters are resold, not fabricated, so there is no cost-plus path and no margin
badge — the dealer markup is explicit, so there is no hidden floor to warn about. Inputs come
from DimensionalLetterConfig: dimMaterial, dimFinish, dimMount, plus
height/count.
Grid interpolation. Each material has a price grid with 6/12/18/24” anchors. Height is
clamped to [6, 24] (and heightClamped is flagged), then lerpGrid linearly interpolates
between the bracketing anchors:
const hClamped = clamp(config.letterHeight, 6, 24)
const heightClamped = hClamped !== config.letterHeight
const gridBasePerLetter = lerpGrid(grid.prices, hClamped)Finish & mount multipliers, then markup:
const finishMult =
config.dimFinish === 'painted_custom_color' ? c.finish_mult_painted_custom // 1.125
: config.dimFinish === 'polished' ? c.finish_mult_polished // 1.5
: 1.0
const mountMult = config.dimMount === 'rail' ? c.mount_mult_rail : 1.0 // rail = 1.35
const basePerLetter = gridBasePerLetter * finishMult * mountMult
const wholesale = basePerLetter * config.letterCount
const supplied = wholesale * config.markupMultiplier // default dealer_markup_dim = 1.5dimFinish | Multiplier | dimMount | Multiplier |
|---|---|---|---|
standard_included | 1.0 | flush_stud | 1.0 |
painted_custom_color | 1.125 | spacer_standoff | 1.0 |
polished | 1.5 | rail | 1.35 |
Install uses a lower hours base and adds the taxonomy’s “2× wholesale” cap before the minimum-fee floor:
const installHours =
(1 + 0.25 * config.letterCount) * ACCESS_MULT[config.installHeightAccess] * SURFACE_MULT[config.wallSurface]
const uncapped = installHours * c.install_rate
const capped = Math.min(2 * wholesale, uncapped) // cap rule
const install = Math.max(c.install_min_fee, capped) +
(config.installHeightAccess === 'crane' ? c.crane_fee : 0)There is no design fee for dimensional letters — patterns come with the resold letters
(D1). TOTAL = supplied + install + ancillary, where ancillary is permit + engineering only.
Rounding & minimums (§4.2.5)
- Precision. The engine computes in full float precision. The UI/PDF render line items to 2 decimals and the final TOTAL to whole dollars (A18).
- Minimum job charge.
minimum_job_priceis seeded0(disabled) — decision D2, to preserve the Example 2 anchor. When an owner sets it above 0 andfab/supplied + install < minimum_job_price, the engine pushes the subtotal up to the minimum and adds a visible “Minimum job charge” line for the difference:
let subtotal = fabPerInch + install
if (c.minimum_job_price > 0 && subtotal < c.minimum_job_price) {
lineItems.push({ key: 'minimum', label: 'Minimum job charge', amount: c.minimum_job_price - subtotal })
subtotal = c.minimum_job_price
}Worked example — seeded CAFE quote #1001
This is the AC-1 fixture: 4 front-lit letters, 18” tall, white 3/16” acrylic,
sloan_prism12 LEDs, flush-stud mount, brick wall, bucket access, return depth 5, markup ×2,
permit required.
Derived geometry
perimeterFt = 2.67×18/12 = 4.005 · faceAreaSqft = 0.711×18²/144 = 1.5998 ·
strokeLengthFt = 2.13×18/12 = 3.195 · estStrokeWidthIn = 18/5 = 3.6 → rows = 1.
LEDs & power
modPerFt = 1.5 (return depth 5 > 3, no shallow rule). modsPerLetter = ceil(3.195 × 1.5) × 1 = ceil(4.7925) = 5. jobModules = 5 × 4 = 20. totalWatts = 20 × 1 = 20 W. psCount = ceil(20 / (60×0.80)) = ceil(20/48) = 1. → 5 modules/letter, 20 modules, 20 W, 1 power
supply.
Path B — displayed price
Front-lit, flush stud → rate $19. fabPerInch = 4 × 18 × 19 = $1,368.00.
Path A — cost floor (badge only)
Per-letter materials = coil 8.01 + face 3.0075 + backing 2.0957 + trim 1.6421 + LEDs 10 + misc 5
= $29.755. × 4 = 119.02; + PS 42.95 = 161.97; × 1.07 = $173.31 material total.
Labor: sizeFactor = clamp(18/24) = 0.75; CNC/bend 0.75 h × $65 = 48.75; assembly 2.25 h ×
$45 = 101.25; burden 15% = 22.50 → $172.50. costSubtotal = 173.31 + 172.50 = $345.81.
fabCostPlus = 345.81 × 2 = $691.62 (the cost-plus reference).
Margin badge
marginPct = (1368 − 345.81) / 1368 = 0.747 (74.7%) → green (≥ 0.45).
Install & ancillary
installHours = (2 + 0.5×4) × 1.25 (bucket) × 1.0 (brick) = 5.0; max(175, 5.0×100) = $500.00.
Permit $300.00; design (auto, CL) $150.00.
TOTAL
1368 + 500 + 300 + 150 = $2,318.
| Line | Amount |
|---|---|
| Fabrication — channel letters (per-inch) | $1,368.00 |
| Installation | $500.00 |
| Permit | $300.00 |
| Design | $150.00 |
| TOTAL | $2,318 |
| Margin badge | green, 74.7% |
Cost-plus reference (fabCostPlus) | $691.62 |
See Acceptance & Testing for the full pinned fixtures (AC-1 through AC-7), including the dimensional LAW OFFICE quote and the live-constants (AC-4) and forced-red-badge (AC-5) cases.