Skip to main content
Glama
estimator.ts8.16 kB
import { ParsedRFP, CostEstimate, QuoteMatch, CostBreakdown } from './types'; import config from './config'; import { getMatchConfidence } from './matcher'; /** * Estimate cost and lead time for a parsed RFP */ export function estimateCostAndLeadTime( parsedRfp: ParsedRFP, matches: QuoteMatch[] ): CostEstimate { // Step 1: Calculate material cost const materialCost = calculateMaterialCost(parsedRfp); // Step 2: Calculate processing cost const processCost = calculateProcessingCost(parsedRfp); // Step 3: Calculate labor cost const laborCost = calculateLaborCost(parsedRfp); // Step 4: Calculate tooling amortization const toolingPerUnit = calculateToolingCost(parsedRfp); // Step 5: Calculate overhead const directCost = (materialCost + processCost + laborCost) / parsedRfp.qty; const overhead = directCost * config.overheadPct; // Step 6: Apply margin const unitCost = directCost + toolingPerUnit + overhead; const pricePerUnit = unitCost * (1 + config.marginPct); const totalPrice = pricePerUnit * parsedRfp.qty; // Step 7: Estimate lead time const leadDays = estimateLeadTime(parsedRfp, matches); // Step 8: Determine confidence const topScore = matches.length > 0 ? matches[0].score : 0; let confidence = getMatchConfidence(topScore); // Adjust confidence based on RFP completeness if (parsedRfp.confidence === 'low') { confidence = 'low'; } else if (parsedRfp.confidence === 'medium' && confidence === 'high') { confidence = 'medium'; } // Apply contingency for low confidence const contingencyPct = confidence === 'low' ? config.contingencyPct : 0; const finalPricePerUnit = pricePerUnit * (1 + contingencyPct); const finalTotalPrice = finalPricePerUnit * parsedRfp.qty; // Step 9: Calculate ranges (best/worst case) const variance = confidence === 'high' ? 0.10 : confidence === 'medium' ? 0.20 : 0.30; const breakdown: CostBreakdown = { materialCost, processCost, laborCost, toolingPerUnit, overhead, marginPct: config.marginPct, contingencyPct, }; return { pricePerUnit: Number(finalPricePerUnit.toFixed(2)), totalPrice: Number(finalTotalPrice.toFixed(2)), leadDays, confidence, breakdown, bestCase: { price: Number((finalTotalPrice * (1 - variance)).toFixed(2)), leadDays: Math.max(7, Math.round(leadDays * 0.8)), }, worstCase: { price: Number((finalTotalPrice * (1 + variance)).toFixed(2)), leadDays: Math.round(leadDays * 1.3), }, }; } /** * Calculate material cost */ function calculateMaterialCost(rfp: ParsedRFP): number { const material = rfp.material?.toLowerCase() || 'aluminum'; // Find matching material price let unitPrice = config.materials['aluminum']; // default for (const [mat, price] of Object.entries(config.materials)) { if (material.includes(mat.toLowerCase())) { unitPrice = price; break; } } // Estimate material usage (simplified - assumes unit price is per part equivalent) // In reality, you'd calculate based on part volume, scrap rate, etc. const materialPerPart = unitPrice; return materialPerPart * rfp.qty; } /** * Calculate processing cost */ function calculateProcessingCost(rfp: ParsedRFP): number { const processes = rfp.processes || []; let totalProcessCost = 0; for (const process of processes) { const processKey = Object.keys(config.processes).find( key => key.toLowerCase() === process.toLowerCase() ); if (processKey) { const value = config.processes[processKey]; // If value > 10, assume it's minutes per part, convert to $ per part const costPerPart = value > 10 ? (value / 60) * config.machineHourRate : value; totalProcessCost += costPerPart * rfp.qty; } } return totalProcessCost; } /** * Calculate labor cost */ function calculateLaborCost(rfp: ParsedRFP): number { // Simplified: assume 0.1 hours per part for setup/inspection // Plus additional for complex tolerances let hoursPerPart = 0.1; if (rfp.tolerances) { // Tighter tolerances = more inspection time if (rfp.tolerances.includes('0.001') || rfp.tolerances.includes('0.0001')) { hoursPerPart += 0.05; } } // Additional labor for finishing operations const finishingOps = ['anodize', 'powder coat', 'paint', 'polish', 'passivate']; const hasFinishing = (rfp.processes || []).some(p => finishingOps.some(f => p.toLowerCase().includes(f)) ); if (hasFinishing) { hoursPerPart += 0.05; } return hoursPerPart * rfp.qty * config.laborRate; } /** * Calculate tooling cost amortization */ function calculateToolingCost(rfp: ParsedRFP): number { // Tooling cost varies by quantity and complexity let toolingCost = 0; if (rfp.qty < 10) { toolingCost = 1000; // High setup for low volume } else if (rfp.qty < 50) { toolingCost = 750; } else if (rfp.qty < 100) { toolingCost = 500; } else if (rfp.qty < 500) { toolingCost = 300; } else { toolingCost = 200; } // Additional tooling for complex processes const complexProcesses = ['weld', 'bend', 'heat treat']; const hasComplex = (rfp.processes || []).some(p => complexProcesses.some(cp => p.toLowerCase().includes(cp)) ); if (hasComplex) { toolingCost *= 1.5; } return toolingCost / Math.max(1, rfp.qty); } /** * Estimate lead time */ function estimateLeadTime(rfp: ParsedRFP, matches: QuoteMatch[]): number { let baseLead = config.defaultLeadDays; // Adjust based on quantity if (rfp.qty < 10) { baseLead = 7; } else if (rfp.qty < 100) { baseLead = 14; } else if (rfp.qty < 500) { baseLead = 21; } else { baseLead = 28; } // Add time for complex processes const processes = rfp.processes || []; if (processes.some(p => /heat.treat|plat/i.test(p))) { baseLead += 7; } if (processes.some(p => /anodize|powder.coat/i.test(p))) { baseLead += 3; } // Adjust based on historical matches if (matches.length > 0 && matches[0].score > 0.7) { const historicalLead = matches[0].quote.actualLeadDays || matches[0].quote.leadDays; // Blend historical and calculated baseLead = Math.round((baseLead + historicalLead * matches[0].score) / (1 + matches[0].score)); } // Check requested due date if (rfp.dueDate) { const dueDate = new Date(rfp.dueDate); const now = new Date(); const requestedDays = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); if (requestedDays > 0 && requestedDays < baseLead) { // Customer needs it faster - flag this baseLead = Math.max(baseLead, requestedDays + 7); // Add buffer } } return baseLead; } /** * Format estimate for display */ export function formatEstimateForReview(estimate: CostEstimate): string { let output = `Cost Estimate (Confidence: ${estimate.confidence.toUpperCase()})\n`; output += '='.repeat(60) + '\n\n'; output += `Price per Unit: $${estimate.pricePerUnit.toFixed(2)}\n`; output += `Total Price: $${estimate.totalPrice.toFixed(2)}\n`; output += `Lead Time: ${estimate.leadDays} days\n\n`; if (estimate.bestCase && estimate.worstCase) { output += `Range:\n`; output += ` Best case: $${estimate.bestCase.price.toFixed(2)} in ${estimate.bestCase.leadDays} days\n`; output += ` Worst case: $${estimate.worstCase.price.toFixed(2)} in ${estimate.worstCase.leadDays} days\n\n`; } output += 'Cost Breakdown:\n'; const b = estimate.breakdown; output += ` Material: $${b.materialCost.toFixed(2)}\n`; output += ` Processing: $${b.processCost.toFixed(2)}\n`; output += ` Labor: $${b.laborCost.toFixed(2)}\n`; output += ` Tooling/unit: $${b.toolingPerUnit.toFixed(2)}\n`; output += ` Overhead: $${b.overhead.toFixed(2)}\n`; output += ` Margin: ${(b.marginPct * 100).toFixed(0)}%\n`; if (b.contingencyPct && b.contingencyPct > 0) { output += ` Contingency: ${(b.contingencyPct * 100).toFixed(0)}%\n`; } return output; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/r-long/mcp-quoting-system'

If you have feedback or need assistance with the MCP directory API, please join our Discord server