@archerjessop/utilities 4.6.30 → 7.0.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
- const E={DSCR_INTEREST_RATE:.075,SELLER_FI_INTEREST_RATE:0,DSCR_AMORTIZATION:30,SELLER_FI_AMORTIZATION:30,DEFAULT_BALLOON_PERIOD_YEARS:7,DEFAULT_DOWN_PAYMENT:.3,SELLER_FI_DOWN_PAYMENT:.6,SELLER_FI_CARRY:.4,MIN_DOWN_PAYMENT_PERCENT:0,DEFAULT_EQUITY_ESTIMATE:.4,DEFAULT_DSCR_PERCENTAGE:.7,MAX_DSCR_PERCENTAGE:.7,DEFAULT_CAP_RATE:.05,MAX_ESTIMATED_CAP_RATE:25,APPRECIATION_RATE:.045,NOI_APPRECIATION:.02},{CALCULATION_TOLERANCE:_,DEFAULT_CAP_RATE:T,DEFAULT_DOWN_PAYMENT:A,DEFAULT_DSCR_PERCENTAGE:R,DEFAULT_EQUITY_ESTIMATE:I,DSCR_AMORTIZATION:L,DSCR_INTEREST_RATE:N,MAX_ITERATIONS:S,SELLER_FI_AMORTIZATION:D,SELLER_FI_CARRY:O,SELLER_FI_DOWN_PAYMENT:C,SELLER_FI_INTEREST_RATE:F}=E;export{_ as CALCULATION_TOLERANCE,T as DEFAULT_CAP_RATE,A as DEFAULT_DOWN_PAYMENT,R as DEFAULT_DSCR_PERCENTAGE,I as DEFAULT_EQUITY_ESTIMATE,L as DSCR_AMORTIZATION,N as DSCR_INTEREST_RATE,E as FINANCIAL_CONSTANTS,S as MAX_ITERATIONS,D as SELLER_FI_AMORTIZATION,O as SELLER_FI_CARRY,C as SELLER_FI_DOWN_PAYMENT,F as SELLER_FI_INTEREST_RATE};
1
+ const E={DEFAULT_INTEREST_RATE_TYPE:"dscr_residential",INTEREST_RATE_TIERS:{commercial:{amortization:25,closingRange:"45-60 days",inspectionDays:60,label:"Commercial",rate:.1},dscr_commercial:{amortization:30,closingRange:"45-60 days",inspectionDays:60,label:"DSCR Commercial",rate:.1},dscr_residential:{amortization:30,closingRange:"45-60 days",inspectionDays:30,label:"DSCR Residential",rate:.08},mixed_use:{amortization:25,closingRange:"45-60 days",inspectionDays:60,label:"Mixed Use",rate:.1},rv_park:{amortization:25,closingRange:"45-60 days",inspectionDays:60,label:"RV Park",rate:.11}},SELLER_FI_INTEREST_RATE:0,SELLER_FI_AMORTIZATION:30,DEFAULT_BALLOON_PERIOD_YEARS:7,DEFAULT_DOWN_PAYMENT:.3,SELLER_FI_DOWN_PAYMENT:.6,SELLER_FI_CARRY:.4,MIN_DOWN_PAYMENT_PERCENT:0,DEFAULT_EQUITY_ESTIMATE:.4,DEFAULT_DSCR_PERCENTAGE:.7,MAX_DSCR_PERCENTAGE:.7,DEFAULT_CAP_RATE:.05,MAX_ESTIMATED_CAP_RATE:25,APPRECIATION_RATE:.045,NOI_APPRECIATION:.02},{CALCULATION_TOLERANCE:_,DEFAULT_CAP_RATE:T,DEFAULT_DOWN_PAYMENT:e,DEFAULT_DSCR_PERCENTAGE:R,DEFAULT_EQUITY_ESTIMATE:A,DEFAULT_INTEREST_RATE_TYPE:a,INTEREST_RATE_TIERS:i,MAX_ITERATIONS:r,SELLER_FI_AMORTIZATION:s,SELLER_FI_CARRY:n,SELLER_FI_DOWN_PAYMENT:t,SELLER_FI_INTEREST_RATE:I}=E;function determineInterestRateType(E,_=4){return"rv_park"===E?"rv_park":"mixed_use"===E?"mixed_use":"business"===E||"commercial"===E?"commercial":("mfr"===E||"multifamily"===E)&&_>=5?"dscr_commercial":"dscr_residential"}export{_ as CALCULATION_TOLERANCE,T as DEFAULT_CAP_RATE,e as DEFAULT_DOWN_PAYMENT,R as DEFAULT_DSCR_PERCENTAGE,A as DEFAULT_EQUITY_ESTIMATE,a as DEFAULT_INTEREST_RATE_TYPE,E as FINANCIAL_CONSTANTS,i as INTEREST_RATE_TIERS,r as MAX_ITERATIONS,s as SELLER_FI_AMORTIZATION,n as SELLER_FI_CARRY,t as SELLER_FI_DOWN_PAYMENT,I as SELLER_FI_INTEREST_RATE,determineInterestRateType};
2
2
  //# sourceMappingURL=financial.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"financial.js","sources":["../../src/config/financial.js"],"sourcesContent":["/**\r\n * Financial calculation constants\r\n */\r\n\r\nexport const FINANCIAL_CONSTANTS = {\r\n // Interest rates\r\n DSCR_INTEREST_RATE: 0.075, // 7.5%\r\n SELLER_FI_INTEREST_RATE: 0.0, // 0% for seller financing\r\n \r\n // Loan terms and amortization\r\n DSCR_AMORTIZATION: 30, // 30 years\r\n SELLER_FI_AMORTIZATION: 30, // 30 years\r\n DEFAULT_BALLOON_PERIOD_YEARS: 7, // 7 years\r\n \r\n // Down payments and financing structure\r\n DEFAULT_DOWN_PAYMENT: 0.30, // 30%\r\n SELLER_FI_DOWN_PAYMENT: 0.60, // 60%\r\n SELLER_FI_CARRY: 0.40, // 40% seller financing\r\n MIN_DOWN_PAYMENT_PERCENT: 0, // 0% minimum down\r\n DEFAULT_EQUITY_ESTIMATE: 0.40, // 40% seller equity\r\n \r\n // DSCR loan percentages\r\n DEFAULT_DSCR_PERCENTAGE: 0.70, // 70%\r\n MAX_DSCR_PERCENTAGE: 0.70, // Maximum DSCR loan\r\n \r\n // Cap rates\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n MAX_ESTIMATED_CAP_RATE: 25, // 20% maximum cap rate\r\n \r\n // Market assumptions\r\n APPRECIATION_RATE: 0.045, // 4.5% annual appreciation\r\n NOI_APPRECIATION: 0.02, // 2% annual NOI appreciation\r\n};\r\n\r\n// Convenience exports for common calculations\r\nexport const {\r\n CALCULATION_TOLERANCE,\r\n DEFAULT_CAP_RATE,\r\n DEFAULT_DOWN_PAYMENT,\r\n DEFAULT_DSCR_PERCENTAGE,\r\n DEFAULT_EQUITY_ESTIMATE,\r\n DSCR_AMORTIZATION,\r\n DSCR_INTEREST_RATE,\r\n MAX_ITERATIONS,\r\n SELLER_FI_AMORTIZATION,\r\n SELLER_FI_CARRY,\r\n SELLER_FI_DOWN_PAYMENT,\r\n SELLER_FI_INTEREST_RATE,\r\n} = FINANCIAL_CONSTANTS;\r\n"],"names":["FINANCIAL_CONSTANTS","DSCR_INTEREST_RATE","SELLER_FI_INTEREST_RATE","DSCR_AMORTIZATION","SELLER_FI_AMORTIZATION","DEFAULT_BALLOON_PERIOD_YEARS","DEFAULT_DOWN_PAYMENT","SELLER_FI_DOWN_PAYMENT","SELLER_FI_CARRY","MIN_DOWN_PAYMENT_PERCENT","DEFAULT_EQUITY_ESTIMATE","DEFAULT_DSCR_PERCENTAGE","MAX_DSCR_PERCENTAGE","DEFAULT_CAP_RATE","MAX_ESTIMATED_CAP_RATE","APPRECIATION_RATE","NOI_APPRECIATION","CALCULATION_TOLERANCE","MAX_ITERATIONS"],"mappings":"AAIY,MAACA,EAAsB,CAEjCC,mBAAoB,KACpBC,wBAAyB,EAGzBC,kBAAmB,GACnBC,uBAAwB,GACxBC,6BAA8B,EAG9BC,qBAAsB,GACtBC,uBAAwB,GACxBC,gBAAiB,GACjBC,yBAA0B,EAC1BC,wBAAyB,GAGzBC,wBAAyB,GACzBC,oBAAqB,GAGrBC,iBAAkB,IAClBC,uBAAwB,GAGxBC,kBAAmB,KACnBC,iBAAkB,MAIPC,sBACXA,EAAqBJ,iBACrBA,EAAgBP,qBAChBA,EAAoBK,wBACpBA,EAAuBD,wBACvBA,EAAuBP,kBACvBA,EAAiBF,mBACjBA,EAAkBiB,eAClBA,EAAcd,uBACdA,EAAsBI,gBACtBA,EAAeD,uBACfA,EAAsBL,wBACtBA,GACEF"}
