@archerjessop/utilities 4.6.30 → 4.6.31

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={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
- 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": "4.6.31",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",