// Open Side Panel when the toolbar icon is clicked chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error); // Rebuild Context Menu on load chrome.storage.local.get(['activeClient']).then(res => { if (res.activeClient) updateContextMenus(res.activeClient); }); // Watch for changes to the active client to update the right-click menu chrome.storage.onChanged.addListener((changes) => { if (changes.activeClient) { updateContextMenus(changes.activeClient.newValue); } }); // Handle right-click menu clicks chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId.startsWith("supa_")) { const base64 = info.menuItemId.replace("supa_", ""); try { const value = decodeURIComponent(escape(atob(base64))); chrome.tabs.sendMessage(tab.id, { action: "paste_to_focused", value }).catch(err => { console.error("Paste failed. Make sure you are clicking on an editable field.", err); }); } catch(e) { console.error(e); } } }); function updateContextMenus(client) { chrome.contextMenus.removeAll(() => { if (!client) return; chrome.contextMenus.create({ id: "supapaste_root", title: "SupaPaste", contexts: ["editable"] }); const usedIds = new Set(); const addMenu = (title, value) => { if (!value) return; let baseId = "supa_" + btoa(unescape(encodeURIComponent(value))).replace(/=/g, ''); let safeId = baseId; let counter = 1; while (usedIds.has(safeId)) { safeId = `${baseId}_${counter}`; counter++; } usedIds.add(safeId); chrome.contextMenus.create({ id: safeId, parentId: "supapaste_root", title: `${title}: ${value}`, contexts: ["editable"] }, () => { if (chrome.runtime.lastError) { console.warn("Menu creation warning:", chrome.runtime.lastError.message); } }); }; addMenu("First Name", client.FirstName || client.firstName); addMenu("Last Name", client.LastName || client.lastName); addMenu("Phone", client.Phone || client.phone || client.cellPhone || client.CellPhone); addMenu("Email", client.Email || client.email); addMenu("SSN", client.SocialSecurityNumber || client.SSN); addMenu("DOB", client.BirthDate || client.dateOfBirth || client.DOB); addMenu("Address", client.Address1 || client.addressLine1 || client.address1); addMenu("City", client.City || client.city); addMenu("State", client.State || client.state); addMenu("Zip", client.Zip || client.zipCode || client.zip); if (client.vehicles) { client.vehicles.forEach((v, i) => addMenu(`VIN ${i+1}`, v.VIN || v.vin)); } if (client.drivers) { client.drivers.forEach((d, i) => addMenu(`Driver DL ${i+1}`, d.LicenseNumber || d.licenseNumber)); } }); } chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message?.action === 'search_nowcerts') { (async () => { try { const results = await searchInsureds(String(message.query || '').trim()); sendResponse({ ok: true, results }); } catch (err) { sendResponse({ ok: false, error: err?.message || String(err) }); } })(); return true; } if (message?.action === 'create_prospect') { (async () => { try { const payload = message.payload || {}; const apiResult = await createProspect(payload); const last = String(payload.lastName || payload.LastName || '').trim(); const q = last || String(payload.firstName || payload.FirstName || '').trim(); const results = q ? await searchInsureds(q) : []; sendResponse({ ok: true, result: apiResult, results }); } catch (err) { sendResponse({ ok: false, error: err?.message || String(err) }); } })(); return true; } if (message?.action === 'get_full_client') { (async () => { try { const full = await getFullClientData(String(message.insuredId || '').trim()); sendResponse({ ok: true, client: full }); } catch (err) { sendResponse({ ok: false, error: err?.message || String(err) }); } })(); return true; } // --- AI SCAN LOGIC (For Page Forms) --- if (message?.action === 'run_ai_scan') { (async () => { try { const { apiKey, fields, hostname } = message; if (!apiKey) throw new Error("Missing Gemini API Key"); const prompt = `You are an intelligent form mapping assistant. Below is a JSON array of input fields extracted from a webpage. Map them to these exact standard keys: FirstName, LastName, Email, Phone, CellPhone, Address1, City, State, Zip, BirthDate, BirthMonth, BirthDay, BirthYear, SSN, VIN, Year, Make, Model, EffectiveDate, YearBuilt, SquareFeet, RoofYear, ConstructionType. Rules: 1. Return ONLY a raw JSON object. Do not wrap it in markdown blockquotes like \`\`\`json. 2. The keys MUST be from the standard list above. 3. The values MUST be the exact 'id' or 'name' of the matched input from the provided list. 4. If a standard key has no logical match, omit it. Fields List: ${JSON.stringify(fields)}`; // USING GEMINI 2.5 FLASH const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.1, response_mime_type: "application/json" } }) }); if (!res.ok) throw new Error(`Gemini API Error: ${res.status}`); const data = await res.json(); const textResponse = data.candidates[0].content.parts[0].text; let smartMap = {}; try { smartMap = JSON.parse(textResponse.replace(/```json/gi, '').replace(/```/g, '').trim()); } catch(e) { throw new Error("AI returned invalid data format."); } const storageKey = `smartMap_${hostname}`; await chrome.storage.local.set({ [storageKey]: smartMap }); sendResponse({ ok: true, smartMap }); } catch (err) { sendResponse({ ok: false, error: err.message }); } })(); return true; } // --- NEW: AI DATA RESCUE (JSON PARSING) --- if (message?.action === 'ai_data_rescue') { (async () => { try { const { apiKey, schema } = message; if (!apiKey) throw new Error("Missing Gemini API Key"); const prompt = `You are a data mapping assistant. Below is a list of structural JSON paths from an insurance API response. Map them to these exact standard keys: FirstName, LastName, Email, Phone, CellPhone, Address1, City, State, Zip, BirthDate, SSN, TaxId. Rules: 1. Return ONLY a raw JSON object. Do not wrap it in markdown blockquotes like \`\`\`json. 2. The keys MUST be from the standard list above. 3. The values MUST be the exact JSON path from the provided list that best represents that data (e.g., "insuredContacts[0].businessEMail"). 4. If a standard key has no logical match, omit it entirely. JSON Paths List: ${JSON.stringify(schema)}`; const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.1, response_mime_type: "application/json" } }) }); if (!res.ok) throw new Error(`Gemini API Error: ${res.status}`); const data = await res.json(); const textResponse = data.candidates[0].content.parts[0].text; let smartMap = {}; try { smartMap = JSON.parse(textResponse.replace(/```json/gi, '').replace(/```/g, '').trim()); } catch(e) { throw new Error("AI returned invalid data format."); } sendResponse({ ok: true, smartMap }); } catch (err) { sendResponse({ ok: false, error: err.message }); } })(); return true; } }); function odataEscapeString(s) { return String(s || '').replace(/'/g, "''"); } function normalizeMeaningfulString(value) { const str = String(value || '').trim(); if (!str || str.toLowerCase() === 'null' || str.toLowerCase() === 'undefined') return ''; return str; } function normalizeNamePart(value) { return normalizeMeaningfulString(value).toLowerCase().replace(/[^a-z0-9]/g, ''); } function dedupeBy(items, keyBuilder) { const seen = new Set(); return (Array.isArray(items) ? items : []).filter((item) => { const key = keyBuilder(item); if (!key || seen.has(key)) return false; seen.add(key); return true; }); } function detectSearchIntent(query) { const raw = String(query || '').trim(); const digits = raw.replace(/\D/g, ''); const normalized = raw.toLowerCase(); const isPhone = digits.length >= 7 && digits.length <= 11; const isDob = /^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}$/.test(raw) || /^\d{4}-\d{2}-\d{2}$/.test(raw); const isPolicy = !isPhone && !isDob && /[a-z0-9]/i.test(raw) && /^[a-z0-9-]{6,}$/i.test(raw) && !/\s/.test(raw); return { raw, digits, normalized, isPhone, isDob, isPolicy }; } function formatSearchDateVariants(raw) { const trimmed = String(raw || '').trim(); if (!trimmed) return []; const parts = trimmed.includes('-') ? trimmed.split('-') : trimmed.split(/[\/-]/); let month = ''; let day = ''; let year = ''; if (trimmed.includes('-') && parts[0]?.length === 4) { [year, month, day] = parts; } else { [month, day, year] = parts; } if (!month || !day || !year) return [trimmed]; const fullYear = year.length === 2 ? `20${year}` : year; const mm = String(month).padStart(2, '0'); const dd = String(day).padStart(2, '0'); return Array.from(new Set([ `${mm}/${dd}/${fullYear}`, `${fullYear}-${mm}-${dd}`, `${mm}/${dd}/${year}` ])); } function findMatchingPersonRecord(client) { const first = normalizeNamePart(client?.FirstName || client?.firstName); const last = normalizeNamePart(client?.LastName || client?.lastName); if (!first || !last) return null; const pools = [ ...(Array.isArray(client?.drivers) ? client.drivers : []), ...(Array.isArray(client?.insuredContacts) ? client.insuredContacts : []), ...(Array.isArray(client?.contacts) ? client.contacts : []) ]; return pools.find((person) => { const personFirst = normalizeNamePart(person?.FirstName || person?.firstName); const personLast = normalizeNamePart(person?.LastName || person?.lastName); return personFirst === first && personLast === last; }) || null; } function enrichClientWithMatchedPersonData(client) { const match = findMatchingPersonRecord(client); if (!match) return client; return { ...client, BirthDate: client?.BirthDate || client?.dateOfBirth || client?.DOB || match?.birthday || match?.BirthDate || '', SSN: client?.SSN || client?.SocialSecurityNumber || client?.socialSecurityNumber || match?.socialSecurityNumber || '', Phone: client?.Phone || client?.phone || client?.PrimaryPhone || match?.cellPhone || match?.homePhone || match?.officePhone || '', CellPhone: client?.CellPhone || client?.cellPhone || client?.MobilePhone || match?.cellPhone || '', Email: client?.Email || client?.email || client?.PrimaryEmail || match?.personalEMail || match?.businessEMail || '', DriverLicenseNumber: client?.DriverLicenseNumber || client?.driverLicenseNumber || client?.LicenseNumber || match?.licenseNumber || match?.dlNumber || '', DriverLicenseState: client?.DriverLicenseState || client?.driverLicenseState || client?.LicenseState || match?.licenseState || match?.dlStateName || '', Gender: client?.Gender || client?.gender || client?.Sex || client?.sex || match?.gender || '', MaritalStatus: client?.MaritalStatus || client?.maritalStatus || match?.maritalStatus || match?.maritalStatusCode || '' }; } function getPolicyId(policy) { return policy?.DatabaseId || policy?.databaseId || policy?.PolicyDatabaseId || policy?.policyDatabaseId || ''; } function getPolicyDates(policy) { const eff = new Date(policy?.EffectiveDate || policy?.effectiveDate || 0); const exp = new Date(policy?.ExpirationDate || policy?.expirationDate || 0); return { eff, exp }; } function getCurrentPolicies(policies) { const list = Array.isArray(policies) ? policies.filter(Boolean) : []; if (!list.length) return []; const now = new Date(); const active = list.filter((policy) => { const status = String(policy?.Status || policy?.status || '').toLowerCase(); const { eff, exp } = getPolicyDates(policy); if (status.includes('active')) return true; return !isNaN(eff.getTime()) && !isNaN(exp.getTime()) && eff <= now && exp >= now; }); if (active.length) return active; const sorted = [...list].sort((a, b) => { const aEff = getPolicyDates(a).eff.getTime() || 0; const bEff = getPolicyDates(b).eff.getTime() || 0; return bEff - aEff; }); return sorted.length ? [sorted[0]] : []; } function filterPolicyChildrenByPolicies(items, policies, possiblePolicyKeys = []) { const allowed = new Set(getCurrentPolicies(policies).map(getPolicyId).filter(Boolean)); if (!allowed.size) return Array.isArray(items) ? items : []; return (Array.isArray(items) ? items : []).filter((item) => { const itemPolicyId = possiblePolicyKeys.map((key) => item?.[key]).find(Boolean); return !itemPolicyId || allowed.has(itemPolicyId); }); } async function searchInsureds(query) { if (!query) return []; const storage = await chrome.storage.local.get(['ncApiToken']); const token = storage.ncApiToken; if (!token) throw new Error('Missing API Token, please sign in in Settings.'); const normalizedQuery = String(query || '').trim(); const lowerQ = normalizedQuery.toLowerCase(); const q = odataEscapeString(normalizedQuery); const searchIntent = detectSearchIntent(normalizedQuery); const dateVariants = searchIntent.isDob ? formatSearchDateVariants(normalizedQuery) : []; console.log('[Carrier Bridge][search] query:', normalizedQuery); console.log('[Carrier Bridge][search] intent:', searchIntent); const terms = Array.from(new Set( normalizedQuery.split(/\s+/).map(t => t.trim()).filter(Boolean) )); console.log('[Carrier Bridge][search] terms:', terms); const headers = { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }; // Helper function: Tries 'contains' first, falls back to 'startswith' if NowCerts throws an error. // Docs indicate InsuredDetailList is the supported insured/prospect endpoint and list endpoints want full paging params. const fetchSafe = async (filterContains, filterStartsWith, label = 'search') => { let url = `https://api.nowcerts.com/api/InsuredDetailList()?$filter=${encodeURIComponent(filterContains)}&$count=true&$orderby=changeDate desc&$skip=0&$top=50`; let response = await fetch(url, { method: 'GET', headers }); console.log(`[Carrier Bridge][search] ${label} contains status:`, response.status); if (!response.ok && response.status === 400) { url = `https://api.nowcerts.com/api/InsuredDetailList()?$filter=${encodeURIComponent(filterStartsWith)}&$count=true&$orderby=changeDate desc&$skip=0&$top=50`; response = await fetch(url, { method: 'GET', headers }); console.log(`[Carrier Bridge][search] ${label} startswith fallback status:`, response.status); } if (!response.ok) { const text = await response.text().catch(() => ''); console.warn(`[Carrier Bridge][search] ${label} failed:`, response.status, text); return []; } const json = await response.json(); const arr = Array.isArray(json?.value) ? json.value : (Array.isArray(json) ? json : []); console.log(`[Carrier Bridge][search] ${label} result count:`, arr.length); return arr; }; const resultMap = new Map(); const addResults = (items) => { items.forEach((item) => { const key = item?.Id || item?.id || item?.DatabaseId || item?.databaseId || JSON.stringify(item); if (!resultMap.has(key)) resultMap.set(key, item); }); }; const getCombinedSearchText = (item) => [ item?.commercialName, item?.CommercialName, item?.dba, item?.DBA, item?.FullName, item?.fullName, item?.Name, item?.name, item?.Phone, item?.phone, item?.CellPhone, item?.cellPhone, item?.PolicyNumber, item?.policyNumber, item?.Number, item?.number, item?.BirthDate, item?.birthDate, item?.dateOfBirth, item?.DOB, `${item?.FirstName || item?.firstName || ''} ${item?.LastName || item?.lastName || ''}`.trim() ].filter(Boolean).join(' ').toLowerCase(); if (searchIntent.isPhone) { const phoneDigits = odataEscapeString(searchIntent.digits); const phoneContains = `active eq true and (contains(phone,'${phoneDigits}') or contains(cellPhone,'${phoneDigits}') or contains(primaryPhone,'${phoneDigits}') or contains(homePhone,'${phoneDigits}'))`; const phoneStarts = `active eq true and (startswith(phone,'${phoneDigits}') or startswith(cellPhone,'${phoneDigits}') or startswith(primaryPhone,'${phoneDigits}') or startswith(homePhone,'${phoneDigits}'))`; addResults(await fetchSafe(phoneContains, phoneStarts, 'phone')); console.log('[Carrier Bridge][search] unique results after phone intent:', resultMap.size); } if (searchIntent.isDob) { for (const variant of dateVariants) { const safeVariant = odataEscapeString(variant); const dobContains = `active eq true and (contains(birthDate,'${safeVariant}') or contains(dateOfBirth,'${safeVariant}') or contains(DOB,'${safeVariant}'))`; const dobStarts = `active eq true and (startswith(birthDate,'${safeVariant}') or startswith(dateOfBirth,'${safeVariant}') or startswith(DOB,'${safeVariant}'))`; addResults(await fetchSafe(dobContains, dobStarts, `dob:${variant}`)); } console.log('[Carrier Bridge][search] unique results after dob intent:', resultMap.size); } if (searchIntent.isPolicy) { try { const policyUrl = `https://api.nowcerts.com/api/PolicyList()?$filter=${encodeURIComponent(`contains(tolower(number),'${odataEscapeString(lowerQ)}') or contains(tolower(policyNumber),'${odataEscapeString(lowerQ)}')`)}&$count=true&$orderby=changeDate desc&$skip=0&$top=25`; const policyRes = await fetch(policyUrl, { method: 'GET', headers }); console.log('[Carrier Bridge][search] policy intent status:', policyRes.status); if (policyRes.ok) { const policyJson = await policyRes.json(); const policies = Array.isArray(policyJson?.value) ? policyJson.value : (Array.isArray(policyJson) ? policyJson : []); policies.forEach((policy) => { const insuredId = policy?.InsuredId || policy?.insuredId || policy?.InsuredDatabaseId || policy?.insuredDatabaseId; if (!insuredId) return; addResults([{ ...policy, Id: insuredId, FullName: policy?.InsuredName || policy?.insuredName || policy?.NamedInsured || policy?.namedInsured || policy?.Number || policy?.number || normalizedQuery, PolicyNumber: policy?.Number || policy?.number || policy?.PolicyNumber || policy?.policyNumber || '' }]); }); } } catch (error) { console.warn('[Carrier Bridge][search] policy intent failed:', error); } console.log('[Carrier Bridge][search] unique results after policy intent:', resultMap.size); } // 1. Search the FULL phrase first. const fullContains = `active eq true and (contains(tolower(lastName),'${odataEscapeString(lowerQ)}') or contains(tolower(firstName),'${odataEscapeString(lowerQ)}') or contains(tolower(commercialName),'${odataEscapeString(lowerQ)}') or contains(tolower(dba),'${odataEscapeString(lowerQ)}') or contains(tolower(email),'${odataEscapeString(lowerQ)}') or contains(tolower(eMail),'${odataEscapeString(lowerQ)}'))`; const fullStarts = `active eq true and (startswith(lastName,'${q}') or startswith(firstName,'${q}') or startswith(commercialName,'${q}') or startswith(eMail,'${q}'))`; addResults(await fetchSafe(fullContains, fullStarts, 'full-phrase')); console.log('[Carrier Bridge][search] unique results after full phrase:', resultMap.size); // 2a. For multi-word queries, require all words to appear in the commercial name, or across first/last name. if (terms.length > 1 && resultMap.size < 10) { const commercialAllTerms = terms .map((term) => `(contains(tolower(commercialName),'${odataEscapeString(term.toLowerCase())}') or contains(tolower(dba),'${odataEscapeString(term.toLowerCase())}'))`) .join(' and '); const personAllTerms = terms .map((term) => `(contains(tolower(firstName),'${odataEscapeString(term.toLowerCase())}') or contains(tolower(lastName),'${odataEscapeString(term.toLowerCase())}'))`) .join(' and '); const allTermsFilter = `active eq true and ((${commercialAllTerms}) or (${personAllTerms}))`; const allTermsFilterStarts = `active eq true and (startswith(commercialName,'${q}') or startswith(lastName,'${q}') or startswith(firstName,'${q}'))`; addResults(await fetchSafe(allTermsFilter, allTermsFilterStarts, 'all-terms')); console.log('[Carrier Bridge][search] unique results after all-terms:', resultMap.size); } // 2b. Only broaden to individual words when the stronger searches are still thin. if (resultMap.size < 10) { for (const term of terms) { const lowerTerm = odataEscapeString(term.toLowerCase()); const normalTerm = odataEscapeString(term); const termContains = `active eq true and (contains(tolower(lastName),'${lowerTerm}') or contains(tolower(firstName),'${lowerTerm}') or contains(tolower(commercialName),'${lowerTerm}') or contains(tolower(dba),'${lowerTerm}') or contains(tolower(email),'${lowerTerm}') or contains(tolower(eMail),'${lowerTerm}'))`; const termStarts = `active eq true and (startswith(lastName,'${normalTerm}') or startswith(firstName,'${normalTerm}') or startswith(commercialName,'${normalTerm}') or startswith(dba,'${normalTerm}') or startswith(eMail,'${normalTerm}'))`; addResults(await fetchSafe(termContains, termStarts, `term:${term}`)); console.log(`[Carrier Bridge][search] unique results after term "${term}":`, resultMap.size); } } if (terms.length > 1) { const commercialIntersectionMap = new Map(); for (const term of terms) { const lowerTerm = odataEscapeString(term.toLowerCase()); const normalTerm = odataEscapeString(term); const commercialTermContains = `active eq true and (contains(tolower(commercialName),'${lowerTerm}') or contains(tolower(dba),'${lowerTerm}'))`; const commercialTermStarts = `active eq true and (startswith(commercialName,'${normalTerm}') or startswith(dba,'${normalTerm}'))`; const items = await fetchSafe(commercialTermContains, commercialTermStarts, `commercial-term:${term}`); items.forEach((item) => { const key = item?.Id || item?.id || item?.DatabaseId || item?.databaseId || JSON.stringify(item); const current = commercialIntersectionMap.get(key) || { item, hits: 0 }; current.hits += 1; commercialIntersectionMap.set(key, current); }); } const commercialIntersection = [...commercialIntersectionMap.values()] .filter((entry) => entry.hits === terms.length || getCombinedSearchText(entry.item).includes(lowerQ)) .map((entry) => entry.item); addResults(commercialIntersection); console.log('[Carrier Bridge][search] commercial intersection result count:', commercialIntersection.length); } // 3. Local Scoring & Sorting const arr = [...resultMap.values()].sort((a, b) => { const getScore = (item) => { const pName = (item.FullName || item.fullName || item.Name || item.name || `${item.FirstName || item.firstName || ''} ${item.LastName || item.lastName || ''}`).trim().toLowerCase(); const cName = `${item.commercialName || item.CommercialName || ''} ${item.dba || item.DBA || ''}`.trim().toLowerCase(); const phoneText = `${item.Phone || item.phone || item.CellPhone || item.cellPhone || ''}`.replace(/\D/g, ''); const policyText = `${item.PolicyNumber || item.policyNumber || item.Number || item.number || ''}`.toLowerCase(); const dobText = `${item.BirthDate || item.birthDate || item.dateOfBirth || item.DOB || ''}`.toLowerCase(); const evaluate = (text) => { if (!text) return 0; if (text === lowerQ) return 100; if (text.startsWith(lowerQ)) return 90; if (text.includes(` ${lowerQ} `) || text.includes(`${lowerQ} `) || text.includes(` ${lowerQ}`)) return 85; if (text.includes(lowerQ)) return 80; return 0; }; let bestScore = Math.max( evaluate(pName), cName ? evaluate(cName) + 5 : 0, searchIntent.isPhone && phoneText.includes(searchIntent.digits) ? 95 : 0, searchIntent.isPolicy && policyText.includes(lowerQ) ? 95 : 0, searchIntent.isDob && dateVariants.some((variant) => dobText.includes(variant.toLowerCase())) ? 95 : 0 ); if (bestScore === 0) { let tokenScore = 0; const combined = `${pName} ${cName}`; for (const term of terms) { if (combined.includes(term.toLowerCase())) tokenScore += 10; } bestScore = tokenScore; } return bestScore; }; return getScore(b) - getScore(a); }); console.log('[Carrier Bridge][search] scored result count:', arr.length); // 4. Return the top 20 matches const getCombinedText = (item) => getCombinedSearchText(item); const getMatchStrength = (item) => { const combined = getCombinedText(item); if (searchIntent.isPhone && getCombinedText(item).replace(/\D/g, '').includes(searchIntent.digits)) return 100; if (searchIntent.isPolicy && combined.includes(lowerQ)) return 100; if (searchIntent.isDob && dateVariants.some((variant) => combined.includes(variant.toLowerCase()))) return 100; if (combined.includes(lowerQ)) return 100; return terms.reduce((score, term) => { return combined.includes(term.toLowerCase()) ? score + 1 : score; }, 0); }; let filtered = arr.filter((item) => getMatchStrength(item) >= Math.min(2, terms.length)); if (!filtered.length) { filtered = arr.filter((item) => getMatchStrength(item) >= 1); } console.log('[Carrier Bridge][search] filtered result count:', filtered.length); console.log('[Carrier Bridge][search] top filtered results:', filtered.slice(0, 10).map((item) => ({ commercialName: item.commercialName || item.CommercialName || '', dba: item.dba || item.DBA || '', fullName: item.FullName || item.fullName || item.Name || item.name || '', firstName: item.FirstName || item.firstName || '', lastName: item.LastName || item.lastName || '', id: item.Id || item.id || item.DatabaseId || item.databaseId || '' }))); return filtered.slice(0, 20).map((x) => { const first = x.FirstName || x.firstName || ''; const last = x.LastName || x.lastName || ''; const commercial = x.commercialName || x.CommercialName || ''; const full = x.FullName || x.fullName || x.Name || x.name || commercial || `${first} ${last}`.trim(); return { ...x, FullName: full, Email: x.Email || x.email || x.Email1 || x.email1 || x.PrimaryEmail || x.primaryEmail || '', Phone: x.Phone || x.phone || x.CellPhone || x.cellPhone || x.PrimaryPhone || x.primaryPhone || '', PolicyNumber: x.PolicyNumber || x.policyNumber || x.Number || x.number || '', Id: x.Id || x.id || x.InsuredId || x.insuredId || x.EntityId || x.entityId || x.QuoteApplicationInsuredId || x.quoteApplicationInsuredId || '' }; }); } async function createProspect(input) { const storage = await chrome.storage.local.get(['ncApiToken']); const token = storage.ncApiToken; if (!token) throw new Error('Missing API Token.'); const body = { FirstName: String(input.firstName || '').trim(), LastName: String(input.lastName || '').trim(), Email: String(input.email || '').trim(), Phone: String(input.phone || '').trim(), Address1: String(input.street || '').trim(), City: String(input.city || '').trim(), State: String(input.state || '').trim(), Zip: String(input.zip || '').trim(), }; if (!body.FirstName || !body.LastName) throw new Error('First name and last name are required.'); let lastErr = null; for (const url of ['https://api.nowcerts.com/api/Insured/OnlyInsertInsured', 'https://api.nowcerts.com/api/Insured/Insert']) { try { const res = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', 'Content-Type': 'application/json'}, body: JSON.stringify(body) }); if (!res.ok) { if (res.status === 404) continue; if (res.status === 401) throw new Error('Unauthorized.'); throw new Error(`API Error ${res.status}`); } return await res.json().catch(async () => ({ message: await res.text() })); } catch (e) { lastErr = e; } } throw lastErr || new Error('Create prospect failed'); } async function getFullClientData(insuredId) { if (!insuredId) throw new Error('No insured ID provided'); const storage = await chrome.storage.local.get(['ncApiToken']); const token = storage.ncApiToken; if (!token) throw new Error('Missing API Token.'); const headers = { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }; // 1. Details const detailRes = await fetch(`https://api.nowcerts.com/api/InsuredDetailList?key=${insuredId}&Active=true&showAll=true`, { method: 'GET', headers }); if (!detailRes.ok) throw new Error(`InsuredDetailList failed (${detailRes.status})`); const detailJson = await detailRes.json(); let insured = Array.isArray(detailJson?.value) ? detailJson.value[0] : Array.isArray(detailJson) ? detailJson[0] : detailJson || {}; // 2. Policies let policies = []; try { const polRes = await fetch('https://api.nowcerts.com/api/Insured/InsuredPolicies', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ insuredDataBaseId: [insuredId] }) }); if (polRes.ok) { const polJson = await polRes.json(); policies = Array.isArray(polJson) ? polJson : (polJson?.value || []); } } catch (e) { console.warn('Policies fetch failed', e); } let vehicles = []; let drivers = []; let properties = []; if (policies.length > 0) { const currentPolicies = getCurrentPolicies(policies); const policyIds = currentPolicies.map(p => getPolicyId(p)).filter(Boolean); if (policyIds.length) { // 3, 4 & 5: Vehicles, Drivers, and Properties try { const [vehRes, drvRes, propRes] = await Promise.all([ fetch('https://api.nowcerts.com/api/Policy/PolicyVehicles', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ policyDataBaseId: policyIds }) }), fetch('https://api.nowcerts.com/api/Policy/PolicyDrivers', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ policyDataBaseId: policyIds }) }), fetch('https://api.nowcerts.com/api/Policy/PolicyProperties', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ policyDataBaseId: policyIds }) }) ]); if (vehRes.ok) vehicles = await vehRes.json(); if (drvRes.ok) drivers = await drvRes.json(); if (propRes.ok) properties = await propRes.json(); } catch (e) { console.warn('Policy sub-data fetch failed', e); } } } // --- Extract Commercial Primary Contact --- let primaryContact = null; const contacts = insured.insuredContacts || insured.InsuredContacts || []; if (Array.isArray(contacts) && contacts.length > 0) { // Try to find the primary contact, otherwise just grab the first one in the list primaryContact = contacts.find(c => c.primaryContact === true || c.PrimaryContact === true) || contacts[0]; } // Safely grab the contact's details (falling back to empty strings if missing) const pcFirst = primaryContact?.firstName || primaryContact?.FirstName || ''; const pcLast = primaryContact?.lastName || primaryContact?.LastName || ''; const pcEmail = primaryContact?.businessEMail || primaryContact?.personalEMail || primaryContact?.email || ''; const pcPhone = primaryContact?.cellPhone || primaryContact?.officePhone || primaryContact?.homePhone || primaryContact?.phone || ''; // Normalize all fields to consistent keys so content.js always finds them const normalizedClient = { ...insured, FullName: insured.FullName || insured.fullName || `${insured.FirstName || insured.firstName || pcFirst} ${insured.LastName || insured.lastName || pcLast}`.trim(), FirstName: insured.FirstName || insured.firstName || pcFirst, LastName: insured.LastName || insured.lastName || pcLast, Email: insured.Email || insured.email || pcEmail, Phone: insured.Phone || insured.phone || insured.cellPhone || insured.CellPhone || pcPhone, Address1: insured.Address1 || insured.address1 || insured.addressLine1 || insured.StreetAddress1 || '', Address2: insured.Address2 || insured.address2 || insured.addressLine2 || insured.StreetAddress2 || '', City: insured.City || insured.city || '', State: insured.State || insured.state || insured.StateAbbreviation || insured.stateAbbreviation || '', Zip: insured.Zip || insured.zip || insured.zipCode || insured.ZipCode || '', BirthDate: insured.BirthDate || insured.dateOfBirth || insured.DateOfBirth || insured.DOB || '', SSN: insured.SocialSecurityNumber || insured.SSN || insured.socialSecurityNumber || insured.TaxId || '', Id: insured.Id || insured.id || insured.DatabaseId || insuredId, policies: dedupeBy(getCurrentPolicies(policies), (policy) => getPolicyId(policy) || normalizeMeaningfulString(policy?.number || policy?.Number)), vehicles: dedupeBy( filterPolicyChildrenByPolicies(vehicles, policies, ['policyDatabaseId', 'PolicyDatabaseId', 'policyId', 'PolicyId']), (vehicle) => normalizeMeaningfulString(vehicle?.VIN || vehicle?.vin || `${vehicle?.Year || ''}${vehicle?.Make || ''}${vehicle?.Model || ''}`) ), drivers: dedupeBy( filterPolicyChildrenByPolicies(drivers, policies, ['policyDatabaseId', 'PolicyDatabaseId', 'policyId', 'PolicyId']), (driver) => normalizeMeaningfulString(driver?.databaseId || driver?.DatabaseId || `${driver?.firstName || driver?.FirstName || ''}|${driver?.lastName || driver?.LastName || ''}|${driver?.licenseNumber || driver?.LicenseNumber || ''}`) ), properties: dedupeBy( filterPolicyChildrenByPolicies(properties, policies, ['policyDatabaseId', 'PolicyDatabaseId', 'policyId', 'PolicyId']), (property) => normalizeMeaningfulString(property?.databaseId || property?.DatabaseId || property?.addressLine1 || property?.Address1) ), contacts // Keeping the raw contacts list attached just in case we need to display all of them later }; return enrichClientWithMatchedPersonData(normalizedClient); }