@archerjessop/utilities 7.32.1 → 7.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{FINANCIAL_CONSTANTS as e}from"../config/financial.js";import{
|
|
1
|
+
import{FINANCIAL_CONSTANTS as e}from"../config/financial.js";import{calculatePMT as n,calculateBalloonBalance as a}from"./calculations.js";const t={DSCR_AMORTIZATION_YEARS:25,DSCR_INTEREST_RATE:.1,EBITDA_ADVANCE_RATE:.2,EBITDA_ADVANCE_THRESHOLD:1e6,FF_E_ADVANCE_RATE:.3,INVENTORY_ADVANCE_RATE:.2,MANAGER_CEILING:12e4,MANAGER_FLOOR:6e4,MANAGER_RATE:.125,OFFER_MULTIPLE_HIGH:3,OFFER_MULTIPLE_LOW:2,OFFER_MULTIPLE_MID:2.5,REAL_ESTATE_ADVANCE_RATE:.5,SELLER_AMORTIZATION_YEARS:60};function toNumber(e){if(null==e||""===e)return null;const n="number"==typeof e?e:parseFloat(String(e).replace(/[$,\s]/g,""));return Number.isFinite(n)?n:null}function toNonNegative(e){const n=toNumber(e);return null===n||n<0?null:n}function resolveBusinessEarnings(e={}){const n=toNumber(e.ebitda);if(null!==n)return{earnings:n,source:"ebitda"};const a=toNumber(e.sde);if(null!==a)return{earnings:a,source:"sde"};const t=toNumber(e.cashFlow??e.cash_flow);return null!==t?{earnings:t,source:"cash_flow"}:{earnings:null,source:null}}function shouldIncludeRealEstate(e={}){if("boolean"==typeof e.statedInclusion)return e.statedInclusion;const n=toNumber(e.askingPrice),a=toNumber(e.realEstateValue);return!(null!==n&&n>0&&null!==a&&a>=n)}function calculateBusinessDownPayment(e={}){const{EBITDA_ADVANCE_RATE:n,EBITDA_ADVANCE_THRESHOLD:a,FF_E_ADVANCE_RATE:l,INVENTORY_ADVANCE_RATE:r,REAL_ESTATE_ADVANCE_RATE:o}=t,{collateralizeEbitda:s=!0}=e,i=toNonNegative(e.realEstateValue)??0,u=toNonNegative(e.ffeValue)??0,c=toNonNegative(e.inventoryValue)??0,f=s?toNumber(e.ebitda)??0:0,d={ebitda:f>=a?f*n:0,ffe:u*l,inventory:c*r,realEstate:i*o};return{downPayment:d.realEstate+d.ffe+d.inventory+d.ebitda,legs:d}}function calculateBusinessOffer(e={}){const{OFFER_MULTIPLE_HIGH:n,OFFER_MULTIPLE_LOW:a,OFFER_MULTIPLE_MID:l}=t,{includeFfe:r=!0,includeInventory:o=!0,includeRealEstate:s=!0}=e,i=(s?toNonNegative(e.realEstateValue)??0:0)+(r?toNonNegative(e.ffeValue)??0:0)+(o?toNonNegative(e.inventoryValue)??0:0),u=toNumber(e.earnings);return null===u?{assetsIncluded:i,offerHigh:null,offerLow:null,offerMid:null}:{assetsIncluded:i,offerHigh:u*n+i,offerLow:u*a+i,offerMid:u*l+i}}function calculateBusinessSellerFinance(t={}){const{amortizationYears:l=e.SELLER_FI_AMORTIZATION,balloonYears:r=e.DEFAULT_BALLOON_PERIOD_YEARS,interestPaymentMode:o="standard",interestRate:s=e.SELLER_FI_INTEREST_RATE}=t,i=toNumber(t.priceOffered);if(null===i)return{balloonBalance:null,performancePayout:0,sellerFinanced:null,sfPayment:null,totalPayments:null};const u=toNonNegative(t.downPayment)??0,c=Math.max(0,i-u);let f,d=0;"standard"===o?f=n(c,s,l):(f=n(c,0,l),"simple_payout"===o?d=c*s*r:"compound_payout"===o&&(d=c*(Math.pow(1+s,r)-1)));return{balloonBalance:("standard"===o?a(c,s,l,r):c)+d,performancePayout:d,sellerFinanced:c,sfPayment:f,totalPayments:12*f*r}}function calculateBusinessCashFlow(a={}){const{DSCR_AMORTIZATION_YEARS:l,DSCR_INTEREST_RATE:r,MANAGER_CEILING:o,MANAGER_FLOOR:s,MANAGER_RATE:i,SELLER_AMORTIZATION_YEARS:u}=t,{askingPrice:c=null,balloonYears:f=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrAmortizationYears:d=l,dscrRate:E=r,earningsSource:_=null,ebitdaLegAmount:A=0,offerHigh:R=null,sellerAmortizationYears:m=u,sellerRate:N=e.SELLER_FI_INTEREST_RATE}=a,g=toNumber(a.priceOffered),T=toNumber(a.earnings);if(null===g||g<=0||null===T)return null;const y=toNonNegative(a.downPayment)??0,I="ebitda"===_?0:Math.min(o,Math.max(s,(toNonNegative(a.grossRevenue)??0)*i)),F=T-I,L=n(y,E,d),{sfPayment:b,sellerFinanced:O}=calculateBusinessSellerFinance({amortizationYears:m,balloonYears:f,downPayment:y,interestRate:N,priceOffered:g}),P=12*L,h=12*b,w=y+(O??0),v=w>0?(y*E+(O??0)*N)/w:0,p=F-P-h,M=p/12,S=p>=0,D=(toNonNegative(A)??0)>0,B=toNumber(R),V=toNumber(c),C=null!==B&&null!==V&&V>0&&B<V,Y=[];return S||Y.push("Cash Flow"),D&&Y.push("EBITDA financed"),{adjustedEarnings:F,blendedRate:v,cashFlowAnnual:p,cashFlowMonthly:M,dscrPaymentAnnual:P,dscrPaymentMonthly:L,ebitdaFinanced:D,flagged:!S||D,managerCost:I,marginAnnual:p/g,marginMonthly:M/g,offerBelowAsking:C,pass:S,pills:Y,price:g,sellerFinanced:O,sellerPaymentAnnual:h,sellerPaymentMonthly:b}}function underwriteBusinessListing(e={}){const n=e.realEstateValue??e.real_estate_value,a=e.ffeValue??e.ff_e_value,t=e.inventoryValue??e.inventory_value,l=e.ebitda,{earnings:r,source:o}=resolveBusinessEarnings({cashFlow:e.cashFlow??e.cash_flow,ebitda:l,sde:e.sde}),{downPayment:s,legs:i}=calculateBusinessDownPayment({collateralizeEbitda:e.collateralizeEbitda??e.collateralize_ebitda??!0,ebitda:l,ffeValue:a,inventoryValue:t,realEstateValue:n}),{assetsIncluded:u,offerHigh:c,offerLow:f,offerMid:d}=calculateBusinessOffer({earnings:r,ffeValue:a,includeFfe:e.includeFfe??e.include_ff_e??!0,includeInventory:e.includeInventory??e.include_inventory??!0,includeRealEstate:e.includeRealEstate??e.include_real_estate??!0,inventoryValue:t,realEstateValue:n}),E=e.earningsSource??e.earnings_source??o,_=e.offerEnd??e.offer_end??"high",A=toNumber(e.priceOffered??e.price_offered)??{high:c,low:f,mid:d}[_]??c,R=toNumber(e.downPaymentPercent??e.down_payment_percent),m=null!==R&&R>0&&null!==A,N=m?A*R:s,g=calculateBusinessSellerFinance({amortizationYears:e.sellerAmortization??e.seller_amortization??void 0,balloonYears:e.balloonLength??e.balloon_length??void 0,downPayment:N,interestPaymentMode:e.interestPaymentMode??e.interest_payment_mode??"standard",interestRate:e.sellerFiRate??e.seller_fi_rate??void 0,priceOffered:e.priceOffered??e.price_offered});return{assetsIncluded:u,cashFlow:calculateBusinessCashFlow({askingPrice:e.askingPrice??e.asking_price,downPayment:N,earnings:r,earningsSource:E,ebitdaLegAmount:i.ebitda,grossRevenue:e.grossRevenue??e.gross_revenue,offerHigh:c,priceOffered:A}),collateralDownPayment:s,collateralShortfall:null!==c&&N>c,downPayment:N,downPaymentPercent:A?N/A:null,downPaymentSource:m?"override":"collateral",earnings:r,earningsSource:E,legs:i,offerEnd:_,offerHigh:c,offerLow:f,offerMid:d,priceOffered:toNumber(e.priceOffered??e.price_offered),sellerCarry:null===c?null:c-N,terms:g}}export{t as BUSINESS_UNDERWRITING_CONSTANTS,calculateBusinessCashFlow,calculateBusinessDownPayment,calculateBusinessOffer,calculateBusinessSellerFinance,resolveBusinessEarnings,shouldIncludeRealEstate,underwriteBusinessListing};
|
|
2
2
|
//# sourceMappingURL=business-underwriting.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"business-underwriting.js","sources":["../../src/financial/business-underwriting.js"],"sourcesContent":["// src/financial/business-underwriting.js\n//\n// Underwriting for businesses backed by real estate (the BizBuySell pipeline).\n// The real-estate model prices a property off NOI and a cap rate; this one\n// prices a business off an earnings multiple and adds the tangible assets that\n// convey with it. Shared by the BizBuySell extension panel and the dashboard so\n// both quote the same numbers from the same inputs.\n\nimport { FINANCIAL_CONSTANTS } from \"../config/financial.js\";\nimport { calculateBalloonBalance, calculatePMT } from \"./calculations.js\";\n\n/**\n * Collateral advance rates for the down payment, and the earnings multiples\n * bounding the offer. The EBITDA leg only contributes at or above its threshold\n * — smaller earnings are not treated as collateral.\n */\nexport const BUSINESS_UNDERWRITING_CONSTANTS = {\n EBITDA_ADVANCE_RATE: 0.20,\n EBITDA_ADVANCE_THRESHOLD: 1000000,\n FF_E_ADVANCE_RATE: 0.30,\n INVENTORY_ADVANCE_RATE: 0.20,\n OFFER_MULTIPLE_HIGH: 3,\n OFFER_MULTIPLE_LOW: 2,\n REAL_ESTATE_ADVANCE_RATE: 0.50,\n};\n\n// Coerce a scraped/stored figure to a finite number, else null. Absent data must\n// stay absent — a missing earnings figure means \"no offer\", never \"an offer of 0\".\nfunction toNumber(value) {\n if (value === null || value === undefined || value === \"\") return null;\n const numeric = typeof value === \"number\" ? value : parseFloat(String(value).replace(/[$,\\s]/g, \"\"));\n return Number.isFinite(numeric) ? numeric : null;\n}\n\nfunction toNonNegative(value) {\n const numeric = toNumber(value);\n return numeric === null || numeric < 0 ? null : numeric;\n}\n\n/**\n * Pick the earnings figure that drives the offer multiple, preferring EBITDA and\n * falling back to SDE then cash flow — most BizBuySell listings publish only\n * \"Cash Flow (SDE)\". The source is returned alongside so callers can label which\n * figure the multiple was applied to instead of implying it was EBITDA.\n * @param {{ebitda?:number, sde?:number, cashFlow?:number}} listing\n * @returns {{earnings:number|null, source:string|null}}\n */\nexport function resolveBusinessEarnings(listing = {}) {\n const ebitda = toNumber(listing.ebitda);\n if (ebitda !== null) return { earnings: ebitda, source: \"ebitda\" };\n\n const sde = toNumber(listing.sde);\n if (sde !== null) return { earnings: sde, source: \"sde\" };\n\n const cashFlow = toNumber(listing.cashFlow ?? listing.cash_flow);\n if (cashFlow !== null) return { earnings: cashFlow, source: \"cash_flow\" };\n\n return { earnings: null, source: null };\n}\n\n/**\n * Whether the real estate should be ADDED to the offer on top of the earnings multiple.\n *\n * Three inputs, in priority order:\n * 1. What the listing says. Explicit wording (\"included in asking price\", or its negation)\n * is a statement of fact from the seller and always wins.\n * 2. The arithmetic. When the stated real-estate value is at or above the asking price,\n * the building IS the ask — adding it again would quote an offer ceiling above the\n * price being asked, which is nonsense. This is the common case on listings that\n * publish a real-estate value and no inclusion wording at all.\n * 3. Otherwise include it, matching the default for every other asset: understating an\n * offer by silently dropping an asset is worse than an inclusion that can be toggled\n * off by hand.\n *\n * @param {{askingPrice?:number, realEstateValue?:number, statedInclusion?:boolean|null}} inputs\n * @returns {boolean}\n */\nexport function shouldIncludeRealEstate(inputs = {}) {\n if (typeof inputs.statedInclusion === \"boolean\") return inputs.statedInclusion;\n\n const askingPrice = toNumber(inputs.askingPrice);\n const realEstateValue = toNumber(inputs.realEstateValue);\n\n if (askingPrice !== null && askingPrice > 0 && realEstateValue !== null && realEstateValue >= askingPrice) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Down payment as the sum of per-asset collateral advances: 50% of the real\n * estate, 30% of FF&E, 20% of inventory, plus 20% of EBITDA once EBITDA reaches\n * $1M. Each leg is capped at its own rate, and a leg with no reported value\n * contributes nothing.\n *\n * Deliberately NOT gated by the include_* flags. Those flags answer a different\n * question — whether an asset is ADDED to the offer on top of the earnings\n * multiple — and an asset excluded there is still being acquired. The common case\n * is a building whose value equals the asking price: it is not added to the offer\n * (that would quote a ceiling above the ask) but it is still bought, and it still\n * secures half its value. Gating this on those flags zeroes the collateral on\n * exactly the listings where the real estate matters most.\n *\n * The EBITDA leg reads EBITDA specifically, not the coalesced earnings figure —\n * an SDE-only listing does not qualify, because SDE includes owner compensation\n * and is not the same measure the threshold was set against. collateralizeEbitda\n * is a collateral-only switch, named apart from the include_* flags because it\n * answers that different question: EBITDA is the base the offer multiple is taken\n * against, so it never joins the offer range at all.\n *\n * @param {object} inputs\n * @param {number} [inputs.realEstateValue]\n * @param {number} [inputs.ffeValue]\n * @param {number} [inputs.inventoryValue]\n * @param {number} [inputs.ebitda]\n * @param {boolean} [inputs.collateralizeEbitda=true]\n * @returns {{downPayment:number, legs:{realEstate:number, ffe:number, inventory:number, ebitda:number}}}\n */\nexport function calculateBusinessDownPayment(inputs = {}) {\n const {\n EBITDA_ADVANCE_RATE,\n EBITDA_ADVANCE_THRESHOLD,\n FF_E_ADVANCE_RATE,\n INVENTORY_ADVANCE_RATE,\n REAL_ESTATE_ADVANCE_RATE,\n } = BUSINESS_UNDERWRITING_CONSTANTS;\n\n const { collateralizeEbitda = true } = inputs;\n\n const realEstateValue = toNonNegative(inputs.realEstateValue) ?? 0;\n const ffeValue = toNonNegative(inputs.ffeValue) ?? 0;\n const inventoryValue = toNonNegative(inputs.inventoryValue) ?? 0;\n const ebitda = collateralizeEbitda ? toNumber(inputs.ebitda) ?? 0 : 0;\n\n const legs = {\n ebitda: ebitda >= EBITDA_ADVANCE_THRESHOLD ? ebitda * EBITDA_ADVANCE_RATE : 0,\n ffe: ffeValue * FF_E_ADVANCE_RATE,\n inventory: inventoryValue * INVENTORY_ADVANCE_RATE,\n realEstate: realEstateValue * REAL_ESTATE_ADVANCE_RATE,\n };\n\n return {\n downPayment: legs.realEstate + legs.ffe + legs.inventory + legs.ebitda,\n legs,\n };\n}\n\n/**\n * The offer range: 2x to 3x earnings, plus the tangible assets that convey.\n * An asset the listing states is already covered by the asking price is excluded\n * by its include flag; the flags default to true, so an asset whose status could\n * not be determined is still offered on (dropping it silently would understate\n * the offer, which is the more damaging error).\n *\n * Returns nulls when no earnings figure exists — there is no honest multiple to\n * take, and quoting the assets alone would read as an offer.\n *\n * @param {object} inputs\n * @param {number} inputs.earnings - Earnings figure the multiple applies to\n * @param {number} [inputs.realEstateValue]\n * @param {number} [inputs.ffeValue]\n * @param {number} [inputs.inventoryValue]\n * @param {boolean} [inputs.includeRealEstate=true]\n * @param {boolean} [inputs.includeFfe=true]\n * @param {boolean} [inputs.includeInventory=true]\n * @returns {{assetsIncluded:number, offerHigh:number|null, offerLow:number|null}}\n */\nexport function calculateBusinessOffer(inputs = {}) {\n const { OFFER_MULTIPLE_HIGH, OFFER_MULTIPLE_LOW } = BUSINESS_UNDERWRITING_CONSTANTS;\n\n const {\n includeFfe = true,\n includeInventory = true,\n includeRealEstate = true,\n } = inputs;\n\n const assetsIncluded =\n (includeRealEstate ? toNonNegative(inputs.realEstateValue) ?? 0 : 0) +\n (includeFfe ? toNonNegative(inputs.ffeValue) ?? 0 : 0) +\n (includeInventory ? toNonNegative(inputs.inventoryValue) ?? 0 : 0);\n\n const earnings = toNumber(inputs.earnings);\n if (earnings === null) return { assetsIncluded, offerHigh: null, offerLow: null };\n\n return {\n assetsIncluded,\n offerHigh: earnings * OFFER_MULTIPLE_HIGH + assetsIncluded,\n offerLow: earnings * OFFER_MULTIPLE_LOW + assetsIncluded,\n };\n}\n\n/**\n * The seller-carry terms for the price actually being offered.\n *\n * The offer range is a range; the LOI needs one number. priceOffered is that\n * choice, and the seller carries whatever the collateral down payment does not\n * cover. From there the arithmetic is the property engine's, so both sides quote\n * a balloon identically — a business is worked like a rental, only the earnings\n * measure differs.\n *\n * Returns nulls when no price has been offered yet. A zeroed payment schedule\n * would read as real terms on an LOI, which is the more damaging error.\n *\n * @param {object} inputs\n * @param {number} inputs.priceOffered - The figure being offered\n * @param {number} inputs.downPayment - Collateral-derived down payment\n * @param {number} [inputs.interestRate=0] - Annual seller-finance rate, as a decimal\n * @param {number} [inputs.amortizationYears=30]\n * @param {number} [inputs.balloonYears=7]\n * @param {string} [inputs.interestPaymentMode=\"standard\"] - standard | simple_payout | compound_payout\n * @returns {{sellerFinanced:number|null, sfPayment:number|null, balloonBalance:number|null, performancePayout:number, totalPayments:number|null}}\n */\nexport function calculateBusinessSellerFinance(inputs = {}) {\n const {\n amortizationYears = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\n interestPaymentMode = \"standard\",\n interestRate = FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE,\n } = inputs;\n\n const priceOffered = toNumber(inputs.priceOffered);\n if (priceOffered === null) {\n return {\n balloonBalance: null,\n performancePayout: 0,\n sellerFinanced: null,\n sfPayment: null,\n totalPayments: null,\n };\n }\n\n const downPayment = toNonNegative(inputs.downPayment) ?? 0;\n // Never negative: collateral outrunning the offer is reported as a shortfall by\n // the caller, not folded back in here as a carry the seller owes the buyer.\n const sellerFinanced = Math.max(0, priceOffered - downPayment);\n\n let performancePayout = 0;\n let sfPayment;\n\n if (interestPaymentMode === \"standard\") {\n sfPayment = calculatePMT(sellerFinanced, interestRate, amortizationYears);\n } else {\n // Payout modes defer all interest to the balloon, so the running payment is\n // principal-only and the balloon carries the accrued interest instead.\n sfPayment = calculatePMT(sellerFinanced, 0, amortizationYears);\n if (interestPaymentMode === \"simple_payout\") {\n performancePayout = sellerFinanced * interestRate * balloonYears;\n } else if (interestPaymentMode === \"compound_payout\") {\n performancePayout = sellerFinanced * (Math.pow(1 + interestRate, balloonYears) - 1);\n }\n }\n\n // Mirrors the property engine: amortized down in standard mode, full principal\n // in the payout modes, with the deferred interest added on top.\n const baseBalloonBalance = interestPaymentMode === \"standard\"\n ? calculateBalloonBalance(sellerFinanced, interestRate, amortizationYears, balloonYears)\n : sellerFinanced;\n\n return {\n balloonBalance: baseBalloonBalance + performancePayout,\n performancePayout,\n sellerFinanced,\n sfPayment,\n totalPayments: sfPayment * 12 * balloonYears,\n };\n}\n\n/**\n * Full underwrite for one listing: resolve earnings, size the down payment from\n * collateral, and bound the offer. The seller carries the balance of the ceiling\n * offer as preferred equity.\n *\n * collateralShortfall is set when the collateral-driven down payment exceeds the\n * offer itself — a real signal that the asset value has outrun what the earnings\n * multiple justifies. It is surfaced, never clamped away.\n *\n * @param {object} listing - Scraped/stored figures (snake_case or camelCase)\n * @returns {object} earnings, earningsSource, downPayment, legs, offerLow, offerHigh, sellerCarry, collateralShortfall\n */\nexport function underwriteBusinessListing(listing = {}) {\n const realEstateValue = listing.realEstateValue ?? listing.real_estate_value;\n const ffeValue = listing.ffeValue ?? listing.ff_e_value;\n const inventoryValue = listing.inventoryValue ?? listing.inventory_value;\n const ebitda = listing.ebitda;\n\n const { earnings, source } = resolveBusinessEarnings({\n cashFlow: listing.cashFlow ?? listing.cash_flow,\n ebitda,\n sde: listing.sde,\n });\n\n // The collateral stack covers everything being acquired, so it takes no include_*\n // flag — those govern only what is ADDED to the offer range. Its one switch is\n // collateralizeEbitda, which has no offer-range counterpart.\n const { downPayment, legs } = calculateBusinessDownPayment({\n collateralizeEbitda: listing.collateralizeEbitda ?? listing.collateralize_ebitda ?? true,\n ebitda,\n ffeValue,\n inventoryValue,\n realEstateValue,\n });\n\n const { assetsIncluded, offerHigh, offerLow } = calculateBusinessOffer({\n earnings,\n ffeValue,\n includeFfe: listing.includeFfe ?? listing.include_ff_e ?? true,\n includeInventory: listing.includeInventory ?? listing.include_inventory ?? true,\n includeRealEstate: listing.includeRealEstate ?? listing.include_real_estate ?? true,\n inventoryValue,\n realEstateValue,\n });\n\n // Terms for the price actually offered. Kept separate from the range figures\n // above so there is no mistaking the deal being made for the ceiling that\n // bounds it — sellerCarry is the carry at offerHigh, terms.sellerFinanced the\n // carry at the price on the LOI.\n const terms = calculateBusinessSellerFinance({\n amortizationYears: listing.sellerAmortization ?? listing.seller_amortization ?? undefined,\n balloonYears: listing.balloonLength ?? listing.balloon_length ?? undefined,\n downPayment,\n interestPaymentMode: listing.interestPaymentMode ?? listing.interest_payment_mode ?? \"standard\",\n interestRate: listing.sellerFiRate ?? listing.seller_fi_rate ?? undefined,\n priceOffered: listing.priceOffered ?? listing.price_offered,\n });\n\n return {\n assetsIncluded,\n collateralShortfall: offerHigh !== null && downPayment > offerHigh,\n downPayment,\n earnings,\n earningsSource: listing.earningsSource ?? listing.earnings_source ?? source,\n legs,\n offerHigh,\n offerLow,\n priceOffered: toNumber(listing.priceOffered ?? listing.price_offered),\n sellerCarry: offerHigh === null ? null : offerHigh - downPayment,\n terms,\n };\n}\n"],"names":["BUSINESS_UNDERWRITING_CONSTANTS","EBITDA_ADVANCE_RATE","EBITDA_ADVANCE_THRESHOLD","FF_E_ADVANCE_RATE","INVENTORY_ADVANCE_RATE","OFFER_MULTIPLE_HIGH","OFFER_MULTIPLE_LOW","REAL_ESTATE_ADVANCE_RATE","toNumber","value","numeric","parseFloat","String","replace","Number","isFinite","toNonNegative","resolveBusinessEarnings","listing","ebitda","earnings","source","sde","cashFlow","cash_flow","shouldIncludeRealEstate","inputs","statedInclusion","askingPrice","realEstateValue","calculateBusinessDownPayment","collateralizeEbitda","ffeValue","inventoryValue","legs","ffe","inventory","realEstate","downPayment","calculateBusinessOffer","includeFfe","includeInventory","includeRealEstate","assetsIncluded","offerHigh","offerLow","calculateBusinessSellerFinance","amortizationYears","FINANCIAL_CONSTANTS","SELLER_FI_AMORTIZATION","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","interestPaymentMode","interestRate","SELLER_FI_INTEREST_RATE","priceOffered","balloonBalance","performancePayout","sellerFinanced","sfPayment","totalPayments","Math","max","calculatePMT","pow","calculateBalloonBalance","underwriteBusinessListing","real_estate_value","ff_e_value","inventory_value","collateralize_ebitda","include_ff_e","include_inventory","include_real_estate","terms","sellerAmortization","seller_amortization","undefined","balloonLength","balloon_length","interest_payment_mode","sellerFiRate","seller_fi_rate","price_offered","collateralShortfall","earningsSource","earnings_source","sellerCarry"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"business-underwriting.js","sources":["../../src/financial/business-underwriting.js"],"sourcesContent":["// src/financial/business-underwriting.js\n//\n// Underwriting for businesses backed by real estate (the BizBuySell pipeline).\n// The real-estate model prices a property off NOI and a cap rate; this one\n// prices a business off an earnings multiple and adds the tangible assets that\n// convey with it. Shared by the BizBuySell extension panel and the dashboard so\n// both quote the same numbers from the same inputs.\n\nimport { FINANCIAL_CONSTANTS } from \"../config/financial.js\";\nimport { calculateBalloonBalance, calculatePMT } from \"./calculations.js\";\n\n/**\n * Collateral advance rates for the down payment, and the earnings multiples\n * bounding the offer. The EBITDA leg only contributes at or above its threshold\n * — smaller earnings are not treated as collateral.\n */\nexport const BUSINESS_UNDERWRITING_CONSTANTS = {\n DSCR_AMORTIZATION_YEARS: 25,\n DSCR_INTEREST_RATE: 0.10,\n EBITDA_ADVANCE_RATE: 0.20,\n EBITDA_ADVANCE_THRESHOLD: 1000000,\n FF_E_ADVANCE_RATE: 0.30,\n INVENTORY_ADVANCE_RATE: 0.20,\n MANAGER_CEILING: 120000,\n MANAGER_FLOOR: 60000,\n MANAGER_RATE: 0.125,\n OFFER_MULTIPLE_HIGH: 3,\n OFFER_MULTIPLE_LOW: 2,\n OFFER_MULTIPLE_MID: 2.5,\n REAL_ESTATE_ADVANCE_RATE: 0.50,\n SELLER_AMORTIZATION_YEARS: 60,\n};\n\n// Coerce a scraped/stored figure to a finite number, else null. Absent data must\n// stay absent — a missing earnings figure means \"no offer\", never \"an offer of 0\".\nfunction toNumber(value) {\n if (value === null || value === undefined || value === \"\") return null;\n const numeric = typeof value === \"number\" ? value : parseFloat(String(value).replace(/[$,\\s]/g, \"\"));\n return Number.isFinite(numeric) ? numeric : null;\n}\n\nfunction toNonNegative(value) {\n const numeric = toNumber(value);\n return numeric === null || numeric < 0 ? null : numeric;\n}\n\n/**\n * Pick the earnings figure that drives the offer multiple, preferring EBITDA and\n * falling back to SDE then cash flow — most BizBuySell listings publish only\n * \"Cash Flow (SDE)\". The source is returned alongside so callers can label which\n * figure the multiple was applied to instead of implying it was EBITDA.\n * @param {{ebitda?:number, sde?:number, cashFlow?:number}} listing\n * @returns {{earnings:number|null, source:string|null}}\n */\nexport function resolveBusinessEarnings(listing = {}) {\n const ebitda = toNumber(listing.ebitda);\n if (ebitda !== null) return { earnings: ebitda, source: \"ebitda\" };\n\n const sde = toNumber(listing.sde);\n if (sde !== null) return { earnings: sde, source: \"sde\" };\n\n const cashFlow = toNumber(listing.cashFlow ?? listing.cash_flow);\n if (cashFlow !== null) return { earnings: cashFlow, source: \"cash_flow\" };\n\n return { earnings: null, source: null };\n}\n\n/**\n * Whether the real estate should be ADDED to the offer on top of the earnings multiple.\n *\n * Three inputs, in priority order:\n * 1. What the listing says. Explicit wording (\"included in asking price\", or its negation)\n * is a statement of fact from the seller and always wins.\n * 2. The arithmetic. When the stated real-estate value is at or above the asking price,\n * the building IS the ask — adding it again would quote an offer ceiling above the\n * price being asked, which is nonsense. This is the common case on listings that\n * publish a real-estate value and no inclusion wording at all.\n * 3. Otherwise include it, matching the default for every other asset: understating an\n * offer by silently dropping an asset is worse than an inclusion that can be toggled\n * off by hand.\n *\n * @param {{askingPrice?:number, realEstateValue?:number, statedInclusion?:boolean|null}} inputs\n * @returns {boolean}\n */\nexport function shouldIncludeRealEstate(inputs = {}) {\n if (typeof inputs.statedInclusion === \"boolean\") return inputs.statedInclusion;\n\n const askingPrice = toNumber(inputs.askingPrice);\n const realEstateValue = toNumber(inputs.realEstateValue);\n\n if (askingPrice !== null && askingPrice > 0 && realEstateValue !== null && realEstateValue >= askingPrice) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Down payment as the sum of per-asset collateral advances: 50% of the real\n * estate, 30% of FF&E, 20% of inventory, plus 20% of EBITDA once EBITDA reaches\n * $1M. Each leg is capped at its own rate, and a leg with no reported value\n * contributes nothing.\n *\n * Deliberately NOT gated by the include_* flags. Those flags answer a different\n * question — whether an asset is ADDED to the offer on top of the earnings\n * multiple — and an asset excluded there is still being acquired. The common case\n * is a building whose value equals the asking price: it is not added to the offer\n * (that would quote a ceiling above the ask) but it is still bought, and it still\n * secures half its value. Gating this on those flags zeroes the collateral on\n * exactly the listings where the real estate matters most.\n *\n * The EBITDA leg reads EBITDA specifically, not the coalesced earnings figure —\n * an SDE-only listing does not qualify, because SDE includes owner compensation\n * and is not the same measure the threshold was set against. collateralizeEbitda\n * is a collateral-only switch, named apart from the include_* flags because it\n * answers that different question: EBITDA is the base the offer multiple is taken\n * against, so it never joins the offer range at all.\n *\n * @param {object} inputs\n * @param {number} [inputs.realEstateValue]\n * @param {number} [inputs.ffeValue]\n * @param {number} [inputs.inventoryValue]\n * @param {number} [inputs.ebitda]\n * @param {boolean} [inputs.collateralizeEbitda=true]\n * @returns {{downPayment:number, legs:{realEstate:number, ffe:number, inventory:number, ebitda:number}}}\n */\nexport function calculateBusinessDownPayment(inputs = {}) {\n const {\n EBITDA_ADVANCE_RATE,\n EBITDA_ADVANCE_THRESHOLD,\n FF_E_ADVANCE_RATE,\n INVENTORY_ADVANCE_RATE,\n REAL_ESTATE_ADVANCE_RATE,\n } = BUSINESS_UNDERWRITING_CONSTANTS;\n\n const { collateralizeEbitda = true } = inputs;\n\n const realEstateValue = toNonNegative(inputs.realEstateValue) ?? 0;\n const ffeValue = toNonNegative(inputs.ffeValue) ?? 0;\n const inventoryValue = toNonNegative(inputs.inventoryValue) ?? 0;\n const ebitda = collateralizeEbitda ? toNumber(inputs.ebitda) ?? 0 : 0;\n\n const legs = {\n ebitda: ebitda >= EBITDA_ADVANCE_THRESHOLD ? ebitda * EBITDA_ADVANCE_RATE : 0,\n ffe: ffeValue * FF_E_ADVANCE_RATE,\n inventory: inventoryValue * INVENTORY_ADVANCE_RATE,\n realEstate: realEstateValue * REAL_ESTATE_ADVANCE_RATE,\n };\n\n return {\n downPayment: legs.realEstate + legs.ffe + legs.inventory + legs.ebitda,\n legs,\n };\n}\n\n/**\n * The offer range: 2x to 3x earnings, plus the tangible assets that convey.\n * An asset the listing states is already covered by the asking price is excluded\n * by its include flag; the flags default to true, so an asset whose status could\n * not be determined is still offered on (dropping it silently would understate\n * the offer, which is the more damaging error).\n *\n * Returns nulls when no earnings figure exists — there is no honest multiple to\n * take, and quoting the assets alone would read as an offer.\n *\n * @param {object} inputs\n * @param {number} inputs.earnings - Earnings figure the multiple applies to\n * @param {number} [inputs.realEstateValue]\n * @param {number} [inputs.ffeValue]\n * @param {number} [inputs.inventoryValue]\n * @param {boolean} [inputs.includeRealEstate=true]\n * @param {boolean} [inputs.includeFfe=true]\n * @param {boolean} [inputs.includeInventory=true]\n * @returns {{assetsIncluded:number, offerHigh:number|null, offerLow:number|null, offerMid:number|null}}\n */\nexport function calculateBusinessOffer(inputs = {}) {\n const { OFFER_MULTIPLE_HIGH, OFFER_MULTIPLE_LOW, OFFER_MULTIPLE_MID } = BUSINESS_UNDERWRITING_CONSTANTS;\n\n const {\n includeFfe = true,\n includeInventory = true,\n includeRealEstate = true,\n } = inputs;\n\n const assetsIncluded =\n (includeRealEstate ? toNonNegative(inputs.realEstateValue) ?? 0 : 0) +\n (includeFfe ? toNonNegative(inputs.ffeValue) ?? 0 : 0) +\n (includeInventory ? toNonNegative(inputs.inventoryValue) ?? 0 : 0);\n\n const earnings = toNumber(inputs.earnings);\n if (earnings === null) return { assetsIncluded, offerHigh: null, offerLow: null, offerMid: null };\n\n return {\n assetsIncluded,\n offerHigh: earnings * OFFER_MULTIPLE_HIGH + assetsIncluded,\n offerLow: earnings * OFFER_MULTIPLE_LOW + assetsIncluded,\n offerMid: earnings * OFFER_MULTIPLE_MID + assetsIncluded,\n };\n}\n\n/**\n * The seller-carry terms for the price actually being offered.\n *\n * The offer range is a range; the LOI needs one number. priceOffered is that\n * choice, and the seller carries whatever the collateral down payment does not\n * cover. From there the arithmetic is the property engine's, so both sides quote\n * a balloon identically — a business is worked like a rental, only the earnings\n * measure differs.\n *\n * Returns nulls when no price has been offered yet. A zeroed payment schedule\n * would read as real terms on an LOI, which is the more damaging error.\n *\n * @param {object} inputs\n * @param {number} inputs.priceOffered - The figure being offered\n * @param {number} inputs.downPayment - Collateral-derived down payment\n * @param {number} [inputs.interestRate=0] - Annual seller-finance rate, as a decimal\n * @param {number} [inputs.amortizationYears=30]\n * @param {number} [inputs.balloonYears=7]\n * @param {string} [inputs.interestPaymentMode=\"standard\"] - standard | simple_payout | compound_payout\n * @returns {{sellerFinanced:number|null, sfPayment:number|null, balloonBalance:number|null, performancePayout:number, totalPayments:number|null}}\n */\nexport function calculateBusinessSellerFinance(inputs = {}) {\n const {\n amortizationYears = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\n interestPaymentMode = \"standard\",\n interestRate = FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE,\n } = inputs;\n\n const priceOffered = toNumber(inputs.priceOffered);\n if (priceOffered === null) {\n return {\n balloonBalance: null,\n performancePayout: 0,\n sellerFinanced: null,\n sfPayment: null,\n totalPayments: null,\n };\n }\n\n const downPayment = toNonNegative(inputs.downPayment) ?? 0;\n // Never negative: collateral outrunning the offer is reported as a shortfall by\n // the caller, not folded back in here as a carry the seller owes the buyer.\n const sellerFinanced = Math.max(0, priceOffered - downPayment);\n\n let performancePayout = 0;\n let sfPayment;\n\n if (interestPaymentMode === \"standard\") {\n sfPayment = calculatePMT(sellerFinanced, interestRate, amortizationYears);\n } else {\n // Payout modes defer all interest to the balloon, so the running payment is\n // principal-only and the balloon carries the accrued interest instead.\n sfPayment = calculatePMT(sellerFinanced, 0, amortizationYears);\n if (interestPaymentMode === \"simple_payout\") {\n performancePayout = sellerFinanced * interestRate * balloonYears;\n } else if (interestPaymentMode === \"compound_payout\") {\n performancePayout = sellerFinanced * (Math.pow(1 + interestRate, balloonYears) - 1);\n }\n }\n\n // Mirrors the property engine: amortized down in standard mode, full principal\n // in the payout modes, with the deferred interest added on top.\n const baseBalloonBalance = interestPaymentMode === \"standard\"\n ? calculateBalloonBalance(sellerFinanced, interestRate, amortizationYears, balloonYears)\n : sellerFinanced;\n\n return {\n balloonBalance: baseBalloonBalance + performancePayout,\n performancePayout,\n sellerFinanced,\n sfPayment,\n totalPayments: sfPayment * 12 * balloonYears,\n };\n}\n\n/**\n * The debt-service gate. Does the business's earnings cover both loans it is bought\n * with, after a professional manager is installed?\n *\n * Two legs, each priced the way the deal is actually financed:\n * - DSCR loan: principal + interest on the collateral advance (the down payment),\n * amortized at the commercial rate/term. This is real money out every month.\n * - Seller carry: principal only (0%), spread over the business carry term. The\n * seller finances whatever the advance did not cover.\n *\n * The manager is the reason EBITDA and SDE are not interchangeable here. EBITDA is\n * already struck after management, so it is used as-is. SDE (and the SDE-shaped\n * \"cash flow\" figure most listings publish) still pays the owner, so a hired\n * manager's cost comes out first — 12.5% of revenue, floored at $60K so a tiny\n * business still budgets a real salary and capped at $120K so a large one does not\n * hand the whole upside to a GM.\n *\n * margin is that leftover cash flow over the price offered — the honest efficiency\n * metric, because this structure puts no buyer cash in and cash-on-cash is infinite.\n *\n * Returns null when there is no earnings figure or no price to gate: a zeroed\n * verdict would read as \"fails\", and a business with no published earnings has not\n * failed, it is simply un-underwritable until a figure exists.\n *\n * @param {object} inputs\n * @param {number} inputs.earnings - Annual earnings the deal is bought on\n * @param {string|null} inputs.earningsSource - \"ebitda\" | \"sde\" | \"cash_flow\" | null\n * @param {number} inputs.downPayment - Collateral advance = the DSCR loan principal\n * @param {number} inputs.priceOffered - The offer being gated (its seller carry = price − advance)\n * @param {number} [inputs.grossRevenue] - Drives the manager cost on non-EBITDA earnings\n * @param {number} [inputs.ebitdaLegAmount=0] - The EBITDA slice of the advance; > 0 flags cash-flow lending\n * @param {number} [inputs.offerHigh] - Ceiling offer, for the below-asking highlight\n * @param {number} [inputs.askingPrice] - For the below-asking highlight\n * @param {number} [inputs.dscrRate] - DSCR annual rate (default 10%)\n * @param {number} [inputs.dscrAmortizationYears] - DSCR term (default 25)\n * @param {number} [inputs.sellerAmortizationYears] - Seller carry term (default 60)\n * @param {number} [inputs.sellerRate] - Seller carry rate (default 0%)\n * @param {number} [inputs.balloonYears] - Seller carry balloon (default from FINANCIAL_CONSTANTS)\n * @returns {object|null} manager cost, both leg payments, blended rate, cash flow + margin (monthly/annual), pass, pills\n */\nexport function calculateBusinessCashFlow(inputs = {}) {\n const {\n DSCR_AMORTIZATION_YEARS,\n DSCR_INTEREST_RATE,\n MANAGER_CEILING,\n MANAGER_FLOOR,\n MANAGER_RATE,\n SELLER_AMORTIZATION_YEARS,\n } = BUSINESS_UNDERWRITING_CONSTANTS;\n\n const {\n askingPrice = null,\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\n dscrAmortizationYears = DSCR_AMORTIZATION_YEARS,\n dscrRate = DSCR_INTEREST_RATE,\n earningsSource = null,\n ebitdaLegAmount = 0,\n offerHigh = null,\n sellerAmortizationYears = SELLER_AMORTIZATION_YEARS,\n sellerRate = FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE,\n } = inputs;\n\n const price = toNumber(inputs.priceOffered);\n const earnings = toNumber(inputs.earnings);\n if (price === null || price <= 0 || earnings === null) return null;\n\n const advance = toNonNegative(inputs.downPayment) ?? 0;\n\n // EBITDA is already net of a manager; everything else still pays the owner, so the\n // manager comes out first. No revenue reported falls to the floor, never to zero.\n const managerCost =\n earningsSource === \"ebitda\"\n ? 0\n : Math.min(MANAGER_CEILING, Math.max(MANAGER_FLOOR, (toNonNegative(inputs.grossRevenue) ?? 0) * MANAGER_RATE));\n const adjustedEarnings = earnings - managerCost;\n\n const dscrPaymentMonthly = calculatePMT(advance, dscrRate, dscrAmortizationYears);\n const { sfPayment: sellerPaymentMonthly, sellerFinanced } = calculateBusinessSellerFinance({\n amortizationYears: sellerAmortizationYears,\n balloonYears,\n downPayment: advance,\n interestRate: sellerRate,\n priceOffered: price,\n });\n\n const dscrPaymentAnnual = dscrPaymentMonthly * 12;\n const sellerPaymentAnnual = sellerPaymentMonthly * 12;\n\n // What the whole capital stack costs, weighted by how much of it each leg is. The\n // property engine blends the same two rates the same way, so a business and a rental\n // quote a comparable number.\n const financed = advance + (sellerFinanced ?? 0);\n const blendedRate = financed > 0 ? (advance * dscrRate + (sellerFinanced ?? 0) * sellerRate) / financed : 0;\n const cashFlowAnnual = adjustedEarnings - dscrPaymentAnnual - sellerPaymentAnnual;\n const cashFlowMonthly = cashFlowAnnual / 12;\n\n const pass = cashFlowAnnual >= 0;\n const ebitdaFinanced = (toNonNegative(ebitdaLegAmount) ?? 0) > 0;\n const ceiling = toNumber(offerHigh);\n const asking = toNumber(askingPrice);\n const offerBelowAsking = ceiling !== null && asking !== null && asking > 0 && ceiling < asking;\n\n // Red flags, most damaging first. offerBelowAsking is a separate soft highlight,\n // not a pill — it does not disqualify the deal, it just says come in under asking.\n const pills = [];\n if (!pass) pills.push(\"Cash Flow\");\n if (ebitdaFinanced) pills.push(\"EBITDA financed\");\n\n return {\n adjustedEarnings,\n blendedRate,\n cashFlowAnnual,\n cashFlowMonthly,\n dscrPaymentAnnual,\n dscrPaymentMonthly,\n ebitdaFinanced,\n flagged: !pass || ebitdaFinanced,\n managerCost,\n marginAnnual: cashFlowAnnual / price,\n marginMonthly: cashFlowMonthly / price,\n offerBelowAsking,\n pass,\n pills,\n price,\n sellerFinanced,\n sellerPaymentAnnual,\n sellerPaymentMonthly,\n };\n}\n\n/**\n * Full underwrite for one listing: resolve earnings, size the down payment from\n * collateral, and bound the offer. The seller carries the balance of the ceiling\n * offer as preferred equity.\n *\n * collateralShortfall is set when the collateral-driven down payment exceeds the\n * offer itself — a real signal that the asset value has outrun what the earnings\n * multiple justifies. It is surfaced, never clamped away.\n *\n * downPaymentPercent lets an analyst answer the collateral back: set it and the down\n * payment is that share of the price instead of the sum of the advances.\n *\n * @param {object} listing - Scraped/stored figures (snake_case or camelCase)\n * @param {number} [listing.downPaymentPercent] - Decimal share of the price to put down, overriding collateral\n * @returns {object} earnings, earningsSource, downPayment, downPaymentPercent, downPaymentSource, collateralDownPayment, legs, offerLow, offerHigh, sellerCarry, collateralShortfall\n */\nexport function underwriteBusinessListing(listing = {}) {\n const realEstateValue = listing.realEstateValue ?? listing.real_estate_value;\n const ffeValue = listing.ffeValue ?? listing.ff_e_value;\n const inventoryValue = listing.inventoryValue ?? listing.inventory_value;\n const ebitda = listing.ebitda;\n\n const { earnings, source } = resolveBusinessEarnings({\n cashFlow: listing.cashFlow ?? listing.cash_flow,\n ebitda,\n sde: listing.sde,\n });\n\n // The collateral stack covers everything being acquired, so it takes no include_*\n // flag — those govern only what is ADDED to the offer range. Its one switch is\n // collateralizeEbitda, which has no offer-range counterpart.\n const { downPayment: collateralDownPayment, legs } = calculateBusinessDownPayment({\n collateralizeEbitda: listing.collateralizeEbitda ?? listing.collateralize_ebitda ?? true,\n ebitda,\n ffeValue,\n inventoryValue,\n realEstateValue,\n });\n\n const { assetsIncluded, offerHigh, offerLow, offerMid } = calculateBusinessOffer({\n earnings,\n ffeValue,\n includeFfe: listing.includeFfe ?? listing.include_ff_e ?? true,\n includeInventory: listing.includeInventory ?? listing.include_inventory ?? true,\n includeRealEstate: listing.includeRealEstate ?? listing.include_real_estate ?? true,\n inventoryValue,\n realEstateValue,\n });\n\n const earningsSource = listing.earningsSource ?? listing.earnings_source ?? source;\n\n // The price every derived figure is struck against: the LOI price when one has been\n // chosen, otherwise the end of the range being looked at (the ceiling by default,\n // which is the thinnest margin).\n const offerEnd = listing.offerEnd ?? listing.offer_end ?? \"high\";\n const explicitPrice = toNumber(listing.priceOffered ?? listing.price_offered);\n const gatePrice =\n explicitPrice ?? { high: offerHigh, low: offerLow, mid: offerMid }[offerEnd] ?? offerHigh;\n\n // A hand-set down payment percentage replaces the collateral stack for this listing.\n // The legs are still returned — they explain what the assets would have advanced — but\n // they stop sizing the deal: the down payment IS the DSCR loan principal, so an override\n // moves the loan, the seller carry, and the cash-flow gate with it. Cleared, the\n // collateral answers again.\n const overridePercent = toNumber(listing.downPaymentPercent ?? listing.down_payment_percent);\n const hasOverride = overridePercent !== null && overridePercent > 0 && gatePrice !== null;\n const downPayment = hasOverride ? gatePrice * overridePercent : collateralDownPayment;\n\n // Terms for the price actually offered. Kept separate from the range figures\n // above so there is no mistaking the deal being made for the ceiling that\n // bounds it — sellerCarry is the carry at offerHigh, terms.sellerFinanced the\n // carry at the price on the LOI.\n const terms = calculateBusinessSellerFinance({\n amortizationYears: listing.sellerAmortization ?? listing.seller_amortization ?? undefined,\n balloonYears: listing.balloonLength ?? listing.balloon_length ?? undefined,\n downPayment,\n interestPaymentMode: listing.interestPaymentMode ?? listing.interest_payment_mode ?? \"standard\",\n interestRate: listing.sellerFiRate ?? listing.seller_fi_rate ?? undefined,\n priceOffered: listing.priceOffered ?? listing.price_offered,\n });\n\n // The debt-service gate, priced at gatePrice above.\n const cashFlow = calculateBusinessCashFlow({\n askingPrice: listing.askingPrice ?? listing.asking_price,\n downPayment,\n earnings,\n earningsSource,\n ebitdaLegAmount: legs.ebitda,\n grossRevenue: listing.grossRevenue ?? listing.gross_revenue,\n offerHigh,\n priceOffered: gatePrice,\n });\n\n return {\n assetsIncluded,\n cashFlow,\n collateralDownPayment,\n collateralShortfall: offerHigh !== null && downPayment > offerHigh,\n downPayment,\n downPaymentPercent: gatePrice ? downPayment / gatePrice : null,\n downPaymentSource: hasOverride ? \"override\" : \"collateral\",\n earnings,\n earningsSource,\n legs,\n offerEnd,\n offerHigh,\n offerLow,\n offerMid,\n priceOffered: toNumber(listing.priceOffered ?? listing.price_offered),\n sellerCarry: offerHigh === null ? null : offerHigh - downPayment,\n terms,\n };\n}\n"],"names":["BUSINESS_UNDERWRITING_CONSTANTS","DSCR_AMORTIZATION_YEARS","DSCR_INTEREST_RATE","EBITDA_ADVANCE_RATE","EBITDA_ADVANCE_THRESHOLD","FF_E_ADVANCE_RATE","INVENTORY_ADVANCE_RATE","MANAGER_CEILING","MANAGER_FLOOR","MANAGER_RATE","OFFER_MULTIPLE_HIGH","OFFER_MULTIPLE_LOW","OFFER_MULTIPLE_MID","REAL_ESTATE_ADVANCE_RATE","SELLER_AMORTIZATION_YEARS","toNumber","value","numeric","parseFloat","String","replace","Number","isFinite","toNonNegative","resolveBusinessEarnings","listing","ebitda","earnings","source","sde","cashFlow","cash_flow","shouldIncludeRealEstate","inputs","statedInclusion","askingPrice","realEstateValue","calculateBusinessDownPayment","collateralizeEbitda","ffeValue","inventoryValue","legs","ffe","inventory","realEstate","downPayment","calculateBusinessOffer","includeFfe","includeInventory","includeRealEstate","assetsIncluded","offerHigh","offerLow","offerMid","calculateBusinessSellerFinance","amortizationYears","FINANCIAL_CONSTANTS","SELLER_FI_AMORTIZATION","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","interestPaymentMode","interestRate","SELLER_FI_INTEREST_RATE","priceOffered","balloonBalance","performancePayout","sellerFinanced","sfPayment","totalPayments","Math","max","calculatePMT","pow","calculateBalloonBalance","calculateBusinessCashFlow","dscrAmortizationYears","dscrRate","earningsSource","ebitdaLegAmount","sellerAmortizationYears","sellerRate","price","advance","managerCost","min","grossRevenue","adjustedEarnings","dscrPaymentMonthly","sellerPaymentMonthly","dscrPaymentAnnual","sellerPaymentAnnual","financed","blendedRate","cashFlowAnnual","cashFlowMonthly","pass","ebitdaFinanced","ceiling","asking","offerBelowAsking","pills","push","flagged","marginAnnual","marginMonthly","underwriteBusinessListing","real_estate_value","ff_e_value","inventory_value","collateralDownPayment","collateralize_ebitda","include_ff_e","include_inventory","include_real_estate","earnings_source","offerEnd","offer_end","gatePrice","price_offered","high","low","mid","overridePercent","downPaymentPercent","down_payment_percent","hasOverride","terms","sellerAmortization","seller_amortization","undefined","balloonLength","balloon_length","interest_payment_mode","sellerFiRate","seller_fi_rate","asking_price","gross_revenue","collateralShortfall","downPaymentSource","sellerCarry"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOfferPrice,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,calculateSellerFinanceOffer,equityPercentFromDebt,resolveListingFinancials,safePercentage}from"./financial/calculations.js";export{EQUITY_CARRY_TIERS,calculateEquityCarryScore}from"./financial/equity-carry.js";export{BUSINESS_UNDERWRITING_CONSTANTS,calculateBusinessDownPayment,calculateBusinessOffer,calculateBusinessSellerFinance,resolveBusinessEarnings,shouldIncludeRealEstate,underwriteBusinessListing}from"./financial/business-underwriting.js";export{fetchDebt}from"./services/debt.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType}from"./export/export-logic.js";export{calculateDOM,formatDate}from"./date/utilities.js";export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput}from"./formatting/financial-formatting.js";export{normalizeWhitespace}from"./formatting/text.js";export{CALCULATION_TOLERANCE,DEFAULT_CAP_RATE,DEFAULT_DOWN_PAYMENT,DEFAULT_DSCR_PERCENTAGE,DEFAULT_EQUITY_ESTIMATE,DEFAULT_INTEREST_RATE_TYPE,FINANCIAL_CONSTANTS,INTEREST_RATE_TIERS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE,determineInterestRateType}from"./config/financial.js";export{ASSISTED_LIVING,MULTIFAMILY,PROPERTY_TYPES,PROPERTY_TYPE_CONSTANTS,STR}from"./config/property-types.js";export{ASSIGNMENT_FEE_PERCENTAGE,BUSINESS_CONSTANTS,BUYER_AGENT_COMMISSION,CASH_OFFER_ASSIGNMENT_PERCENTAGE,CASH_OFFER_ROUNDING,CLOSING_COSTS_PERCENTAGE,CONSERVATIVE_COCR15_PRICE_MULTIPLIER,HARD_MONEY_RATE,MAX_COCR15_PRICE_MULTIPLIER,MINIMUM_COCR15_PRICE,NET_TO_BUYER_PERCENTAGE,REHAB_RATE,SELLER_AGENT_COMMISSION,SELLER_FINANCE_MAX_DOWN_PERCENT}from"./config/business.js";export{lookupLOI}from"./services/loi-lookup.js";export{LOI_LOOKUP_CONFIG,LOI_SENT_STATUS,MATCH_TYPES}from"./config/loi-lookup.js";export{getEnvVar,isBrowserEnvironment,isNodeEnvironment}from"./environment/utilities.js";const e="./dist/styles/base.css";export{e as STYLES_PATH};
|
|
1
|
+
export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOfferPrice,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,calculateSellerFinanceOffer,equityPercentFromDebt,resolveListingFinancials,safePercentage}from"./financial/calculations.js";export{EQUITY_CARRY_TIERS,calculateEquityCarryScore}from"./financial/equity-carry.js";export{BUSINESS_UNDERWRITING_CONSTANTS,calculateBusinessCashFlow,calculateBusinessDownPayment,calculateBusinessOffer,calculateBusinessSellerFinance,resolveBusinessEarnings,shouldIncludeRealEstate,underwriteBusinessListing}from"./financial/business-underwriting.js";export{fetchDebt}from"./services/debt.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType}from"./export/export-logic.js";export{calculateDOM,formatDate}from"./date/utilities.js";export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput}from"./formatting/financial-formatting.js";export{normalizeWhitespace}from"./formatting/text.js";export{CALCULATION_TOLERANCE,DEFAULT_CAP_RATE,DEFAULT_DOWN_PAYMENT,DEFAULT_DSCR_PERCENTAGE,DEFAULT_EQUITY_ESTIMATE,DEFAULT_INTEREST_RATE_TYPE,FINANCIAL_CONSTANTS,INTEREST_RATE_TIERS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE,determineInterestRateType}from"./config/financial.js";export{ASSISTED_LIVING,MULTIFAMILY,PROPERTY_TYPES,PROPERTY_TYPE_CONSTANTS,STR}from"./config/property-types.js";export{ASSIGNMENT_FEE_PERCENTAGE,BUSINESS_CONSTANTS,BUYER_AGENT_COMMISSION,CASH_OFFER_ASSIGNMENT_PERCENTAGE,CASH_OFFER_ROUNDING,CLOSING_COSTS_PERCENTAGE,CONSERVATIVE_COCR15_PRICE_MULTIPLIER,HARD_MONEY_RATE,MAX_COCR15_PRICE_MULTIPLIER,MINIMUM_COCR15_PRICE,NET_TO_BUYER_PERCENTAGE,REHAB_RATE,SELLER_AGENT_COMMISSION,SELLER_FINANCE_MAX_DOWN_PERCENT}from"./config/business.js";export{lookupLOI}from"./services/loi-lookup.js";export{LOI_LOOKUP_CONFIG,LOI_SENT_STATUS,MATCH_TYPES}from"./config/loi-lookup.js";export{getEnvVar,isBrowserEnvironment,isNodeEnvironment}from"./environment/utilities.js";const e="./dist/styles/base.css";export{e as STYLES_PATH};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\r\n * @archerjessop/utilities\r\n * Shared utilities for ArcherJessop property analysis tools\r\n */\r\n\r\n// Financial calculations\r\nexport { \r\n calculateAppreciatedValue,\r\n calculateAssignmentFee,\r\n calculateBalloonBalance,\r\n calculateCashFlow,\r\n calculateCashFlowYield,\r\n calculateCashOfferPrice,\r\n calculateCashOutAfterRefi,\r\n calculateCOCR30, \r\n calculateCOCRAtPercent,\r\n calculateDiscountFromPrice,\r\n calculateNetToBuyer,\r\n calculateNOIByType,\r\n calculatePMT,\r\n calculatePriceForCOCR,\r\n calculatePriceFromDiscount,\r\n calculateSellerFinanceOffer,\r\n calculateSTRNOI,\r\n equityPercentFromDebt,\r\n resolveListingFinancials,\r\n safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Equity Carry scoring engine (pure; scores scraped listings into deal pools)\r\nexport { EQUITY_CARRY_TIERS, calculateEquityCarryScore } from \"./financial/equity-carry.js\";\r\n\r\n// Business-backed-by-real-estate underwriting (pure; offer range + collateral down payment)\r\nexport {\r\n BUSINESS_UNDERWRITING_CONSTANTS,\r\n calculateBusinessDownPayment,\r\n calculateBusinessOffer,\r\n calculateBusinessSellerFinance,\r\n resolveBusinessEarnings,\r\n shouldIncludeRealEstate,\r\n underwriteBusinessListing,\r\n} from \"./financial/business-underwriting.js\";\r\n\r\n// Agnostic debt service (pure IO; Node + browser)\r\nexport { fetchDebt } from \"./services/debt.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Export logic (pure export-object creation)\r\nexport {\r\n calculateOriginalPrice,\r\n convertCapRateToDecimal,\r\n createExportObjectCore,\r\n formatDownPaymentPercent,\r\n mapPropertyType,\r\n} from \"./export/export-logic.js\";\r\n\r\n// Date utilities\r\nexport { calculateDOM, formatDate } from \"./date/utilities.js\";\r\n\r\n// Formatting utilities\r\nexport { \r\n calculateCursorPosition,\r\n extractNumericValue,\r\n filterNumericInput,\r\n formatInputDisplay,\r\n formatLiveInput,\r\n formatLiveNumber,\r\n parseNumericInput\r\n} from \"./formatting/financial-formatting.js\";\r\n\r\n// Text formatting utilities\r\nexport { normalizeWhitespace } from \"./formatting/text.js\";\r\n\r\n// Configuration constants\r\nexport * from \"./config/financial.js\";\r\nexport * from \"./config/property-types.js\";\r\nexport * from \"./config/business.js\";\r\n\r\nexport const STYLES_PATH = \"./dist/styles/base.css\";\r\n\r\n// LOI Lookup service and config\r\nexport { lookupLOI } from \"./services/loi-lookup.js\";\r\nexport { LOI_LOOKUP_CONFIG, MATCH_TYPES, LOI_SENT_STATUS } from \"./config/loi-lookup.js\";\r\n\r\n// Environment utilities\r\nexport { \r\n getEnvVar, \r\n isNodeEnvironment, \r\n isBrowserEnvironment \r\n} from \"./environment/utilities.js\";"],"names":["STYLES_PATH"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\r\n * @archerjessop/utilities\r\n * Shared utilities for ArcherJessop property analysis tools\r\n */\r\n\r\n// Financial calculations\r\nexport { \r\n calculateAppreciatedValue,\r\n calculateAssignmentFee,\r\n calculateBalloonBalance,\r\n calculateCashFlow,\r\n calculateCashFlowYield,\r\n calculateCashOfferPrice,\r\n calculateCashOutAfterRefi,\r\n calculateCOCR30, \r\n calculateCOCRAtPercent,\r\n calculateDiscountFromPrice,\r\n calculateNetToBuyer,\r\n calculateNOIByType,\r\n calculatePMT,\r\n calculatePriceForCOCR,\r\n calculatePriceFromDiscount,\r\n calculateSellerFinanceOffer,\r\n calculateSTRNOI,\r\n equityPercentFromDebt,\r\n resolveListingFinancials,\r\n safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Equity Carry scoring engine (pure; scores scraped listings into deal pools)\r\nexport { EQUITY_CARRY_TIERS, calculateEquityCarryScore } from \"./financial/equity-carry.js\";\r\n\r\n// Business-backed-by-real-estate underwriting (pure; offer range + collateral down payment)\r\nexport {\r\n BUSINESS_UNDERWRITING_CONSTANTS,\r\n calculateBusinessCashFlow,\r\n calculateBusinessDownPayment,\r\n calculateBusinessOffer,\r\n calculateBusinessSellerFinance,\r\n resolveBusinessEarnings,\r\n shouldIncludeRealEstate,\r\n underwriteBusinessListing,\r\n} from \"./financial/business-underwriting.js\";\r\n\r\n// Agnostic debt service (pure IO; Node + browser)\r\nexport { fetchDebt } from \"./services/debt.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Export logic (pure export-object creation)\r\nexport {\r\n calculateOriginalPrice,\r\n convertCapRateToDecimal,\r\n createExportObjectCore,\r\n formatDownPaymentPercent,\r\n mapPropertyType,\r\n} from \"./export/export-logic.js\";\r\n\r\n// Date utilities\r\nexport { calculateDOM, formatDate } from \"./date/utilities.js\";\r\n\r\n// Formatting utilities\r\nexport { \r\n calculateCursorPosition,\r\n extractNumericValue,\r\n filterNumericInput,\r\n formatInputDisplay,\r\n formatLiveInput,\r\n formatLiveNumber,\r\n parseNumericInput\r\n} from \"./formatting/financial-formatting.js\";\r\n\r\n// Text formatting utilities\r\nexport { normalizeWhitespace } from \"./formatting/text.js\";\r\n\r\n// Configuration constants\r\nexport * from \"./config/financial.js\";\r\nexport * from \"./config/property-types.js\";\r\nexport * from \"./config/business.js\";\r\n\r\nexport const STYLES_PATH = \"./dist/styles/base.css\";\r\n\r\n// LOI Lookup service and config\r\nexport { lookupLOI } from \"./services/loi-lookup.js\";\r\nexport { LOI_LOOKUP_CONFIG, MATCH_TYPES, LOI_SENT_STATUS } from \"./config/loi-lookup.js\";\r\n\r\n// Environment utilities\r\nexport { \r\n getEnvVar, \r\n isNodeEnvironment, \r\n isBrowserEnvironment \r\n} from \"./environment/utilities.js\";"],"names":["STYLES_PATH"],"mappings":"04EAiFY,MAACA,EAAc"}
|