diff --git a/src/sources/google-places.mjs b/src/sources/google-places.mjs index 9d11ab0..4894179 100644 --- a/src/sources/google-places.mjs +++ b/src/sources/google-places.mjs @@ -20,14 +20,26 @@ async function searchPage(apiKey, params) { return data; } -async function search(query, apiKey, { maxPerQuery, pages }) { +async function search(query, apiKey, { maxPerQuery, pages }, ctx) { const places = []; let page = 0; let pageToken = ''; while (page < pages && places.length < maxPerQuery) { if (pageToken) await sleep(2500); - const data = await searchPage(apiKey, pageToken ? { pagetoken: pageToken } : { query }); + let data; + try { + data = await searchPage(apiKey, pageToken ? { pagetoken: pageToken } : { query }); + } catch (error) { + // Uma falha na pagina 2/3 (comum: o next_page_token do Google ainda nao + // ativou apesar da espera de 2.5s) nao pode descartar os leads ja + // trazidos pelas paginas anteriores bem-sucedidas. So propaga quando + // ainda nao ha nada acumulado (mesmo comportamento de antes: o chamador + // loga e pula a query inteira). + if (!places.length) throw error; + ctx?.log(`google-places: pagina ${page + 1} de "${query}" falhou apos ${places.length} resultado(s) ja obtidos: ${error.message}`); + break; + } // Resultados por pagina sao no maximo 20 — spread aqui e seguro. places.push(...(data.results || [])); page += 1; @@ -80,7 +92,7 @@ export async function* collect(params, ctx) { let places = []; try { - places = await search(query, apiKey, { maxPerQuery: limits.maxPerQuery || 10, pages }); + places = await search(query, apiKey, { maxPerQuery: limits.maxPerQuery || 10, pages }, ctx); } catch (error) { ctx.log(`google-places falhou em ${query}: ${error.message}`); continue; diff --git a/tests/google-places.test.mjs b/tests/google-places.test.mjs new file mode 100644 index 0000000..35aaf62 --- /dev/null +++ b/tests/google-places.test.mjs @@ -0,0 +1,88 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { collect } from '../src/sources/google-places.mjs'; + +function fakeCtx() { + const logs = []; + return { emit: () => {}, log: (message) => logs.push(message), isCancelled: () => false, logs }; +} + +// Antes, uma falha na pagina 2/3 (comum: o next_page_token do Google ainda +// nao ativou apesar da espera de 2.5s entre paginas, ou uma falha transitoria +// de rede) fazia search() lancar e descartar TODOS os leads ja obtidos nas +// paginas anteriores bem-sucedidas, nao so a pagina que falhou. +test('falha na pagina 2 nao descarta os leads ja obtidos na pagina 1', async () => { + process.env.GOOGLE_PLACES_API_KEY = 'chave-teste'; + const originalFetch = globalThis.fetch; + let textsearchCalls = 0; + + globalThis.fetch = async (url) => { + const href = url.toString(); + if (href.includes('textsearch')) { + textsearchCalls += 1; + if (textsearchCalls === 1) { + return new Response(JSON.stringify({ + status: 'OK', + results: [{ place_id: 'p1', name: 'Pizzaria Um' }], + next_page_token: 'tok-pagina-2' + })); + } + // Pagina 2: falha transitoria (token ainda nao ativo, erro do provedor, etc.) + return new Response(JSON.stringify({ status: 'INVALID_REQUEST' })); + } + if (href.includes('details')) { + return new Response(JSON.stringify({ result: { place_id: 'p1', name: 'Pizzaria Um' } })); + } + throw new Error(`fetch inesperado: ${href}`); + }; + + try { + const ctx = fakeCtx(); + const params = { + uf: 'BA', + cities: ['Salvador'], + segments: [{ name: 'restaurantes', queries: ['pizzaria'], interest: 'comida', pains: [] }], + limits: { maxPerQuery: 10, googlePages: 3 } + }; + + const batches = []; + for await (const batch of collect(params, ctx)) batches.push(batch); + + assert.equal(batches.length, 1, JSON.stringify(batches)); + assert.equal(batches[0].leads.length, 1); + assert.equal(batches[0].leads[0].company.name, 'Pizzaria Um'); + assert.ok(ctx.logs.some((message) => /pagina 2/.test(message)), ctx.logs.join(' | ')); + } finally { + globalThis.fetch = originalFetch; + delete process.env.GOOGLE_PLACES_API_KEY; + } +}); + +// Uma falha logo na primeira pagina (nada acumulado ainda) continua +// propagando e sendo tratada pelo chamador (loga e pula a query inteira) — +// esse caminho nao muda. +test('falha na primeira pagina ainda propaga e pula a query', async () => { + process.env.GOOGLE_PLACES_API_KEY = 'chave-teste'; + const originalFetch = globalThis.fetch; + + globalThis.fetch = async () => new Response(JSON.stringify({ status: 'INVALID_REQUEST' })); + + try { + const ctx = fakeCtx(); + const params = { + uf: 'BA', + cities: ['Salvador'], + segments: [{ name: 'restaurantes', queries: ['pizzaria'], interest: 'comida', pains: [] }], + limits: { maxPerQuery: 10, googlePages: 3 } + }; + + const batches = []; + for await (const batch of collect(params, ctx)) batches.push(batch); + + assert.equal(batches.length, 0); + assert.ok(ctx.logs.some((message) => /falhou em/.test(message)), ctx.logs.join(' | ')); + } finally { + globalThis.fetch = originalFetch; + delete process.env.GOOGLE_PLACES_API_KEY; + } +});