@horizon-modules/arbo-crm-integration
Version:
Integração CRM Arbo para conversion de dados imobiliários para property-model-v3
1 lines • 305 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/discoverConfigs.ts","../src/converter/index.ts","../src/services/ArboApiClient.ts","../src/services/ArboDownloader.ts","../src/services/ProfilerService.ts","../src/data/mock/validos.json","../src/data/mock/problematicos.json","../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/index.ts"],"sourcesContent":["import { ArboImovel } from \"../types\"\n\nconst operationLabels: { [key: string]: string } = {\n venda: \"Venda\",\n locacao: \"Locação\",\n aluguel: \"Locação\",\n temporada: \"Temporada\",\n}\n\nfunction normalizeString(str: string) {\n return str\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .toLowerCase()\n .trim()\n}\n\nexport function discoverOperation(imovel: ArboImovel): string[] {\n if (!imovel.finalidade) return []\n\n const normalized = normalizeString(imovel.finalidade)\n const operations: string[] = []\n\n // Se contém \" e \", significa múltiplas operações\n if (normalized.includes(\" e \")) {\n const parts = normalized.split(\" e \")\n parts.forEach(part => {\n const label = operationLabels[part]\n if (label) operations.push(label)\n })\n } else {\n const label = operationLabels[normalized]\n if (label) operations.push(label)\n }\n\n return operations\n}","import { ArboImovel } from \"../types\"\nimport { discoverOperation } from \"./discoverOperation\"\n\nexport function discoverAttributes(imovel: ArboImovel): 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\n if (imovel.valor_venda) attributes.valor_venda = imovel.valor_venda\n if (imovel.valor_aluguel) attributes.valor_locacao = imovel.valor_aluguel\n\n // Valores de condomínio e IPTU\n if (imovel.valor_condominio) attributes.valor_condominio = imovel.valor_condominio\n if (imovel.valor_iptu) attributes.valor_iptu = imovel.valor_iptu\n\n // Áreas\n if (imovel.area_total) attributes.area_total = imovel.area_total\n if (imovel.area_privativa) attributes.area_util = imovel.area_privativa\n\n // Dependências\n if (imovel.qtd_quartos) attributes.dormitorios = imovel.qtd_quartos\n if (imovel.qtd_suites) attributes.suites = imovel.qtd_suites\n if (imovel.qtd_banheiro) attributes.banheiros = imovel.qtd_banheiro\n if (imovel.qtd_vagas) attributes.vagas_garagem = imovel.qtd_vagas\n\n // Características booleanas\n if (imovel.mobiliado) attributes.mobiliado = true\n if (imovel.financiamento) attributes.financiavel = true\n if (imovel.permuta) attributes.aceita_permuta = true\n if (imovel.end_condominio) attributes.em_condominio = true\n\n // Informações do imóvel\n if (imovel.status_comercial) attributes.status_comercial = imovel.status_comercial\n if (imovel.categoria) attributes.tipo = imovel.categoria\n if (imovel.tipo_imovel) attributes.finalidade = imovel.tipo_imovel\n if (imovel.categoria_imovel) attributes.padrao_imovel = imovel.categoria_imovel\n\n // Localização\n if (imovel.end_cep) attributes.endereco_cep = imovel.end_cep\n if (imovel.end_estado) attributes.endereco_estado = imovel.end_estado\n if (imovel.end_cidade) attributes.endereco_cidade = imovel.end_cidade\n if (imovel.end_bairro) attributes.endereco_bairro = imovel.end_bairro\n if (imovel.end_logradouro) attributes.endereco_logradouro = imovel.end_logradouro\n if (imovel.end_numero) attributes.endereco_numero = imovel.end_numero\n if (imovel.end_complemento) attributes.endereco_complemento = imovel.end_complemento\n if (imovel.end_condominio) attributes.condominio_nome = imovel.end_condominio\n\n // Coordenadas\n if (imovel.latitude) attributes.latitude = imovel.latitude\n if (imovel.longitude) attributes.longitude = imovel.longitude\n\n // Corretor\n if (imovel?.corretor?.codigo) attributes.corretor_id = imovel.corretor.codigo\n if (imovel?.corretor?.nome) attributes.corretor_nome = imovel.corretor.nome\n\n // Características\n if (imovel.caracteristicas) attributes.caracteristicas = imovel.caracteristicas\n\n return attributes\n}","import { ArboImovel } from \"../types\"\nimport { MediaAssets, ImageMedia, VideoMedia, DocumentMedia, VirtualTourMedia } from \"../property-model-v3\"\n\nexport function discoverMedias(imovel: ArboImovel): MediaAssets {\n // Imagens com diferentes resoluções\n const images: ImageMedia[] = imovel.fotos\n ?.slice()\n .sort((a, b) => (a.ordem ?? 0) - (b.ordem ?? 0))\n .map((foto, index) => ({\n full: foto.marcadagua_url || foto.url,\n md: foto.sizes?.medium,\n sm: foto.sizes?.small,\n cover: index === 0 || foto.principal === true\n })) ?? []\n\n // Vídeos\n const videos: VideoMedia[] = imovel.url_video ? [generateVideo(imovel.url_video)] : []\n\n // Tours virtuais\n const virtual_tours: VirtualTourMedia[] = imovel.url_tour ? [{ embed_url: imovel.url_tour }] : []\n\n // Documentos (por enquanto vazio, pode ser expandido futuramente)\n const documents: DocumentMedia[] = []\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 { ArboImovel } from \"../types\"\nimport { SettingsFormat } from \"@horizon-modules/property-model-v3\"\n\nexport function discoverSettings(_imovel: ArboImovel): SettingsFormat {\n return {\n currency_unit: \"BRL\",\n area_unit: \"m2\",\n distance_unit: \"meters\",\n exibir_no_mapa: true,\n }\n}","import { ArboImovel } from './types'\nimport { discoverAttributes } from './discoverers/discoverAttributes'\nimport { discoverMedias } from './discoverers/discoverMedia'\nimport { discoverSettings } from './discoverers/discoverConfigs'\nimport { PropertyModel } from './property-model-v3'\n\nexport function convertArboToPropertyV3(imovel: ArboImovel): PropertyModel {\n return {\n reference: imovel.codigo ?? \"\",\n title: imovel.titulo ?? \"\",\n description: imovel.descricao ?? \"\",\n media_assets: discoverMedias(imovel),\n attributes: discoverAttributes(imovel),\n settings: discoverSettings(imovel),\n updated_at: imovel.updated_at,\n }\n}\n\n// Re-exportar tipos relacionados ao converter\nexport type { ArboImovel } 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 { discoverSettings } from './discoverers/discoverConfigs'","import { ArboImovel } from \"../converter/types\"\nimport { ArboApiClientConfig, ArboApiResponse } from \"./types\"\n\nexport class ArboApiClient {\n private token: string\n private baseUrl: string\n\n constructor(config: ArboApiClientConfig) {\n this.token = config.token\n this.baseUrl = config.baseUrl || \"https://app-integracao.arboimoveis.com/api\"\n }\n\n private async request<T>(endpoint: string): Promise<T> {\n const url = `${this.baseUrl}${endpoint}`\n \n const response = await fetch(url, {\n headers: {\n Authorization: this.token,\n \"Content-Type\": \"application/json\",\n },\n })\n\n if (!response.ok) {\n throw new Error(\n `Erro na requisição: ${response.status} - ${response.statusText}`\n )\n }\n\n return response.json() as Promise<T>\n }\n\n async getImoveis(page: number = 1, perPage: number = 50): Promise<ArboApiResponse<ArboImovel[]>> {\n return this.request<ArboApiResponse<ArboImovel[]>>(\n `/imoveis?page=${page}&perPage=${perPage}&fields=%5B%5D&search=%7B%7D`\n )\n }\n\n async getImovel(id: string | number): Promise<ArboImovel> {\n return this.request<ArboImovel>(`/imoveis/${id}`)\n }\n\n async searchImoveis(\n searchParams: Record<string, any>,\n page: number = 1,\n perPage: number = 50\n ): Promise<ArboApiResponse<ArboImovel[]>> {\n const search = encodeURIComponent(JSON.stringify(searchParams))\n return this.request<ArboApiResponse<ArboImovel[]>>(\n `/imoveis?page=${page}&perPage=${perPage}&search=${search}`\n )\n }\n\n async getAllPages(perPage: number = 50): Promise<ArboImovel[]> {\n const firstPage = await this.getImoveis(1, perPage)\n const totalPages = firstPage.meta?.last_page || 1\n \n const allImoveis: ArboImovel[] = [...firstPage.data]\n \n for (let page = 2; page <= totalPages; page++) {\n const pageData = await this.getImoveis(page, perPage)\n allImoveis.push(...pageData.data)\n }\n \n return allImoveis\n }\n}","import { promises as fs } from \"fs\"\nimport { join } from \"path\"\nimport { ArboApiClient } from \"./ArboApiClient\"\nimport { ArboDownloaderConfig, DownloadOptions, DownloadResult, ApiUploadConfig, UploadResult } from \"./types\"\n\nexport class ArboDownloader {\n private apiClient: ArboApiClient\n private outputDir: string\n\n constructor(config: ArboDownloaderConfig) {\n this.apiClient = new ArboApiClient({\n token: config.token,\n baseUrl: config.baseUrl || \"https://app-integracao.arboimoveis.com/api\",\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 // Não salvar se os dados estiverem vazios\n if (!data.data || !Array.isArray(data.data) || data.data.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 await fs.writeFile(filePath, JSON.stringify(data, null, 2), \"utf8\")\n return true\n }\n\n async downloadPage(page: number, perPage: number = 50): Promise<void> {\n await this.ensureOutputDir()\n \n const response = await this.apiClient.getImoveis(page, perPage)\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 perPage = 50 \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\n const firstPage = await this.apiClient.getImoveis(1, perPage)\n totalPages = firstPage.meta?.last_page || 1\n totalItems = firstPage.meta?.total || 0\n\n // Determina o range de páginas para download\n const finalEndPage = endPage || (maxPages ? Math.min(startPage + maxPages - 1, totalPages) : totalPages)\n \n // Salva a primeira página se ela estiver no range\n if (startPage === 1) {\n const saved = await this.savePageData(1, firstPage)\n if (saved) {\n downloadedItems += firstPage.data.length\n }\n // Se a primeira página já está vazia, não há mais dados\n if (!firstPage.data || firstPage.data.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\n // Download das páginas restantes\n for (let page = Math.max(startPage, 2); page <= finalEndPage; page++) {\n try {\n const response = await this.apiClient.getImoveis(page, perPage)\n \n // Se a página estiver vazia, finalizar o download\n if (!response.data || response.data.length === 0) {\n console.log(`⚠️ Página ${page} vazia, finalizando download`)\n break\n }\n \n const saved = await this.savePageData(page, response)\n if (saved) {\n downloadedItems += response.data.length\n }\n } catch (error) {\n const errorMsg = `Erro ao baixar página ${page}: ${error instanceof Error ? error.message : String(error)}`\n errors.push(errorMsg)\n console.error(errorMsg)\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 return {\n totalPages,\n totalItems,\n downloadedItems,\n errors,\n }\n }\n\n async downloadAll(perPage: number = 50): Promise<DownloadResult> {\n return this.downloadPages({ perPage })\n }\n\n async uploadToApi(uploadConfig: ApiUploadConfig): Promise<UploadResult> {\n const { endpoint, headers = {} } = 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(`📁 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 (!pageData.data || !Array.isArray(pageData.data)) {\n errors.push(`Arquivo ${file} não possui estrutura válida`)\n console.log(' 📄 Arquivo vazio, pulando...')\n continue\n }\n\n const properties = pageData.data\n if (properties.length === 0) {\n console.log(' 📄 Arquivo vazio, pulando...')\n continue\n }\n\n console.log(` 📊 ${properties.length} imóveis encontrados`)\n totalProcessed += properties.length\n\n try {\n const response = await fetch(endpoint, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n ...headers\n },\n // Envia dados originais da Arbo - API faz a conversão\n body: JSON.stringify({ properties: properties })\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 inseridos`)\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:')\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 const downloadResult = await this.downloadPages(downloadOptions)\n const uploadResult = await this.uploadToApi(uploadConfig)\n \n return {\n downloadResult,\n uploadResult\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}\n\nexport type ProfileResult = Record<string, any[]>\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 ...config\n }\n }\n\n async profile(): Promise<ProfileResult> {\n if (this.config.verbose) {\n console.log('📊 Iniciando profiling dos dados...')\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...`)\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 // Assumir que cada arquivo tem uma propriedade 'data' com array de objetos\n if (fileData.data && Array.isArray(fileData.data)) {\n allData.push(...fileData.data)\n \n if (this.config.verbose) {\n console.log(` ✅ ${file}: ${fileData.data.length} itens`)\n }\n }\n }\n\n if (this.config.verbose) {\n console.log(`🔍 Analisando ${allData.length} itens...`)\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, concatenar todos os valores\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 // Concatenar todos os valores 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 salvo em: ${outputPath} (${sizeKB}KB)`)\n console.log(`📊 Total de campos analisados: ${Object.keys(result).length}`)\n }\n }\n}","[\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_001\",\n \"titulo\": \"Apartamento Teste - Mock 1\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.100Z\",\n \"updated_at\": \"2025-07-08T19:46:41.100Z\",\n \"qtd_quartos\": 2,\n \"valor_venda\": 250000,\n \"area_privativa\": 0,\n \"corretor.codigo\": 908381,\n \"end_logradouro\": \"Rua Mathias de Albuquerque\",\n \"end_bairro\": \"Oficinas\",\n \"end_cidade\": \"Ponta Grossa\",\n \"end_estado\": \"PR\",\n \"longitude\": -50.1480833,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_001_1\",\n \"url\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_001_2\",\n \"url\": \"https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800&q=80\",\n \"descricao\": \"Vista externa\",\n \"destaque\": false,\n \"ordem\": 2\n }\n ],\n \"observacoes\": \"Observações do Apartamento Teste: Documentação em ordem\"\n },\n {\n \"codigo\": \"MOCK_TEST-CASA_001\",\n \"titulo\": \"Casa Teste com 2 quartos, 120m²\",\n \"descricao\": \"Mock de casa para testes gerado automaticamente para testes\",\n \"categoria\": \"Casa\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.100Z\",\n \"updated_at\": \"2025-07-08T19:46:41.100Z\",\n \"qtd_quartos\": 2,\n \"qtd_banheiro\": 3,\n \"area_construida\": 120,\n \"area_terreno\": 200,\n \"valor_venda\": 350000,\n \"area_total\": 518,\n \"caracteristicas\": \"Sala\",\n \"condominio\": 237551,\n \"corretor.codigo\": 913153,\n \"end_logradouro\": \"Rua Tinguis\",\n \"end_numero\": 8,\n \"latitude\": -25.1122291,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_001_1\",\n \"url\": \"https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80\",\n \"descricao\": \"Casa Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_001_2\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_001_3\",\n \"url\": \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 3\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_001_4\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 4\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_002\",\n \"titulo\": \"Apartamento Teste com 2 quartos, 45m²\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 2,\n \"qtd_banheiro\": 2,\n \"area_construida\": 45,\n \"valor_venda\": 250000,\n \"area_total\": 270,\n \"end_bairro\": \"Jardins\",\n \"end_cep\": \"84017316\",\n \"longitude\": -50.1217041015625,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_002_1\",\n \"url\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_002_2\",\n \"url\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_002_3\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 3\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_002_4\",\n \"url\": \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 4\n }\n ],\n \"end_logradouro\": \"Rua Mock 438\",\n \"end_numero\": \"637\",\n \"end_cidade\": \"São Paulo\",\n \"end_estado\": \"RS\"\n },\n {\n \"codigo\": \"MOCK_TEST-CASA_002\",\n \"titulo\": \"Casa Teste - Mock 2\",\n \"descricao\": \"Mock de casa para testes gerado automaticamente para testes\",\n \"categoria\": \"Casa\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_banheiro\": 3,\n \"area_construida\": 120,\n \"valor_venda\": 550000,\n \"area_privativa\": 98.25,\n \"area_total\": 1,\n \"codigo_origem\": \"GD0002\",\n \"condominio\": 237885,\n \"corretor.codigo\": 908218,\n \"end_logradouro\": \"Rua Tinguis\",\n \"end_numero\": 8,\n \"end_cidade\": \"Ponta Grossa\",\n \"end_estado\": \"PR\",\n \"longitude\": -50.1461068,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_002_1\",\n \"url\": \"https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&q=80\",\n \"descricao\": \"Casa Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_002_2\",\n \"url\": \"https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&q=80\",\n \"descricao\": \"Vista externa\",\n \"destaque\": false,\n \"ordem\": 2\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_003\",\n \"titulo\": \"Apartamento Teste - Mock 3\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 3,\n \"valor_venda\": 250000,\n \"area_total\": 155,\n \"categoria_imovel\": \"Padrão\",\n \"end_logradouro\": \"Rua Mathias de Albuquerque\",\n \"end_bairro\": \"Oficinas\",\n \"end_cidade\": \"Ponta Grossa\",\n \"end_estado\": \"PR\",\n \"end_cep\": \"84036140\",\n \"latitude\": -25.1187197,\n \"longitude\": -50.1480833,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_003_1\",\n \"url\": \"https://images.unsplash.com/photo-1565183997392-2f6f122e5912?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_003_2\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_003_3\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 3\n }\n ],\n \"qtd_suites\": 1\n },\n {\n \"codigo\": \"MOCK_TEST-COMERCIAL_001\",\n \"titulo\": \"Comercial Teste - Mock 1\",\n \"descricao\": \"Mock de imóvel comercial para testes gerado automaticamente para testes\",\n \"categoria\": \"Comercial\",\n \"finalidade\": \"Locação\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"area_construida\": 30,\n \"valor_locacao\": 3000,\n \"area_privativa\": 114,\n \"area_total\": 100,\n \"caracteristicas\": \"Sala de estar\",\n \"categoria_imovel\": \"Padrão\",\n \"corretor.codigo\": 908392,\n \"end_logradouro\": \"Rua General Cândido Rondon\",\n \"end_bairro\": \"Nova Rússia\",\n \"end_cidade\": \"Ponta Grossa\",\n \"end_estado\": \"PR\",\n \"end_cep\": \"84070-020\",\n \"latitude\": -14.235004,\n \"longitude\": -51.92528,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_001_1\",\n \"url\": \"https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=800&q=80\",\n \"descricao\": \"Comercial Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_001_2\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n }\n ],\n \"observacoes\": \"Observações do Comercial Teste: Excelente localização\"\n },\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_004\",\n \"titulo\": \"Apartamento Teste com 3 quartos, 65m²\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 3,\n \"qtd_banheiro\": 1,\n \"area_construida\": 65,\n \"area_privativa\": 98.25,\n \"area_total\": 537.6,\n \"condominio\": 237885,\n \"corretor.codigo\": 913157,\n \"qtd_suites\": 1,\n \"end_logradouro\": \"Rua Mock 303\",\n \"end_numero\": \"520\",\n \"end_bairro\": \"Zona Sul\",\n \"end_cidade\": \"Porto Alegre\",\n \"end_estado\": \"PR\",\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_004_1\",\n \"url\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_004_2\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_004_3\",\n \"url\": \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 3\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_004_4\",\n \"url\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 4\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-COMERCIAL_002\",\n \"titulo\": \"Comercial Teste - Mock 2\",\n \"descricao\": \"Mock de imóvel comercial para testes gerado automaticamente para testes\",\n \"categoria\": \"Comercial\",\n \"finalidade\": \"Locação\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"area_construida\": 80,\n \"qtd_banheiro\": 2,\n \"valor_locacao\": 1500,\n \"andar\": 1,\n \"area_privativa\": 138,\n \"caracteristicas\": \"Lavanderia\",\n \"categoria_imovel\": \"Padrão\",\n \"end_logradouro\": \"Rua Mock 942\",\n \"end_numero\": \"822\",\n \"end_bairro\": \"Zona Sul\",\n \"end_cidade\": \"Porto Alegre\",\n \"end_estado\": \"SP\",\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_002_1\",\n \"url\": \"https://images.unsplash.com/photo-1462826303086-329426d1aef5?w=800&q=80\",\n \"descricao\": \"Comercial Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_002_2\",\n \"url\": \"https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80\",\n \"descricao\": \"Vista externa\",\n \"destaque\": false,\n \"ordem\": 2\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-COMERCIAL_003\",\n \"titulo\": \"Comercial Teste - Mock 3\",\n \"descricao\": \"Mock de imóvel comercial para testes gerado automaticamente para testes\",\n \"categoria\": \"Comercial\",\n \"finalidade\": \"Locação\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"area_construida\": 50,\n \"andar\": 1,\n \"caracteristicas\": \"Lavanderia\",\n \"codigo_origem\": \"AR0015\",\n \"condominio\": 237952,\n \"corretor.codigo\": 1227644,\n \"end_logradouro\": \"Rua Herculano de Freitas\",\n \"end_bairro\": \"Orfãs\",\n \"end_estado\": \"PR\",\n \"end_cep\": \"84015-105\",\n \"longitude\": -50.16290679999999,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_003_1\",\n \"url\": \"https://images.unsplash.com/photo-1462826303086-329426d1aef5?w=800&q=80\",\n \"descricao\": \"Comercial Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_003_2\",\n \"url\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_003_3\",\n \"url\": \"https://images.unsplash.com/photo-1462826303086-329426d1aef5?w=800&q=80\",\n \"descricao\": \"Vista externa\",\n \"destaque\": false,\n \"ordem\": 3\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_003_4\",\n \"url\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 4\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-CASA_003\",\n \"titulo\": \"Casa Teste com 4 quartos, 120m²\",\n \"descricao\": \"Mock de casa para testes gerado automaticamente para testes\",\n \"categoria\": \"Casa\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 4,\n \"qtd_banheiro\": 3,\n \"area_construida\": 120,\n \"area_terreno\": 300,\n \"area_privativa\": 100,\n \"area_total\": 1,\n \"corretor.codigo\": 908223,\n \"end_numero\": \"263\",\n \"end_bairro\": \"Zona Sul\",\n \"end_cidade\": \"Curitiba\",\n \"end_estado\": \"RJ\",\n \"latitude\": -25.0787607,\n \"longitude\": -50.16290679999999,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_003_1\",\n \"url\": \"https://images.unsplash.com/photo-1583608205776-bfd35f0d9f83?w=800&q=80\",\n \"descricao\": \"Casa Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_003_2\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_003_3\",\n \"url\": \"https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80\",\n \"descricao\": \"Vista externa\",\n \"destaque\": false,\n \"ordem\": 3\n }\n ],\n \"qtd_suites\": 1,\n \"end_logradouro\": \"Rua Mock 637\"\n },\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_005\",\n \"titulo\": \"Apartamento Teste - Mock 5\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 3,\n \"qtd_banheiro\": 2,\n \"valor_venda\": 250000,\n \"area_privativa\": 15,\n \"area_total\": 270,\n \"caracteristicas\": \"Lavanderia\",\n \"categoria_imovel\": \"Padrão\",\n \"condominio\": 237318,\n \"corretor.codigo\": 908392,\n \"qtd_suites\": 1,\n \"end_logradouro\": \"Rua Mock 139\",\n \"end_numero\": \"827\",\n \"end_bairro\": \"Jardins\",\n \"end_cidade\": \"Rio de Janeiro\",\n \"end_estado\": \"RS\",\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_005_1\",\n \"url\": \"https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_005_2\",\n \"url\": \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_005_3\",\n \"url\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&q=80\",\n \"descricao\": \"Vista externa\",\n \"destaque\": false,\n \"ordem\": 3\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_006\",\n \"titulo\": \"Apartamento Teste com 3 quartos, 85m²\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 3,\n \"area_construida\": 85,\n \"valor_venda\": 450000,\n \"caracteristicas\": \"Em condomínio fechado\",\n \"categoria_imovel\": \"Padrão\",\n \"corretor.codigo\": 908381,\n \"qtd_suites\": 1,\n \"end_logradouro\": \"Rua Mock 531\",\n \"end_numero\": \"442\",\n \"end_bairro\": \"Zona Sul\",\n \"end_cidade\": \"São Paulo\",\n \"end_estado\": \"RJ\",\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_006_1\",\n \"url\": \"https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_006_2\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_006_3\",\n \"url\": \"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 3\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_006_4\",\n \"url\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 4\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-COMERCIAL_004\",\n \"titulo\": \"Comercial Teste - Mock 4\",\n \"descricao\": \"Mock de imóvel comercial para testes gerado automaticamente para testes\",\n \"categoria\": \"Comercial\",\n \"finalidade\": \"Locação\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"area_construida\": 30,\n \"qtd_banheiro\": 2,\n \"valor_locacao\": 3000,\n \"andar\": 5,\n \"area_privativa\": 15,\n \"area_total\": 307.5,\n \"caracteristicas\": \"Churrasqueira\",\n \"categoria_imovel\": \"Padrão\",\n \"codigo_origem\": \"SO0609\",\n \"condominio\": 237652,\n \"end_logradouro\": \"Rua Mock 311\",\n \"end_numero\": \"570\",\n \"end_bairro\": \"Vila Nova\",\n \"end_cidade\": \"São Paulo\",\n \"end_estado\": \"SP\",\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_004_1\",\n \"url\": \"https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80\",\n \"descricao\": \"Comercial Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-COMERCIAL_004_2\",\n \"url\": \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-CASA_004\",\n \"titulo\": \"Casa Teste com 3 quartos, 180m²\",\n \"descricao\": \"Mock de casa para testes gerado automaticamente para testes\",\n \"categoria\": \"Casa\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 3,\n \"area_construida\": 180,\n \"area_terreno\": 200,\n \"valor_venda\": 550000,\n \"caracteristicas\": \"Porcelanato\",\n \"categoria_imovel\": \"Padrão\",\n \"codigo_origem\": \"SO0609\",\n \"qtd_suites\": 1,\n \"end_logradouro\": \"Rua Mock 93\",\n \"end_numero\": \"423\",\n \"end_bairro\": \"Vila Nova\",\n \"end_cidade\": \"Curitiba\",\n \"end_estado\": \"PR\",\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_004_1\",\n \"url\": \"https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80\",\n \"descricao\": \"Casa Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-CASA_004_2\",\n \"url\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_007\",\n \"titulo\": \"Apartamento Teste com 1 quartos, 85m²\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"qtd_quartos\": 1,\n \"qtd_banheiro\": 2,\n \"area_construida\": 85,\n \"valor_venda\": 450000,\n \"area_total\": 270,\n \"caracteristicas\": \"Cozinha\",\n \"codigo_origem\": \"GD0002\",\n \"corretor.codigo\": 908218,\n \"end_logradouro\": \"Rua Francisco Ribas\",\n \"end_numero\": 1012,\n \"end_bairro\": \"Orfãs\",\n \"end_cidade\": \"Ponta Grossa\",\n \"end_estado\": \"PR\",\n \"longitude\": -50.1623448,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_007_1\",\n \"url\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - Vista principal\",\n \"destaque\": true,\n \"ordem\": 1\n },\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_007_2\",\n \"url\": \"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800&q=80\",\n \"descricao\": \"Vista interior\",\n \"destaque\": false,\n \"ordem\": 2\n }\n ]\n },\n {\n \"codigo\": \"MOCK_TEST-APARTAMENTO_008\",\n \"titulo\": \"Apartamento Teste - Mock 8\",\n \"descricao\": \"Mock de apartamento para testes gerado automaticamente para testes\",\n \"categoria\": \"Apartamento\",\n \"finalidade\": \"Venda\",\n \"ativo\": true,\n \"publicado\": true,\n \"created_at\": \"2025-07-08T19:46:41.101Z\",\n \"updated_at\": \"2025-07-08T19:46:41.101Z\",\n \"valor_venda\": 450000,\n \"area_privativa\": 0,\n \"condominio\": 237321,\n \"corretor.codigo\": 908392,\n \"end_logradouro\": \"Rua Herculano de Freitas\",\n \"end_numero\": 667,\n \"end_bairro\": \"Orfãs\",\n \"end_cidade\": \"Ponta Grossa\",\n \"end_estado\": \"PR\",\n \"latitude\": -25.0787607,\n \"longitude\": -50.16290679999999,\n \"fotos\": [\n {\n \"id\": \"MOCK_FOTO_MOCK_TEST-APARTAMENTO_008_1\",\n \"url\": \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&q=80\",\n \"descricao\": \"Apartamento Teste - V