diff --git a/api/src/routes/fcc-lookup.ts b/api/src/routes/fcc-lookup.ts index a1a6465..0d01902 100644 --- a/api/src/routes/fcc-lookup.ts +++ b/api/src/routes/fcc-lookup.ts @@ -118,7 +118,7 @@ router.get("/api/v1/fcc/lookup", async (req, res) => { p2[k] = phase2Results[i].status === "fulfilled" ? (phase2Results[i] as PromiseFulfilledResult).value : null; }); - let filerDetail = (p2.filerDetail as { current_as_of: string | null; comm_type: string | null; contributor: boolean | null; error: string | null }) || null; + let filerDetail = (p2.filerDetail as { current_as_of: string | null; comm_type: string | null; contributor: boolean | null; hq_address: string | null; hq_city: string | null; hq_state: string | null; hq_zip: string | null; error: string | null }) || null; let cpniResult = (p2.cpniResult as { filed: boolean; cert_year: number | null; date_filed: string | null; error: string | null }) || null; const coresResult = (p2.cores as CoresData) || { frn, entity_name: null, address: null, city: null, state: null, zip: null, status: null, red_light: null, error: "CORES lookup failed" } as CoresData; const rmdResult = (p2.rmd as RmdData) || { found: false, business_name: null, frn: null, rmd_number: null, certification_date: null, implementation_type: null, contact_name: null, contact_email: null, removed: false, removal_reason: null, error: "RMD lookup failed" } as RmdData; @@ -782,10 +782,10 @@ router.get("/api/v1/fcc/lookup", async (req, res) => { entity_name: entityName, cores: { entity_name: coresResult.entity_name, - address: coresResult.address, - city: coresResult.city, - state: coresResult.state, - zip: coresResult.zip, + address: coresResult.address || filerDetail?.hq_address || null, + city: coresResult.city || filerDetail?.hq_city || null, + state: coresResult.state || filerDetail?.hq_state || null, + zip: coresResult.zip || filerDetail?.hq_zip || null, red_light: coresResult.red_light, error: coresResult.error, }, @@ -1010,34 +1010,47 @@ async function fetchLocal499Filer(frn: string): Promise { } } -async function fetch499Detail(filerId: string): Promise<{ current_as_of: string | null; comm_type: string | null; contributor: boolean | null; error: string | null }> { - // Scrape the FCC 499 filer detail page to get "Registration Current as of" date - // This tells us whether the carrier has filed their most recent 499-A +async function fetch499Detail(filerId: string): Promise<{ + current_as_of: string | null; comm_type: string | null; contributor: boolean | null; + hq_address: string | null; hq_city: string | null; hq_state: string | null; hq_zip: string | null; + error: string | null; +}> { + // Scrape the FCC 499 filer detail page for filing status + address const url = `https://apps.fcc.gov/cgb/form499/499detail.cfm?FilerNum=${filerId}`; try { const resp = await fetch(url, { signal: AbortSignal.timeout(10000), headers: { "Accept": "text/html", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" }, }); - if (!resp.ok) return { current_as_of: null, comm_type: null, contributor: null, error: `499 detail returned ${resp.status}` }; + if (!resp.ok) return { current_as_of: null, comm_type: null, contributor: null, hq_address: null, hq_city: null, hq_state: null, hq_zip: null, error: `499 detail returned ${resp.status}` }; const html = await resp.text(); - // Extract "Registration Current as of: 4/1/2025" const currentMatch = html.match(/Registration Current as of:\s*([^<]+)<\/b>/i); const current_as_of = currentMatch ? currentMatch[1].trim() : null; - // Extract "Principal Communications Type: Interconnected VoIP" const commMatch = html.match(/Principal Communications Type:\s*([^<]+)<\/b>/i); const comm_type = commMatch ? commMatch[1].trim() || null : null; - // Extract "Universal Service Fund Contributor: No" const contribMatch = html.match(/Universal Service Fund Contributor:\s*([^<]+)<\/b>/i); const contributor = contribMatch ? contribMatch[1].trim().toLowerCase() === "yes" : null; - return { current_as_of, comm_type, contributor, error: null }; + // Extract headquarters address (more reliable than CORES for some FRNs) + const addrMatch = html.match(/Headquarters Address:\s*([^<]*)<\/b>/i); + const cityMatch = html.match(/Headquarters Address:[\s\S]*?City:\s*([^<]*)<\/b>/i); + const stateMatch = html.match(/Headquarters Address:[\s\S]*?State:\s*([^<]*)<\/b>/i); + const zipMatch = html.match(/Headquarters Address:[\s\S]*?ZIP Code:\s*([^<]*)<\/b>/i); + + return { + current_as_of, comm_type, contributor, + hq_address: addrMatch ? addrMatch[1].trim() || null : null, + hq_city: cityMatch ? cityMatch[1].trim() || null : null, + hq_state: stateMatch ? stateMatch[1].trim() || null : null, + hq_zip: zipMatch ? zipMatch[1].trim() || null : null, + error: null, + }; } catch (err: any) { - return { current_as_of: null, comm_type: null, contributor: null, error: err.message }; + return { current_as_of: null, comm_type: null, contributor: null, hq_address: null, hq_city: null, hq_state: null, hq_zip: null, error: err.message }; } }