/** * Pure commercial-terms calculation engine. * * Applies contingency, discount, and payment milestone validation * to base cost/price totals from demand lines. */ import type { CommercialTerms, CommercialTermsSummary, PaymentMilestone } from "@planarchy/shared"; export interface CommercialTermsInput { baseCostCents: number; basePriceCents: number; terms: CommercialTerms; } /** * Compute adjusted totals after applying contingency and discount. * * Contingency is added to cost (risk buffer on cost side). * Discount is subtracted from price (reduction on sell side). * * adjustedCost = baseCost * (1 + contingency%) * adjustedPrice = basePrice * (1 - discount%) * margin = adjustedPrice - adjustedCost */ export function computeCommercialTermsSummary( input: CommercialTermsInput, ): CommercialTermsSummary { const { baseCostCents, basePriceCents, terms } = input; const contingencyFactor = terms.contingencyPercent / 100; const discountFactor = terms.discountPercent / 100; const contingencyCents = Math.round(baseCostCents * contingencyFactor); const discountCents = Math.round(basePriceCents * discountFactor); const adjustedCostCents = baseCostCents + contingencyCents; const adjustedPriceCents = basePriceCents - discountCents; const adjustedMarginCents = adjustedPriceCents - adjustedCostCents; const adjustedMarginPercent = adjustedPriceCents > 0 ? (adjustedMarginCents / adjustedPriceCents) * 100 : 0; return { baseCostCents, basePriceCents, contingencyCents, discountCents, adjustedCostCents, adjustedPriceCents, adjustedMarginCents, adjustedMarginPercent, }; } /** * Validate that payment milestones sum to 100%. * Returns list of validation warnings (empty = valid). */ export function validatePaymentMilestones( milestones: PaymentMilestone[], ): string[] { const warnings: string[] = []; if (milestones.length === 0) return warnings; const totalPercent = milestones.reduce((sum, m) => sum + m.percent, 0); if (Math.abs(totalPercent - 100) > 0.01) { warnings.push( `Payment milestones sum to ${totalPercent.toFixed(1)}%, expected 100%`, ); } for (let i = 0; i < milestones.length; i++) { const m = milestones[i]!; if (m.percent <= 0) { warnings.push(`Milestone "${m.label}" has 0% or negative allocation`); } if (!m.label.trim()) { warnings.push(`Milestone at position ${i + 1} has empty label`); } } // Check chronological order when dates are provided const datedMilestones = milestones.filter( (m): m is PaymentMilestone & { dueDate: string } => m.dueDate != null && m.dueDate !== "", ); for (let i = 1; i < datedMilestones.length; i++) { if (datedMilestones[i]!.dueDate < datedMilestones[i - 1]!.dueDate) { warnings.push( `Milestone "${datedMilestones[i]!.label}" has an earlier date than "${datedMilestones[i - 1]!.label}"`, ); } } return warnings; } /** * Compute per-milestone payment amounts from adjusted price. */ export function computeMilestoneAmounts( adjustedPriceCents: number, milestones: PaymentMilestone[], ): Array<{ label: string; percent: number; amountCents: number; dueDate?: string | null }> { return milestones.map((m) => ({ label: m.label, percent: m.percent, amountCents: Math.round(adjustedPriceCents * (m.percent / 100)), ...(m.dueDate !== undefined ? { dueDate: m.dueDate } : {}), })); } /** * Default commercial terms for a new estimate. */ export function defaultCommercialTerms(): CommercialTerms { return { pricingModel: "fixed_price", contingencyPercent: 0, discountPercent: 0, paymentTermDays: 30, paymentMilestones: [], warrantyMonths: 0, }; }