@horizon-modules/jetimob-crm-integration
Version:
Integração CRM Jetimob para conversão de dados imobiliários para property-model-v3
1 lines • 307 kB
Source Map (JSON)
{"version":3,"sources":["../src/converter/discoverers/discoverOperation.ts","../src/converter/discoverers/discoverAttributes.ts","../src/converter/discoverers/discoverMedia.ts","../src/converter/discoverers/discoverSettings.ts","../src/converter/discoverers/discoverSEO.ts","../src/converter/index.ts","../src/services/JetimobApiClient.ts","../src/services/JetimobDownloader.ts","../src/services/ProfilerService.ts","../src/data/fake-data/apartamentos.json","../src/data/fake-data/casas.json","../src/data/fake-data/comerciais.json","../src/data/fake-data/terrenos.json","../src/data/fake-data/coberturas.json","../src/index.ts"],"sourcesContent":["import { JetimobImovel } from \"../types\"\n\n/**\n * Descobre as operações do imóvel baseado no campo contrato\n * Mantém os valores EXATAMENTE como vêm do Jetimob, sem mapeamento\n * - contrato: \"Compra\" → operation: [\"Compra\"]\n * \n * Nota: Valores originais do Jetimob são preservados\n */\nexport function discoverOperation(imovel: JetimobImovel): string[] {\n // Manter valores originais sem mapeamento\n if (imovel.contrato) {\n return [imovel.contrato]\n }\n \n // Fallback vazio se não houver contrato\n return []\n}","import { JetimobImovel } from \"../types\"\nimport { discoverOperation } from \"./discoverOperation\"\n\nexport function discoverAttributes(imovel: JetimobImovel): Record<string, any> {\n const attributes: Record<string, any> = {}\n\n // Operação\n const operacao = discoverOperation(imovel)\n if (operacao.length > 0) attributes.operacao = operacao\n\n // Valores - MAPEAMENTO SSOT: valor_venda SEMPRE mapear (remover filtro visibilidade)\n if (imovel.valor_venda) {\n const valor = Number(imovel.valor_venda)\n if (!isNaN(valor) && valor > 0) attributes.valor_venda = valor\n }\n if (imovel.valor_locacao && imovel.valor_locacao_visivel) {\n const valor = Number(imovel.valor_locacao)\n if (!isNaN(valor) && valor > 0) attributes.valor_aluguel = valor\n }\n if (imovel.valor_temporada && imovel.valor_temporada_visivel) {\n const valor = Number(imovel.valor_temporada)\n if (!isNaN(valor) && valor > 0) attributes.valor_temporada = valor\n }\n if (imovel.valor_condominio && imovel.valor_condominio_visivel) {\n const valor = Number(imovel.valor_condominio)\n if (!isNaN(valor) && valor >= 0) attributes.valor_condominio = valor\n }\n if (imovel.valor_iptu && imovel.valor_iptu_visivel) {\n const valor = Number(imovel.valor_iptu)\n if (!isNaN(valor) && valor >= 0) attributes.valor_iptu = valor\n }\n\n // Áreas - campos JETIMOB conforme MAPEAMENTO_COMPLETO_DE_PARA.md\n if (imovel.area_total) {\n const area = Number(imovel.area_total)\n if (!isNaN(area) && area > 0) attributes.area_total = area\n }\n if (imovel.area_privativa) {\n const area = Number(imovel.area_privativa)\n if (!isNaN(area) && area > 0) attributes.area_privativa = area\n }\n\n // Dependências - campos JETIMOB conforme MAPEAMENTO_COMPLETO_DE_PARA.md\n if (imovel.dormitorios) attributes.dormitorios = Number(imovel.dormitorios)\n if (imovel.suites) attributes.suites = Number(imovel.suites)\n if (imovel.banheiros) attributes.banheiros = Number(imovel.banheiros)\n if (imovel.garagens) attributes.vagas_garagem = Number(imovel.garagens)\n\n // Tipo e finalidade - SSOT: tipo → finalidade, subtipo → tipo\n if (imovel.tipo) attributes.finalidade = imovel.tipo\n if (imovel.subtipo) attributes.tipo = imovel.subtipo\n\n // Localização - MAPEAMENTO SSOT: SEMPRE mapear endereços (remover filtros visibilidade)\n if (imovel.endereco_cep) attributes.endereco_cep = imovel.endereco_cep\n if (imovel.endereco_estado) attributes.endereco_estado = imovel.endereco_estado\n if (imovel.endereco_cidade) attributes.endereco_cidade = imovel.endereco_cidade\n if (imovel.endereco_bairro) attributes.endereco_bairro = imovel.endereco_bairro\n if (imovel.endereco_logradouro) attributes.endereco_logradouro = imovel.endereco_logradouro\n if (imovel.endereco_numero) attributes.endereco_numero = imovel.endereco_numero\n if (imovel.endereco_complemento) attributes.endereco_complemento = imovel.endereco_complemento\n if (imovel.endereco_referencia) attributes.endereco_referencia = imovel.endereco_referencia\n if (imovel.andar) attributes.andar = Number(imovel.andar)\n\n // Coordenadas - SÓ POPULAR SE GEOPOSICIONAMENTO != 0 (0=não exibir, 1=exibir, 2=aproximado)\n const geoValue = imovel.geoposicionamento_visivel ? Number(imovel.geoposicionamento_visivel) : 0\n if (imovel.latitude && imovel.longitude && geoValue !== 0) {\n const lat = Number(imovel.latitude)\n const lng = Number(imovel.longitude)\n if (!isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0) {\n attributes.latitude = lat\n attributes.longitude = lng\n }\n }\n\n // Características booleanas - MAPEAMENTO SSOT: SÓ incluir quando true\n if (String(imovel.mobiliado) === \"1\" || String(imovel.mobiliado) === \"true\") {\n attributes.mobiliado = true\n }\n if (String(imovel.financiavel) === \"1\" || String(imovel.financiavel) === \"true\") {\n attributes.financiavel = true\n }\n if (String(imovel.exclusividade) === \"true\" || String(imovel.exclusividade) === \"1\") {\n attributes.exclusividade = true\n }\n if (String(imovel.permuta) === \"true\" || String(imovel.permuta) === \"1\") {\n attributes.permuta = true\n }\n if (String(imovel.seguro_fianca) === \"true\" || String(imovel.seguro_fianca) === \"1\") {\n attributes.seguro_fianca = true\n }\n\n // Comodidades - campos JETIMOB conforme MAPEAMENTO_COMPLETO_DE_PARA.md\n if (imovel.imovel_comodidades) {\n const comodidadesArray = String(imovel.imovel_comodidades)\n .split(',')\n .map(c => c.trim())\n .filter(c => c.length > 0)\n if (comodidadesArray.length > 0) {\n attributes.comodidades = comodidadesArray\n }\n }\n\n // Campos específicos Jetimob - conforme MAPEAMENTO_COMPLETO_DE_PARA.md\n if (imovel.situacao) attributes.situacao = imovel.situacao\n // Campo destaque - SÓ incluir quando true\n if (imovel.destaque && imovel.destaque !== \"Sem destaque\") {\n if (imovel.destaque === \"Destaque\" || imovel.destaque === \"true\" || String(imovel.destaque) === \"true\") {\n attributes.destaque = true\n }\n }\n if (imovel.distancia_mar) attributes.distancia_mar = Number(imovel.distancia_mar)\n if (imovel.posicao_solar) attributes.posicao_solar = imovel.posicao_solar\n if (imovel.id_corretor) attributes.corretor_id = String(imovel.id_corretor)\n if (imovel.updated_at) attributes.data_atualizacao = imovel.updated_at\n if (imovel.tags) attributes.tags = imovel.tags\n if (imovel.status) attributes.status = imovel.status\n\n // Valores adicionais - campos solicitados\n if (imovel.valor_seguro_incendio) {\n const valor = Number(imovel.valor_seguro_incendio)\n if (!isNaN(valor) && valor >= 0) attributes.valor_seguro_incendio = valor\n }\n if (imovel.valor_taxa_limpeza) {\n const valor = Number(imovel.valor_taxa_limpeza)\n if (!isNaN(valor) && valor >= 0) attributes.valor_taxa_limpeza = valor\n }\n\n // Calendário de temporada\n if (imovel.calendario_temporada) attributes.calendario_temporada = imovel.calendario_temporada\n\n // Condomínio - MAPEAMENTO SSOT: adicionar condominio_comodidades\n if (imovel.condominio_comodidades) attributes.condominio_comodidades = imovel.condominio_comodidades\n\n return attributes\n}","import { JetimobImovel } from \"../types\"\nimport { MediaAssets, ImageMedia, VideoMedia, DocumentMedia, VirtualTourMedia } from \"../property-model-v3\"\n\nexport function discoverMedias(imovel: JetimobImovel): MediaAssets {\n // Imagens - campos JETIMOB conforme MAPEAMENTO_COMPLETO_DE_PARA.md\n const images: ImageMedia[] = imovel.imagens\n ?.map((imagem, index) => ({\n full: imagem.link,\n md: imagem.link_thumb || imagem.link,\n sm: imagem.link_thumb || imagem.link,\n cover: index === 0 // primeira imagem como cover\n })) ?? []\n\n // Vídeos\n const videos: VideoMedia[] = imovel.videos && imovel.videos.length > 0 \n ? imovel.videos.map(url => generateVideo(url)) \n : []\n\n // Tours virtuais - campos JETIMOB conforme MAPEAMENTO_COMPLETO_DE_PARA.md\n const virtual_tours: VirtualTourMedia[] = imovel.tour360\n ? [{ embed_url: imovel.tour360 }]\n : []\n\n // Documentos - plantas baixas\n const documents: DocumentMedia[] = imovel.plantas && imovel.plantas.length > 0\n ? imovel.plantas.map((plantaUrl, index) => ({\n name: `Planta ${index + 1}`,\n url: plantaUrl\n }))\n : []\n\n const result: MediaAssets = {}\n \n if (images.length > 0) result.images = images\n if (videos.length > 0) result.videos = videos\n if (virtual_tours.length > 0) result.virtual_tours = virtual_tours\n if (documents.length > 0) result.documents = documents\n\n return result\n}\n\nfunction generateVideo(url: string): VideoMedia {\n try {\n const parsed = new URL(url)\n const host = parsed.hostname\n\n if (host.includes(\"youtube.com\") || host.includes(\"youtu.be\")) {\n const id =\n parsed.searchParams.get(\"v\") || parsed.pathname.split(\"/\").pop()\n return {\n provider: \"youtube\",\n id: id || undefined,\n embed_url: `https://www.youtube.com/embed/${id}`,\n }\n }\n\n return {\n embed_url: url,\n }\n } catch (e) {\n return {\n embed_url: url,\n }\n }\n}","import { JetimobImovel } from \"../types\"\n\n/**\n * Descobrir configurações baseado EXATAMENTE no MAPEAMENTO_COMPLETO_DE_PARA.md\n */\nexport function discoverSettings(imovel: JetimobImovel): Record<string, any> {\n return {\n currency_unit: \"BRL\", // sempre BRL no Jetimob\n area_unit: imovel.medida === \"m²\" ? \"m2\" : (imovel.medida || \"m2\"), // mapeia m² -> m2\n distance_unit: \"meters\",\n exibir_no_mapa: true,\n }\n}","import { JetimobImovel } from \"../types\"\nimport { SEO } from \"../property-model-v3\"\n\n/**\n * Descobrir campos SEO do Jetimob\n */\nexport function discoverSEO(imovel: JetimobImovel): SEO | undefined {\n const seo: SEO = {}\n\n if (imovel.meta_title) {\n seo.meta_title = imovel.meta_title\n }\n\n if (imovel.meta_description) {\n seo.meta_description = imovel.meta_description\n }\n\n // Só retorna o objeto SEO se pelo menos um campo foi preenchido\n if (Object.keys(seo).length > 0) {\n return seo\n }\n\n return undefined\n}","import { JetimobImovel } from './types'\nimport { PropertyModel } from './property-model-v3'\nimport { discoverAttributes } from './discoverers/discoverAttributes'\nimport { discoverMedias } from './discoverers/discoverMedia'\nimport { discoverOperation } from './discoverers/discoverOperation'\nimport { discoverSettings } from './discoverers/discoverSettings'\nimport { discoverSEO } from './discoverers/discoverSEO'\n\n/**\n * Converte um imóvel do formato Jetimob para o formato PropertyV3\n * Baseado no MAPEAMENTO_COMPLETO_DE_PARA.md\n */\nexport function convertJetimobToPropertyV3(imovel: JetimobImovel): PropertyModel {\n const seo = discoverSEO(imovel)\n \n return {\n // Identificação básica\n reference: imovel.codigo ?? \"\",\n title: imovel.titulo_anuncio ?? \"\",\n description: imovel.observacoes ?? \"\",\n \n // SEO (apenas se houver dados)\n ...(seo && { seo }),\n \n // Descoberta de atributos complexos (inclui operação)\n attributes: discoverAttributes(imovel),\n \n // Descoberta de mídias (imagens)\n media_assets: discoverMedias(imovel),\n \n // Descoberta de configurações e visibilidade\n settings: discoverSettings(imovel),\n \n // Data de atualização\n updated_at: imovel.updated_at || new Date().toISOString(),\n }\n}\n\n// Re-exportar tipos relacionados ao converter\nexport type { JetimobImovel } from './types'\nexport type { PropertyModel } from './property-model-v3'\n\n// Re-exportar discoverers para uso individual se necessário\nexport { discoverAttributes } from './discoverers/discoverAttributes'\nexport { discoverMedias } from './discoverers/discoverMedia'\nexport { discoverOperation } from './discoverers/discoverOperation'\nexport { discoverSettings } from './discoverers/discoverSettings'\nexport { discoverSEO } from './discoverers/discoverSEO'","import { JetimobApiClientConfig, JetimobApiResponse } from \"./types\"\n\nexport class JetimobApiClient {\n private webserviceKey: string\n private baseUrl: string\n\n constructor(config: { webserviceKey: string; baseUrl?: string }) {\n this.webserviceKey = config.webserviceKey\n this.baseUrl = config.baseUrl || \"https://api.jetimob.com\"\n }\n\n private async request<T>(endpoint: string): Promise<T> {\n const url = `${this.baseUrl}${endpoint}`\n \n console.log(`🔗 Requisição para: ${url}`)\n \n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 10000) // 10s timeout\n \n const response = await fetch(url, {\n signal: controller.signal,\n })\n \n clearTimeout(timeoutId)\n\n console.log(`📊 Status: ${response.status}`)\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Erro na requisição: ${response.status} - ${response.statusText} - ${errorText}`\n )\n }\n\n return response.json() as Promise<T>\n }\n\n async getImoveis(page: number = 1, pageSize: number = 50): Promise<any> {\n return this.request<any>(\n `/webservice/${this.webserviceKey}/imoveis?v=4&page=${page}&pageSize=${pageSize}`\n )\n }\n\n async getImovel(id: string | number): Promise<any> {\n return this.request<any>(`/webservice/${this.webserviceKey}/imoveis/${id}`)\n }\n\n async testConnection(): Promise<any> {\n try {\n console.log(\"🚀 Testando conexão com API Jetimob...\")\n console.log(`📍 Base URL: ${this.baseUrl}`)\n console.log(`🔑 Webservice Key: ${this.webserviceKey.substring(0, 10)}...`)\n \n const result = await this.getImoveis(1, 1)\n console.log(\"✅ Conexão bem sucedida!\")\n return result\n } catch (error) {\n console.error(\"❌ Erro na conexão:\", error)\n throw error\n }\n }\n}","import { promises as fs } from \"fs\"\nimport { join } from \"path\"\nimport { JetimobApiClient } from \"./JetimobApiClient\"\nimport { JetimobDownloaderConfig, DownloadOptions, DownloadResult, ApiUploadConfig, UploadResult } from \"./types\"\n\nexport class JetimobDownloader {\n private apiClient: JetimobApiClient\n private outputDir: string\n\n constructor(config: JetimobDownloaderConfig) {\n this.apiClient = new JetimobApiClient({\n webserviceKey: config.webserviceKey,\n baseUrl: config.baseUrl || \"https://api.jetimob.com\",\n })\n this.outputDir = config.outputDir\n }\n\n private async ensureOutputDir(): Promise<void> {\n try {\n await fs.access(this.outputDir)\n } catch {\n await fs.mkdir(this.outputDir, { recursive: true })\n }\n }\n\n private async savePageData(page: number, data: any): Promise<boolean> {\n // Jetimob retorna um objeto com propriedade 'data' que contém o array\n const properties = data?.data || data\n \n // Não salvar se os dados estiverem vazios\n if (!Array.isArray(properties) || properties.length === 0) {\n console.log(` 📄 Página ${page} vazia, não salvando arquivo`)\n return false\n }\n \n const fileName = `page-${page}.json`\n const filePath = join(this.outputDir, fileName)\n // Salva apenas o array de propriedades para compatibilidade\n await fs.writeFile(filePath, JSON.stringify(properties, null, 2), \"utf8\")\n return true\n }\n\n async downloadPage(page: number, pageSize: number = 100): Promise<void> {\n await this.ensureOutputDir()\n \n const response = await this.apiClient.getImoveis(page, pageSize)\n await this.savePageData(page, response)\n }\n\n async downloadPages(options: DownloadOptions = {}): Promise<DownloadResult> {\n const { \n startPage = 1, \n endPage, \n maxPages, \n pageSize = 100 \n } = options\n\n await this.ensureOutputDir()\n\n const errors: string[] = []\n let totalPages = 0\n let totalItems = 0\n let downloadedItems = 0\n\n try {\n // Primeiro, descobre quantas páginas existem fazendo download da primeira página\n console.log(`🔍 Descobrindo total de páginas disponíveis...`)\n const firstPageResponse = await this.apiClient.getImoveis(1, pageSize)\n const firstPage = firstPageResponse?.data || firstPageResponse\n \n if (!Array.isArray(firstPage) || firstPage.length === 0) {\n console.log(`⚠️ Primeira página vazia, finalizando download`)\n return {\n totalPages: 1,\n totalItems: 0,\n downloadedItems: 0,\n errors\n }\n }\n\n // Para Jetimob, podemos usar os metadados se disponíveis\n totalPages = firstPageResponse?.totalPages || 0\n totalItems = firstPageResponse?.total || firstPage.length\n \n console.log(`📊 Total de páginas: ${totalPages}`)\n console.log(`📊 Total de imóveis: ${totalItems}`)\n \n // Salva a primeira página se ela estiver no range\n if (startPage === 1) {\n const saved = await this.savePageData(1, firstPageResponse)\n if (saved) {\n downloadedItems += firstPage.length\n console.log(` 📄 Página 1: ${firstPage.length} imóveis salvos`)\n }\n }\n\n // Determina se há um limite específico ou se deve baixar tudo\n let currentPage = Math.max(startPage, 2)\n let hasMorePages = true\n \n // Se temos totalPages definido, usar como limite\n const finalEndPage = endPage || (maxPages ? Math.min(startPage + maxPages - 1, totalPages) : totalPages)\n \n while (hasMorePages && currentPage <= finalEndPage) {\n try {\n console.log(`🔄 Baixando página ${currentPage}...`)\n const response = await this.apiClient.getImoveis(currentPage, pageSize)\n const properties = response?.data || response\n \n // Se a página estiver vazia, finalizar o download\n if (!Array.isArray(properties) || properties.length === 0) {\n console.log(`⚠️ Página ${currentPage} vazia, finalizando download`)\n hasMorePages = false\n break\n }\n \n const saved = await this.savePageData(currentPage, response)\n if (saved) {\n downloadedItems += properties.length\n console.log(` 📄 Página ${currentPage}: ${properties.length} imóveis salvos`)\n }\n \n currentPage++\n totalPages = currentPage - 1\n \n // Pausa entre páginas para não sobrecarregar a API\n await new Promise(resolve => setTimeout(resolve, 200))\n \n } catch (error) {\n const errorMsg = `Erro ao baixar página ${currentPage}: ${error instanceof Error ? error.message : String(error)}`\n errors.push(errorMsg)\n console.error(` ❌ ${errorMsg}`)\n \n // Se der erro, tenta mais uma vez\n try {\n console.log(`🔄 Tentando novamente página ${currentPage}...`)\n await new Promise(resolve => setTimeout(resolve, 1000))\n const response = await this.apiClient.getImoveis(currentPage, pageSize)\n \n if (Array.isArray(response) && response.length > 0) {\n const saved = await this.savePageData(currentPage, response)\n if (saved) {\n downloadedItems += response.length\n totalItems += response.length\n console.log(` 📄 Página ${currentPage}: ${response.length} imóveis salvos (retry)`)\n }\n currentPage++\n totalPages = currentPage - 1\n } else {\n hasMorePages = false\n }\n } catch (retryError) {\n const retryErrorMsg = `Erro na segunda tentativa página ${currentPage}: ${retryError instanceof Error ? retryError.message : String(retryError)}`\n errors.push(retryErrorMsg)\n console.error(` ❌ ${retryErrorMsg}`)\n hasMorePages = false\n }\n }\n }\n\n } catch (error) {\n const errorMsg = `Erro geral no download: ${error instanceof Error ? error.message : String(error)}`\n errors.push(errorMsg)\n console.error(`❌ ${errorMsg}`)\n }\n\n console.log(`\\n📊 RESUMO DO DOWNLOAD:`)\n console.log(`📄 Total de páginas: ${totalPages}`)\n console.log(`📦 Total de imóveis: ${totalItems}`)\n console.log(`✅ Imóveis baixados: ${downloadedItems}`)\n if (errors.length > 0) {\n console.log(`❌ Erros: ${errors.length}`)\n }\n\n return {\n totalPages,\n totalItems,\n downloadedItems,\n errors,\n }\n }\n\n async downloadAll(pageSize: number = 100): Promise<DownloadResult> {\n console.log(`🚀 Iniciando download completo da API Jetimob...`)\n console.log(`📄 Tamanho da página: ${pageSize} imóveis`)\n return this.downloadPages({ pageSize })\n }\n\n async uploadToApi(uploadConfig: ApiUploadConfig): Promise<UploadResult> {\n const { endpoint, headers = {}, convertData = true } = uploadConfig\n \n const errors: string[] = []\n let totalProcessed = 0\n let totalSent = 0\n let totalErrors = 0\n\n try {\n // Lê todos os arquivos JSON do diretório\n const files = await fs.readdir(this.outputDir)\n const jsonFiles = files.filter(file => file.endsWith('.json'))\n .sort((a, b) => {\n const numA = parseInt(a.match(/page-(\\d+)/)?.[1] || '0')\n const numB = parseInt(b.match(/page-(\\d+)/)?.[1] || '0')\n return numA - numB\n })\n\n console.log(`\\n📁 Encontrados ${jsonFiles.length} arquivos para processar`)\n\n for (const file of jsonFiles) {\n console.log(`\\n📄 Processando ${file}...`)\n \n try {\n const filePath = join(this.outputDir, file)\n const content = await fs.readFile(filePath, 'utf8')\n const pageData = JSON.parse(content)\n \n if (!Array.isArray(pageData) || pageData.length === 0) {\n console.log(' 📄 Arquivo vazio, pulando...')\n continue\n }\n\n const properties = pageData\n console.log(` 📊 ${properties.length} imóveis encontrados`)\n totalProcessed += properties.length\n\n try {\n // Constrói o payload baseado na configuração\n let payload: any\n \n if (convertData) {\n // Se deve converter os dados, aqui seria onde aplicaríamos o converter\n // Por enquanto, mantém os dados originais da Jetimob\n payload = { properties: properties, source: 'jetimob' }\n } else {\n // Envia dados originais da Jetimob\n payload = { properties: properties }\n }\n\n const response = await fetch(endpoint, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n ...headers\n },\n body: JSON.stringify(payload)\n })\n\n if (response.ok) {\n const result = await response.json() as { message?: string }\n totalSent += properties.length\n console.log(` ✅ Sucesso: ${properties.length} imóveis enviados`)\n console.log(` 📝 ${result.message || 'Processado com sucesso'}`)\n } else {\n const errorText = await response.text()\n totalErrors += properties.length\n const errorMsg = `Erro ${response.status} ao enviar arquivo ${file}: ${errorText}`\n errors.push(errorMsg)\n console.log(` ❌ ${errorMsg}`)\n break // Para se der erro\n }\n } catch (error) {\n totalErrors += properties.length\n const errorMsg = `Erro de rede ao enviar arquivo ${file}: ${error instanceof Error ? error.message : String(error)}`\n errors.push(errorMsg)\n console.error(` ❌ ${errorMsg}`)\n break // Para se der erro\n }\n\n // Pausa entre arquivos\n await new Promise(resolve => setTimeout(resolve, 500))\n\n } catch (error) {\n const errorMsg = `Erro ao processar arquivo ${file}: ${error instanceof Error ? error.message : String(error)}`\n errors.push(errorMsg)\n console.error(` ❌ ${errorMsg}`)\n }\n }\n } catch (error) {\n const errorMsg = `Erro ao acessar diretório ${this.outputDir}: ${error instanceof Error ? error.message : String(error)}`\n errors.push(errorMsg)\n console.error(`❌ ${errorMsg}`)\n }\n\n // Log final\n console.log('\\n🎉 RESUMO FINAL DO UPLOAD:')\n console.log(`📊 Total processado: ${totalProcessed} imóveis`)\n console.log(`✅ Sucessos: ${totalSent} imóveis`)\n console.log(`❌ Erros: ${totalErrors} imóveis`)\n if (totalProcessed > 0) {\n console.log(`📈 Taxa de sucesso: ${Math.round((totalSent/totalProcessed)*100)}%`)\n }\n\n return {\n totalProcessed,\n totalSent,\n totalErrors,\n errors\n }\n }\n\n async downloadAndUpload(downloadOptions: DownloadOptions, uploadConfig: ApiUploadConfig): Promise<{\n downloadResult: DownloadResult\n uploadResult: UploadResult\n }> {\n console.log(`🚀 Iniciando processo completo: Download + Upload`)\n \n const downloadResult = await this.downloadPages(downloadOptions)\n \n if (downloadResult.downloadedItems > 0) {\n console.log(`\\n📤 Iniciando upload de ${downloadResult.downloadedItems} imóveis...`)\n const uploadResult = await this.uploadToApi(uploadConfig)\n \n return {\n downloadResult,\n uploadResult\n }\n } else {\n console.log(`⚠️ Nenhum imóvel foi baixado, pulando upload`)\n return {\n downloadResult,\n uploadResult: {\n totalProcessed: 0,\n totalSent: 0,\n totalErrors: 0,\n errors: ['Nenhum dado para enviar']\n }\n }\n }\n }\n}","import * as fs from 'fs'\nimport * as path from 'path'\n\nexport interface FieldConfig {\n maxExamples?: number // Quantos exemplos guardar (undefined = todos)\n}\n\nexport interface ProfilerConfig {\n inputDir: string // Diretório com os dados para analisar\n outputDir: string // Onde salvar o resultado\n outputFileName?: string // Nome do arquivo de saída\n fieldConfigs?: Record<string, FieldConfig> // Configurações por campo\n defaultMaxExamples?: number // Padrão para campos não configurados\n verbose?: boolean\n uniqueField?: string // Campo para deduplicação (ex: 'codigo', 'id', 'reference')\n dataLabel?: string // Label para os dados (ex: 'imóveis', 'products', 'users')\n serviceLabel?: string // Label do serviço (ex: 'Jetimob', 'API', 'Database')\n}\n\nexport type ProfileResult = Record<string, any[]>\n\n/**\n * Serviço genérico para análise e profiling de dados JSON\n * Baseado no ProfilerService do Arbo-CRM\n */\nexport class ProfilerService {\n private config: ProfilerConfig\n private fieldData: Map<string, Set<any>> = new Map()\n private fieldExamples: Map<string, any[]> = new Map()\n \n constructor(config: ProfilerConfig) {\n this.config = {\n outputFileName: 'profiling-report.json',\n defaultMaxExamples: 10,\n verbose: false,\n uniqueField: 'id',\n dataLabel: 'items',\n serviceLabel: 'Data',\n ...config\n }\n }\n\n async profile(): Promise<ProfileResult> {\n if (this.config.verbose) {\n console.log(`📊 Iniciando profiling dos dados ${this.config.serviceLabel}...`)\n }\n\n // Verificar se diretório existe\n if (!fs.existsSync(this.config.inputDir)) {\n throw new Error(`Diretório de entrada não encontrado: ${this.config.inputDir}`)\n }\n\n // Carregar dados\n const data = await this.loadData()\n \n // Processar dados\n this.processData(data)\n \n // Gerar resultado\n const result = this.generateResult()\n \n // Salvar resultado\n await this.saveResult(result)\n \n return result\n }\n\n private async loadData(): Promise<any[]> {\n const files = fs.readdirSync(this.config.inputDir)\n .filter(file => file.endsWith('.json'))\n .sort()\n\n if (this.config.verbose) {\n console.log(`📁 Carregando ${files.length} arquivos ${this.config.serviceLabel}...`)\n }\n\n const allData: any[] = []\n \n for (const file of files) {\n const filePath = path.join(this.config.inputDir, file)\n const content = fs.readFileSync(filePath, 'utf8')\n const fileData = JSON.parse(content)\n \n // Suporte para arrays diretos ou objetos com .data\n if (Array.isArray(fileData)) {\n allData.push(...fileData)\n \n if (this.config.verbose) {\n console.log(` ✅ ${file}: ${fileData.length} ${this.config.dataLabel}`)\n }\n } else if (fileData.data && Array.isArray(fileData.data)) {\n // Suporte para formato com .data também\n allData.push(...fileData.data)\n \n if (this.config.verbose) {\n console.log(` ✅ ${file}: ${fileData.data.length} ${this.config.dataLabel}`)\n }\n }\n }\n\n if (this.config.verbose) {\n console.log(`🔍 Analisando ${allData.length} ${this.config.dataLabel} ${this.config.serviceLabel}...`)\n }\n\n return allData\n }\n\n private processData(data: any[]): void {\n data.forEach((item, index) => {\n if (this.config.verbose && (index + 1) % 100 === 0) {\n console.log(` Processando ${index + 1}/${data.length}...`)\n }\n\n this.processObject(item, '')\n })\n }\n\n private processObject(obj: any, prefix: string): void {\n for (const [key, value] of Object.entries(obj)) {\n const fieldName = prefix ? `${prefix}.${key}` : key\n \n if (value === null || value === undefined) {\n continue\n }\n\n if (Array.isArray(value)) {\n // Para arrays, processar cada item\n this.processArrayField(fieldName, value)\n } else if (typeof value === 'object') {\n // Para objetos, processar recursivamente\n this.processObject(value, fieldName)\n } else {\n // Para valores simples\n this.processSimpleField(fieldName, value)\n }\n }\n }\n\n private processArrayField(fieldName: string, array: any[]): void {\n // Processar cada item do array\n for (const item of array) {\n if (item !== null && item !== undefined) {\n if (typeof item === 'object') {\n this.processObject(item, fieldName)\n } else {\n this.processSimpleField(fieldName, item)\n }\n }\n }\n }\n\n private processSimpleField(fieldName: string, value: any): void {\n // Inicializar estruturas se necessário\n if (!this.fieldData.has(fieldName)) {\n this.fieldData.set(fieldName, new Set())\n this.fieldExamples.set(fieldName, [])\n }\n\n const valueSet = this.fieldData.get(fieldName)!\n const examples = this.fieldExamples.get(fieldName)!\n \n // Adicionar ao set de valores únicos\n valueSet.add(value)\n \n // Adicionar aos exemplos se não estiver presente e não exceder o limite\n const fieldConfig = this.config.fieldConfigs?.[fieldName] || {}\n const maxExamples = fieldConfig.maxExamples ?? this.config.defaultMaxExamples!\n \n if (!examples.includes(value) && examples.length < maxExamples) {\n examples.push(value)\n }\n }\n\n private generateResult(): ProfileResult {\n const result: ProfileResult = {}\n\n // Ordenar campos alfabeticamente\n const sortedFields = Array.from(this.fieldData.keys()).sort()\n\n for (const fieldName of sortedFields) {\n const examples = this.fieldExamples.get(fieldName) || []\n // Ordenar os valores internos também\n const sortedExamples = examples.sort((a, b) => {\n // Converter para string para comparação consistente\n const aStr = String(a)\n const bStr = String(b)\n return aStr.localeCompare(bStr)\n })\n result[fieldName] = sortedExamples\n }\n\n return result\n }\n\n private async saveResult(result: ProfileResult): Promise<void> {\n // Criar diretório de saída se não existir\n if (!fs.existsSync(this.config.outputDir)) {\n fs.mkdirSync(this.config.outputDir, { recursive: true })\n }\n\n const outputPath = path.join(this.config.outputDir, this.config.outputFileName!)\n const jsonContent = JSON.stringify(result, null, 2)\n \n fs.writeFileSync(outputPath, jsonContent, 'utf8')\n \n if (this.config.verbose) {\n const sizeKB = (jsonContent.length / 1024).toFixed(1)\n console.log(`✅ Profiling ${this.config.serviceLabel} salvo em: ${outputPath} (${sizeKB}KB)`)\n console.log(`📊 Total de campos analisados: ${Object.keys(result).length}`)\n }\n }\n\n /**\n * Método para analisar apenas dados únicos\n * Remove duplicatas baseado no campo configurado em uniqueField\n */\n async profileUnique(): Promise<ProfileResult> {\n const allData = await this.loadData()\n \n // Remover duplicatas baseado no campo único configurado\n const uniqueData = Array.from(\n new Map(allData.map(item => [item[this.config.uniqueField!], item])).values()\n )\n \n if (this.config.verbose) {\n console.log(`🔍 Removidas ${allData.length - uniqueData.length} duplicatas`)\n console.log(`📊 Analisando ${uniqueData.length} ${this.config.dataLabel} únicos...`)\n }\n \n this.processData(uniqueData)\n const result = this.generateResult()\n await this.saveResult(result)\n \n return result\n }\n}","[\n {\n \"codigo\": \"FAKE_APARTAMENTOS_001\",\n \"titulo_anuncio\": \"Apartamento Centro - 2 dorm, 2 suítes, 165m², 1 vaga\",\n \"observacoes\": \"Apartamento com vista para o mar em Torres, a praia mais linda do RS.\",\n \"contrato\": \"Compra\",\n \"tipo\": \"Residencial\",\n \"subtipo\": \"Apartamento\",\n \"mobiliado\": 0,\n \"financiavel\": 0,\n \"exclusividade\": false,\n \"medida\": \"m²\",\n \"endereco_estado\": \"Rio Grande do Sul\",\n \"endereco_cidade\": \"Torres\",\n \"data_cadastro\": \"2024-01-06 13:26:01\",\n \"data_update\": \"2025-03-03 00:46:23\",\n \"data_atualizacao\": \"2025-03-16 10:42:00\",\n \"updated_at\": \"2025-03-19 08:18:57\",\n \"dormitorios\": 2,\n \"suites\": 2,\n \"banheiros\": 3,\n \"garagens\": 1,\n \"area_total\": 150,\n \"area_privativa\": 165,\n \"andar\": 1,\n \"valor_venda\": 280000,\n \"endereco_bairro\": \"Vila São João\",\n \"endereco_logradouro\": \"Rua Júlio de Castilhos\",\n \"endereco_numero\": \"491\",\n \"endereco_cep\": \"95560-070\",\n \"endereco_complemento\": \"Apto 1\",\n \"latitude\": -29.342936776054497,\n \"longitude\": -49.72035923955464,\n \"imovel_comodidades\": \"Sacada,Próximo à praia,Área de serviço,Churrasqueira,Lavabo\",\n \"imagens\": [\n {\n \"link\": \"https://images.unsplash.com/photo-1565183997392-2f6f122e5912?w=800&q=80\",\n \"titulo\": \"1.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1565183997392-2f6f122e5912?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?w=800&q=80\",\n \"titulo\": \"2.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1568605117036-5fe5e7bab0b7?w=800&q=80\",\n \"titulo\": \"3.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1568605117036-5fe5e7bab0b7?w=400&q=80\"\n }\n ],\n \"videos\": [],\n \"tour360\": [],\n \"plantas\": [],\n \"valor_venda_visivel\": true,\n \"valor_locacao_visivel\": false,\n \"valor_temporada_visivel\": false,\n \"valor_condominio_visivel\": false,\n \"valor_iptu_visivel\": false,\n \"endereco_estado_visivel\": true,\n \"endereco_cidade_visivel\": false,\n \"endereco_bairro_visivel\": false,\n \"endereco_logradouro_visivel\": false,\n \"endereco_numero_visivel\": false,\n \"endereco_complemento_visivel\": false\n },\n {\n \"codigo\": \"FAKE_APARTAMENTOS_002\",\n \"titulo_anuncio\": \"Apartamento Centro - 4 dorm, 3 suítes, 165m², 3 vagas\",\n \"observacoes\": \"Unidade moderna próxima ao Centro Histórico e ao Farol de Torres.\",\n \"contrato\": \"Compra\",\n \"tipo\": \"Residencial\",\n \"subtipo\": \"Apartamento\",\n \"mobiliado\": 0,\n \"financiavel\": 0,\n \"exclusividade\": false,\n \"medida\": \"m²\",\n \"endereco_estado\": \"Rio Grande do Sul\",\n \"endereco_cidade\": \"Torres\",\n \"data_cadastro\": \"2024-03-10 02:20:33\",\n \"data_update\": \"2025-05-18 22:01:44\",\n \"data_atualizacao\": \"2025-04-13 15:23:31\",\n \"updated_at\": \"2025-01-14 22:31:12\",\n \"dormitorios\": 4,\n \"suites\": 3,\n \"banheiros\": 3,\n \"garagens\": 3,\n \"area_total\": 105,\n \"area_privativa\": 165,\n \"andar\": 10,\n \"valor_venda\": 850000,\n \"endereco_bairro\": \"Vila São João\",\n \"endereco_logradouro\": \"Rua João Neves da Fontoura\",\n \"endereco_numero\": \"882\",\n \"endereco_cep\": \"95560-020\",\n \"endereco_complemento\": \"\",\n \"latitude\": -29.326109044782587,\n \"longitude\": -49.72886046595862,\n \"imovel_comodidades\": \"Área gourmet,Área de serviço,Lavabo,Hidromassagem\",\n \"imagens\": [\n {\n \"link\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=800&q=80\",\n \"titulo\": \"1.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=800&q=80\",\n \"titulo\": \"2.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1560185127-6ed189bf02f4?w=800&q=80\",\n \"titulo\": \"3.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1560185127-6ed189bf02f4?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1560185127-6ed189bf02f4?w=800&q=80\",\n \"titulo\": \"4.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1560185127-6ed189bf02f4?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?w=800&q=80\",\n \"titulo\": \"5.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?w=400&q=80\"\n }\n ],\n \"videos\": [],\n \"tour360\": [],\n \"plantas\": [],\n \"valor_venda_visivel\": true,\n \"valor_locacao_visivel\": false,\n \"valor_temporada_visivel\": false,\n \"valor_condominio_visivel\": false,\n \"valor_iptu_visivel\": false,\n \"endereco_estado_visivel\": true,\n \"endereco_cidade_visivel\": false,\n \"endereco_bairro_visivel\": false,\n \"endereco_logradouro_visivel\": false,\n \"endereco_numero_visivel\": false,\n \"endereco_complemento_visivel\": false\n },\n {\n \"codigo\": \"FAKE_APARTAMENTOS_003\",\n \"titulo_anuncio\": \"Apartamento Centro - 3 dorm, 115m², 3 vagas\",\n \"observacoes\": \"Imóvel com excelente localização e acabamentos de primeira linha.\",\n \"contrato\": \"Compra\",\n \"tipo\": \"Residencial\",\n \"subtipo\": \"Apartamento\",\n \"mobiliado\": 0,\n \"financiavel\": 0,\n \"exclusividade\": false,\n \"medida\": \"m²\",\n \"endereco_estado\": \"Rio Grande do Sul\",\n \"endereco_cidade\": \"Torres\",\n \"data_cadastro\": \"2025-07-23 02:33:46\",\n \"data_update\": \"2024-06-18 14:20:49\",\n \"data_atualizacao\": \"2025-05-26 22:11:42\",\n \"updated_at\": \"2025-07-18 14:46:17\",\n \"dormitorios\": 3,\n \"suites\": 0,\n \"banheiros\": 3,\n \"garagens\": 3,\n \"area_total\": 45,\n \"area_privativa\": 115,\n \"andar\": 15,\n \"valor_venda\": 850000,\n \"endereco_bairro\": \"Praia Grande\",\n \"endereco_logradouro\": \"Rua Senador Pinheiro Machado\",\n \"endereco_numero\": \"430\",\n \"endereco_cep\": \"95560-060\",\n \"endereco_complemento\": \"Apto 2\",\n \"latitude\": -29.336375878338483,\n \"longitude\": -49.72503985134924,\n \"imovel_comodidades\": \"Ar condicionado,Varanda,Área gourmet,Área de serviço\",\n \"imagens\": [\n {\n \"link\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&q=80\",\n \"titulo\": \"1.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=800&q=80\",\n \"titulo\": \"2.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&q=80\",\n \"titulo\": \"3.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1560185127-6ed189bf02f4?w=800&q=80\",\n \"titulo\": \"4.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1560185127-6ed189bf02f4?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=800&q=80\",\n \"titulo\": \"5.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=400&q=80\"\n }\n ],\n \"videos\": [],\n \"tour360\": [],\n \"plantas\": [],\n \"valor_venda_visivel\": true,\n \"valor_locacao_visivel\": false,\n \"valor_temporada_visivel\": false,\n \"valor_condominio_visivel\": false,\n \"valor_iptu_visivel\": false,\n \"endereco_estado_visivel\": true,\n \"endereco_cidade_visivel\": false,\n \"endereco_bairro_visivel\": false,\n \"endereco_logradouro_visivel\": false,\n \"endereco_numero_visivel\": false,\n \"endereco_complemento_visivel\": false\n },\n {\n \"codigo\": \"FAKE_APARTAMENTOS_004\",\n \"titulo_anuncio\": \"Apartamento Centro - 3 dorm, 2 suítes, 140m², 1 vaga\",\n \"observacoes\": \"Imóvel com excelente localização e acabamentos de primeira linha.\",\n \"contrato\": \"Compra\",\n \"tipo\": \"Residencial\",\n \"subtipo\": \"Apartamento\",\n \"mobiliado\": 0,\n \"financiavel\": 0,\n \"exclusividade\": false,\n \"medida\": \"m²\",\n \"endereco_estado\": \"Rio Grande do Sul\",\n \"endereco_cidade\": \"Torres\",\n \"data_cadastro\": \"2025-01-12 05:21:57\",\n \"data_update\": \"2025-07-10 14:50:25\",\n \"data_atualizacao\": \"2024-10-07 07:40:13\",\n \"updated_at\": \"2025-05-05 10:51:02\",\n \"dormitorios\": 3,\n \"suites\": 2,\n \"banheiros\": 1,\n \"garagens\": 1,\n \"area_total\": 65,\n \"area_privativa\": 140,\n \"andar\": 12,\n \"valor_venda\": 850000,\n \"endereco_bairro\": \"Jardim Ubatuba\",\n \"endereco_logradouro\": \"Rua Senador Pinheiro Machado\",\n \"endereco_numero\": \"427\",\n \"endereco_cep\": \"95560-020\",\n \"endereco_complemento\": \"Apto 6\",\n \"latitude\": -29.339157171441027,\n \"longitude\": -49.72632203630522,\n \"imovel_comodidades\": \"Ar condicionado,Jardim,Garagem coberta,Terraço,Sacada,Próximo à praia\",\n \"imagens\": [\n {\n \"link\": \"https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800&q=80\",\n \"titulo\": \"1.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&q=80\",\n \"titulo\": \"2.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1568605117036-5fe5e7bab0b7?w=800&q=80\",\n \"titulo\": \"3.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1568605117036-5fe5e7bab0b7?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"titulo\": \"4.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&q=80\"\n }\n ],\n \"videos\": [],\n \"tour360\": [],\n \"plantas\": [],\n \"valor_venda_visivel\": true,\n \"valor_locacao_visivel\": false,\n \"valor_temporada_visivel\": false,\n \"valor_condominio_visivel\": false,\n \"valor_iptu_visivel\": false,\n \"endereco_estado_visivel\": true,\n \"endereco_cidade_visivel\": false,\n \"endereco_bairro_visivel\": false,\n \"endereco_logradouro_visivel\": false,\n \"endereco_numero_visivel\": false,\n \"endereco_complemento_visivel\": false\n },\n {\n \"codigo\": \"FAKE_APARTAMENTOS_005\",\n \"titulo_anuncio\": \"Apartamento Centro - 3 dorm, 3 suítes, 55m², 2 vagas\",\n \"observacoes\": \"Apartamento com sacada, a poucos metros da Praia Grande.\",\n \"contrato\": \"Compra\",\n \"tipo\": \"Residencial\",\n \"subtipo\": \"Apartamento\",\n \"mobiliado\": 0,\n \"financiavel\": 0,\n \"exclusividade\": false,\n \"medida\": \"m²\",\n \"endereco_estado\": \"Rio Grande do Sul\",\n \"endereco_cidade\": \"Torres\",\n \"data_cadastro\": \"2024-05-08 10:23:39\",\n \"data_update\": \"2024-09-18 14:42:42\",\n \"data_atualizacao\": \"2024-01-07 03:13:44\",\n \"updated_at\": \"2025-02-09 06:49:29\",\n \"dormitorios\": 3,\n \"suites\": 3,\n \"banheiros\": 4,\n \"garagens\": 2,\n \"area_total\": 180,\n \"area_privativa\": 55,\n \"andar\": 12,\n \"valor_venda\": 180000,\n \"endereco_bairro\": \"Prainha\",\n \"endereco_logradouro\": \"Rua Barão do Rio Branco\",\n \"endereco_numero\": \"526\",\n \"endereco_cep\": \"95560-060\",\n \"endereco_complemento\": \"\",\n \"latitude\": -29.33708529462322,\n \"longitude\": -49.73430782637095,\n \"imovel_comodidades\": \"Lareira,Varanda\",\n \"imagens\": [\n {\n \"link\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&q=80\",\n \"titulo\": \"1.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&q=80\",\n \"titulo\": \"2.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=800&q=80\",\n \"titulo\": \"3.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?w=400&q=80\"\n },\n {\n \"link\": \"https://images.unsplash.com/photo-1581858726788-75bc0f6a952d?w=800&q=80\",\n \"titulo\": \"4.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1581858726788-75bc0f6a952d?w=400&q=80\"\n }\n ],\n \"videos\": [],\n \"tour360\": [],\n \"plantas\": [],\n \"valor_venda_visivel\": true,\n \"valor_locacao_visivel\": false,\n \"valor_temporada_visivel\": false,\n \"valor_condominio_visivel\": false,\n \"valor_iptu_visivel\": false,\n \"endereco_estado_visivel\": true,\n \"endereco_cidade_visivel\": false,\n \"endereco_bairro_visivel\": false,\n \"endereco_logradouro_visivel\": false,\n \"endereco_numero_visivel\": false,\n \"endereco_complemento_visivel\": false\n },\n {\n \"codigo\": \"FAKE_APARTAMENTOS_006\",\n \"titulo_anuncio\": \"Apartamento Centro - 4 dorm, 2 suítes, 55m², 3 vagas\",\n \"observacoes\": \"Apartamento com vista para o mar em Torres, a praia mais linda do RS.\",\n \"contrato\": \"Compra\",\n \"tipo\": \"Residencial\",\n \"subtipo\": \"Apartamento\",\n \"mobiliado\": 0,\n \"financiavel\": 0,\n \"exclusividade\": false,\n \"medida\": \"m²\",\n \"endereco_estado\": \"Rio Grande do Sul\",\n \"endereco_cidade\": \"Torres\",\n \"data_cadastro\": \"2025-06-12 10:45:18\",\n \"data_update\": \"2024-09-07 03:24:40\",\n \"data_atualizacao\": \"2024-12-04 16:01:29\",\n \"updated_at\": \"2024-07-27 13:34:42\",\n \"dormitorios\": 4,\n \"suites\": 2,\n \"banheiros\": 4,\n \"garagens\": 3,\n \"area_total\": 105,\n \"area_privativa\": 55,\n \"andar\": 12,\n \"valor_venda\": 620000,\n \"endereco_bairro\": \"Residencial Quinta da Colina\",\n \"endereco_logradouro\": \"Rua Cristóvão Colombo\",\n \"endereco_numero\": \"52\",\n \"endereco_cep\": \"95560-010\",\n \"endereco_complemento\": \"\",\n \"latitude\": -29.3424800569058,\n \"longitude\": -49.71831372337415,\n \"imovel_comodidades\": \"Churrasqueira,Vista para o mar,Garagem coberta,Ar condicionado\",\n \"imagens\": [\n {\n \"link\": \"https://images.unsplash.com/photo-1574362848149-11496d93a7c7?w=800&q=80\",\n \"titulo\": \"1.jpg\",\n \"link_thumb\": \"https://images.unsplash.com/photo-1574362848149-11496d93a7c7?w=400&q=80\"\n },\n {\n \"link\": \"