@archerjessop/utilities 4.6.26 → 4.6.27

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
- function formatCurrency(e,t=!1){if(isNaN(e)||!isFinite(e))return"N/A";const i=Math.abs(e),r=e<0?"-$":"$";if(t)return r+i.toLocaleString("en-US",{maximumFractionDigits:0});if(i>=1e6){return r+(i/1e6).toFixed(3).replace(/\.?0+$/,"")+"M"}if(i>=1e3){return r+(i/1e3).toFixed(3).replace(/\.?0+$/,"")+"K"}return r+i.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPriceValue(e){if(isNaN(e)||!isFinite(e))return"N/A";const t=Math.abs(e),i=e<0?"-$":"$";return t>=1e6?i+(t/1e6).toFixed(1)+"M":t>=1e3?i+(t/1e3).toFixed(0)+"K":i+t.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPercentage(e){if(isNaN(e)||!isFinite(e))return"N/A";let t=e.toString();t.includes("e")&&(t=Number(t).toFixed(12));const[i,r=""]=t.split("."),n=r.slice(0,2);return(n?`${i}.${n}`:i).replace(/\.?0+$/,"")+"%"}export{formatCurrency,formatPercentage,formatPriceValue};
1
+ function formatCurrency(e,t=!1){if(isNaN(e)||!isFinite(e))return"N/A";const i=Math.abs(e),r=e<0?"-$":"$";if(t)return r+i.toLocaleString("en-US",{maximumFractionDigits:0});if(i>=1e6){return r+(i/1e6).toFixed(3).replace(/\.?0+$/,"")+"M"}if(i>=1e3){return r+(i/1e3).toFixed(3).replace(/\.?0+$/,"")+"K"}return r+i.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPriceValue(e){if(isNaN(e)||!isFinite(e))return"N/A";const t=Math.abs(e),i=e<0?"-$":"$";return t>=1e6?i+(t/1e6).toFixed(1)+"M":t>=1e3?i+(t/1e3).toFixed(0)+"K":i+t.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPercentage(e){if(isNaN(e)||!isFinite(e))return"N/A";let t=e.toString();t.includes("e")&&(t=Number(t).toFixed(12));const[i,r=""]=t.split(".");if(!r)return i+"%";const n=r.slice(0,2).replace(/0+$/,"");return n?i+"."+n+"%":i+"%"}export{formatCurrency,formatPercentage,formatPriceValue};
2
2
  //# sourceMappingURL=formatters.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatters.js","sources":["../../src/financial/formatters.js"],"sourcesContent":["/**\r\n * src/financial/formatters.js\r\n * \r\n * OUTPUT/DISPLAY FORMATTERS - For read-only display of financial data\r\n * \r\n * Purpose: Format calculated financial values for compact, readable display\r\n * Use cases:\r\n * - Browser extension metrics and tooltips\r\n * - Dashboard display values\r\n * - Comparison pages\r\n * - Any read-only financial data presentation\r\n * \r\n * Characteristics:\r\n * - Uses K/M notation for compact display (e.g., \"$2.5M\", \"$125K\")\r\n * - Optimized for space-constrained UI elements\r\n * - Not for user input fields (see formatting/financial-formatting.js)\r\n * \r\n * Related files:\r\n * - formatting/financial-formatting.js: Input formatters for editable fields\r\n */\r\n\r\n/**\r\n * Format currency with K/M notation for compact display\r\n * @param {number} amount - The amount to format\r\n * @param {boolean} isMonthly - If true, shows full amount with commas (for monthly payments)\r\n * @returns {string} Formatted currency string (e.g., \"$2.5M\", \"$125K\", \"$1,234\")\r\n */\r\nexport function formatCurrency(amount, isMonthly = false) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (isMonthly) {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n } else {\r\n if (absAmount >= 1000000) {\r\n const millions = absAmount / 1000000;\r\n const formatted = millions.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"M\";\r\n } else if (absAmount >= 1000) {\r\n const thousands = absAmount / 1000;\r\n const formatted = thousands.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Format price value with K/M notation for compact display\r\n * Similar to formatCurrency but with fixed decimal places\r\n * @param {number} amount - The amount to format\r\n * @returns {string} Formatted price string (e.g., \"$2.5M\", \"$125K\")\r\n */\r\nexport function formatPriceValue(amount) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (absAmount >= 1000000) {\r\n return prefix + (absAmount / 1000000).toFixed(1) + \"M\";\r\n } else if (absAmount >= 1000) {\r\n return prefix + (absAmount / 1000).toFixed(0) + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n}\r\n\r\n/**\r\n * Format percentage for display\r\n * @param {number} percentage - The percentage value to format (e.g., 7.5 for 7.5%)\r\n * @returns {string} Formatted percentage string (e.g., \"7.5%\")\r\n * \r\n */\r\nexport function formatPercentage(percentage) {\r\n if (isNaN(percentage) || !isFinite(percentage)) return \"N/A\";\r\n\r\n // Convert to string with enough decimals\r\n let str = percentage.toString();\r\n\r\n if (str.includes(\"e\")) {\r\n // Handle scientific notation\r\n str = Number(str).toFixed(2 + 10);\r\n }\r\n\r\n // Split integer and decimal parts\r\n const [intPart, decPart = \"\"] = str.split(\".\");\r\n\r\n // Take up to 2 decimals without rounding\r\n const truncatedDec = decPart.slice(0, 2);\r\n\r\n // Combine and remove trailing zeros\r\n const combined = truncatedDec ? `${intPart}.${truncatedDec}` : intPart;\r\n const cleaned = combined.replace(/\\.?0+$/, \"\");\r\n\r\n return cleaned + \"%\";\r\n}\r\n"],"names":["formatCurrency","amount","isMonthly","isNaN","isFinite","absAmount","Math","abs","prefix","toLocaleString","maximumFractionDigits","toFixed","replace","formatPriceValue","formatPercentage","percentage","str","toString","includes","Number","intPart","decPart","split","truncatedDec","slice"],"mappings":"AA2BO,SAASA,eAAeC,EAAQC,GAAY,GACjD,GAAIC,MAAMF,KAAYG,SAASH,GAAS,MAAO,MAE/C,MAAMI,EAAYC,KAAKC,IAAIN,GAErBO,EADaP,EAAS,EACA,KAAO,IAEnC,GAAIC,EACF,OAAOM,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,IAE3E,GAAIL,GAAa,IAAS,CAGxB,OAAOG,GAFUH,EAAY,KACFM,QAAQ,GAAGC,QAAQ,SAAU,IAC5B,GAC9B,CAAO,GAAIP,GAAa,IAAM,CAG5B,OAAOG,GAFWH,EAAY,KACFM,QAAQ,GAAGC,QAAQ,SAAU,IAC7B,GAC9B,CACE,OAAOJ,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,GAGjF,CAQO,SAASG,iBAAiBZ,GAC/B,GAAIE,MAAMF,KAAYG,SAASH,GAAS,MAAO,MAE/C,MAAMI,EAAYC,KAAKC,IAAIN,GAErBO,EADaP,EAAS,EACA,KAAO,IAEnC,OAAII,GAAa,IACRG,GAAUH,EAAY,KAASM,QAAQ,GAAK,IAC1CN,GAAa,IACfG,GAAUH,EAAY,KAAMM,QAAQ,GAAK,IAEzCH,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,GAE/E,CAQO,SAASI,iBAAiBC,GAC/B,GAAIZ,MAAMY,KAAgBX,SAASW,GAAa,MAAO,MAGvD,IAAIC,EAAMD,EAAWE,WAEjBD,EAAIE,SAAS,OAEfF,EAAMG,OAAOH,GAAKL,QAAQ,KAI5B,MAAOS,EAASC,EAAU,IAAML,EAAIM,MAAM,KAGpCC,EAAeF,EAAQG,MAAM,EAAG,GAMtC,OAHiBD,EAAe,GAAGH,KAAWG,IAAiBH,GACtCR,QAAQ,SAAU,IAE1B,GACnB"}
1
+ {"version":3,"file":"formatters.js","sources":["../../src/financial/formatters.js"],"sourcesContent":["/**\r\n * src/financial/formatters.js\r\n * \r\n * OUTPUT/DISPLAY FORMATTERS - For read-only display of financial data\r\n * \r\n * Purpose: Format calculated financial values for compact, readable display\r\n * Use cases:\r\n * - Browser extension metrics and tooltips\r\n * - Dashboard display values\r\n * - Comparison pages\r\n * - Any read-only financial data presentation\r\n * \r\n * Characteristics:\r\n * - Uses K/M notation for compact display (e.g., \"$2.5M\", \"$125K\")\r\n * - Optimized for space-constrained UI elements\r\n * - Not for user input fields (see formatting/financial-formatting.js)\r\n * \r\n * Related files:\r\n * - formatting/financial-formatting.js: Input formatters for editable fields\r\n */\r\n\r\n/**\r\n * Format currency with K/M notation for compact display\r\n * @param {number} amount - The amount to format\r\n * @param {boolean} isMonthly - If true, shows full amount with commas (for monthly payments)\r\n * @returns {string} Formatted currency string (e.g., \"$2.5M\", \"$125K\", \"$1,234\")\r\n */\r\nexport function formatCurrency(amount, isMonthly = false) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (isMonthly) {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n } else {\r\n if (absAmount >= 1000000) {\r\n const millions = absAmount / 1000000;\r\n const formatted = millions.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"M\";\r\n } else if (absAmount >= 1000) {\r\n const thousands = absAmount / 1000;\r\n const formatted = thousands.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Format price value with K/M notation for compact display\r\n * Similar to formatCurrency but with fixed decimal places\r\n * @param {number} amount - The amount to format\r\n * @returns {string} Formatted price string (e.g., \"$2.5M\", \"$125K\")\r\n */\r\nexport function formatPriceValue(amount) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (absAmount >= 1000000) {\r\n return prefix + (absAmount / 1000000).toFixed(1) + \"M\";\r\n } else if (absAmount >= 1000) {\r\n return prefix + (absAmount / 1000).toFixed(0) + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n}\r\n\r\n/**\r\n * Format percentage for display\r\n * @param {number} percentage - The percentage value to format (e.g., 7.5 for 7.5%)\r\n * @returns {string} Formatted percentage string (e.g., \"7.5%\")\r\n * \r\n */\r\nexport function formatPercentage(percentage) {\r\n if (isNaN(percentage) || !isFinite(percentage)) return \"N/A\";\r\n\r\n let str = percentage.toString();\r\n\r\n if (str.includes(\"e\")) {\r\n str = Number(str).toFixed(2 + 10);\r\n }\r\n\r\n const [intPart, decPart = \"\"] = str.split(\".\");\r\n\r\n if (!decPart) {\r\n // No decimal part - just return the integer\r\n return intPart + \"%\";\r\n }\r\n\r\n // Only remove trailing zeros from decimal part\r\n const truncatedDec = decPart.slice(0, 2);\r\n const cleanedDec = truncatedDec.replace(/0+$/, \"\");\r\n \r\n if (cleanedDec) {\r\n return intPart + \".\" + cleanedDec + \"%\";\r\n } else {\r\n return intPart + \"%\";\r\n }\r\n}\r\n"],"names":["formatCurrency","amount","isMonthly","isNaN","isFinite","absAmount","Math","abs","prefix","toLocaleString","maximumFractionDigits","toFixed","replace","formatPriceValue","formatPercentage","percentage","str","toString","includes","Number","intPart","decPart","split","cleanedDec","slice"],"mappings":"AA2BO,SAASA,eAAeC,EAAQC,GAAY,GACjD,GAAIC,MAAMF,KAAYG,SAASH,GAAS,MAAO,MAE/C,MAAMI,EAAYC,KAAKC,IAAIN,GAErBO,EADaP,EAAS,EACA,KAAO,IAEnC,GAAIC,EACF,OAAOM,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,IAE3E,GAAIL,GAAa,IAAS,CAGxB,OAAOG,GAFUH,EAAY,KACFM,QAAQ,GAAGC,QAAQ,SAAU,IAC5B,GAC9B,CAAO,GAAIP,GAAa,IAAM,CAG5B,OAAOG,GAFWH,EAAY,KACFM,QAAQ,GAAGC,QAAQ,SAAU,IAC7B,GAC9B,CACE,OAAOJ,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,GAGjF,CAQO,SAASG,iBAAiBZ,GAC/B,GAAIE,MAAMF,KAAYG,SAASH,GAAS,MAAO,MAE/C,MAAMI,EAAYC,KAAKC,IAAIN,GAErBO,EADaP,EAAS,EACA,KAAO,IAEnC,OAAII,GAAa,IACRG,GAAUH,EAAY,KAASM,QAAQ,GAAK,IAC1CN,GAAa,IACfG,GAAUH,EAAY,KAAMM,QAAQ,GAAK,IAEzCH,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,GAE/E,CAQO,SAASI,iBAAiBC,GAC/B,GAAIZ,MAAMY,KAAgBX,SAASW,GAAa,MAAO,MAEvD,IAAIC,EAAMD,EAAWE,WAEjBD,EAAIE,SAAS,OACfF,EAAMG,OAAOH,GAAKL,QAAQ,KAG5B,MAAOS,EAASC,EAAU,IAAML,EAAIM,MAAM,KAE1C,IAAKD,EAEH,OAAOD,EAAU,IAInB,MACMG,EADeF,EAAQG,MAAM,EAAG,GACNZ,QAAQ,MAAO,IAE/C,OAAIW,EACKH,EAAU,IAAMG,EAAa,IAE7BH,EAAU,GAErB"}
@@ -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,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],s=[];let c="",n=!1;for(let t=0;t<a.length;t++){const e=a[t];'"'===e?n=!n:","!==e||n?c+=e:(s.push(c.trim()),c="")}s.push(c.trim()),s.length>=4&&s[1]&&r.push({address:s[1],contactName:s[3]||null,date:s[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};
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(console.log("lookupFromSpreadsheet response",a),!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],s=[];let c="",n=!1;for(let t=0;t<a.length;t++){const e=a[t];'"'===e?n=!n:","!==e||n?c+=e:(s.push(c.trim()),c="")}s.push(c.trim()),s.length>=4&&s[1]&&r.push({address:s[1],contactName:s[3]||null,date:s[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(console.log("lookupFromAPI response",s),!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, \" \")\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 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","current","inQuotes","j","char","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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"}
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 response', response)\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","console","log","ok","rows","csvText","lines","line","i","cols","current","inQuotes","j","char","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.26",
3
+ "version": "4.6.27",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",