1
+ {"version":3,"file":"financial.js","sources":["../../src/config/financial.js"],"sourcesContent":["/**\r\n * Financial calculation constants\r\n */\r\n\r\nexport const FINANCIAL_CONSTANTS = {\r\n // Interest rate tier system (replaces old DSCR_INTEREST_RATE / DSCR_AMORTIZATION)\r\n DEFAULT_INTEREST_RATE_TYPE: \"dscr_residential\",\r\n INTEREST_RATE_TIERS: {\r\n commercial: { amortization: 25, closingRange: \"45-60 days\", inspectionDays: 60, label: \"Commercial\", rate: 0.10 },\r\n dscr_commercial: { amortization: 30, closingRange: \"45-60 days\", inspectionDays: 60, label: \"DSCR Commercial\", rate: 0.10 },\r\n dscr_residential: { amortization: 30, closingRange: \"45-60 days\", inspectionDays: 30, label: \"DSCR Residential\", rate: 0.08 },\r\n mixed_use: { amortization: 25, closingRange: \"45-60 days\", inspectionDays: 60, label: \"Mixed Use\", rate: 0.10 },\r\n rv_park: { amortization: 25, closingRange: \"45-60 days\", inspectionDays: 60, label: \"RV Park\", rate: 0.11 },\r\n },\r\n\r\n // Seller financing\r\n SELLER_FI_INTEREST_RATE: 0.0, // 0% for seller financing\r\n SELLER_FI_AMORTIZATION: 30, // 30 years\r\n\r\n // Loan terms\r\n DEFAULT_BALLOON_PERIOD_YEARS: 7, // 7 years\r\n\r\n // Down payments and financing structure\r\n DEFAULT_DOWN_PAYMENT: 0.30, // 30%\r\n SELLER_FI_DOWN_PAYMENT: 0.60, // 60%\r\n SELLER_FI_CARRY: 0.40, // 40% seller financing\r\n MIN_DOWN_PAYMENT_PERCENT: 0, // 0% minimum down\r\n DEFAULT_EQUITY_ESTIMATE: 0.40, // 40% seller equity\r\n\r\n // DSCR loan percentages\r\n DEFAULT_DSCR_PERCENTAGE: 0.70, // 70%\r\n MAX_DSCR_PERCENTAGE: 0.70, // Maximum DSCR loan\r\n\r\n // Cap rates\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n MAX_ESTIMATED_CAP_RATE: 25, // 20% maximum cap rate\r\n\r\n // Market assumptions\r\n APPRECIATION_RATE: 0.045, // 4.5% annual appreciation\r\n NOI_APPRECIATION: 0.02, // 2% annual NOI appreciation\r\n};\r\n\r\n// Convenience exports for common calculations\r\nexport const {\r\n CALCULATION_TOLERANCE,\r\n DEFAULT_CAP_RATE,\r\n DEFAULT_DOWN_PAYMENT,\r\n DEFAULT_DSCR_PERCENTAGE,\r\n DEFAULT_EQUITY_ESTIMATE,\r\n DEFAULT_INTEREST_RATE_TYPE,\r\n INTEREST_RATE_TIERS,\r\n MAX_ITERATIONS,\r\n SELLER_FI_AMORTIZATION,\r\n SELLER_FI_CARRY,\r\n SELLER_FI_DOWN_PAYMENT,\r\n SELLER_FI_INTEREST_RATE,\r\n} = FINANCIAL_CONSTANTS;\r\n\r\nexport function determineInterestRateType(propertyType, numberOfUnits = 4) {\r\n if (propertyType === \"rv_park\") return \"rv_park\";\r\n if (propertyType === \"mixed_use\") return \"mixed_use\";\r\n if (propertyType === \"business\" || propertyType === \"commercial\") return \"commercial\";\r\n if ((propertyType === \"mfr\" || propertyType === \"multifamily\") && numberOfUnits >= 5) return \"dscr_commercial\";\r\n return \"dscr_residential\";\r\n}\r\n"],"names":["FINANCIAL_CONSTANTS","DEFAULT_INTEREST_RATE_TYPE","INTEREST_RATE_TIERS","commercial","amortization","closingRange","inspectionDays","label","rate","dscr_commercial","dscr_residential","mixed_use","rv_park","SELLER_FI_INTEREST_RATE","SELLER_FI_AMORTIZATION","DEFAULT_BALLOON_PERIOD_YEARS","DEFAULT_DOWN_PAYMENT","SELLER_FI_DOWN_PAYMENT","SELLER_FI_CARRY","MIN_DOWN_PAYMENT_PERCENT","DEFAULT_EQUITY_ESTIMATE","DEFAULT_DSCR_PERCENTAGE","MAX_DSCR_PERCENTAGE","DEFAULT_CAP_RATE","MAX_ESTIMATED_CAP_RATE","APPRECIATION_RATE","NOI_APPRECIATION","CALCULATION_TOLERANCE","MAX_ITERATIONS","determineInterestRateType","propertyType","numberOfUnits"],"mappings":"AAIY,MAACA,EAAsB,CAEjCC,2BAA4B,mBAC5BC,oBAAqB,CACnBC,WAAY,CAAEC,aAAc,GAAIC,aAAc,aAAcC,eAAgB,GAAIC,MAAO,aAAcC,KAAM,IAC3GC,gBAAiB,CAAEL,aAAc,GAAIC,aAAc,aAAcC,eAAgB,GAAIC,MAAO,kBAAmBC,KAAM,IACrHE,iBAAkB,CAAEN,aAAc,GAAIC,aAAc,aAAcC,eAAgB,GAAIC,MAAO,mBAAoBC,KAAM,KACvHG,UAAW,CAAEP,aAAc,GAAIC,aAAc,aAAcC,eAAgB,GAAIC,MAAO,YAAaC,KAAM,IACzGI,QAAS,CAAER,aAAc,GAAIC,aAAc,aAAcC,eAAgB,GAAIC,MAAO,UAAWC,KAAM,MAIvGK,wBAAyB,EACzBC,uBAAwB,GAGxBC,6BAA8B,EAG9BC,qBAAsB,GACtBC,uBAAwB,GACxBC,gBAAiB,GACjBC,yBAA0B,EAC1BC,wBAAyB,GAGzBC,wBAAyB,GACzBC,oBAAqB,GAGrBC,iBAAkB,IAClBC,uBAAwB,GAGxBC,kBAAmB,KACnBC,iBAAkB,MAIPC,sBACXA,EAAqBJ,iBACrBA,EAAgBP,qBAChBA,EAAoBK,wBACpBA,EAAuBD,wBACvBA,EAAuBnB,2BACvBA,EAA0BC,oBAC1BA,EAAmB0B,eACnBA,EAAcd,uBACdA,EAAsBI,gBACtBA,EAAeD,uBACfA,EAAsBJ,wBACtBA,GACEb,EAEG,SAAS6B,0BAA0BC,EAAcC,EAAgB,GACtE,MAAqB,YAAjBD,EAAmC,UAClB,cAAjBA,EAAqC,YACpB,aAAjBA,GAAgD,eAAjBA,EAAsC,cACnD,QAAjBA,GAA2C,gBAAjBA,IAAmCC,GAAiB,EAAU,kBACtF,kBACT"}
@@ -1,2 +1,2 @@
1
- const e={API_BASE_URL:"https://n8n-whai-u45960.vm.elestio.app",LOCATION_ID:"KjMMUEqwj4uFZvx4hWzq",SPREADSHEET_URL:"https://docs.google.com/spreadsheets/d/1bSAVIhJbm0HQShCN-TI0Fev29DfKhFlxYCDHG4Dy8xs/export?format=csv&gid=1131505700",WEBHOOK_PATH:"/webhook/e15233bc-0623-4365-9e65-334bc5fc72e2",MATCH_TYPES:{EXACT:"exact",FUZZY:"fuzzy",NO_MATCH:"no-match",NO_RESPONSE:"no-response"},LOI_SENT_STATUS:"LOI Sent"},{MATCH_TYPES:S,LOI_SENT_STATUS:T}=e;export{e as LOI_LOOKUP_CONFIG,T as LOI_SENT_STATUS,S as MATCH_TYPES};
1
+ const e={API_BASE_URL:"https://n8n-whai-u45960.vm.elestio.app",LOCATION_ID:"KjMMUEqwj4uFZvx4hWzq",SPREADSHEET_URL:"https://docs.google.com/spreadsheets/d/1bSAVIhJbm0HQShCN-TI0Fev29DfKhFlxYCDHG4Dy8xs/export?format=csv&gid=1131505700",WEBHOOK_PATH:"/webhook/e15233bc-0623-4365-9e65-334bc5fc72e2",MATCH_TYPES:{EXACT:"exact",FUZZY:"fuzzy",NO_MATCH:"service-replied-no-match",NO_RESPONSE:"no-response-from-service"},LOI_SENT_STATUS:"LOI Sent"},{MATCH_TYPES:S,LOI_SENT_STATUS:T}=e;export{e as LOI_LOOKUP_CONFIG,T as LOI_SENT_STATUS,S as MATCH_TYPES};
2
2
  //# sourceMappingURL=loi-lookup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loi-lookup.js","sources":["../../src/config/loi-lookup.js"],"sourcesContent":["/**\r\n * LOI Lookup service configuration\r\n */\r\n\r\nexport const LOI_LOOKUP_CONFIG = {\r\n API_BASE_URL: \"https://n8n-whai-u45960.vm.elestio.app\",\r\n LOCATION_ID: \"KjMMUEqwj4uFZvx4hWzq\",\r\n SPREADSHEET_URL: \"https://docs.google.com/spreadsheets/d/1bSAVIhJbm0HQShCN-TI0Fev29DfKhFlxYCDHG4Dy8xs/export?format=csv&gid=1131505700\",\r\n WEBHOOK_PATH: \"/webhook/e15233bc-0623-4365-9e65-334bc5fc72e2\",\r\n \r\n MATCH_TYPES: {\r\n EXACT: \"exact\",\r\n FUZZY: \"fuzzy\",\r\n NO_MATCH: \"no-match\",\r\n NO_RESPONSE: \"no-response\",\r\n },\r\n \r\n LOI_SENT_STATUS: \"LOI Sent\",\r\n};\r\n\r\nexport const { MATCH_TYPES, LOI_SENT_STATUS } = LOI_LOOKUP_CONFIG;"],"names":["LOI_LOOKUP_CONFIG","API_BASE_URL","LOCATION_ID","SPREADSHEET_URL","WEBHOOK_PATH","MATCH_TYPES","EXACT","FUZZY","NO_MATCH","NO_RESPONSE","LOI_SENT_STATUS"],"mappings":"AAIY,MAACA,EAAoB,CAC/BC,aAAc,yCACdC,YAAa,uBACbC,gBAAiB,uHACjBC,aAAc,gDAEdC,YAAa,CACXC,MAAO,QACPC,MAAO,QACPC,SAAU,WACVC,YAAa,eAGfC,gBAAiB,aAGNL,YAAEA,EAAWK,gBAAEA,GAAoBV"}
