UNPKG

@horizon-modules/property-model-v3

Version:

Modelo de propriedades imobiliárias v3 - Sistema de atributos dinâmicos

1,315 lines (1,305 loc) 52.9 kB
// src/models/property-type.schema.ts import { z } from "zod"; var ImageMediaSchema = z.object({ full: z.string(), md: z.string().optional(), sm: z.string().optional(), cover: z.boolean().optional() }); var VideoMediaSchema = z.object({ provider: z.string().optional(), id: z.string().optional(), embed_url: z.string().optional() }); var DocumentMediaSchema = z.object({ name: z.string(), url: z.string() }); var VirtualTourMediaSchema = z.object({ embed_url: z.string() }); var MediaAssetsSchema = z.object({ images: z.array(ImageMediaSchema).optional(), videos: z.array(VideoMediaSchema).optional(), virtual_tours: z.array(VirtualTourMediaSchema).optional(), documents: z.array(DocumentMediaSchema).optional() }); var SettingsFormatSchema = z.object({ currency_unit: z.union([z.literal("BRL"), z.literal("USD")]).optional(), area_unit: z.union([z.literal("m2"), z.literal("ft2")]).optional(), distance_unit: z.union([z.literal("km"), z.literal("mi"), z.literal("meters")]).optional(), exibir_no_mapa: z.boolean().optional() }); var SEOSchema = z.object({ meta_title: z.string().optional(), meta_description: z.string().optional() }); var PropertyModelSchema = z.object({ reference: z.string(), title: z.string(), description: z.string(), media_assets: MediaAssetsSchema, attributes: z.record(z.string(), z.any()), settings: SettingsFormatSchema.optional(), seo: SEOSchema.optional(), updated_at: z.string().optional() }); var validatePropertyModel = (data) => { return PropertyModelSchema.parse(data); }; var safeValidatePropertyModel = (data) => { return PropertyModelSchema.safeParse(data); }; var AttributeSchema = z.object({ key: z.string(), value: z.any(), valueLabel: z.union([z.string(), z.array(z.string())]).optional(), composedLabel: z.string().optional(), type: z.string().optional(), unit: z.string().optional(), format: z.string().optional() }); var PropertyTypeSchema = PropertyModelSchema; var validatePropertyType = validatePropertyModel; var safeValidatePropertyType = safeValidatePropertyModel; // src/models/property-attributes-model.ts var PropertyAttributesModel = [ // Comercial { key: "operacao", label: "Opera\xE7\xE3o", type: "String[]", cat: "comercial", order: 1 }, { key: "etapa_lancamento", label: "Etapa do lan\xE7amento", type: "String[]", cat: "comercial", order: 2 }, { key: "status_comercial", label: "Status comercial", type: "String[]", cat: "comercial", order: 3 }, { key: "destaque", label: "Im\xF3vel em destaque", type: "Boolean", cat: "comercial", order: 4 }, { key: "exclusividade", label: "Exclusividade do im\xF3vel", type: "Boolean", cat: "comercial", order: 5 }, { key: "financiavel", label: "Financi\xE1vel", type: "Boolean", cat: "comercial", order: 6 }, { key: "seguro_fianca", label: "Seguro fian\xE7a", type: "Boolean", cat: "comercial", order: 7 }, { key: "aceita_permuta", label: "Aceita permuta", type: "Boolean", cat: "comercial", order: 8 }, { key: "aceita_parcelamento", label: "Aceita parcelamento", type: "Boolean", cat: "comercial", order: 9 }, // Valores { key: "valor_condominio", label: "Valor do condom\xEDnio", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 20 }, { key: "valor_total_venda", label: "Valor total de venda", type: "Number", role: "currency", unit: "BRL", cat: "valores", composedLabel: "{{valueLabel}}", order: 21 }, { key: "valor_total_mensal", label: "Valor total de loca\xE7\xE3o", type: "Number", role: "currency", unit: "BRL", cat: "valores", composedLabel: "{{valueLabel}} /m\xEAs", order: 22 }, { key: "valor_venda", label: "Valor de venda", type: "Number", role: "currency", unit: "BRL", cat: "valores", composedLabel: "{{valueLabel}}", order: 23 }, { key: "valor_locacao", label: "Valor de loca\xE7\xE3o mensal", type: "Number", role: "currency", unit: "BRL", cat: "valores", composedLabel: "{{valueLabel}} /m\xEAs", order: 24 }, { key: "valor_diaria", label: "Valor da di\xE1ria", type: "Number", role: "currency", unit: "BRL", cat: "valores", composedLabel: "{{valueLabel}} /dia", order: 25 }, { key: "valor_pacote_dias", label: "Valor do pacote de dias", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 26 }, { key: "valor_iptu", label: "Valor do IPTU", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 27 }, { key: "valor_fci", label: "Valor do Fundo de conserva\xE7\xE3o", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 28 }, { key: "valor_seguro_incendio", label: "Valor do seguro inc\xEAndio", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 29 }, { key: "valor_m2", label: "Valor do m\xB2", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 30 }, { key: "valor_taxa_limpeza", label: "Valor da taxa de limpeza", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 31 }, { key: "valor_taxa_esgoto", label: "Valor da taxa de esgoto", type: "Number", role: "currency", unit: "BRL", cat: "valores", order: 32 }, // Situações { key: "alugado", label: "Alugado", type: "Boolean", cat: "situacoes", order: 40 }, { key: "vendido", label: "Vendido", type: "Boolean", cat: "situacoes", order: 41 }, { key: "reservado", label: "Reservado", type: "Boolean", cat: "situacoes", order: 42 }, { key: "situacao_ocupacao", label: "Situa\xE7\xE3o de ocupa\xE7\xE3o", type: "String", cat: "situacoes", order: 43 }, { key: "financiado", label: "Financiado", type: "Boolean", cat: "situacoes", order: 44 }, // Legal / Documental { key: "averbado", label: "Averbado", type: "Boolean", cat: "legal", order: 60 }, { key: "escriturado", label: "Escriturado", type: "Boolean", cat: "legal", order: 61 }, { key: "incorporado", label: "Incorporado", type: "Boolean", cat: "legal", order: 62 }, // Normas / Permissões / Exigências { key: "numero_pessoas", label: "N\xFAmero de pessoas", type: "Number", role: "count", cat: "normas", order: 81 }, // Localização { key: "endereco_pais", label: "Pa\xEDs", type: "String", cat: "localizacao", order: 101 }, { key: "endereco_cep", label: "CEP", type: "String", cat: "localizacao", order: 102 }, { key: "endereco_estado", label: "Estado", type: "String", cat: "localizacao", order: 103 }, { key: "endereco_cidade", label: "Cidade", type: "String", cat: "localizacao", order: 104 }, { key: "endereco_bairro", label: "Bairro", type: "String", cat: "localizacao", order: 105 }, { key: "endereco_logradouro", label: "Logradouro", type: "String", cat: "localizacao", order: 106 }, { key: "endereco_numero", label: "N\xFAmero do im\xF3vel", type: "String", cat: "localizacao", order: 107 }, { key: "endereco_complemento", label: "Complemento", type: "String", cat: "localizacao", order: 108 }, { key: "endereco_referencia", label: "Refer\xEAncia (endere\xE7o)", type: "String", cat: "localizacao", order: 109 }, { key: "latitude", label: "Latitude", type: "Number", cat: "localizacao", order: 110 }, { key: "longitude", label: "Longitude", type: "Number", cat: "localizacao", order: 111 }, // Condomínio { key: "condominio_nome", label: "Nome do condom\xEDnio", type: "String", cat: "condominio", order: 120 }, { key: "condominio_tipo", label: "Tipo do condom\xEDnio", type: "String", cat: "condominio", order: 121 }, { key: "condominio_fechado", label: "Condom\xEDnio fechado", type: "Boolean", cat: "condominio", order: 122 }, // Localidade { key: "distancia_mar", label: "Dist\xE2ncia do mar", type: "Number", role: "distance", unit: "m", cat: "localidade", order: 141 }, { key: "imovel_no_litoral", label: "Im\xF3vel no litoral", type: "Boolean", cat: "localidade", order: 142 }, { key: "frente_mar", label: "Frente para o mar", type: "Boolean", cat: "localidade", order: 144 }, { key: "vista_mar", label: "Vista para o mar", type: "Boolean", cat: "localidade", order: 146 }, // Estrutura do imóvel { key: "tipo", label: "Tipo", type: "String", cat: "estrutura", order: 160 }, { key: "subtipo", label: "Subtipo", type: "String", cat: "estrutura", order: 161 }, { key: "area_privativa", label: "\xC1rea privativa", type: "Number", role: "area", unit: "m\xB2", cat: "estrutura", composedLabel: "{{valueLabel}} priv.", iconName: "area", order: 163 }, { key: "area_total", label: "\xC1rea Total", type: "Number", role: "area", unit: "m\xB2", cat: "estrutura", composedLabel: "{{valueLabel}} total", iconName: "area", order: 164 }, { key: "area_terreno", label: "\xC1rea do terreno", type: "Number", role: "area", unit: "m\xB2", cat: "estrutura", composedLabel: "{{valueLabel}} terreno", iconName: "area", order: 165 }, { key: "padrao_imovel", label: "Padr\xE3o do im\xF3vel", type: "String", cat: "estrutura", order: 166 }, { key: "fase_obra", label: "Fase da obra", type: "String", cat: "estrutura", order: 167 }, { key: "em_condominio", label: "Em condom\xEDnio", type: "Boolean", cat: "estrutura", order: 168 }, { key: "finalidade", label: "Finalidade (prop\xF3sito)", type: "String", cat: "estrutura", order: 169 }, { key: "estado_conservacao", label: "Estado da conserva\xE7\xE3o do im\xF3vel", type: "String", cat: "estrutura", order: 170 }, { key: "estado_imovel", label: "Status da obra + uso do im\xF3vel", type: "String", cat: "estrutura", order: 171 }, { key: "posicao_solar", label: "Posi\xE7\xE3o solar", type: "String", cat: "estrutura", order: 172 }, { key: "posicao_no_terreno", label: "Posi\xE7\xE3o do im\xF3vel no terreno", type: "String", cat: "estrutura", order: 173 }, { key: "tipo_piso", label: "Tipo de piso", type: "String", cat: "estrutura", order: 174 }, { key: "tipos_piso", label: "Tipos de piso", type: "String[]", cat: "estrutura", order: 175 }, { key: "material_imovel", label: "Material do im\xF3vel", type: "String", cat: "estrutura", order: 176 }, { key: "materiais_imovel", label: "Materiais do im\xF3vel", type: "String[]", cat: "estrutura", order: 177 }, { key: "area_construida", label: "\xC1rea constru\xEDda", type: "Number", role: "area", unit: "m\xB2", cat: "estrutura", order: 178 }, { key: "area_util", label: "\xC1rea \xFAtil", type: "Number", role: "area", unit: "m\xB2", cat: "estrutura", order: 179 }, { key: "data_entrega_obra", label: "Data de entrega da obra", type: "String", role: "date", cat: "estrutura", order: 180 }, { key: "ano_construcao", label: "Ano da constru\xE7\xE3o", type: "String", role: "year", cat: "estrutura", order: 181 }, { key: "topografia_terreno", label: "Topografia do terreno", type: "String", cat: "estrutura", order: 182 }, { key: "pintura", label: "Pintura", type: "String", cat: "estrutura", order: 183 }, { key: "pinturas_revestimentos", label: "Pinturas e revestimentos", type: "String[]", cat: "estrutura", order: 184 }, { key: "tipo_esquadria", label: "Tipo de esquadrias", type: "String", cat: "estrutura", order: 185 }, { key: "tipos_esquadrias", label: "Tipos de esquadrias", type: "String[]", cat: "estrutura", order: 186 }, { key: "tipo_forro", label: "Tipo do forro", type: "String", cat: "estrutura", order: 187 }, { key: "tipos_forros", label: "Tipos de forros", type: "String[]", cat: "estrutura", order: 188 }, { key: "tipos_coberturas", label: "Tipos de coberturas", type: "String[]", cat: "estrutura", order: 189 }, { key: "terreno_comprimento", label: "Comprimento do terreno", type: "Number", role: "distance", unit: "m", cat: "estrutura", order: 190 }, { key: "terreno_largura", label: "Largura do Terreno", type: "Number", role: "distance", unit: "m", cat: "estrutura", order: 191 }, { key: "cores", label: "Cores do im\xF3vel", type: "String[]", cat: "estrutura", order: 192 }, { key: "andares", label: "N\xFAmero de andares", type: "Number", role: "count", cat: "estrutura", order: 193 }, { key: "medida_distancia", label: "Tipo de medida de dist\xE2ncia", type: "String", cat: "estrutura", order: 194 }, { key: "medida_area", label: "Tipo de medida de \xE1rea", type: "String", cat: "estrutura", order: 195 }, // Corretor { key: "corretor_id", label: "Corretor \u2192 ID", type: "String", cat: "corretor", order: 210 }, { key: "corretor_nome", label: "Corretor \u2192 Nome", type: "String", cat: "corretor", order: 211 }, // Dependências { key: "dormitorios", label: "Dormit\xF3rios", type: "Number", role: "count", cat: "dependencias", composedLabel: "{{valueLabel}} dormit\xF3rio{{p:s}}", iconName: "bedroom", order: 230 }, { key: "banheiros", label: "Banheiros", type: "Number", role: "count", cat: "dependencias", composedLabel: "{{valueLabel}} banheiro{{p:s}}", iconName: "bathroom", order: 231 }, { key: "suites", label: "Su\xEDtes", type: "Number", role: "count", cat: "dependencias", composedLabel: "Sendo {{valueLabel}} su\xEDte{{p:s}}", iconName: "suite", order: 232 }, { key: "vagas_garagem", label: "Vagas na garagem", type: "Number", role: "count", cat: "dependencias", composedLabel: "{{valueLabel}} vaga{{p:s}}", iconName: "garage", order: 233 }, // Características em geral { key: "mobiliado", label: "Mobiliado", type: "Boolean", cat: "caracteristicas", order: 250 } // { key: "caracteristicas", label: "Características (lista)", type: "String[]", cat: "caracteristicas" order: 1, }, ]; // src/models/property-attributes-fields-list-model.ts var unitListModel = { area: [ { key: "m\xB2", aliases: ["m2"], label: "m\xB2", labelDisplay: "Metros quadrados" }, { key: "ha", aliases: ["hectare", "hectares"], label: "ha", labelDisplay: "Hectares" }, { key: "km\xB2", aliases: ["km2"], label: "km\xB2", labelDisplay: "Quil\xF4metros quadrados" } ], length: [ { key: "m", aliases: ["metro", "meter", "meters"], label: "m", labelDisplay: "Metros" }, { key: "km", aliases: ["quilometro", "kilometer", "kilometers"], label: "km", labelDisplay: "Quil\xF4metros" } ], currency: [ { key: "BRL", aliases: [], label: "R$", labelDisplay: "Real Brasileiro", locale: "pt-BR" } ] }; var formatListModel = { number: [ { key: "integer", label: "Inteiro", labelDisplay: "N\xFAmero inteiro", pattern: "0" }, { key: "decimal", label: "Decimal", labelDisplay: "N\xFAmero decimal", pattern: "0.00" }, { key: "percentage", label: "Porcentagem", labelDisplay: "Porcentagem", pattern: "0.00%" } ], currency: [ { key: "currency", label: "Moeda", labelDisplay: "Formato monet\xE1rio", pattern: "\xA4#,##0.00" }, { key: "currency_compact", label: "Moeda compacta", labelDisplay: "Formato monet\xE1rio compacto", pattern: "\xA4#,##0" } ], date: [ { key: "date", label: "Data", labelDisplay: "Data (DD/MM/AAAA)", pattern: "dd/MM/yyyy" }, { key: "datetime", label: "Data e hora", labelDisplay: "Data e hora completa", pattern: "dd/MM/yyyy HH:mm" }, { key: "year", label: "Ano", labelDisplay: "Apenas ano", pattern: "yyyy" }, { key: "month_year", label: "M\xEAs/Ano", labelDisplay: "M\xEAs e ano", pattern: "MM/yyyy" } ], text: [ { key: "uppercase", label: "Mai\xFAscula", labelDisplay: "Texto em mai\xFAscula" }, { key: "lowercase", label: "Min\xFAscula", labelDisplay: "Texto em min\xFAscula" }, { key: "capitalize", label: "Capitalizado", labelDisplay: "Primeira letra mai\xFAscula" }, { key: "title", label: "T\xEDtulo", labelDisplay: "Cada palavra capitalizada" } ] // Em desenvolvimento futuro // color: [ // { // key: "rgb", // label: "RGB", // }, // { // key: "cmyk", // label: "CMYK", // }, // ], }; // src/utils/index.ts function verifyAttrKeyInPropertyV3Model(key) { return PropertyAttributesModel.find((attr) => attr.key == key); } function mergePropertyAttributesModel(base, overrides) { const map = new Map(base.map((attr) => [attr.key, { ...attr }])); for (const overrideAttr of overrides) { const baseAttr = map.get(overrideAttr.key); if (baseAttr) { for (const field in overrideAttr) { baseAttr[field] = overrideAttr[field]; } map.set(overrideAttr.key, baseAttr); } else { map.set(overrideAttr.key, { ...overrideAttr }); } } return Array.from(map.values()); } function sortAttributes(attributes, sortKeys = []) { return [...attributes].sort((a, b) => { for (const rule of sortKeys) { const [field, direction] = rule.split(":"); const valA = a[field]; const valB = b[field]; if (valA === void 0 || valB === void 0) continue; const isAsc = direction === "asc"; const isDesc = direction === "desc"; if (!isAsc && !isDesc) { console.warn(`Dire\xE7\xE3o inv\xE1lida: "${direction}". Use "asc" ou "desc".`); continue; } if (valA < valB) return isAsc ? -1 : 1; if (valA > valB) return isAsc ? 1 : -1; } return 0; }); } var preparaAttrValueLabel = function(valor) { return Array.isArray(valor) ? `${valor.slice(0, -1).join(", ")} e ${valor[valor.length - 1]}` : valor; }; // src/services/fake-data-generator/index.ts import * as fs from "fs"; import * as path from "path"; function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function randomChoice(array) { return array[Math.floor(Math.random() * array.length)]; } function randomFromArray(array, count) { const shuffled = [...array].sort(() => 0.5 - Math.random()); return shuffled.slice(0, count); } var profilingCache = null; function loadProfilingReport() { if (profilingCache) { return profilingCache; } try { const reportPath = path.join(__dirname, "..", "..", "..", "__dev__", "data", "property-v3-profiling", "profiling-report.json"); const reportData = fs.readFileSync(reportPath, "utf8"); profilingCache = JSON.parse(reportData); return profilingCache; } catch (error) { console.warn("N\xE3o foi poss\xEDvel carregar o profiling report, usando valores padr\xE3o"); return {}; } } function generateEnhancedAttributes(tipo) { const attributes = []; const profiling = loadProfilingReport(); function getProfilingValue(key) { const profilingKey = `attributes.${key}`; const values = profiling[profilingKey]; return values && values.length > 0 ? randomChoice(values) : null; } function getProfilingNumber(key, fallbackMin, fallbackMax) { const value = getProfilingValue(key); if (typeof value === "number") { return value; } return randomInt(fallbackMin, fallbackMax); } attributes.push({ key: "tipo", value: tipo }); const operacao = getProfilingValue("operacao") || ["venda"]; attributes.push({ key: "operacao", value: Array.isArray(operacao) ? operacao : [operacao] }); const cidade = getProfilingValue("endereco_cidade") || "S\xE3o Paulo"; const estado = getProfilingValue("endereco_estado") || "SP"; const bairro = getProfilingValue("endereco_bairro") || "Centro"; const cep = getProfilingValue("endereco_cep") || "01000-000"; attributes.push( { key: "endereco_cidade", value: cidade }, { key: "endereco_estado", value: estado }, { key: "endereco_bairro", value: bairro }, { key: "endereco_cep", value: cep }, { key: "endereco_numero", value: getProfilingNumber("endereco_numero", 10, 9999) } ); const logradouro = getProfilingValue("endereco_logradouro"); if (logradouro) { attributes.push({ key: "endereco_logradouro", value: logradouro }); } const complemento = getProfilingValue("endereco_complemento"); if (complemento) { attributes.push({ key: "endereco_complemento", value: complemento }); } const valorVenda = getProfilingNumber("valor_venda", 2e5, 2e6); if (valorVenda && (Array.isArray(operacao) ? operacao.includes("venda") : operacao === "venda")) { attributes.push({ key: "valor_venda", value: valorVenda }); } const valorLocacao = getProfilingNumber("valor_locacao", 800, 8e3); if (valorLocacao && (Array.isArray(operacao) ? operacao.includes("locacao") : operacao === "locacao")) { attributes.push({ key: "valor_locacao", value: valorLocacao }); } if (tipo !== "Terreno") { const dormitorios = getProfilingNumber("dormitorios", 1, 5); const banheiros = getProfilingNumber("banheiros", 1, 4); const suites = getProfilingNumber("suites", 0, 3); const areaUtil = getProfilingNumber("area_util", 40, 400); attributes.push( { key: "dormitorios", value: dormitorios }, { key: "banheiros", value: banheiros }, { key: "suites", value: suites }, { key: "area_util", value: areaUtil } ); } const areaTotal = getProfilingNumber("area_total", 50, 500); if (areaTotal) { attributes.push({ key: "area_total", value: areaTotal }); } const vagas = getProfilingNumber("vagas", 1, 4); if (vagas) { attributes.push({ key: "vagas", value: vagas }); } const caracteristicas = profiling["attributes.caracteristicas"]; if (caracteristicas && caracteristicas.length > 0) { const selectedCaracteristicas = randomFromArray(caracteristicas, randomInt(2, 6)); attributes.push({ key: "caracteristicas", value: selectedCaracteristicas }); } const mobiliado = getProfilingValue("mobiliado"); if (mobiliado !== null) { attributes.push({ key: "mobiliado", value: mobiliado }); } const financiavel = getProfilingValue("financiavel"); if (financiavel !== null) { attributes.push({ key: "financiavel", value: financiavel }); } const aceitaPermuta = getProfilingValue("aceita_permuta"); if (aceitaPermuta !== null) { attributes.push({ key: "aceita_permuta", value: aceitaPermuta }); } const valorCondominio = getProfilingNumber("valor_condominio", 150, 1500); if (valorCondominio) { attributes.push({ key: "valor_condominio", value: valorCondominio }); } const valorIptu = getProfilingNumber("valor_iptu", 500, 5e3); if (valorIptu) { attributes.push({ key: "valor_iptu", value: valorIptu }); } const corretorId = getProfilingValue("corretor_id"); if (corretorId) { attributes.push({ key: "corretor_id", value: corretorId }); } const corretorNome = getProfilingValue("corretor_nome"); if (corretorNome) { attributes.push({ key: "corretor_nome", value: corretorNome }); } return attributes; } function generateFakeProperty(options = {}) { const { tipo = randomChoice(["Casa", "Apartamento", "Sobrado", "Terreno", "Cobertura"]), exteriorPhotos = 1, interiorPhotos = 3 } = options; const reference = `${tipo.substring(0, 3).toUpperCase()}${randomInt(1e3, 9999)}_FK`; const attributes = generateEnhancedAttributes(tipo); const operacaoAttr = attributes.find((a) => a.key === "operacao"); const operacao = operacaoAttr?.value.join(" e ") || "venda"; const cidadeAttr = attributes.find((a) => a.key === "endereco_cidade"); const bairroAttr = attributes.find((a) => a.key === "endereco_bairro"); const unsplashImages = generateUnsplashImages(tipo, exteriorPhotos + interiorPhotos); const images = unsplashImages.map((url, index) => ({ full: url, md: url.replace("w=800", "w=640").replace("h=600", "h=480"), sm: url.replace("w=800", "w=320").replace("h=600", "h=240"), cover: index === 0 })); const dormitoriosAttr = attributes.find((a) => a.key === "dormitorios"); const areaAttr = attributes.find((a) => a.key === "area_util" || a.key === "area_total"); let title = `${tipo} para ${operacao}`; if (dormitoriosAttr) { title += `, ${dormitoriosAttr.value} dormit\xF3rios`; } if (areaAttr) { title += `, ${areaAttr.value}m\xB2`; } const caracteristicasAttr = attributes.find((a) => a.key === "caracteristicas"); const caracteristicas = caracteristicasAttr?.value || []; const featuresText = Array.isArray(caracteristicas) && caracteristicas.length > 0 ? caracteristicas.slice(0, 5).join(", ") : ""; const description = `Excelente ${tipo.toLowerCase()} em ${bairroAttr?.value}, ${cidadeAttr?.value}. Im\xF3vel com \xF3tima localiza\xE7\xE3o e acabamento de qualidade. ` + (featuresText ? `Caracter\xEDsticas: ${featuresText}. ` : "") + `Agende sua visita!`; const attributesObj = {}; attributes.forEach((attr) => { attributesObj[attr.key] = attr.value; }); return { reference, title: title || `${tipo} para ${operacao}`, description: description || `Im\xF3vel dispon\xEDvel para ${operacao}`, attributes: attributesObj, media_assets: { images, videos: [], virtual_tours: [], documents: [] }, settings: { currency_unit: "BRL", area_unit: "m2", exibir_no_mapa: true }, updated_at: (/* @__PURE__ */ new Date()).toISOString() }; } function generateFakeProperties(count = 20, options = {}) { const { tipos = ["Casa", "Apartamento", "Sobrado", "Terreno", "Cobertura"] } = options; const properties = []; for (let i = 0; i < count; i++) { const tipo = randomChoice(tipos); properties.push(generateFakeProperty({ ...options, tipo })); } return properties; } function generateFakePropertyByType(tipo) { const tiposValidos = ["Casa", "Apartamento", "Sobrado", "Terreno", "Cobertura"]; if (!tiposValidos.includes(tipo)) return null; return generateFakeProperty({ tipo }); } function getAvailablePropertyTypes() { return ["Casa", "Apartamento", "Sobrado", "Terreno", "Cobertura"]; } var UNSPLASH_IMAGES = { apartamento: [ "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&h=600&q=80", "https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&h=600&q=80", "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800&h=600&q=80", "https://images.unsplash.com/photo-1565183997392-2f6f122e5912?w=800&h=600&q=80" ], casa: [ "https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&h=600&q=80", "https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=800&h=600&q=80", "https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=600&q=80", "https://images.unsplash.com/photo-1583608205776-bfd35f0d9f83?w=800&h=600&q=80" ], comercial: [ "https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=600&q=80", "https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=600&q=80", "https://images.unsplash.com/photo-1462826303086-329426d1aef5?w=800&h=600&q=80", "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=800&h=600&q=80" ], terreno: [ "https://images.unsplash.com/photo-1500382017468-9049fed747ef?w=800&h=600&q=80", "https://images.unsplash.com/photo-1416331108676-a22ccb276e35?w=800&h=600&q=80", "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=800&h=600&q=80", "https://images.unsplash.com/photo-1448630360428-65456885c650?w=800&h=600&q=80" ], sobrado: [ "https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&h=600&q=80", "https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=800&h=600&q=80", "https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=600&q=80" ], cobertura: [ "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&h=600&q=80", "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800&h=600&q=80", "https://images.unsplash.com/photo-1565183997392-2f6f122e5912?w=800&h=600&q=80" ] }; function generateUnsplashImages(tipo, count) { const tipoKey = tipo.toLowerCase(); const availableImages = UNSPLASH_IMAGES[tipoKey] || UNSPLASH_IMAGES.casa; const images = []; for (let i = 0; i < count; i++) { const randomImage = randomChoice(availableImages); images.push(randomImage); } return images; } // src/services/profiler/index.ts import * as fs2 from "fs"; import * as path2 from "path"; var ProfilerService = class { constructor(config) { this.fieldData = /* @__PURE__ */ new Map(); this.fieldExamples = /* @__PURE__ */ new Map(); this.config = { inputDir: config.inputDir || "", inputData: config.inputData || [], outputDir: config.outputDir || process.cwd(), outputFileName: config.outputFileName || "profiling-report.json", fieldConfigs: config.fieldConfigs || {}, defaultMaxExamples: config.defaultMaxExamples || 10, verbose: config.verbose ?? true }; } /** * Executa o profiling dos dados */ async profile() { if (this.config.verbose) { console.log("\u{1F4CA} Iniciando profiling dos dados PropertyModel..."); } const data = await this.loadData(); this.processData(data); const result = this.generateResult(); if (this.config.outputDir) { await this.saveResult(result); } return result; } /** * Carrega dados de arquivos ou usa dados fornecidos */ async loadData() { if (this.config.inputData.length > 0) { if (this.config.verbose) { console.log(`\u{1F4C2} Usando ${this.config.inputData.length} PropertyModel fornecidos`); } return this.config.inputData; } if (!this.config.inputDir || !fs2.existsSync(this.config.inputDir)) { throw new Error(`Diret\xF3rio de entrada n\xE3o encontrado: ${this.config.inputDir}`); } const allData = []; const files = fs2.readdirSync(this.config.inputDir).filter((file) => file.endsWith(".json")); if (this.config.verbose) { console.log(`\u{1F4C2} Carregando dados de ${files.length} arquivos...`); } for (const file of files) { const filePath = path2.join(this.config.inputDir, file); const fileContent = fs2.readFileSync(filePath, "utf8"); const jsonData = JSON.parse(fileContent); let properties = []; if (Array.isArray(jsonData)) { properties = jsonData; } else if (jsonData.data && Array.isArray(jsonData.data)) { properties = jsonData.data; } else if (typeof jsonData === "object") { const allArrays = Object.values(jsonData).filter(Array.isArray); properties = allArrays.flat(); } allData.push(...properties); if (this.config.verbose) { console.log(` \u2705 ${file}: ${properties.length} PropertyModel`); } } if (this.config.verbose) { console.log(`\u{1F4CA} Total carregado: ${allData.length} PropertyModel`); } return allData; } /** * Processa os dados coletados e extrai informações estatísticas */ processData(data) { if (this.config.verbose) { console.log("\u{1F50D} Processando dados..."); } for (const property of data) { this.processObject(property, ""); } if (this.config.verbose) { console.log(`\u{1F50D} ${this.fieldData.size} campos \xFAnicos encontrados`); } } /** * Processa recursivamente um objeto para extrair todos os campos */ processObject(obj, prefix) { if (obj === null || obj === void 0) return; if (Array.isArray(obj)) { obj.forEach((item) => { if (item !== null && item !== void 0) { this.addFieldValue(prefix, item); } }); return; } if (typeof obj === "object") { Object.entries(obj).forEach(([key, value]) => { const fullKey = prefix ? `${prefix}.${key}` : key; if (Array.isArray(value)) { this.processObject(value, fullKey); } else { this.addFieldValue(fullKey, value); if (value && typeof value === "object") { this.processObject(value, fullKey); } } }); } else { this.addFieldValue(prefix, obj); } } /** * Adiciona um valor para um campo específico */ addFieldValue(fieldName, value) { if (value === null || value === void 0) return; const serializedValue = typeof value === "object" ? JSON.stringify(value) : value; if (!this.fieldData.has(fieldName)) { this.fieldData.set(fieldName, /* @__PURE__ */ new Set()); this.fieldExamples.set(fieldName, []); } const fieldSet = this.fieldData.get(fieldName); const examples = this.fieldExamples.get(fieldName); if (!fieldSet.has(serializedValue)) { fieldSet.add(serializedValue); const fieldConfig = this.config.fieldConfigs[fieldName]; const maxExamples = fieldConfig?.maxExamples || this.config.defaultMaxExamples; if (examples.length < maxExamples) { examples.push(value); } } } /** * Gera o resultado final do profiling */ generateResult() { const result = {}; const sortedFields = Array.from(this.fieldData.keys()).sort(); for (const fieldName of sortedFields) { const fieldConfig = this.config.fieldConfigs[fieldName]; const maxExamples = fieldConfig?.maxExamples !== void 0 ? fieldConfig.maxExamples : this.config.defaultMaxExamples; if (maxExamples === 0) { continue; } const examples = this.fieldExamples.get(fieldName) || []; const sortedExamples = examples.slice().sort((a, b) => { if (typeof a === "string" && typeof b === "string") { return a.localeCompare(b); } if (typeof a === "number" && typeof b === "number") { return a - b; } return String(a).localeCompare(String(b)); }); result[fieldName] = sortedExamples; } if (this.config.verbose) { console.log(`\u{1F4C8} Profiling conclu\xEDdo: ${Object.keys(result).length} campos analisados`); } return result; } /** * Salva o resultado em arquivo JSON */ async saveResult(result) { const outputPath = path2.join(this.config.outputDir, this.config.outputFileName); const outputDir = path2.dirname(outputPath); if (!fs2.existsSync(outputDir)) { fs2.mkdirSync(outputDir, { recursive: true }); } fs2.writeFileSync(outputPath, JSON.stringify(result, null, 2)); if (this.config.verbose) { console.log(`\u{1F4C1} Relat\xF3rio salvo em: ${outputPath}`); } } }; async function profileProperties(properties, options = {}) { const profiler = new ProfilerService({ inputData: properties, ...options }); return profiler.profile(); } async function profileFromFiles(inputDir, options = {}) { const profiler = new ProfilerService({ inputDir, ...options }); return profiler.profile(); } // src/services/api-client/index.ts import * as fs3 from "fs"; import * as path3 from "path"; var ApiClientService = class { constructor(config) { this.config = { endpoint: config.endpoint, headers: config.headers || { "Content-Type": "application/json" }, timeout: config.timeout || 3e4, batchSize: config.batchSize || 10 }; } /** * Envia uma propriedade para a API */ async sendProperty(property) { try { const response = await this.makeHttpRequest(property); return response.ok; } catch (error) { return false; } } /** * Envia múltiplas propriedades */ async sendProperties(properties) { for (const property of properties) { await this.sendProperty(property); } } /** * Carrega propriedades de uma pasta com arquivos JSON */ async sendPropertiesFromDir(inputDir) { const properties = await this.loadPropertiesFromDir(inputDir); await this.sendInBatches(properties); } /** * Carrega propriedades de arquivos JSON */ async loadPropertiesFromDir(inputDir) { if (!fs3.existsSync(inputDir)) { throw new Error(`Diret\xF3rio n\xE3o encontrado: ${inputDir}`); } const allData = []; const files = fs3.readdirSync(inputDir).filter((file) => file.endsWith(".json")); for (const file of files) { const filePath = path3.join(inputDir, file); const fileContent = fs3.readFileSync(filePath, "utf8"); const jsonData = JSON.parse(fileContent); let properties = []; if (Array.isArray(jsonData)) { properties = jsonData; } else if (jsonData.data && Array.isArray(jsonData.data)) { properties = jsonData.data; } else if (typeof jsonData === "object") { const allArrays = Object.values(jsonData).filter(Array.isArray); properties = allArrays.flat(); } allData.push(...properties); } return allData; } /** * Envia propriedades em lotes */ async sendInBatches(properties) { const batches = this.createBatches(properties, this.config.batchSize); for (const batch of batches) { await Promise.all( batch.map((property) => this.sendProperty(property)) ); } } /** * Divide array em lotes menores */ createBatches(array, batchSize) { const batches = []; for (let i = 0; i < array.length; i += batchSize) { batches.push(array.slice(i, i + batchSize)); } return batches; } /** * Faz requisição HTTP para a API */ async makeHttpRequest(data) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.config.timeout); try { const response = await fetch(this.config.endpoint, { method: "POST", headers: this.config.headers, body: JSON.stringify(data), signal: controller.signal }); clearTimeout(timeout); return response; } catch (error) { clearTimeout(timeout); throw error; } } }; // src/services/property-customizer/index.ts import { clone, union } from "ramda"; function upsertAttribute(attributes, key, value) { attributes[key] = value; } function addToArray(property, action) { const currentValue = property.attributes[action.key]; const newValues = Array.isArray(action.value) ? action.value : [action.value]; if (!currentValue) { property.attributes[action.key] = newValues; } else if (Array.isArray(currentValue)) { property.attributes[action.key] = union(currentValue, newValues); } else { property.attributes[action.key] = union([currentValue], newValues); } } function removeAttr(attributes, key) { delete attributes[key]; } function removeFromArray(property, action) { const currentValue = property.attributes[action.key]; if (!currentValue || !Array.isArray(currentValue)) return; property.attributes[action.key] = currentValue.filter((item) => item !== action.value); if (property.attributes[action.key].length === 0) { delete property.attributes[action.key]; } } var actionRunner = { upsertAttr: (property, action) => { upsertAttribute(property.attributes, action.key, action.value); }, removeAttr: (property, action) => { removeAttr(property.attributes, action.key); }, addToArray: (property, action) => { addToArray(property, action); }, removeFromArray: (property, action) => { removeFromArray(property, action); } }; function processDescriptionTags(property) { if (!property.description) return; let description = property.description; const tagRegex = /\[\[([^\]]+)\]\](.*?)\[\[\/\1\]\]/gs; const matches = [...description.matchAll(tagRegex)]; for (const match of matches) { const [, tagName, content] = match; const cleanTagName = tagName.replace(/-/g, "_"); const cleanContent = content.trim(); if (tagName === "custom-attributes") { try { const jsonData = JSON.parse(cleanContent); if (jsonData && typeof jsonData === "object") { Object.assign(property.attributes, jsonData); } } catch (error) { console.warn(`\u26A0\uFE0F Erro ao processar JSON em custom-attributes: ${error instanceof Error ? error.message : String(error)}`); property.attributes[cleanTagName] = cleanContent; } } else { property.attributes[cleanTagName] = cleanContent; } } description = description.replace(tagRegex, "").trim(); description = description.replace(/ +/g, " "); description = description.replace(/\n{3,}/g, "\n\n"); property.description = description; } function applyReferenceRules(reference, rules, property) { const actions = rules?.[reference]; if (!actions) return; actions.forEach((action) => { const fn = actionRunner[action.fn]; if (fn) fn(property, action); }); } function matchCondition(attrValue, condition) { if (!condition) return true; const attrAsArray = Array.isArray(attrValue) ? attrValue : typeof attrValue === "string" ? [attrValue] : []; const attrAsString = typeof attrValue === "string" ? attrValue : String(attrValue); for (const operator in condition) { const expected = condition[operator]; switch (operator) { // Igualdade case "eq": if (attrValue !== expected) return false; break; case "not_eq": if (attrValue === expected) return false; break; // Arrays case "has": { const attrAsArray2 = Array.isArray(attrValue) ? attrValue : [attrValue]; const expectedArray = Array.isArray(expected) ? expected : [expected]; if (!expectedArray.every((val) => attrAsArray2.includes(val))) return false; break; } case "has_any": { const attrAsArray2 = Array.isArray(attrValue) ? attrValue : [attrValue]; const expectedArray = Array.isArray(expected) ? expected : [expected]; if (!expectedArray.some((val) => attrAsArray2.includes(val))) return false; break; } case "not_has": if (Array.isArray(expected)) { if (expected.some((val) => attrAsArray.includes(val))) return false; } else { if (attrAsArray.includes(expected)) return false; } break; // Numéricos case "gt": if (!(Number(attrValue) > Number(expected))) return false; break; case "gte": if (!(Number(attrValue) >= Number(expected))) return false; break; case "lt": if (!(Number(attrValue) < Number(expected))) return false; break; case "lte": if (!(Number(attrValue) <= Number(expected))) return false; break; // Texto case "contains": if (!attrAsString.toLowerCase().includes(String(expected).toLowerCase())) return false; break; case "starts_with": if (!attrAsString.toLowerCase().startsWith(String(expected).toLowerCase())) return false; break; case "ends_with": if (!attrAsString.toLowerCase().endsWith(String(expected).toLowerCase())) return false; break; // Booleanos case "is_true": if (attrValue !== true && attrValue !== "true" && attrValue !== 1) return false; break; case "is_false": if (attrValue !== false && attrValue !== "false" && attrValue !== 0) return false; break; // Existência case "exists": if (attrValue === null || attrValue === void 0 || attrValue === "") return false; break; case "not_exists": if (attrValue !== null && attrValue !== void 0 && attrValue !== "") return false; break; default: return false; } } return true; } function resolvePlaceholders(action, sourceAttr) { const resolved = { ...action }; if (resolved.value === "{{value}}") { resolved.value = sourceAttr.value; } else if (typeof resolved.value === "string" && resolved.value.includes("{{value}}")) { resolved.value = resolved.value.replace( "{{value}}", String(sourceAttr.value) ); } return resolved; } function applyAttributesRules(attributes, rules, property) { for (const rule of rules) { const attrValue = attributes[rule.key]; if (attrValue === void 0) continue; if (matchCondition(attrValue, rule.condition)) { for (const action of rule.rules) { const fn = actionRunner[action.fn]; if (fn) { const resolvedAction = resolvePlaceholders(action, { key: rule.key, value: attrValue }); fn(property, resolvedAction); } } } } } function PropertyCustomizer(property, mapRules) { const newProperty = clone(property); const { reference } = newProperty; processDescriptionTags(newProperty); const { referenceRules, attributesRules } = mapRules; if (referenceRules && reference) { applyReferenceRules(reference, referenceRules, newProperty); } if (attributesRules) { applyAttributesRules(newProperty.attributes, attributesRules, newProperty); } return newProperty; } // src/data/json-schema/property-model.json var property_model_default = { $ref: "#/definitions/PropertyModel", definitions: { PropertyModel: { type: "object", properties: { reference: { type: "string" }, title: { type: "string" }, description: { type: "string" }, media_assets: { type: "object", properties: { images: { type: "array", items: { type: "object", properties: { full: { type: "string" }, md: { type: "string" }, sm: { type: "string" }, cover: { type: "boolean" } }, required: [ "full" ], additionalProperties: false } }, videos: { type: "array", items: { type: "object", properties: { provider: { type: "string" }, id: { type: "string" }, embed_url: { type: "string" } }, additionalProperties: false } }, virtual_tours: { type: "array", items: { type: "object", properties: { embed_url: { type: "string" } }, required: [ "embed_url" ], additionalProperties: false } }, documents: { type: "array", items: { type: "object", properties: { name: { type: "string" }, url: { type: "string" } }, required: [ "name", "url" ], additionalProperties: false } } }, additionalProperties: false }, attributes: { type: "object", additionalProperties: {} }, settings: { type: "object", properties: { currency_unit: { type: "string", enum: [ "BRL", "USD" ] }, area_unit: { type: "string", enum: [ "m2", "ft2" ] }, distance_unit: { type: "string", enum: [ "km", "mi", "meters" ] }, exibir_no_mapa: { type: "boolean" } }, additionalProperties: false }, updated_at: { type: "string" } }, required: [ "reference", "title", "description", "media_assets", "attributes" ], additionalProperties: false } }, $schema: "http://json-schema.org/draft-07/schema#" }; // src/models/property-type.schema.json var property_type_schema_default = { $ref: "#/definitions/PropertyModel", definitions: { PropertyModel: { type: "object", properties: { reference: { type: "string" }, title: { type: "string" }, description: { type: "string" }, media_assets: { type: "object", properties: { images: { type: "array", items: { type: "object", properties: { full: { type: "string" }, md: { type: "string" }, sm: { type: "string" }, cover: { type: "boolean" } }, required: [ "full" ], additionalProperties: false } }, videos: { type: "array", items: { type: "object", properties: { provider: { type: "string" }, id: { type: "string" }, embed_url: { type: "string" } }, additionalProperties: false }