@archerjessop/utilities 4.6.19 → 4.6.21

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 T={API_BASE_URL:"https://n8n-whai-u45960.vm.elestio.app",WEBHOOK_PATH:"/webhook/e15233bc-0623-4365-9e65-334bc5fc72e2",LOCATION_ID:"KjMMUEqwj4uFZvx4hWzq",MATCH_TYPES:{NO_RESPONSE:"no-response",NO_MATCH:"no-match",FUZZY:"fuzzy",EXACT:"exact"},LOI_SENT_STATUS:"LOI Sent"},{MATCH_TYPES:S,LOI_SENT_STATUS:_}=T;export{T as LOI_LOOKUP_CONFIG,_ 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:"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};
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 WEBHOOK_PATH: \"/webhook/e15233bc-0623-4365-9e65-334bc5fc72e2\",\r\n LOCATION_ID: \"KjMMUEqwj4uFZvx4hWzq\",\r\n \r\n // Match type constants\r\n MATCH_TYPES: {\r\n NO_RESPONSE: \"no-response\",\r\n NO_MATCH: \"no-match\",\r\n FUZZY: \"fuzzy\",\r\n EXACT: \"exact\",\r\n },\r\n \r\n // Status constants\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","WEBHOOK_PATH","LOCATION_ID","MATCH_TYPES","NO_RESPONSE","NO_MATCH","FUZZY","EXACT","LOI_SENT_STATUS"],"mappings":"AAIY,MAACA,EAAoB,CAC/BC,aAAc,yCACdC,aAAc,gDACdC,YAAa,uBAGbC,YAAa,CACXC,YAAa,cACbC,SAAU,WACVC,MAAO,QACPC,MAAO,SAITC,gBAAiB,aAGNL,YAAEA,EAAWK,gBAAEA,GAAoBT"}
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,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/),a=r?r[0]:null,s=e.match(/\b([a-z]{2})\s+\d{5}\b/),c=s?s[1]:null,n=e.match(/([a-z\s]+)\s+[a-z]{2}\s+\d{5}/),o=n?n[1].trim():null,i=e.match(/^(.+?)\s+[a-z\s]+\s+[a-z]{2}\s+\d{5}/);return{city:o,full:e,state:c,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 c=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),c=a.split(" ").filter(t=>t.length>2);return 0===s.length||0===c.length?0:2*s.filter(t=>c.includes(t)).length/(s.length+c.length)}(e,r);return c>=.95?{matchType:t.EXACT,score:c}:c>=.6?{matchType:t.FUZZY,score:c}:{matchType:t.NO_MATCH,score:c}}async function lookupLOI(r){if(!r)return{data:null,matchType:t.NO_RESPONSE,searchQuery: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{data:null,error:`HTTP ${s.status}`,matchType:t.NO_RESPONSE,searchQuery:r};const c=await s.json();if(!c||!c.opportunityName)return{data:null,matchType:t.NO_RESPONSE,searchQuery:r};const n=c.opportunityName.split("+")[0].trim(),o=matchAddresses(r,n);return{data:{contactName:c.contactName,createdAt:c.createdAt,opportunityAddress:n,opportunityName:c.opportunityName,statusOrStage:c.statusOrStage,updatedAt:c.updatedAt},matchType:o.matchType,score:o.score,searchQuery:r}}catch(e){return{data:null,error:e.message,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/),c=s?s[1]:null,n=e.match(/([a-z\s]+)\s+[a-z]{2}\s+\d{5}/),o=n?n[1].trim():null,u=e.match(/^(.+?)\s+[a-z\s]+\s+[a-z]{2}\s+\d{5}/);return{city:o,full:e,state:c,street:u?u[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 c=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),c=a.split(" ").filter(t=>t.length>2);return 0===s.length||0===c.length?0:2*s.filter(t=>c.includes(t)).length/(s.length+c.length)}(e,r);return c>=.95?{matchType:t.EXACT,score:c}:c>=.6?{matchType:t.FUZZY,score:c}:{matchType:t.NO_MATCH,score:c}}async function lookupLOI(r){if(!r)return{data:null,matchType:t.NO_RESPONSE,searchQuery:r};const a=await async function(r){try{const a=await fetch(e.SPREADSHEET_URL,{headers:{Accept:"text/csv"},method:"GET"});if(!a.ok)return{data:null,matchType:t.NO_RESPONSE,searchQuery:r};const s=function(t){const e=t.split("\n").filter(t=>t.trim());if(0===e.length)return[];const r=[];for(let t=1;t<e.length;t++){const a=e[t].split(",").map(t=>t.trim().replace(/^"|"$/g,""));a.length>=4&&a[1]&&r.push({address:a[1],contactName:a[3]||null,date:a[2]||null})}return r}(await a.text());let c=null,n=0;for(const a of s){const s=matchAddresses(r,a.address);s.matchType!==t.NO_MATCH&&s.score>n&&(n=s.score,c={data:{contactName:a.contactName,createdAt:a.date,opportunityAddress:a.address,opportunityName:a.address,statusOrStage:e.LOI_SENT_STATUS,updatedAt:a.date,foundIn:"spreadsheet"},matchType:s.matchType,score:s.score,searchQuery:r})}return c||{data:null,matchType:t.NO_MATCH,searchQuery:r}}catch(e){return{data:null,error:e.message,matchType:t.NO_RESPONSE,searchQuery:r}}}(r);return a.matchType!==t.NO_MATCH&&a.matchType!==t.NO_RESPONSE?a: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{data:null,error:`HTTP ${s.status}`,matchType:t.NO_RESPONSE,searchQuery:r};const c=await s.json();if(!c||!c.opportunityName)return{data:null,matchType:t.NO_RESPONSE,searchQuery:r};const n=c.opportunityName.split("+")[0].trim(),o=matchAddresses(r,n);return{data:{contactName:c.contactName,createdAt:c.createdAt,opportunityAddress:n,opportunityName:c.opportunityName,statusOrStage:c.statusOrStage,updatedAt:c.updatedAt,foundIn:"api"},matchType:o.matchType,score:o.score,searchQuery:r}}catch(e){return{data:null,error:e.message,matchType:t.NO_RESPONSE,searchQuery:r}}}(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, \" \") // Replace punctuation with spaces\r\n .replace(/\\s+/g, \" \") // Collapse multiple spaces\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 // Extract zip code (5 digits)\r\n const zipMatch = normalized.match(/\\b\\d{5}\\b/);\r\n const zip = zipMatch ? zipMatch[0] : null;\r\n \r\n // Extract state (2 letter code before zip)\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 // Extract city (word(s) before state)\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 // Street address is everything before city\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 // Exact match\r\n if (s1 === s2) return 1.0;\r\n \r\n // Contains match\r\n if (s1.includes(s2) || s2.includes(s1)) return 0.8;\r\n \r\n // Word overlap\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 // Check for city match if both present\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 // Check for zip code match if both present\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 // Check for state match if both present\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 // Calculate overall similarity\r\n const similarity = calculateSimilarity(searchQuery, opportunityAddress);\r\n \r\n // Exact match threshold\r\n if (similarity >= 0.95) {\r\n return { matchType: MATCH_TYPES.EXACT, score: similarity };\r\n }\r\n \r\n // Fuzzy match threshold\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 * 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 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 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 // Check if response has required fields\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 // Extract address from opportunityName (format: \"ADDRESS + CONTACT_NAME\")\r\n const opportunityAddress = data.opportunityName.split(\"+\")[0].trim();\r\n \r\n // Perform fuzzy matching\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 },\r\n matchType: matchResult.matchType,\r\n score: matchResult.score,\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}"],"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","data","NO_RESPONSE","url","URL","LOI_LOOKUP_CONFIG","API_BASE_URL","pathname","WEBHOOK_PATH","searchParams","set","LOCATION_ID","response","fetch","toString","headers","Accept","method","ok","error","status","json","opportunityName","matchResult","contactName","createdAt","statusOrStage","updatedAt","message"],"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 * @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 for (let i = 1; i < lines.length; i++) {\r\n const line = lines[i];\r\n const cols = line.split(\",\").map(col => col.trim().replace(/^\"|\"$/g, \"\"));\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 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 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 || !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 \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 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","data","NO_RESPONSE","spreadsheetResult","response","fetch","LOI_LOOKUP_CONFIG","SPREADSHEET_URL","headers","Accept","method","ok","rows","csvText","lines","line","i","cols","map","col","push","contactName","date","parseCSV","text","bestMatch","bestScore","row","matchResult","createdAt","opportunityName","statusOrStage","LOI_SENT_STATUS","updatedAt","foundIn","error","message","lookupFromSpreadsheet","url","URL","API_BASE_URL","pathname","WEBHOOK_PATH","searchParams","set","LOCATION_ID","toString","status","json","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.19",
3
+ "version": "4.6.21",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",