1
+ {"version":3,"file":"loi-lookup.js","sources":["../../src/config/loi-lookup.js"],"sourcesContent":["/**\r\n * LOI Lookup service configuration\r\n */\r\n\r\nexport const LOI_LOOKUP_CONFIG = {\r\n API_BASE_URL: \"https://n8n-whai-u45960.vm.elestio.app\",\r\n LOCATION_ID: \"KjMMUEqwj4uFZvx4hWzq\",\r\n SPREADSHEET_URL: \"https://docs.google.com/spreadsheets/d/1bSAVIhJbm0HQShCN-TI0Fev29DfKhFlxYCDHG4Dy8xs/export?format=csv&gid=1131505700\",\r\n WEBHOOK_PATH: \"/webhook/e15233bc-0623-4365-9e65-334bc5fc72e2\",\r\n \r\n MATCH_TYPES: {\r\n EXACT: \"exact\",\r\n FUZZY: \"fuzzy\",\r\n NO_MATCH: \"service-replied-no-match\",\r\n NO_RESPONSE: \"no-response-from-service\",\r\n },\r\n \r\n LOI_SENT_STATUS: \"LOI Sent\",\r\n};\r\n\r\nexport const { MATCH_TYPES, LOI_SENT_STATUS } = LOI_LOOKUP_CONFIG;"],"names":["LOI_LOOKUP_CONFIG","API_BASE_URL","LOCATION_ID","SPREADSHEET_URL","WEBHOOK_PATH","MATCH_TYPES","EXACT","FUZZY","NO_MATCH","NO_RESPONSE","LOI_SENT_STATUS"],"mappings":"AAIY,MAACA,EAAoB,CAC/BC,aAAc,yCACdC,YAAa,uBACbC,gBAAiB,uHACjBC,aAAc,gDAEdC,YAAa,CACXC,MAAO,QACPC,MAAO,QACPC,SAAU,2BACVC,YAAa,4BAGfC,gBAAiB,aAGNL,YAAEA,EAAWK,gBAAEA,GAAoBV"}
@@ -1,2 +1,2 @@
1
- const T={STR:{ESTIMATED_GROSS_RATE:.1,NOI_PERCENTAGE:.55,DEFAULT_CAP_RATE:.05},ASSISTED_LIVING:{INCOME_PER_BEDROOM_MONTHLY:1500,DEFAULT_BEDROOM_COUNT:10,DEFAULT_CAP_RATE:.05},MULTIFAMILY:{DEFAULT_CAP_RATE:.05}},E={MULTIFAMILY:"multifamily",STR:"str",ASSISTED_LIVING:"assisted"},{STR:A,ASSISTED_LIVING:I,MULTIFAMILY:_}=T;export{I as ASSISTED_LIVING,_ as MULTIFAMILY,E as PROPERTY_TYPES,T as PROPERTY_TYPE_CONSTANTS,A as STR};
1
+ const E={STR:{ESTIMATED_GROSS_RATE:.1,NOI_PERCENTAGE:.55,DEFAULT_CAP_RATE:.05},ASSISTED_LIVING:{INCOME_PER_BEDROOM_MONTHLY:1500,DEFAULT_BEDROOM_COUNT:10,DEFAULT_CAP_RATE:.05},MULTIFAMILY:{DEFAULT_CAP_RATE:.05}},T={ASSISTED_LIVING:"assisted",BUSINESS:"business",MIXED_USE:"mixed_use",MULTIFAMILY:"multifamily",RV_PARK:"rv_park",STR:"str"},{STR:_,ASSISTED_LIVING:A,MULTIFAMILY:I}=E;export{A as ASSISTED_LIVING,I as MULTIFAMILY,T as PROPERTY_TYPES,E as PROPERTY_TYPE_CONSTANTS,_ as STR};
2
2
  //# sourceMappingURL=property-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"property-types.js","sources":["../../src/config/property-types.js"],"sourcesContent":["/**\r\n * Property type specific constants\r\n */\r\n\r\nexport const PROPERTY_TYPE_CONSTANTS = {\r\n // Short-term rental (STR) calculations\r\n STR: {\r\n ESTIMATED_GROSS_RATE: 0.10, // 10% of price\r\n NOI_PERCENTAGE: 0.55, // 55% of gross income\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n },\r\n \r\n // Assisted living calculations\r\n ASSISTED_LIVING: {\r\n INCOME_PER_BEDROOM_MONTHLY: 1500, // $1,500 per bedroom per month\r\n DEFAULT_BEDROOM_COUNT: 10, // Default assumption\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n },\r\n \r\n // Multifamily calculations\r\n MULTIFAMILY: {\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n },\r\n};\r\n\r\n// Property type enum for consistency\r\nexport const PROPERTY_TYPES = {\r\n MULTIFAMILY: 'multifamily',\r\n STR: 'str',\r\n ASSISTED_LIVING: 'assisted',\r\n};\r\n\r\n// Convenience exports\r\nexport const {\r\n STR,\r\n ASSISTED_LIVING,\r\n MULTIFAMILY\r\n} = PROPERTY_TYPE_CONSTANTS;\r\n"],"names":["PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","NOI_PERCENTAGE","DEFAULT_CAP_RATE","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","DEFAULT_BEDROOM_COUNT","MULTIFAMILY","PROPERTY_TYPES"],"mappings":"AAIY,MAACA,EAA0B,CAErCC,IAAK,CACHC,qBAAsB,GACtBC,eAAgB,IAChBC,iBAAkB,KAIpBC,gBAAiB,CACfC,2BAA4B,KAC5BC,sBAAuB,GACvBH,iBAAkB,KAIpBI,YAAa,CACXJ,iBAAkB,MAKTK,EAAiB,CAC5BD,YAAa,cACbP,IAAK,MACLI,gBAAiB,aAINJ,IACXA,EAAGI,gBACHA,EAAeG,YACfA,GACER"}
