import { CalcDiscPrice } from "../shared/math-operations"; import { IsNotNull, IsNull } from "../shared/util"; type QuantityTier = { MinQty?: number | null; Price?: number | null; Disc?: number | null; }; type PriceListDoc = { _id?: any; TType: "SALE" | "PUR"; PrCalc: "FIXED" | "DISC" | "FORMULA"; BasedOn?: "SP" | "PC" | "PCBD" | "PCAD"; Fixed?: { Price?: number | null }; CalcDisc?: { Mark?: boolean; Perc?: number | null }; Round?: string | null; Deci?: number | null; QtyTiers?: QuantityTier[]; Currency?: string | null; }; const ROUND_VALUES = new Set(["-1", "0", "0.99", "0.50", "0.49", "Dec"]); export function ApplyServicePriceListToProcedure( procedure: any, priceList: PriceListDoc | null, quantity: number = 1 ): any { if (!procedure || !priceList) { return procedure; } const resolvedQty = Math.max(1, Number(quantity) || 1); const warnings: string[] = []; if (priceList.TType === "SALE" && procedure.Sale) { const basePrice = IsNotNull(procedure.Sale.Price) ? procedure.Sale.Price : null; const { value, tierWarnings, appliedTier, appliedCalc } = evaluatePriceBlock( basePrice, procedure.Pur, priceList, resolvedQty ); procedure.Sale.PriceBeforePriceList = basePrice; if (value != null) { procedure.Sale.Price = value; procedure.Sale.AppliedPriceListId = priceList._id ?? null; procedure.Sale.AppliedPriceTier = appliedTier ?? undefined; procedure.Sale.AppliedPriceCalc = appliedCalc ?? undefined; } if (tierWarnings.length > 0) { warnings.push(...tierWarnings); } } if (priceList.TType === "PUR" && procedure.Pur) { const baseCost = IsNotNull(procedure.Pur.Cost) ? procedure.Pur.Cost : null; const { value, tierWarnings, appliedTier, appliedCalc } = evaluatePriceBlock( baseCost, procedure.Pur, priceList, resolvedQty ); procedure.Pur.CostBeforePriceList = baseCost; if (value != null) { procedure.Pur.Cost = value; procedure.Pur.AppliedPriceListId = priceList._id ?? null; procedure.Pur.AppliedPriceTier = appliedTier ?? undefined; procedure.Pur.AppliedPriceCalc = appliedCalc ?? undefined; } if (tierWarnings.length > 0) { warnings.push(...tierWarnings); } } if (warnings.length > 0) { procedure.PriceListWarnings = warnings; } return procedure; } export function ApplyServicePriceListToOpcode( opcode: any, priceList: PriceListDoc | null, quantity: number = 1 ): any { if (!opcode || !priceList) { return opcode; } const resolvedQty = Math.max(1, Number(quantity) || 1); const warnings: string[] = []; if (Array.isArray(opcode.Prices) && opcode.Prices.length > 0) { opcode.Prices = opcode.Prices.map((pricing: any) => { const cloned: any = { ...pricing }; if (cloned.Sale) { cloned.Sale = { ...cloned.Sale }; } if (cloned.Pur) { cloned.Pur = { ...cloned.Pur }; } if ( priceList.TType === "SALE" && cloned.Sale && (!cloned.PS || cloned.PS === "S" || cloned.PS === "SP") ) { const basePrice = IsNotNull(cloned.Sale.Price) ? cloned.Sale.Price : null; const { value, tierWarnings, appliedTier, appliedCalc } = evaluatePriceBlock( basePrice, cloned.Pur, priceList, resolvedQty ); cloned.Sale.PriceBeforePriceList = basePrice; if (value != null) { cloned.Sale.Price = value; cloned.Sale.AppliedPriceListId = priceList._id ?? null; cloned.Sale.AppliedPriceTier = appliedTier ?? undefined; cloned.Sale.AppliedPriceCalc = appliedCalc ?? undefined; } if (tierWarnings.length > 0) { warnings.push(...tierWarnings); } } if ( priceList.TType === "PUR" && cloned.Pur && (!cloned.PS || cloned.PS === "P" || cloned.PS === "SP") ) { const baseCost = IsNotNull(cloned.Pur.Cost) ? cloned.Pur.Cost : null; const { value, tierWarnings, appliedTier, appliedCalc } = evaluatePriceBlock( baseCost, cloned.Pur, priceList, resolvedQty ); cloned.Pur.CostBeforePriceList = baseCost; if (value != null) { cloned.Pur.Cost = value; cloned.Pur.AppliedPriceListId = priceList._id ?? null; cloned.Pur.AppliedPriceTier = appliedTier ?? undefined; cloned.Pur.AppliedPriceCalc = appliedCalc ?? undefined; } if (tierWarnings.length > 0) { warnings.push(...tierWarnings); } } return cloned; }); } if (warnings.length > 0) { opcode.PriceListWarnings = warnings; } return opcode; } function evaluatePriceBlock( baseValue: number | null, purchaseBlock: any, priceList: PriceListDoc, quantity: number ): { value: number | null; tierWarnings: string[]; appliedTier?: any; appliedCalc?: string; } { const warnings: string[] = []; const tier = resolveTier(quantity, priceList.QtyTiers); if (tier) { const tierValue = applyTierAdjustment(baseValue, purchaseBlock, tier, priceList); const rounded = applyRounding(priceList, tierValue); return { value: rounded, tierWarnings: warnings, appliedTier: buildAppliedTierMeta(tier), appliedCalc: "TIER", }; } switch (priceList.PrCalc) { case "FIXED": { if (IsNull(priceList.Fixed?.Price)) { warnings.push("Fixed price list selected but Fixed.Price is not configured."); return { value: null, tierWarnings: warnings }; } const rounded = applyRounding(priceList, priceList.Fixed?.Price ?? null); return { value: rounded, tierWarnings: warnings, appliedCalc: "FIXED" }; } case "DISC": { const base = resolveDiscountBase(priceList, baseValue, purchaseBlock); if (base == null) { warnings.push("Discount price list requires a base value but none was available."); return { value: null, tierWarnings: warnings }; } const perc = priceList.CalcDisc?.Perc ?? 0; const mark = priceList.CalcDisc?.Mark ?? false; const price = CalcDiscPrice(base, perc, mark); return { value: applyRounding(priceList, price), tierWarnings: warnings, appliedCalc: "DISCOUNT", }; } case "FORMULA": { warnings.push("Formula-based service price lists are not implemented."); return { value: null, tierWarnings: warnings }; } default: return { value: null, tierWarnings: warnings }; } } function applyTierAdjustment( baseValue: number | null, purchaseBlock: any, tier: QuantityTier, priceList: PriceListDoc ): number | null { if (IsNotNull(tier.Price)) { return tier.Price ?? null; } const targetBase = resolveDiscountBase(priceList, baseValue, purchaseBlock); if (targetBase == null || IsNull(tier.Disc)) { return baseValue; } return CalcDiscPrice(targetBase, tier.Disc ?? 0, false); } function resolveDiscountBase( priceList: PriceListDoc, baseValue: number | null, purchaseBlock: any ): number | null { const basedOn = priceList.BasedOn ?? "SP"; switch (basedOn) { case "SP": return baseValue; case "PC": case "PCBD": case "PCAD": if (purchaseBlock && IsNotNull(purchaseBlock.Cost)) { return purchaseBlock.Cost; } return null; default: return baseValue; } } function applyRounding(priceList: PriceListDoc, value: number | null): number | null { if (value == null) { return null; } if (!priceList.Round || !ROUND_VALUES.has(priceList.Round)) { return value; } switch (priceList.Round) { case "-1": return value; case "0": return Math.round(value); case "0.99": return Math.floor(value) + 0.99; case "0.50": return Math.round(value * 2) / 2; case "0.49": return Math.floor(value) + 0.49; case "Dec": if (IsNotNull(priceList.Deci)) { const factor = Math.pow(10, priceList.Deci ?? 0); return Math.round(value * factor) / factor; } return value; default: return value; } } function resolveTier(quantity: number, tiers?: QuantityTier[] | null): QuantityTier | null { if (!Array.isArray(tiers) || tiers.length === 0) { return null; } const sorted = [...tiers] .filter((tier) => IsNotNull(tier?.MinQty)) .sort((a, b) => (a?.MinQty ?? 0) - (b?.MinQty ?? 0)); let selected: QuantityTier | null = null; for (const tier of sorted) { if (quantity >= (tier?.MinQty ?? 0)) { selected = tier ?? null; } else { break; } } return selected; } function buildAppliedTierMeta(tier: QuantityTier | null) { if (!tier) { return undefined; } return { MinQty: IsNotNull(tier.MinQty) ? tier.MinQty : null, Price: IsNotNull(tier.Price) ? tier.Price : null, Disc: IsNotNull(tier.Disc) ? tier.Disc : null, }; } /** * Applies price list to Service with support for equipment-based pricing (FitPriceCosts) * Handles both simple services (healthcare) and equipment-based services (automotive, HVAC, medical, industrial, etc.) */ export function GetServicePriceForPriceList( service: any, priceList: PriceListDoc | null, equipment: any = null, quantity: number = 1 ): any { if (!service || !priceList) { return service; } const resolvedQty = Math.max(1, Number(quantity) || 1); const warnings: string[] = []; const priceListId = priceList?._id?.toString?.(); // Step 1: Determine base pricing (equipment-specific or simple) let basePrice: any = null; let basePriceSource: string = "BASE"; let matchedFitPriceCost: any = null; if (priceList.TType === "SALE" && service.Sale) { // Check if equipment-based service (FitPriceCosts) or simple service (Price) if (service.FitPriceCosts && service.FitPriceCosts.length > 0) { // Equipment-based service - need equipment information if (!equipment) { warnings.push("Equipment information required for equipment-based service pricing."); service.PriceListWarnings = warnings; return service; } // Find matching FitPriceCost for equipment matchedFitPriceCost = findMatchingFitPriceCost(service.FitPriceCosts, equipment); if (!matchedFitPriceCost || !matchedFitPriceCost.Sale) { warnings.push(`No pricing found for ${equipment.Make || 'Unknown'} ${equipment.Model || ''}`); service.PriceListWarnings = warnings; return service; } basePrice = matchedFitPriceCost.Sale; basePriceSource = "FIT"; } else if (IsNotNull(service.Sale.Price)) { // Simple service - single base price basePrice = { Price: service.Sale.Price, FSale: service.Sale.FSale, BHours: service.Sale.BHours, Dur: service.Sale.Dur, }; basePriceSource = "BASE"; } else { warnings.push("No base pricing configured for service."); service.PriceListWarnings = warnings; return service; } // Step 2: Check for item-specific price list overrides (SalePrices) // For equipment services: Look inside matched FitPriceCost // For simple services: Look at root level let salePriceOverride: any = null; if (priceListId) { if (matchedFitPriceCost && Array.isArray(matchedFitPriceCost.Sale?.SalePrices)) { // Equipment service - check nested SalePrices inside matched FitPriceCost salePriceOverride = matchedFitPriceCost.Sale.SalePrices.find( (sp: any) => sp?.PrListId && sp.PrListId.toString() === priceListId ); } else if (Array.isArray(service.SalePrices)) { // Simple service - check root-level SalePrices salePriceOverride = service.SalePrices.find( (sp: any) => sp?.PrListId && sp.PrListId.toString() === priceListId ); } } if (salePriceOverride) { // Apply item-specific override const originalPrice = basePrice.Price; service.Sale.PriceBeforePriceList = originalPrice; // Check for quantity tiers in override const tierList = salePriceOverride.QtyTiers ?? priceList.QtyTiers; const tier = resolveTier(resolvedQty, tierList); if (tier) { // Apply tier pricing const tierValue = applyTierAdjustmentForSale(basePrice, tier); service.Sale.Price = applyRounding(priceList, tierValue); service.Sale.AppliedPriceListId = priceList._id ?? null; service.Sale.AppliedPriceTier = buildAppliedTierMeta(tier); service.Sale.AppliedPriceCalc = "TIER_OVERRIDE"; service.Sale.PriceSource = basePriceSource; } else if (IsNotNull(salePriceOverride.Price)) { // Fixed price override service.Sale.Price = applyRounding(priceList, salePriceOverride.Price); service.Sale.AppliedPriceListId = priceList._id ?? null; service.Sale.AppliedPriceCalc = "FIXED_OVERRIDE"; service.Sale.PriceSource = basePriceSource; } else if (IsNotNull(salePriceOverride.Disc)) { // Percentage discount override const discountedPrice = CalcDiscPrice(originalPrice, salePriceOverride.Disc, false); service.Sale.Price = applyRounding(priceList, discountedPrice); service.Sale.AppliedPriceListId = priceList._id ?? null; service.Sale.AppliedPriceCalc = "DISCOUNT_OVERRIDE"; service.Sale.PriceSource = basePriceSource; } // Apply other override fields if (IsNotNull(salePriceOverride.BHours)) { service.Sale.BHours = salePriceOverride.BHours; } if (IsNotNull(salePriceOverride.Dur)) { service.Sale.Dur = salePriceOverride.Dur; } if (IsNotNull(salePriceOverride.FSale)) { service.Sale.FSale = salePriceOverride.FSale; } } else { // Step 3: Apply global price list calculation const originalPrice = basePrice.Price; service.Sale.PriceBeforePriceList = originalPrice; const tier = resolveTier(resolvedQty, priceList.QtyTiers); if (tier) { const tierValue = applyTierAdjustmentForSale(basePrice, tier); service.Sale.Price = applyRounding(priceList, tierValue); service.Sale.AppliedPriceListId = priceList._id ?? null; service.Sale.AppliedPriceTier = buildAppliedTierMeta(tier); service.Sale.AppliedPriceCalc = "TIER"; service.Sale.PriceSource = basePriceSource; } else { // Apply global calculation const { value, tierWarnings, appliedCalc } = evaluatePriceBlock( originalPrice, service.Pur, priceList, resolvedQty ); if (value != null) { service.Sale.Price = value; service.Sale.AppliedPriceListId = priceList._id ?? null; service.Sale.AppliedPriceCalc = appliedCalc ?? "GLOBAL"; service.Sale.PriceSource = basePriceSource; } if (tierWarnings.length > 0) { warnings.push(...tierWarnings); } } } } // Step 4: Handle Purchase pricing (similar logic for Pur side) if (priceList.TType === "PUR" && service.Pur) { let baseCost: any = null; let baseCostSource: string = "BASE"; if (service.FitPriceCosts && service.FitPriceCosts.length > 0) { // Equipment-based service - need equipment information if (!equipment) { warnings.push("Equipment information required for equipment-based service costing."); service.PriceListWarnings = warnings; return service; } // Reuse the matchedFitPriceCost from Sale section if available const fitPriceCostEntry = matchedFitPriceCost || findMatchingFitPriceCost(service.FitPriceCosts, equipment); if (!fitPriceCostEntry || !fitPriceCostEntry.Pur) { warnings.push(`No costing found for ${equipment.Make || 'Unknown'} ${equipment.Model || ''}`); service.PriceListWarnings = warnings; return service; } baseCost = fitPriceCostEntry.Pur; baseCostSource = "FIT"; matchedFitPriceCost = fitPriceCostEntry; // Store for later use } else if (IsNotNull(service.Pur.Cost)) { baseCost = { Cost: service.Pur.Cost, FCost: service.Pur.FCost, }; baseCostSource = "BASE"; } if (baseCost) { // Check for purchase cost overrides // For equipment services: Look inside matched FitPriceCost // For simple services: Look at root level let purchaseCostOverride: any = null; if (priceListId) { if (matchedFitPriceCost && Array.isArray(matchedFitPriceCost.Pur?.PurchaseCosts)) { // Equipment service - check nested PurchaseCosts inside matched FitPriceCost purchaseCostOverride = matchedFitPriceCost.Pur.PurchaseCosts.find( (pc: any) => pc?.PrListId && pc.PrListId.toString() === priceListId ); } else if (Array.isArray(service.PurchaseCosts)) { // Simple service - check root-level PurchaseCosts purchaseCostOverride = service.PurchaseCosts.find( (pc: any) => pc?.PrListId && pc.PrListId.toString() === priceListId ); } } const originalCost = baseCost.Cost; service.Pur.CostBeforePriceList = originalCost; if (purchaseCostOverride) { if (IsNotNull(purchaseCostOverride.Cost)) { service.Pur.Cost = applyRounding(priceList, purchaseCostOverride.Cost); service.Pur.AppliedPriceListId = priceList._id ?? null; service.Pur.CostSource = baseCostSource; } else if (IsNotNull(purchaseCostOverride.Disc)) { const discountedCost = CalcDiscPrice(originalCost, purchaseCostOverride.Disc, false); service.Pur.Cost = applyRounding(priceList, discountedCost); service.Pur.AppliedPriceListId = priceList._id ?? null; service.Pur.CostSource = baseCostSource; } } else { const { value, tierWarnings } = evaluatePriceBlock( originalCost, service.Pur, priceList, resolvedQty ); if (value != null) { service.Pur.Cost = value; service.Pur.AppliedPriceListId = priceList._id ?? null; service.Pur.CostSource = baseCostSource; } if (tierWarnings.length > 0) { warnings.push(...tierWarnings); } } } } if (warnings.length > 0) { service.PriceListWarnings = warnings; } return service; } /** * Finds the matching FitPriceCost entry for a given equipment (automotive, HVAC, medical, industrial, etc.) * Tries most specific match first (Make + Model + Variant), then progressively less specific */ function findMatchingFitPriceCost(fitPriceCosts: any[], equipment: any): any | null { if (!Array.isArray(fitPriceCosts) || fitPriceCosts.length === 0 || !equipment) { return null; } const equipmentMakeId = equipment.Make_Id?.toString?.(); const equipmentModelId = equipment.Model_Id?.toString?.(); const equipmentVarId = equipment.Var_Id?.toString?.() || equipment.Variant_Id?.toString?.(); // Try Make + Model + Variant if (equipmentMakeId && equipmentModelId && equipmentVarId) { const match = fitPriceCosts.find((fpc: any) => { if (!Array.isArray(fpc.Fit) || fpc.Fit.length === 0) return false; return fpc.Fit.some((fit: any) => fit.Make_Id?.toString?.() === equipmentMakeId && fit.Model_Id?.toString?.() === equipmentModelId && fit.Var_Id?.toString?.() === equipmentVarId ); }); if (match) return match; } // Try Make + Model if (equipmentMakeId && equipmentModelId) { const match = fitPriceCosts.find((fpc: any) => { if (!Array.isArray(fpc.Fit) || fpc.Fit.length === 0) return false; return fpc.Fit.some((fit: any) => fit.Make_Id?.toString?.() === equipmentMakeId && fit.Model_Id?.toString?.() === equipmentModelId && !fit.Var_Id ); }); if (match) return match; } // Try Make only if (equipmentMakeId) { const match = fitPriceCosts.find((fpc: any) => { if (!Array.isArray(fpc.Fit) || fpc.Fit.length === 0) return false; return fpc.Fit.some((fit: any) => fit.Make_Id?.toString?.() === equipmentMakeId && !fit.Model_Id ); }); if (match) return match; } // Try generic (no Make/Model/Variant specified = applies to all) const genericMatch = fitPriceCosts.find((fpc: any) => { if (!Array.isArray(fpc.Fit) || fpc.Fit.length === 0) return true; // No fit = applies to all return fpc.Fit.some((fit: any) => !fit.Make_Id && !fit.Model_Id && !fit.Var_Id); }); return genericMatch || null; } /** * Applies tier adjustment to sale pricing */ function applyTierAdjustmentForSale( baseSale: any, tier: QuantityTier ): number | null { if (IsNotNull(tier.Price)) { return tier.Price ?? null; } if (IsNotNull(tier.Disc)) { const basePrice = baseSale?.Price ?? 0; return CalcDiscPrice(basePrice, tier.Disc ?? 0, false); } return baseSale?.Price ?? null; }