1
+ {"version":3,"file":"property-types.js","sources":["../../src/config/property-types.js"],"sourcesContent":["/**\r\n * Property type specific constants\r\n */\r\n\r\nexport const PROPERTY_TYPE_CONSTANTS = {\r\n // Short-term rental (STR) calculations\r\n STR: {\r\n ESTIMATED_GROSS_RATE: 0.10, // 10% of price\r\n NOI_PERCENTAGE: 0.55, // 55% of gross income\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n },\r\n \r\n // Assisted living calculations\r\n ASSISTED_LIVING: {\r\n INCOME_PER_BEDROOM_MONTHLY: 1500, // $1,500 per bedroom per month\r\n DEFAULT_BEDROOM_COUNT: 10, // Default assumption\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n },\r\n \r\n // Multifamily calculations\r\n MULTIFAMILY: {\r\n DEFAULT_CAP_RATE: 0.05, // 5% default cap rate\r\n },\r\n};\r\n\r\n// Property type enum for consistency\r\nexport const PROPERTY_TYPES = {\r\n ASSISTED_LIVING: 'assisted',\r\n BUSINESS: 'business',\r\n MIXED_USE: 'mixed_use',\r\n MULTIFAMILY: 'multifamily',\r\n RV_PARK: 'rv_park',\r\n STR: 'str',\r\n};\r\n\r\n// Convenience exports\r\nexport const {\r\n STR,\r\n ASSISTED_LIVING,\r\n MULTIFAMILY\r\n} = PROPERTY_TYPE_CONSTANTS;\r\n"],"names":["PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","NOI_PERCENTAGE","DEFAULT_CAP_RATE","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","DEFAULT_BEDROOM_COUNT","MULTIFAMILY","PROPERTY_TYPES","BUSINESS","MIXED_USE","RV_PARK"],"mappings":"AAIY,MAACA,EAA0B,CAErCC,IAAK,CACHC,qBAAsB,GACtBC,eAAgB,IAChBC,iBAAkB,KAIpBC,gBAAiB,CACfC,2BAA4B,KAC5BC,sBAAuB,GACvBH,iBAAkB,KAIpBI,YAAa,CACXJ,iBAAkB,MAKTK,EAAiB,CAC5BJ,gBAAiB,WACjBK,SAAU,WACVC,UAAW,YACXH,YAAa,cACbI,QAAS,UACTX,IAAK,QAIMA,IACXA,EAAGI,gBACHA,EAAeG,YACfA,GACER"}
@@ -1,2 +1,2 @@
1
- import{FINANCIAL_CONSTANTS as t}from"../config/financial.js";import{BUSINESS_CONSTANTS as e}from"../config/business.js";import{PROPERTY_TYPES as c,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";function calculatePMT(t,e,c){if(0===e)return t/(12*c);const a=e/12,r=12*c;return t*(a*Math.pow(1+a,r))/(Math.pow(1+a,r)-1)}function calculateCOCR30(t,e){try{const c=.3*t,a=12*calculatePMT(.7*t,.075,30);return(e-a)/c*100}catch(t){return 0}}function calculateCashFlowYield(t,e){if(!e||e<=0)return 0;return 12*t/e*100}function calculatePriceForCOCR(c,a=.15,r={}){const{downPercent:n=100*t.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:l=100*t.DEFAULT_DSCR_PERCENTAGE,dscrRate:u=t.DSCR_INTEREST_RATE,dscrTerm:o=t.DSCR_AMORTIZATION,maxIterations:E=e.MAX_ITERATIONS,tolerance:T=e.CALCULATION_TOLERANCE}=r;try{let t=c/.08,r=0;for(;r<E;){const E=t*(n/100),R=12*calculatePMT(t*(l/100),u,o),s=(c-R)/E;if(Math.abs(s-a)<T)break;const A=s-a,i=A*e.ADJUSTMENT_FACTOR;t*=A>0?1+Math.abs(i):1-Math.abs(i),t<1e3&&(t=1e3),t>c*e.MAX_COCR15_PRICE_MULTIPLIER&&(t=c*e.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),r++}return t<e.MINIMUM_COCR15_PRICE&&(t=e.MINIMUM_COCR15_PRICE),t}catch(t){return 0}}function calculateCOCRAtPercent(e,c,a,r={}){const{dscrRate:n=t.DSCR_INTEREST_RATE,dscrTerm:l=t.DSCR_AMORTIZATION}=r;try{const t=e*(a/100),r=12*calculatePMT(e-t,n,l);return(c-r)/t*100}catch(t){return 0}}function calculateNOIByType(t,e,r=c.MULTIFAMILY,n={}){const{strGrossIncomeMultiplier:l=a.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:u=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:o=a.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:E=a.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch(r.toLowerCase()){case c.STR:return t*l*u;case c.ASSISTED_LIVING:return E*o*12;case c.MULTIFAMILY:default:return t*e}}catch(t){return 0}}function calculateAssignmentFee(t,c=100*e.ASSIGNMENT_FEE_PERCENTAGE){try{return t*(c/100)}catch(t){return 0}}function calculateNetToBuyer(c,a={}){const{buyerCostPercent:r=100*e.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*e.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*e.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*e.REHAB_RATE,additionalCostFinancing:o=100*e.HARD_MONEY_RATE,dscrLtvPercent:E=100*t.DEFAULT_DSCR_PERCENTAGE}=a;try{return c*(r/100)-c*((n+l)/100)-c*(u/100)-o/100*(c-c*(E/100))}catch(t){return 0}}function calculateBalloonBalance(e,c,a,r=t.DEFAULT_BALLOON_PERIOD_YEARS){try{if(e<=0||c<0||a<=0||r<=0)return 0;if(r>=a)return 0;if(0===c){const t=12*a;return e*(t-12*r)/t}const t=c/12,n=12*a,l=12*r,u=Math.pow(1+t,n),o=e*(u-Math.pow(1+t,l))/(u-1);return Math.max(0,o)}catch(t){return 0}}function calculateAppreciatedValue(e,c=t.APPRECIATION_RATE,a=t.DEFAULT_BALLOON_PERIOD_YEARS){try{return e<=0||c<0||a<0?e:e*Math.pow(1+c,a)}catch(t){return e}}function calculateCashOutAfterRefi(e,c,a,r={}){const{appreciationRate:n=t.APPRECIATION_RATE,balloonYears:l=t.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:u=t.DSCR_INTEREST_RATE,dscrTerm:o=t.DSCR_AMORTIZATION,sellerFiTerm:E=t.SELLER_FI_AMORTIZATION,refiLtvPercent:T=70}=r;try{const r=calculateAppreciatedValue(e,n,l),R=calculateBalloonBalance(c,u,o,l),s=calculateBalloonBalance(a,t.SELLER_FI_INTEREST_RATE,E,l);return r*(T/100)-(R+s)}catch(t){return 0}}function calculateCashFlow(t,e,c){return t-(e+c)}function calculateDiscountFromPrice(t,e){return!t||t<=0?0:(t-e)/t}function calculatePriceFromDiscount(t,e){return!t||t<=0?0:t*(1-e)}function safePercentage(t,e=100){return null==t||isNaN(t)?e:100*t}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,safePercentage};
1
+ import{FINANCIAL_CONSTANTS as t}from"../config/financial.js";import{BUSINESS_CONSTANTS as e}from"../config/business.js";import{PROPERTY_TYPES as c,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";const r=t.INTEREST_RATE_TIERS[t.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(t,e,c){if(0===e)return t/(12*c);const a=e/12,r=12*c;return t*(a*Math.pow(1+a,r))/(Math.pow(1+a,r)-1)}function calculateCOCR30(t,e){try{const c=.3*t,a=12*calculatePMT(.7*t,.075,30);return(e-a)/c*100}catch(t){return 0}}function calculateCashFlowYield(t,e){if(!e||e<=0)return 0;return 12*t/e*100}function calculatePriceForCOCR(c,a=.15,n={}){const{downPercent:l=100*t.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:o=100*t.DEFAULT_DSCR_PERCENTAGE,dscrRate:u=r.rate,dscrTerm:E=r.amortization,maxIterations:T=e.MAX_ITERATIONS,tolerance:i=e.CALCULATION_TOLERANCE}=n;try{let t=c/.08,r=0;for(;r<T;){const n=t*(l/100),T=12*calculatePMT(t*(o/100),u,E),s=(c-T)/n;if(Math.abs(s-a)<i)break;const R=s-a,A=R*e.ADJUSTMENT_FACTOR;t*=R>0?1+Math.abs(A):1-Math.abs(A),t<1e3&&(t=1e3),t>c*e.MAX_COCR15_PRICE_MULTIPLIER&&(t=c*e.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),r++}return t<e.MINIMUM_COCR15_PRICE&&(t=e.MINIMUM_COCR15_PRICE),t}catch(t){return 0}}function calculateCOCRAtPercent(t,e,c,a={}){const{dscrRate:n=r.rate,dscrTerm:l=r.amortization}=a;try{const a=t*(c/100),r=12*calculatePMT(t-a,n,l);return(e-r)/a*100}catch(t){return 0}}function calculateNOIByType(t,e,r=c.MULTIFAMILY,n={}){const{strGrossIncomeMultiplier:l=a.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:o=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:u=a.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:E=a.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch(r.toLowerCase()){case c.STR:return t*l*o;case c.ASSISTED_LIVING:return E*u*12;case c.MULTIFAMILY:default:return t*e}}catch(t){return 0}}function calculateAssignmentFee(t,c=100*e.ASSIGNMENT_FEE_PERCENTAGE){try{return t*(c/100)}catch(t){return 0}}function calculateNetToBuyer(c,a={}){const{buyerCostPercent:r=100*e.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*e.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*e.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:o=100*e.REHAB_RATE,additionalCostFinancing:u=100*e.HARD_MONEY_RATE,dscrLtvPercent:E=100*t.DEFAULT_DSCR_PERCENTAGE}=a;try{return c*(r/100)-c*((n+l)/100)-c*(o/100)-u/100*(c-c*(E/100))}catch(t){return 0}}function calculateBalloonBalance(e,c,a,r=t.DEFAULT_BALLOON_PERIOD_YEARS){try{if(e<=0||c<0||a<=0||r<=0)return 0;if(r>=a)return 0;if(0===c){const t=12*a;return e*(t-12*r)/t}const t=c/12,n=12*a,l=12*r,o=Math.pow(1+t,n),u=e*(o-Math.pow(1+t,l))/(o-1);return Math.max(0,u)}catch(t){return 0}}function calculateAppreciatedValue(e,c=t.APPRECIATION_RATE,a=t.DEFAULT_BALLOON_PERIOD_YEARS){try{return e<=0||c<0||a<0?e:e*Math.pow(1+c,a)}catch(t){return e}}function calculateCashOutAfterRefi(e,c,a,n={}){const{appreciationRate:l=t.APPRECIATION_RATE,balloonYears:o=t.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:u=r.rate,dscrTerm:E=r.amortization,sellerFiTerm:T=t.SELLER_FI_AMORTIZATION,refiLtvPercent:i=70}=n;try{const r=calculateAppreciatedValue(e,l,o),n=calculateBalloonBalance(c,u,E,o),s=calculateBalloonBalance(a,t.SELLER_FI_INTEREST_RATE,T,o);return r*(i/100)-(n+s)}catch(t){return 0}}function calculateCashFlow(t,e,c){return t-(e+c)}function calculateDiscountFromPrice(t,e){return!t||t<=0?0:(t-e)/t}function calculatePriceFromDiscount(t,e){return!t||t<=0?0:t*(1-e)}function safePercentage(t,e=100){return null==t||isNaN(t)?e:100*t}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,safePercentage};
2
2
  //# sourceMappingURL=calculations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n const estimatedGrossIncome = askingPrice * strGrossIncomeMultiplier;\r\n return estimatedGrossIncome * strNoiPercentage;\r\n \r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","calculatePriceForCOCR","targetCOCR","options","downPercent","FINANCIAL_CONSTANTS","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","DSCR_INTEREST_RATE","dscrTerm","DSCR_AMORTIZATION","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strGrossIncomeMultiplier","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","strNoiPercentage","NOI_PERCENTAGE","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","value","fallback","isNaN"],"mappings":"kNAcO,SAASA,aAAaC,EAAWC,EAAYC,GAClD,GAAmB,IAAfD,EACF,OAAOD,GAAqB,GAARE,GAGtB,MAAMC,EAAcF,EAAa,GAC3BG,EAAsB,GAARF,EAGpB,OAFYF,GAAaG,EAAcE,KAAKC,IAAI,EAAIH,EAAaC,KACpDC,KAAKC,IAAI,EAAIH,EAAaC,GAAe,EAExD,CAEO,SAASG,gBAAgBC,EAAaC,GAC3C,IACE,MAAMC,EAA6B,GAAdF,EAEfG,EAAwD,GAA1CZ,aADiB,GAAdS,EAC0B,KAAO,IAIxD,OAHuBC,EAAME,GACED,EAAgB,GAGjD,CAAE,MAAOE,GACP,OAAO,CACT,CACF,CAEO,SAASC,uBAAuBC,EAAiBC,GACtD,IAAKA,GAAiBA,GAAiB,EAAG,OAAO,EAEjD,OADyC,GAAlBD,EACEC,EAAiB,GAC5C,CAUO,SAASC,sBAAsBP,EAAKQ,EAAa,IAAMC,EAAU,CAAA,GACtE,MAAMC,YACJA,EAAyD,IAA3CC,EAAoBC,qBAA0BC,eAC5DA,EAA+D,IAA9CF,EAAoBG,wBAA6BC,SAClEA,EAAWJ,EAAoBK,mBAAkBC,SACjDA,EAAWN,EAAoBO,kBAAiBC,cAChDA,EAAgBC,EAAmBC,eAAcC,UACjDA,EAAYF,EAAmBG,uBAC7Bd,EAEJ,IACE,IAAIe,EAAcxB,EAAM,IACpByB,EAAa,EAEjB,KAAOA,EAAaN,GAAe,CACjC,MAAMlB,EAAeuB,GAAed,EAAc,KAE5CR,EAAiE,GAAnDZ,aADGkC,GAAeX,EAAiB,KACNE,EAAUE,GAErDS,GADiB1B,EAAME,GACQD,EAErC,GAAIL,KAAK+B,IAAID,EAAclB,GAAcc,EACvC,MAGF,MAAMnB,EAAQuB,EAAclB,EACtBoB,EAAazB,EAAQiB,EAAmBS,kBAG5CL,GADErB,EAAQ,EACmB,EAAIP,KAAK+B,IAAIC,GAEb,EAAIhC,KAAK+B,IAAIC,GAIxCJ,EAAc,MAAMA,EAAc,KAClCA,EAAcxB,EAAMoB,EAAmBU,8BACzCN,EAAcxB,EAAMoB,EAAmBW,sCAGzCN,GACF,CAOA,OAJID,EAAcJ,EAAmBY,uBACnCR,EAAcJ,EAAmBY,sBAG5BR,CACT,CAAE,MAAOrB,GACP,OAAO,CACT,CACF,CAUO,SAAS8B,uBAAuBlC,EAAaC,EAAKU,EAAaD,EAAU,CAAA,GAC9E,MAAMM,SACJA,EAAWJ,EAAoBK,mBAAkBC,SACjDA,EAAWN,EAAoBO,mBAC7BT,EAEJ,IACE,MACMR,EAAeF,GADDW,EAAc,KAM5BR,EAAiE,GAAnDZ,aAFGS,EAAcE,EAEYc,EAAUE,GAK3D,OAHuBjB,EAAME,GACED,EAAgB,GAGjD,CAAE,MAAOE,GACP,OAAO,CACT,CACF,CAUO,SAAS+B,mBAAmBnC,EAAaoC,EAASC,EAAeC,EAAeC,YAAa7B,EAAU,IAC5G,MAAM8B,yBACJA,EAA2BC,EAAwBC,IAAIC,qBAAoBC,iBAC3EA,EAAmBH,EAAwBC,IAAIG,eAAcC,yBAC7DA,EAA2BL,EAAwBM,gBAAgBC,2BAA0BC,aAC7FA,EAAeR,EAAwBM,gBAAgBG,uBACrDxC,EAEJ,IACE,OAAQ2B,EAAac,eACnB,KAAKb,EAAeI,IAElB,OAD6B1C,EAAcwC,EACbI,EAEhC,KAAKN,EAAeS,gBAClB,OAAOE,EAAeH,EAA2B,GAEnD,KAAKR,EAAeC,YACpB,QACE,OAAOvC,EAAcoC,EAE3B,CAAE,MAAOhC,GACP,OAAO,CACT,CACF,CAQO,SAASgD,uBAAuBpD,EAAaqD,EAAmE,IAA/ChC,EAAmBiC,2BACzF,IACE,OAAOtD,GAAeqD,EAAoB,IAC5C,CAAE,MAAOjD,GACP,OAAO,CACT,CACF,CAQO,SAASmD,oBAAoBvD,EAAaU,EAAU,IACzD,MAAM8C,iBACJA,EAAgE,IAA7CnC,EAAmBoC,wBAA6BC,qBACnEA,EAAsE,IAA/CrC,EAAmBiC,0BAA+BK,kBACzEA,EAAkE,IAA9CtC,EAAmBuC,yBAA8BC,oBACrEA,EAAsD,IAAhCxC,EAAmByC,WAAgBC,wBACzDA,EAA+D,IAArC1C,EAAmB2C,gBAAqBlD,eAClEA,EAA+D,IAA9CF,EAAoBG,yBACnCL,EAEJ,IAGE,OAAOV,GAAewD,EAAmB,KAClCxD,IAAgB0D,EAAuBC,GAAqB,KAC5D3D,GAAe6D,EAAsB,KACpCE,EAA0B,KAAQ/D,EALnBA,GAAec,EAAiB,KAMzD,CAAE,MAAOV,GACP,OAAO,CACT,CACF,CAUO,SAAS6D,wBAAwBC,EAAYC,EAAcC,EAAmBC,EAAezD,EAAoB0D,8BACtH,IACE,GAAIJ,GAAc,GAAKC,EAAe,GAAKC,GAAqB,GAAKC,GAAgB,EACnF,OAAO,EAIT,GAAIA,GAAgBD,EAClB,OAAO,EAIT,GAAqB,IAAjBD,EAAoB,CACtB,MAAMI,EAAoC,GAApBH,EAEtB,OAAOF,GAAcK,EADe,GAAfF,GACgCE,CACvD,CAEA,MAAM5E,EAAcwE,EAAe,GAC7BI,EAAoC,GAApBH,EAChBI,EAAiC,GAAfH,EAMlBI,EAAU5E,KAAKC,IAAI,EAAIH,EAAa4E,GAGpCG,EAAmBR,GAAcO,EAFvB5E,KAAKC,IAAI,EAAIH,EAAa6E,KAEmBC,EAAU,GAEvE,OAAO5E,KAAK8E,IAAI,EAAGD,EACrB,CAAE,MAAOtE,GACP,OAAO,CACT,CACF,CASO,SAASwE,0BAA0BC,EAAcC,EAAmBlE,EAAoBmE,kBAAmBrF,EAAQkB,EAAoB0D,8BAC5I,IACE,OAAIO,GAAgB,GAAKC,EAAmB,GAAKpF,EAAQ,EAChDmF,EAGFA,EAAehF,KAAKC,IAAI,EAAIgF,EAAkBpF,EACvD,CAAE,MAAOU,GACP,OAAOyE,CACT,CACF,CAUO,SAASG,0BAA0BC,EAAeC,EAAgBC,EAAgBzE,EAAU,CAAA,GACjG,MAAMoE,iBACJA,EAAmBlE,EAAoBmE,kBAAiBV,aACxDA,EAAezD,EAAoB0D,6BAA4BtD,SAC/DA,EAAWJ,EAAoBK,mBAAkBC,SACjDA,EAAWN,EAAoBO,kBAAiBiE,aAChDA,EAAexE,EAAoByE,uBAAsBC,eACzDA,EAAiB,IACf5E,EAEJ,IAEE,MAAM6E,EAAmBX,0BAA0BK,EAAeH,EAAkBT,GAG9EmB,EAAuBvB,wBAAwBiB,EAAgBlE,EAAUE,EAAUmD,GAGnFoB,EAA2BxB,wBAAwBkB,EAAgBvE,EAAoB8E,wBAAyBN,EAAcf,GAWpI,OALsBkB,GAAoBD,EAAiB,MAHhCE,EAAuBC,EASpD,CAAE,MAAOrF,GACP,OAAO,CACT,CACF,CAGO,SAASuF,kBAAkBC,EAAYzF,EAAa0F,GACzD,OAAOD,GAAczF,EAAc0F,EACrC,CAQO,SAASC,2BAA2B9F,EAAa+F,GACtD,OAAK/F,GAAeA,GAAe,EAAU,GAErCA,EAAc+F,GAAgB/F,CACxC,CAQO,SAASgG,2BAA2BhG,EAAaiG,GACtD,OAAKjG,GAAeA,GAAe,EAAU,EAEtCA,GAAe,EAAIiG,EAC5B,CAEO,SAASC,eAAeC,EAAOC,EAAW,KAC/C,OAAiB,MAATD,GAAkBE,MAAMF,GAA0BC,EAAP,IAARD,CAC7C"}
1
+ {"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n const estimatedGrossIncome = askingPrice * strGrossIncomeMultiplier;\r\n return estimatedGrossIncome * strNoiPercentage;\r\n \r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","amortization","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strGrossIncomeMultiplier","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","strNoiPercentage","NOI_PERCENTAGE","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","value","fallback","isNaN"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,safePercentage}from"./financial/calculations.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{extractBedrooms,extractPhoneNumber}from"./data/extractors.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,DSCR_AMORTIZATION,DSCR_INTEREST_RATE,FINANCIAL_CONSTANTS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE}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,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}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,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,safePercentage}from"./financial/calculations.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{extractBedrooms,extractPhoneNumber}from"./data/extractors.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,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}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 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 safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Data extractors\r\nexport { extractPhoneNumber, extractBedrooms } from \"./data/extractors.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":"qsDAoDY,MAACA,EAAc"}
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 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 safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Data extractors\r\nexport { extractPhoneNumber, extractBedrooms } from \"./data/extractors.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":"yuDAoDY,MAACA,EAAc"}
@@ -1,2 +1,2 @@
1
- import{MATCH_TYPES as t,LOI_LOOKUP_CONFIG as e}from"../config/loi-lookup.js";function normalizeAddress(t){return t?t.toLowerCase().replace(/[^\w\s]/g," ").replace(/\s+/g," ").trim():""}function parseAddress(t){const e=normalizeAddress(t),r=e.match(/\b\d{5}\b/),o=r?r[0]:null,s=e.match(/\b([a-z]{2})\s+\d{5}\b/),a=s?s[1]:null,n=e.match(/([a-z\s]+)\s+[a-z]{2}\s+\d{5}/),c=n?n[1].trim():null,l=e.match(/^(.+?)\s+[a-z\s]+\s+[a-z]{2}\s+\d{5}/);return{city:c,full:e,state:a,street:l?l[1].trim():e,zip:o}}function matchAddresses(e,r){const o=parseAddress(e),s=parseAddress(r);if(o.city&&s.city&&o.city!==s.city)return{matchType:t.NO_MATCH,score:0};if(o.zip&&s.zip&&o.zip!==s.zip)return{matchType:t.NO_MATCH,score:0};if(o.state&&s.state&&o.state!==s.state)return{matchType:t.NO_MATCH,score:0};const a=function(t,e){if(!t||!e)return 0;const r=normalizeAddress(t),o=normalizeAddress(e);if(r===o)return 1;if(r.includes(o)||o.includes(r))return.8;const s=r.split(" ").filter(t=>t.length>2),a=o.split(" ").filter(t=>t.length>2);return 0===s.length||0===a.length?0:2*s.filter(t=>a.includes(t)).length/(s.length+a.length)}(e,r);return a>=.95?{matchType:t.EXACT,score:a}:a>=.6?{matchType:t.FUZZY,score:a}:{matchType:t.NO_MATCH,score:a}}async function lookupLOI(r){return r?await async function(r){try{const o=new URL(e.API_BASE_URL);o.pathname=e.WEBHOOK_PATH,o.searchParams.set("location_id",e.LOCATION_ID),o.searchParams.set("q",r);const s=await fetch(o.toString(),{headers:{Accept:"application/json"},method:"GET"});if(console.log("lookupFromAPI URL",o),console.log("lookupFromAPI response",s),!s.ok)return console.log("!response.ok",s),{data:null,error:`HTTP ${s.status}`,matchType:t.NO_RESPONSE,searchQuery:r};const a=await s.json();if(console.log("lookupFromAPI data",a),!a||!a.opportunityName)return{data:null,matchType:t.NO_RESPONSE,searchQuery:r};const n=a.opportunityName.split("+")[0].trim();console.log("lookupFromAPI opportunityAddress",n);const c=matchAddresses(r,n);return console.log("lookupFromAPI matchResult",c),{data:{contactName:a.contactName,createdAt:a.createdAt,opportunityAddress:n,opportunityName:a.opportunityName,statusOrStage:a.statusOrStage,updatedAt:a.updatedAt,foundIn:"api"},matchType:c.matchType,score:c.score,searchQuery:r}}catch(e){return console.log("lookupFromAPI error",e),{data:null,error:e.message,matchType:t.NO_RESPONSE,searchQuery:r}}}(r):{data:null,matchType:t.NO_RESPONSE,searchQuery:r}}export{lookupLOI};
1
+ import{MATCH_TYPES as t,LOI_LOOKUP_CONFIG as e}from"../config/loi-lookup.js";function normalizeAddress(t){return t?t.toLowerCase().replace(/[^\w\s]/g," ").replace(/\s+/g," ").trim():""}function parseAddress(t){const e=normalizeAddress(t),r=e.match(/\b\d{5}\b/),a=r?r[0]:null,s=e.match(/\b([a-z]{2})\s+\d{5}\b/),o=s?s[1]:null,n=e.match(/([a-z\s]+)\s+[a-z]{2}\s+\d{5}/),c=n?n[1].trim():null,i=e.match(/^(.+?)\s+[a-z\s]+\s+[a-z]{2}\s+\d{5}/);return{city:c,full:e,state:o,street:i?i[1].trim():e,zip:a}}function matchAddresses(e,r){const a=parseAddress(e),s=parseAddress(r);if(a.city&&s.city&&a.city!==s.city)return{matchType:t.NO_MATCH,score:0};if(a.zip&&s.zip&&a.zip!==s.zip)return{matchType:t.NO_MATCH,score:0};if(a.state&&s.state&&a.state!==s.state)return{matchType:t.NO_MATCH,score:0};const o=function(t,e){if(!t||!e)return 0;const r=normalizeAddress(t),a=normalizeAddress(e);if(r===a)return 1;if(r.includes(a)||a.includes(r))return.8;const s=r.split(" ").filter(t=>t.length>2),o=a.split(" ").filter(t=>t.length>2);return 0===s.length||0===o.length?0:2*s.filter(t=>o.includes(t)).length/(s.length+o.length)}(e,r);return o>=.95?{matchType:t.EXACT,score:o}:o>=.6?{matchType:t.FUZZY,score:o}:{matchType:t.NO_MATCH,score:o}}async function lookupLOI(r){return r?await async function(r){try{const a=new URL(e.API_BASE_URL);a.pathname=e.WEBHOOK_PATH,a.searchParams.set("location_id",e.LOCATION_ID),a.searchParams.set("q",r);const s=await fetch(a.toString(),{headers:{Accept:"application/json"},method:"GET"});if(!s.ok)return console.log("LOI lookupFromAPI !response.ok",s),{data:null,error:`HTTP ${s.status}`,matchType:t.NO_RESPONSE,searchQuery:r};const o=await s.json();if(!o)return console.log("LOI lookupFromAPI !data",s),{data:null,matchType:t.NO_RESPONSE,searchQuery:r};if(o.notFound)return{data:null,matchType:t.NO_MATCH,searchQuery:r};const n=o.opportunityName.split("+")[0].trim(),c=matchAddresses(r,n);return{data:{contactName:o.contactName,createdAt:o.createdAt,opportunityAddress:n,opportunityName:o.opportunityName,statusOrStage:o.statusOrStage,updatedAt:o.updatedAt,foundIn:"api"},matchType:c.matchType,score:c.score,searchQuery:r}}catch(e){return console.log("lookupFromAPI error",e),{data:null,error:e.message,matchType:t.NO_RESPONSE,searchQuery:r}}}(r):{data:null,matchType:t.NO_RESPONSE,searchQuery:r}}export{lookupLOI};
2
2
  //# sourceMappingURL=loi-lookup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loi-lookup.js","sources":["../../src/services/loi-lookup.js"],"sourcesContent":["/**\r\n * LOI Lookup Service\r\n * Checks if a property address has an existing LOI in the CRM system\r\n */\r\n\r\nimport { LOI_LOOKUP_CONFIG, MATCH_TYPES } from \"../config/loi-lookup.js\";\r\n\r\n/**\r\n * Normalize an address string for comparison\r\n * @param {string} address - Address string to normalize\r\n * @returns {string} Normalized address\r\n */\r\nfunction normalizeAddress(address) {\r\n if (!address) return \"\";\r\n \r\n return address\r\n .toLowerCase()\r\n .replace(/[^\\w\\s]/g, \" \")\r\n .replace(/\\s+/g, \" \")\r\n .trim();\r\n}\r\n\r\n/**\r\n * Extract components from an address string\r\n * @param {string} address - Address to parse\r\n * @returns {Object} Address components\r\n */\r\nfunction parseAddress(address) {\r\n const normalized = normalizeAddress(address);\r\n \r\n const zipMatch = normalized.match(/\\b\\d{5}\\b/);\r\n const zip = zipMatch ? zipMatch[0] : null;\r\n \r\n const stateMatch = normalized.match(/\\b([a-z]{2})\\s+\\d{5}\\b/);\r\n const state = stateMatch ? stateMatch[1] : null;\r\n \r\n const cityMatch = normalized.match(/([a-z\\s]+)\\s+[a-z]{2}\\s+\\d{5}/);\r\n const city = cityMatch ? cityMatch[1].trim() : null;\r\n \r\n const streetMatch = normalized.match(/^(.+?)\\s+[a-z\\s]+\\s+[a-z]{2}\\s+\\d{5}/);\r\n const street = streetMatch ? streetMatch[1].trim() : normalized;\r\n \r\n return {\r\n city,\r\n full: normalized,\r\n state,\r\n street,\r\n zip,\r\n };\r\n}\r\n\r\n/**\r\n * Calculate similarity score between two strings (0-1)\r\n * @param {string} str1 - First string\r\n * @param {string} str2 - Second string\r\n * @returns {number} Similarity score\r\n */\r\nfunction calculateSimilarity(str1, str2) {\r\n if (!str1 || !str2) return 0;\r\n \r\n const s1 = normalizeAddress(str1);\r\n const s2 = normalizeAddress(str2);\r\n \r\n if (s1 === s2) return 1.0;\r\n \r\n if (s1.includes(s2) || s2.includes(s1)) return 0.8;\r\n \r\n const words1 = s1.split(\" \").filter(w => w.length > 2);\r\n const words2 = s2.split(\" \").filter(w => w.length > 2);\r\n \r\n if (words1.length === 0 || words2.length === 0) return 0;\r\n \r\n const commonWords = words1.filter(w => words2.includes(w));\r\n const overlapRatio = (commonWords.length * 2) / (words1.length + words2.length);\r\n \r\n return overlapRatio;\r\n}\r\n\r\n/**\r\n * Match two addresses using fuzzy matching\r\n * @param {string} searchQuery - The address being searched\r\n * @param {string} opportunityAddress - Address from the CRM opportunity\r\n * @returns {Object} Match result with type and score\r\n */\r\nfunction matchAddresses(searchQuery, opportunityAddress) {\r\n const search = parseAddress(searchQuery);\r\n const opportunity = parseAddress(opportunityAddress);\r\n \r\n if (search.city && opportunity.city) {\r\n if (search.city !== opportunity.city) {\r\n return { matchType: MATCH_TYPES.NO_MATCH, score: 0 };\r\n }\r\n }\r\n \r\n if (search.zip && opportunity.zip) {\r\n if (search.zip !== opportunity.zip) {\r\n return { matchType: MATCH_TYPES.NO_MATCH, score: 0 };\r\n }\r\n }\r\n \r\n if (search.state && opportunity.state) {\r\n if (search.state !== opportunity.state) {\r\n return { matchType: MATCH_TYPES.NO_MATCH, score: 0 };\r\n }\r\n }\r\n \r\n const similarity = calculateSimilarity(searchQuery, opportunityAddress);\r\n \r\n if (similarity >= 0.95) {\r\n return { matchType: MATCH_TYPES.EXACT, score: similarity };\r\n }\r\n \r\n if (similarity >= 0.6) {\r\n return { matchType: MATCH_TYPES.FUZZY, score: similarity };\r\n }\r\n \r\n return { matchType: MATCH_TYPES.NO_MATCH, score: similarity };\r\n}\r\n\r\n/**\r\n * Parse CSV text into array of objects\r\n * Handles quoted fields with commas correctly\r\n * @param {string} csvText - CSV content\r\n * @returns {Array<Object>} Parsed rows\r\n */\r\nfunction parseCSV(csvText) {\r\n const lines = csvText.split(\"\\n\").filter(line => line.trim());\r\n if (lines.length === 0) return [];\r\n \r\n const rows = [];\r\n \r\n for (let i = 1; i < lines.length; i++) {\r\n const line = lines[i];\r\n const cols = [];\r\n let current = \"\";\r\n let inQuotes = false;\r\n \r\n for (let j = 0; j < line.length; j++) {\r\n const char = line[j];\r\n \r\n if (char === '\"') {\r\n inQuotes = !inQuotes;\r\n } else if (char === \",\" && !inQuotes) {\r\n cols.push(current.trim());\r\n current = \"\";\r\n } else {\r\n current += char;\r\n }\r\n }\r\n cols.push(current.trim());\r\n \r\n if (cols.length >= 4 && cols[1]) {\r\n rows.push({\r\n address: cols[1],\r\n contactName: cols[3] || null,\r\n date: cols[2] || null,\r\n });\r\n }\r\n }\r\n \r\n return rows;\r\n}\r\n\r\n/**\r\n * Lookup LOI from Google Spreadsheet\r\n * DELETE THIS FUNCTION when phasing out spreadsheet\r\n * @param {string} searchQuery - Property address to search\r\n * @returns {Promise<Object>} Lookup result\r\n */\r\nasync function lookupFromSpreadsheet(searchQuery) {\r\n try {\r\n const response = await fetch(LOI_LOOKUP_CONFIG.SPREADSHEET_URL, {\r\n headers: {\r\n \"Accept\": \"text/csv\",\r\n },\r\n method: \"GET\",\r\n });\r\n\r\n console.log('lookupFromSpreadsheet response', response)\r\n \r\n if (!response.ok) {\r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n \r\n const csvText = await response.text();\r\n const rows = parseCSV(csvText);\r\n \r\n let bestMatch = null;\r\n let bestScore = 0;\r\n \r\n for (const row of rows) {\r\n const matchResult = matchAddresses(searchQuery, row.address);\r\n \r\n if (matchResult.matchType !== MATCH_TYPES.NO_MATCH && matchResult.score > bestScore) {\r\n bestScore = matchResult.score;\r\n bestMatch = {\r\n data: {\r\n contactName: row.contactName,\r\n createdAt: row.date,\r\n opportunityAddress: row.address,\r\n opportunityName: row.address,\r\n statusOrStage: LOI_LOOKUP_CONFIG.LOI_SENT_STATUS,\r\n updatedAt: row.date,\r\n foundIn: \"spreadsheet\",\r\n },\r\n matchType: matchResult.matchType,\r\n score: matchResult.score,\r\n searchQuery,\r\n };\r\n }\r\n }\r\n \r\n if (bestMatch) {\r\n return bestMatch;\r\n }\r\n \r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_MATCH,\r\n searchQuery,\r\n };\r\n \r\n } catch (error) {\r\n return {\r\n data: null,\r\n error: error.message,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Lookup LOI from API\r\n * This function will remain after spreadsheet is phased out\r\n * @param {string} searchQuery - Property address to search\r\n * @returns {Promise<Object>} Lookup result\r\n */\r\nasync function lookupFromAPI(searchQuery) {\r\n try {\r\n const url = new URL(LOI_LOOKUP_CONFIG.API_BASE_URL);\r\n url.pathname = LOI_LOOKUP_CONFIG.WEBHOOK_PATH;\r\n url.searchParams.set(\"location_id\", LOI_LOOKUP_CONFIG.LOCATION_ID);\r\n url.searchParams.set(\"q\", searchQuery);\r\n \r\n const response = await fetch(url.toString(), {\r\n headers: {\r\n \"Accept\": \"application/json\",\r\n },\r\n method: \"GET\",\r\n });\r\n \r\n console.log('lookupFromAPI URL', url)\r\n console.log('lookupFromAPI response', response)\r\n\r\n if (!response.ok) {\r\n console.log(\"!response.ok\", response)\r\n return {\r\n data: null,\r\n error: `HTTP ${response.status}`,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n \r\n const data = await response.json();\r\n console.log('lookupFromAPI data', data)\r\n \r\n if (!data || !data.opportunityName) {\r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n \r\n const opportunityAddress = data.opportunityName.split(\"+\")[0].trim();\r\n console.log('lookupFromAPI opportunityAddress', opportunityAddress)\r\n \r\n const matchResult = matchAddresses(searchQuery, opportunityAddress);\r\n console.log('lookupFromAPI matchResult', matchResult)\r\n \r\n return {\r\n data: {\r\n contactName: data.contactName,\r\n createdAt: data.createdAt,\r\n opportunityAddress,\r\n opportunityName: data.opportunityName,\r\n statusOrStage: data.statusOrStage,\r\n updatedAt: data.updatedAt,\r\n foundIn: \"api\",\r\n },\r\n matchType: matchResult.matchType,\r\n score: matchResult.score,\r\n searchQuery,\r\n };\r\n \r\n } catch (error) {\r\n console.log('lookupFromAPI error', error)\r\n return {\r\n data: null,\r\n error: error.message,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Lookup LOI status for a property address\r\n * @param {string} searchQuery - Property address to search\r\n * @returns {Promise<Object>} Lookup result\r\n */\r\nexport async function lookupLOI(searchQuery) {\r\n if (!searchQuery) {\r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n \r\n // const spreadsheetResult = await lookupFromSpreadsheet(searchQuery);\r\n // if (spreadsheetResult.matchType !== MATCH_TYPES.NO_MATCH && spreadsheetResult.matchType !== MATCH_TYPES.NO_RESPONSE) {\r\n // return spreadsheetResult;\r\n // }\r\n \r\n return await lookupFromAPI(searchQuery);\r\n}"],"names":["normalizeAddress","address","toLowerCase","replace","trim","parseAddress","normalized","zipMatch","match","zip","stateMatch","state","cityMatch","city","streetMatch","full","street","matchAddresses","searchQuery","opportunityAddress","search","opportunity","matchType","MATCH_TYPES","NO_MATCH","score","similarity","str1","str2","s1","s2","includes","words1","split","filter","w","length","words2","calculateSimilarity","EXACT","FUZZY","async","lookupLOI","url","URL","LOI_LOOKUP_CONFIG","API_BASE_URL","pathname","WEBHOOK_PATH","searchParams","set","LOCATION_ID","response","fetch","toString","headers","Accept","method","console","log","ok","data","error","status","NO_RESPONSE","json","opportunityName","matchResult","contactName","createdAt","statusOrStage","updatedAt","foundIn","message","lookupFromAPI"],"mappings":"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"}
1
+ {"version":3,"file":"loi-lookup.js","sources":["../../src/services/loi-lookup.js"],"sourcesContent":["/**\r\n * LOI Lookup Service\r\n * Checks if a property address has an existing LOI in the CRM system\r\n */\r\n\r\nimport { LOI_LOOKUP_CONFIG, MATCH_TYPES } from \"../config/loi-lookup.js\";\r\n\r\n/**\r\n * Normalize an address string for comparison\r\n * @param {string} address - Address string to normalize\r\n * @returns {string} Normalized address\r\n */\r\nfunction normalizeAddress(address) {\r\n if (!address) return \"\";\r\n \r\n return address\r\n .toLowerCase()\r\n .replace(/[^\\w\\s]/g, \" \")\r\n .replace(/\\s+/g, \" \")\r\n .trim();\r\n}\r\n\r\n/**\r\n * Extract components from an address string\r\n * @param {string} address - Address to parse\r\n * @returns {Object} Address components\r\n */\r\nfunction parseAddress(address) {\r\n const normalized = normalizeAddress(address);\r\n \r\n const zipMatch = normalized.match(/\\b\\d{5}\\b/);\r\n const zip = zipMatch ? zipMatch[0] : null;\r\n \r\n const stateMatch = normalized.match(/\\b([a-z]{2})\\s+\\d{5}\\b/);\r\n const state = stateMatch ? stateMatch[1] : null;\r\n \r\n const cityMatch = normalized.match(/([a-z\\s]+)\\s+[a-z]{2}\\s+\\d{5}/);\r\n const city = cityMatch ? cityMatch[1].trim() : null;\r\n \r\n const streetMatch = normalized.match(/^(.+?)\\s+[a-z\\s]+\\s+[a-z]{2}\\s+\\d{5}/);\r\n const street = streetMatch ? streetMatch[1].trim() : normalized;\r\n \r\n return {\r\n city,\r\n full: normalized,\r\n state,\r\n street,\r\n zip,\r\n };\r\n}\r\n\r\n/**\r\n * Calculate similarity score between two strings (0-1)\r\n * @param {string} str1 - First string\r\n * @param {string} str2 - Second string\r\n * @returns {number} Similarity score\r\n */\r\nfunction calculateSimilarity(str1, str2) {\r\n if (!str1 || !str2) return 0;\r\n \r\n const s1 = normalizeAddress(str1);\r\n const s2 = normalizeAddress(str2);\r\n \r\n if (s1 === s2) return 1.0;\r\n \r\n if (s1.includes(s2) || s2.includes(s1)) return 0.8;\r\n \r\n const words1 = s1.split(\" \").filter(w => w.length > 2);\r\n const words2 = s2.split(\" \").filter(w => w.length > 2);\r\n \r\n if (words1.length === 0 || words2.length === 0) return 0;\r\n \r\n const commonWords = words1.filter(w => words2.includes(w));\r\n const overlapRatio = (commonWords.length * 2) / (words1.length + words2.length);\r\n \r\n return overlapRatio;\r\n}\r\n\r\n/**\r\n * Match two addresses using fuzzy matching\r\n * @param {string} searchQuery - The address being searched\r\n * @param {string} opportunityAddress - Address from the CRM opportunity\r\n * @returns {Object} Match result with type and score\r\n */\r\nfunction matchAddresses(searchQuery, opportunityAddress) {\r\n const search = parseAddress(searchQuery);\r\n const opportunity = parseAddress(opportunityAddress);\r\n \r\n if (search.city && opportunity.city) {\r\n if (search.city !== opportunity.city) {\r\n return { matchType: MATCH_TYPES.NO_MATCH, score: 0 };\r\n }\r\n }\r\n \r\n if (search.zip && opportunity.zip) {\r\n if (search.zip !== opportunity.zip) {\r\n return { matchType: MATCH_TYPES.NO_MATCH, score: 0 };\r\n }\r\n }\r\n \r\n if (search.state && opportunity.state) {\r\n if (search.state !== opportunity.state) {\r\n return { matchType: MATCH_TYPES.NO_MATCH, score: 0 };\r\n }\r\n }\r\n \r\n const similarity = calculateSimilarity(searchQuery, opportunityAddress);\r\n \r\n if (similarity >= 0.95) {\r\n return { matchType: MATCH_TYPES.EXACT, score: similarity };\r\n }\r\n \r\n if (similarity >= 0.6) {\r\n return { matchType: MATCH_TYPES.FUZZY, score: similarity };\r\n }\r\n \r\n return { matchType: MATCH_TYPES.NO_MATCH, score: similarity };\r\n}\r\n\r\n/**\r\n * Parse CSV text into array of objects\r\n * Handles quoted fields with commas correctly\r\n * @param {string} csvText - CSV content\r\n * @returns {Array<Object>} Parsed rows\r\n */\r\nfunction parseCSV(csvText) {\r\n const lines = csvText.split(\"\\n\").filter(line => line.trim());\r\n if (lines.length === 0) return [];\r\n \r\n const rows = [];\r\n \r\n for (let i = 1; i < lines.length; i++) {\r\n const line = lines[i];\r\n const cols = [];\r\n let current = \"\";\r\n let inQuotes = false;\r\n \r\n for (let j = 0; j < line.length; j++) {\r\n const char = line[j];\r\n \r\n if (char === '\"') {\r\n inQuotes = !inQuotes;\r\n } else if (char === \",\" && !inQuotes) {\r\n cols.push(current.trim());\r\n current = \"\";\r\n } else {\r\n current += char;\r\n }\r\n }\r\n cols.push(current.trim());\r\n \r\n if (cols.length >= 4 && cols[1]) {\r\n rows.push({\r\n address: cols[1],\r\n contactName: cols[3] || null,\r\n date: cols[2] || null,\r\n });\r\n }\r\n }\r\n \r\n return rows;\r\n}\r\n\r\n/**\r\n * Lookup LOI from Google Spreadsheet\r\n * DELETE THIS FUNCTION when phasing out spreadsheet\r\n * @param {string} searchQuery - Property address to search\r\n * @returns {Promise<Object>} Lookup result\r\n */\r\nasync function lookupFromSpreadsheet(searchQuery) {\r\n try {\r\n const response = await fetch(LOI_LOOKUP_CONFIG.SPREADSHEET_URL, {\r\n headers: {\r\n \"Accept\": \"text/csv\",\r\n },\r\n method: \"GET\",\r\n });\r\n \r\n if (!response.ok) {\r\n console.log('lookupFromSpreadsheet response not ok', response)\r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n \r\n const csvText = await response.text();\r\n const rows = parseCSV(csvText);\r\n \r\n let bestMatch = null;\r\n let bestScore = 0;\r\n \r\n for (const row of rows) {\r\n const matchResult = matchAddresses(searchQuery, row.address);\r\n \r\n if (matchResult.matchType !== MATCH_TYPES.NO_MATCH && matchResult.score > bestScore) {\r\n bestScore = matchResult.score;\r\n bestMatch = {\r\n data: {\r\n contactName: row.contactName,\r\n createdAt: row.date,\r\n opportunityAddress: row.address,\r\n opportunityName: row.address,\r\n statusOrStage: LOI_LOOKUP_CONFIG.LOI_SENT_STATUS,\r\n updatedAt: row.date,\r\n foundIn: \"spreadsheet\",\r\n },\r\n matchType: matchResult.matchType,\r\n score: matchResult.score,\r\n searchQuery,\r\n };\r\n }\r\n }\r\n \r\n if (bestMatch) {\r\n return bestMatch;\r\n }\r\n \r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_MATCH,\r\n searchQuery,\r\n };\r\n \r\n } catch (error) {\r\n return {\r\n data: null,\r\n error: error.message,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Lookup LOI from API\r\n * This function will remain after spreadsheet is phased out\r\n * @param {string} searchQuery - Property address to search\r\n * @returns {Promise<Object>} Lookup result\r\n */\r\nasync function lookupFromAPI(searchQuery) {\r\n try {\r\n const url = new URL(LOI_LOOKUP_CONFIG.API_BASE_URL);\r\n url.pathname = LOI_LOOKUP_CONFIG.WEBHOOK_PATH;\r\n url.searchParams.set(\"location_id\", LOI_LOOKUP_CONFIG.LOCATION_ID);\r\n url.searchParams.set(\"q\", searchQuery);\r\n \r\n const response = await fetch(url.toString(), {\r\n headers: {\r\n \"Accept\": \"application/json\",\r\n },\r\n method: \"GET\",\r\n });\r\n \r\n if (!response.ok) {\r\n console.log(\"LOI lookupFromAPI !response.ok\", response)\r\n return {\r\n data: null,\r\n error: `HTTP ${response.status}`,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n \r\n const data = await response.json();\r\n \r\n if (!data) {\r\n console.log(\"LOI lookupFromAPI !data\", response)\r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n\r\n if (data.notFound) {\r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_MATCH,\r\n searchQuery,\r\n };\r\n }\r\n \r\n const opportunityAddress = data.opportunityName.split(\"+\")[0].trim();\r\n const matchResult = matchAddresses(searchQuery, opportunityAddress);\r\n \r\n return {\r\n data: {\r\n contactName: data.contactName,\r\n createdAt: data.createdAt,\r\n opportunityAddress,\r\n opportunityName: data.opportunityName,\r\n statusOrStage: data.statusOrStage,\r\n updatedAt: data.updatedAt,\r\n foundIn: \"api\",\r\n },\r\n matchType: matchResult.matchType,\r\n score: matchResult.score,\r\n searchQuery,\r\n };\r\n \r\n } catch (error) {\r\n console.log('lookupFromAPI error', error)\r\n return {\r\n data: null,\r\n error: error.message,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Lookup LOI status for a property address\r\n * @param {string} searchQuery - Property address to search\r\n * @returns {Promise<Object>} Lookup result\r\n */\r\nexport async function lookupLOI(searchQuery) {\r\n if (!searchQuery) {\r\n return {\r\n data: null,\r\n matchType: MATCH_TYPES.NO_RESPONSE,\r\n searchQuery,\r\n };\r\n }\r\n \r\n // const spreadsheetResult = await lookupFromSpreadsheet(searchQuery);\r\n // if (spreadsheetResult.matchType !== MATCH_TYPES.NO_MATCH && spreadsheetResult.matchType !== MATCH_TYPES.NO_RESPONSE) {\r\n // return spreadsheetResult;\r\n // }\r\n \r\n return await lookupFromAPI(searchQuery);\r\n}"],"names":["normalizeAddress","address","toLowerCase","replace","trim","parseAddress","normalized","zipMatch","match","zip","stateMatch","state","cityMatch","city","streetMatch","full","street","matchAddresses","searchQuery","opportunityAddress","search","opportunity","matchType","MATCH_TYPES","NO_MATCH","score","similarity","str1","str2","s1","s2","includes","words1","split","filter","w","length","words2","calculateSimilarity","EXACT","FUZZY","async","lookupLOI","url","URL","LOI_LOOKUP_CONFIG","API_BASE_URL","pathname","WEBHOOK_PATH","searchParams","set","LOCATION_ID","response","fetch","toString","headers","Accept","method","ok","console","log","data","error","status","NO_RESPONSE","json","notFound","opportunityName","matchResult","contactName","createdAt","statusOrStage","updatedAt","foundIn","message","lookupFromAPI"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "4.6.30",
3
+ "version": "7.0.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",