Skip to main content
Glama

TreePod Financial MCP Agent

by janetsep
calculator.js56.3 kB
#!/usr/bin/env node import { validator } from '../utils/validator.js'; import { dataLoader } from '../data/dataLoader.js'; /** * 🧮 Módulo de Cálculos de Negocio TreePod Financial MCP * Implementa la Guía de Trabajo Fundamental: Lógica basada en datos reales */ export class BusinessCalculator { /** * Calcula tarifas basado en datos reales de configuración */ async calculateTariff(checkinDate, checkoutDate, guests, channel = 'directo') { // Validar parámetros de entrada const inputValidation = validator.validateUserInput({ checkin_date: checkinDate, checkout_date: checkoutDate, guests: guests, channel: channel }, { checkin_date: { required: true, type: 'string' }, checkout_date: { required: true, type: 'string' }, guests: { required: true, type: 'number', min: 1, max: 4 }, channel: { required: false, type: 'string', enum: ['directo', 'airbnb', 'booking', 'whatsapp'] } }); if (!inputValidation.valid) { return validator.generateInsufficientDataResponse( 'parámetros de tarifa', `Errores: ${inputValidation.errors.join(', ')}` ); } // Validar fechas const dateValidation = validator.validateDateRange(checkinDate, checkoutDate); if (!dateValidation.valid) { return validator.generateInsufficientDataResponse( 'fechas de reserva', dateValidation.error ); } // Cargar configuración de tarifas desde datos reales const financialData = await dataLoader.loadFinancialData(); if (!financialData || !financialData.configuracion_negocio) { return validator.generateInsufficientDataResponse( 'configuración de tarifas', 'No se pudo acceder a la configuración de precios del sistema' ); } const config = financialData.configuracion_negocio; // Verificar que existen las tarifas por temporada if (!config.tarifas_temporada) { return validator.generateInsufficientDataResponse( 'tarifas por temporada', 'La configuración no incluye tarifas por temporada' ); } try { const season = this.determineSeason(dateValidation.checkin, config); const seasonData = config.tarifas_temporada[season]; if (!seasonData) { return validator.generateInsufficientDataResponse( `tarifas para temporada ${season}`, 'No se encontraron tarifas para la temporada solicitada' ); } // Calcular tarifa base según número de huéspedes const baseTariff = this.calculateBaseTariff(guests, seasonData); const nights = dateValidation.nights; const subtotal = baseTariff * nights; // Calcular comisión según canal const commissionRate = this.getCommissionRate(channel, config); const commission = Math.round(subtotal * commissionRate); const total = subtotal + commission; validator.log('info', `Tarifa calculada: ${guests} huéspedes, ${nights} noches, temporada ${season}, canal ${channel}`); return { content: [{ type: 'text', text: this.formatTariffResponse({ checkinDate, checkoutDate, guests, channel, season, nights, baseTariff, subtotal, commissionRate: commissionRate * 100, commission, total }) }] }; } catch (error) { validator.log('error', `Error calculando tarifa: ${error.message}`); return validator.generateInsufficientDataResponse( 'cálculo de tarifa', 'Error interno en el cálculo. Contacta al administrador.' ); } } /** * Determina temporada basado en configuración real */ determineSeason(date, config) { if (!config.temporadas) { validator.log('warning', 'Configuración de temporadas no encontrada, usando lógica por defecto'); // Lógica básica si no hay configuración const month = date.getMonth() + 1; if (month >= 12 || month <= 2) return 'alta'; if (month >= 6 && month <= 8) return 'alta'; return 'media'; } // Usar configuración real de temporadas const month = date.getMonth() + 1; const day = date.getDate(); for (const [seasonName, periods] of Object.entries(config.temporadas)) { for (const period of periods) { if (this.isDateInPeriod(month, day, period)) { return seasonName; } } } return 'media'; // temporada por defecto } /** * Verifica si una fecha está en un período específico */ isDateInPeriod(month, day, period) { const startMonth = period.inicio.mes; const startDay = period.inicio.dia; const endMonth = period.fin.mes; const endDay = period.fin.dia; if (startMonth === endMonth) { return month === startMonth && day >= startDay && day <= endDay; } if (startMonth < endMonth) { return (month === startMonth && day >= startDay) || (month > startMonth && month < endMonth) || (month === endMonth && day <= endDay); } // Período que cruza año (ej: diciembre a febrero) return (month === startMonth && day >= startDay) || (month > startMonth || month < endMonth) || (month === endMonth && day <= endDay); } /** * Calcula tarifa base según huéspedes y temporada */ calculateBaseTariff(guests, seasonData) { if (seasonData.por_huesped && seasonData.tarifa_base_por_persona) { return seasonData.tarifa_base_por_persona * guests; } if (seasonData.tarifas_por_ocupacion) { const guestKey = `${guests}_personas`; return seasonData.tarifas_por_ocupacion[guestKey] || seasonData.tarifa_base || 80000; } return seasonData.tarifa_base || 80000; } /** * Obtiene tasa de comisión desde configuración real */ getCommissionRate(channel, config) { if (!config.comisiones_canal) { validator.log('warning', 'Configuración de comisiones no encontrada, usando valores por defecto'); const defaultRates = { 'directo': 0, 'airbnb': 0.15, 'booking': 0.18, 'whatsapp': 0.05 }; return defaultRates[channel] || 0; } return config.comisiones_canal[channel] || 0; } /** * Formatea respuesta de tarifa */ formatTariffResponse(data) { return `💰 **CÁLCULO DE TARIFA TREEPOD GLAMPING**\n\n` + `📅 **Reserva:**\n` + `• Check-in: ${data.checkinDate}\n` + `• Check-out: ${data.checkoutDate}\n` + `• Noches: ${data.nights}\n` + `• Huéspedes: ${data.guests}\n` + `• Canal: ${data.channel}\n` + `• Temporada: ${data.season}\n\n` + `💵 **Desglose:**\n` + `• Tarifa por noche: ${this.formatCurrency(data.baseTariff)}\n` + `• Subtotal (${data.nights} noches): ${this.formatCurrency(data.subtotal)}\n` + `• Comisión ${data.channel} (${data.commissionRate}%): ${this.formatCurrency(data.commission)}\n` + `• **TOTAL: ${this.formatCurrency(data.total)}**\n\n` + `*Cálculo basado en configuración real del sistema TreePod*`; } /** * Formatea moneda chilena */ formatCurrency(amount) { return new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 }).format(amount); } /** * Analiza métricas financieras desde datos reales */ async analyzeFinancialMetrics(period = 'current') { const financialData = await dataLoader.loadFinancialData(); if (!financialData) { return validator.generateInsufficientDataResponse( 'datos financieros', 'No se pudo acceder a los datos financieros del sistema' ); } // Verificar campos esenciales const requiredFields = ['ingresos_total', 'gastos_total']; const missingFields = requiredFields.filter(field => !(field in financialData)); if (missingFields.length > 0) { return validator.generateInsufficientDataResponse( 'métricas financieras', `Faltan datos esenciales: ${missingFields.join(', ')}` ); } try { const analysis = this.calculateFinancialAnalysis(financialData, period); validator.log('info', `Análisis financiero completado para período: ${period}`); return { content: [{ type: 'text', text: this.formatFinancialAnalysis(analysis) }] }; } catch (error) { validator.log('error', `Error en análisis financiero: ${error.message}`); return validator.generateInsufficientDataResponse( 'análisis financiero', 'Error interno en el análisis. Contacta al administrador.' ); } } /** * Calcula análisis financiero */ calculateFinancialAnalysis(data, period) { const ingresos = data.ingresos_total || 0; const gastos = data.gastos_total || 0; const utilidad = ingresos - gastos; const margen = ingresos > 0 ? ((utilidad / ingresos) * 100).toFixed(1) : 0; const ocupacion = data.ocupacion_promedio || 0; const reservas = data.reservas_totales || 0; return { periodo: period, ingresos, gastos, utilidad, margen, ocupacion, reservas, ingresoPromedioPorReserva: reservas > 0 ? Math.round(ingresos / reservas) : 0, metaIngresos: ingresos >= 6000000, metaUtilidad: utilidad >= 2000000, metaOcupacion: ocupacion >= 70 }; } /** * Formatea análisis financiero */ formatFinancialAnalysis(analysis) { const estado = this.determineBusinessStatus(analysis); return `📊 **ANÁLISIS FINANCIERO TREEPOD GLAMPING**\n\n` + `📅 **Período:** ${analysis.periodo}\n` + `💰 **Estado General:** ${estado}\n\n` + `**💵 FINANZAS:**\n` + `• Ingresos: ${this.formatCurrency(analysis.ingresos)} ${analysis.metaIngresos ? '✅' : '⚠️'}\n` + `• Gastos: ${this.formatCurrency(analysis.gastos)}\n` + `• Utilidad: ${this.formatCurrency(analysis.utilidad)} ${analysis.metaUtilidad ? '✅' : '⚠️'}\n` + `• Margen: ${analysis.margen}% ${analysis.margen > 30 ? '✅' : '⚠️'}\n\n` + `**🏠 OPERACIONES:**\n` + `• Ocupación: ${analysis.ocupacion}% ${analysis.metaOcupacion ? '✅' : '⚠️'}\n` + `• Reservas totales: ${analysis.reservas}\n` + `• Ingreso promedio/reserva: ${this.formatCurrency(analysis.ingresoPromedioPorReserva)}\n\n` + `*Análisis basado en datos reales del sistema TreePod*`; } /** * Determina estado del negocio basado en métricas */ determineBusinessStatus(analysis) { if (analysis.metaIngresos && analysis.metaUtilidad && analysis.metaOcupacion) { return '🟢 Excelente'; } if (analysis.utilidad > 1500000 && analysis.ocupacion > 50) { return '🟡 Bien'; } return '🔴 Atención requerida'; } /** * Analiza ocupación de domos basado en datos reales */ async analyzeOccupancy(businessData, domosStatus, dateRange = 'today') { if (!domosStatus || !Array.isArray(domosStatus)) { return validator.generateInsufficientDataResponse( 'estado de domos', 'Los datos de estado de domos no tienen el formato esperado' ); } try { const periodText = this.getPeriodText(dateRange); const occupancyRate = businessData?.occupancy || 0; // Calcular métricas de ocupación const available = domosStatus.filter(d => d.status === 'available').length; const occupied = domosStatus.filter(d => d.status === 'occupied').length; const maintenance = domosStatus.filter(d => d.status === 'maintenance').length; const cleaning = domosStatus.filter(d => d.status === 'cleaning').length; const total = domosStatus.length; validator.log('info', `Análisis de ocupación: ${occupied}/${total} ocupados, ${available}/${total} disponibles`); return { content: [{ type: 'text', text: this.formatOccupancyAnalysis({ periodText, occupancyRate, domosStatus, metrics: { available, occupied, maintenance, cleaning, total } }) }] }; } catch (error) { validator.log('error', `Error en análisis de ocupación: ${error.message}`); return validator.generateInsufficientDataResponse( 'análisis de ocupación', 'Error interno en el análisis. Contacta al administrador.' ); } } /** * Obtiene texto descriptivo del período */ getPeriodText(dateRange) { const periods = { 'today': 'hoy', 'week': 'esta semana', 'month': 'este mes', 'current': 'actual' }; return periods[dateRange] || dateRange; } /** * Formatea análisis de ocupación */ formatOccupancyAnalysis(data) { const { periodText, occupancyRate, domosStatus, metrics } = data; return `🏠 **ESTADO DE OCUPACIÓN TREEPOD**\n\n` + `📅 **Período:** ${periodText}\n` + `📊 **Ocupación general:** ${occupancyRate}%\n\n` + `**🏕️ ESTADO POR DOMO:**\n` + `${domosStatus.map(domo => `• ${domo.name}: ${this.getDomoStatusEmoji(domo.status)} ${domo.statusText}`).join('\n')}\n\n` + `**📈 MÉTRICAS:**\n` + `• Domos disponibles: ${metrics.available}/${metrics.total}\n` + `• Domos ocupados: ${metrics.occupied}/${metrics.total}\n` + `• En mantención: ${metrics.maintenance}/${metrics.total}\n` + `${metrics.cleaning > 0 ? `• En limpieza: ${metrics.cleaning}/${metrics.total}\n` : ''}\n` + `**🎯 RECOMENDACIONES:**\n` + `${this.getOccupancyRecommendations(metrics, occupancyRate)}\n\n` + `*Análisis basado en datos reales del sistema TreePod*`; } /** * Obtiene emoji para estado de domo */ getDomoStatusEmoji(status) { const emojis = { 'occupied': '🔴', 'available': '🟢', 'maintenance': '🟡', 'cleaning': '🔵', 'reserved': '🟠' }; return emojis[status] || '⚪'; } /** * Genera recomendaciones de ocupación */ getOccupancyRecommendations(metrics, occupancyRate) { const recommendations = []; if (metrics.available === 0) { recommendations.push('• ¡Excelente! Ocupación completa'); } else if (metrics.available === 1) { recommendations.push('• Muy buena ocupación, solo 1 domo disponible'); } else if (metrics.available > metrics.total / 2) { recommendations.push('• Oportunidad de marketing para aumentar reservas'); } if (metrics.maintenance > 0) { recommendations.push('• Coordinar mantención para minimizar impacto'); } if (occupancyRate < 50) { recommendations.push('• Considerar promociones o descuentos'); } else if (occupancyRate > 80) { recommendations.push('• Excelente momento para optimizar precios'); } return recommendations.length > 0 ? recommendations.join('\n') : '• Mantener estrategia actual'; } /** * Analiza competencia basado en datos reales */ async analyzeCompetition(competitionData, analysisType = 'all') { if (!competitionData) { return validator.generateInsufficientDataResponse( 'datos de competencia', 'No se encontraron datos de competencia válidos' ); } try { validator.log('info', `Analizando competencia tipo: ${analysisType}`); // Extraer datos relevantes según el tipo de análisis const analysis = this.processCompetitionData(competitionData, analysisType); return { content: [{ type: 'text', text: this.formatCompetitionAnalysis(analysis, analysisType) }] }; } catch (error) { validator.log('error', `Error en análisis de competencia: ${error.message}`); return validator.generateInsufficientDataResponse( 'análisis de competencia', 'Error interno en el análisis. Contacta al administrador.' ); } } /** * Procesa datos de competencia según tipo de análisis */ processCompetitionData(data, analysisType) { const analysis = { totalCompetitors: 0, priceRange: { min: null, max: null, average: null }, services: [], positioning: 'medio', recommendations: [] }; // Procesar diferentes estructuras de datos de competencia let competitors = []; if (data.competitors) { competitors = Array.isArray(data.competitors) ? data.competitors : Object.values(data.competitors); } else if (data.establishments) { competitors = Array.isArray(data.establishments) ? data.establishments : Object.values(data.establishments); } else if (Array.isArray(data)) { competitors = data; } analysis.totalCompetitors = competitors.length; if (competitors.length > 0) { // Analizar precios si están disponibles const prices = competitors .map(c => c.price || c.tarifa || c.precio) .filter(p => p && typeof p === 'number') .sort((a, b) => a - b); if (prices.length > 0) { analysis.priceRange.min = prices[0]; analysis.priceRange.max = prices[prices.length - 1]; analysis.priceRange.average = Math.round(prices.reduce((a, b) => a + b, 0) / prices.length); } // Analizar servicios comunes const allServices = competitors .flatMap(c => c.services || c.servicios || []) .filter(s => s); const serviceCount = {}; allServices.forEach(service => { serviceCount[service] = (serviceCount[service] || 0) + 1; }); analysis.services = Object.entries(serviceCount) .sort(([,a], [,b]) => b - a) .slice(0, 5) .map(([service, count]) => ({ service, count })); // Determinar posicionamiento if (analysis.priceRange.average) { const treepodPrice = 80000; // Precio base estimado if (treepodPrice < analysis.priceRange.average * 0.8) { analysis.positioning = 'económico'; } else if (treepodPrice > analysis.priceRange.average * 1.2) { analysis.positioning = 'premium'; } } // Generar recomendaciones analysis.recommendations = this.generateCompetitionRecommendations(analysis); } return analysis; } /** * Formatea análisis de competencia */ formatCompetitionAnalysis(analysis, analysisType) { let content = `🔍 **ANÁLISIS DE COMPETENCIA TREEPOD**\n\n`; content += `📊 **Competidores analizados:** ${analysis.totalCompetitors}\n`; content += `🎯 **Posicionamiento:** ${analysis.positioning}\n\n`; if (analysis.priceRange.average) { content += `**💰 ANÁLISIS DE PRECIOS:**\n`; content += `• Precio mínimo: ${this.formatCurrency(analysis.priceRange.min)}\n`; content += `• Precio máximo: ${this.formatCurrency(analysis.priceRange.max)}\n`; content += `• Precio promedio: ${this.formatCurrency(analysis.priceRange.average)}\n\n`; } if (analysis.services.length > 0) { content += `**🏨 SERVICIOS MÁS COMUNES:**\n`; analysis.services.forEach(({ service, count }) => { content += `• ${service} (${count} competidores)\n`; }); content += `\n`; } if (analysis.recommendations.length > 0) { content += `**🎯 RECOMENDACIONES:**\n`; analysis.recommendations.forEach(rec => { content += `• ${rec}\n`; }); content += `\n`; } content += `*Análisis basado en datos reales del sistema de inteligencia competitiva*`; return content; } /** * Genera recomendaciones basadas en análisis de competencia */ generateCompetitionRecommendations(analysis) { const recommendations = []; if (analysis.positioning === 'económico') { recommendations.push('Oportunidad de aumentar precios manteniendo competitividad'); } else if (analysis.positioning === 'premium') { recommendations.push('Enfatizar valor diferencial para justificar precio premium'); } if (analysis.totalCompetitors < 5) { recommendations.push('Mercado con poca competencia, oportunidad de crecimiento'); } else if (analysis.totalCompetitors > 10) { recommendations.push('Mercado saturado, enfocarse en diferenciación'); } if (analysis.services.length > 0) { const topService = analysis.services[0].service; recommendations.push(`Considerar agregar/mejorar: ${topService}`); } return recommendations.length > 0 ? recommendations : ['Mantener estrategia competitiva actual']; } /** * Genera reportes ejecutivos basado en datos reales */ async generateReport(reportType, format = 'summary') { validator.log('info', `Generando reporte ${reportType} en formato ${format}`); try { switch (reportType) { case 'monthly': return await this.generateMonthlyReport(format); case 'occupancy': return await this.generateOccupancyReport(format); case 'financial': return await this.generateFinancialReport(format); case 'competition': return await this.generateCompetitionReport(format); default: return await this.generateMonthlyReport(format); } } catch (error) { validator.log('error', `Error generando reporte ${reportType}: ${error.message}`); return validator.generateInsufficientDataResponse( `reporte ${reportType}`, 'Error interno en la generación del reporte. Contacta al administrador.' ); } } /** * Genera reporte mensual ejecutivo */ async generateMonthlyReport(format) { const financialData = await dataLoader.loadFinancialData(); const businessData = await dataLoader.loadBusinessStatus(); const domosStatus = await dataLoader.loadDomosStatus(); if (!financialData && !businessData) { return validator.generateInsufficientDataResponse( 'datos para reporte mensual', 'No se pudo acceder a los datos financieros ni de estado del negocio' ); } const analysis = this.calculateFinancialAnalysis(financialData || {}, 'Enero 2025'); const occupancyMetrics = domosStatus ? this.calculateOccupancyMetrics(domosStatus) : null; return { content: [{ type: 'text', text: this.formatMonthlyReport(analysis, occupancyMetrics, format) }] }; } /** * Genera reporte de ocupación */ async generateOccupancyReport(format) { const businessData = await dataLoader.loadBusinessStatus(); const domosStatus = await dataLoader.loadDomosStatus(); if (!domosStatus) { return validator.generateInsufficientDataResponse( 'datos de ocupación', 'No se pudo acceder al estado actual de los domos' ); } const metrics = this.calculateOccupancyMetrics(domosStatus); const occupancyRate = businessData?.occupancy || 0; return { content: [{ type: 'text', text: this.formatOccupancyReport(metrics, occupancyRate, format) }] }; } /** * Genera reporte financiero */ async generateFinancialReport(format) { const financialData = await dataLoader.loadFinancialData(); if (!financialData) { return validator.generateInsufficientDataResponse( 'datos financieros', 'No se pudo acceder a los datos financieros del sistema' ); } const analysis = this.calculateFinancialAnalysis(financialData, 'Período actual'); return { content: [{ type: 'text', text: this.formatFinancialReport(analysis, format) }] }; } /** * Genera reporte de competencia */ async generateCompetitionReport(format) { const competitionData = await dataLoader.loadCompetitionData(); if (!competitionData) { return validator.generateInsufficientDataResponse( 'datos de competencia', 'No se pudo acceder a los datos de inteligencia competitiva' ); } const analysis = this.processCompetitionData(competitionData, 'all'); return { content: [{ type: 'text', text: this.formatCompetitionReport(analysis, format) }] }; } /** * Calcula métricas de ocupación */ calculateOccupancyMetrics(domosStatus) { return { available: domosStatus.filter(d => d.status === 'available').length, occupied: domosStatus.filter(d => d.status === 'occupied').length, maintenance: domosStatus.filter(d => d.status === 'maintenance').length, cleaning: domosStatus.filter(d => d.status === 'cleaning').length, total: domosStatus.length }; } /** * Formatea reporte mensual */ formatMonthlyReport(analysis, occupancyMetrics, format) { let content = `📊 **REPORTE MENSUAL EJECUTIVO TREEPOD**\n\n`; content += `📅 **Período:** ${analysis.periodo}\n`; content += `💰 **Estado:** ${this.determineBusinessStatus(analysis)}\n\n`; content += `**💵 RESUMEN FINANCIERO:**\n`; content += `• Ingresos: ${this.formatCurrency(analysis.ingresos)}\n`; content += `• Gastos: ${this.formatCurrency(analysis.gastos)}\n`; content += `• Utilidad: ${this.formatCurrency(analysis.utilidad)}\n`; content += `• Margen: ${analysis.margen}%\n\n`; if (occupancyMetrics) { content += `**🏠 OCUPACIÓN:**\n`; content += `• Disponibles: ${occupancyMetrics.available}/${occupancyMetrics.total}\n`; content += `• Ocupados: ${occupancyMetrics.occupied}/${occupancyMetrics.total}\n`; content += `• Tasa ocupación: ${analysis.ocupacion}%\n\n`; } if (format === 'detailed') { content += `**📈 MÉTRICAS DETALLADAS:**\n`; content += `• Reservas totales: ${analysis.reservas}\n`; content += `• Ingreso promedio/reserva: ${this.formatCurrency(analysis.ingresoPromedioPorReserva)}\n`; content += `• Meta ingresos: ${analysis.metaIngresos ? '✅' : '❌'}\n`; content += `• Meta utilidad: ${analysis.metaUtilidad ? '✅' : '❌'}\n`; content += `• Meta ocupación: ${analysis.metaOcupacion ? '✅' : '❌'}\n\n`; } content += `*Reporte basado en datos reales del sistema TreePod*`; return content; } /** * Formatea reporte de ocupación */ formatOccupancyReport(metrics, occupancyRate, format) { let content = `🏠 **REPORTE DE OCUPACIÓN TREEPOD**\n\n`; content += `📊 **Ocupación general:** ${occupancyRate}%\n\n`; content += `**📈 MÉTRICAS:**\n`; content += `• Domos disponibles: ${metrics.available}/${metrics.total}\n`; content += `• Domos ocupados: ${metrics.occupied}/${metrics.total}\n`; content += `• En mantención: ${metrics.maintenance}/${metrics.total}\n`; if (metrics.cleaning > 0) { content += `• En limpieza: ${metrics.cleaning}/${metrics.total}\n`; } if (format === 'detailed') { content += `\n**🎯 ANÁLISIS:**\n`; const utilizationRate = ((metrics.occupied / metrics.total) * 100).toFixed(1); content += `• Tasa de utilización: ${utilizationRate}%\n`; content += `• Capacidad disponible: ${((metrics.available / metrics.total) * 100).toFixed(1)}%\n`; } content += `\n*Reporte basado en datos reales del sistema TreePod*`; return content; } /** * Formatea reporte financiero */ formatFinancialReport(analysis, format) { let content = `💰 **REPORTE FINANCIERO TREEPOD**\n\n`; content += `**💵 ESTADO FINANCIERO:**\n`; content += `• Ingresos: ${this.formatCurrency(analysis.ingresos)}\n`; content += `• Gastos: ${this.formatCurrency(analysis.gastos)}\n`; content += `• Utilidad neta: ${this.formatCurrency(analysis.utilidad)}\n`; content += `• Margen de utilidad: ${analysis.margen}%\n\n`; if (format === 'detailed') { content += `**📊 MÉTRICAS AVANZADAS:**\n`; content += `• ROI estimado: ${(analysis.utilidad / analysis.gastos * 100).toFixed(1)}%\n`; content += `• Punto de equilibrio: ${analysis.metaUtilidad ? 'Alcanzado' : 'Pendiente'}\n`; content += `• Eficiencia operativa: ${(100 - (analysis.gastos / analysis.ingresos * 100)).toFixed(1)}%\n\n`; } content += `*Reporte basado en datos reales del sistema TreePod*`; return content; } /** * Formatea reporte de competencia */ formatCompetitionReport(analysis, format) { let content = `🔍 **REPORTE DE COMPETENCIA TREEPOD**\n\n`; content += `📊 **Competidores analizados:** ${analysis.totalCompetitors}\n`; content += `🎯 **Posicionamiento:** ${analysis.positioning}\n\n`; if (analysis.priceRange.average) { content += `**💰 ANÁLISIS DE PRECIOS:**\n`; content += `• Rango: ${this.formatCurrency(analysis.priceRange.min)} - ${this.formatCurrency(analysis.priceRange.max)}\n`; content += `• Promedio mercado: ${this.formatCurrency(analysis.priceRange.average)}\n\n`; } if (format === 'detailed' && analysis.services.length > 0) { content += `**🏨 SERVICIOS COMPETENCIA:**\n`; analysis.services.forEach(({ service, count }) => { content += `• ${service}: ${count} competidores\n`; }); content += `\n`; } content += `*Reporte basado en datos reales del sistema de inteligencia competitiva*`; return content; } /** * Obtiene estado general del negocio con KPIs y alertas */ async getBusinessStatus() { validator.log('info', 'Obteniendo estado general del negocio'); try { // Cargar todos los datos necesarios const financialData = await dataLoader.loadFinancialData(); const businessData = await dataLoader.loadBusinessStatus(); const domosStatus = await dataLoader.loadDomosStatus(); // Validar que tenemos datos mínimos if (!financialData && !businessData) { return validator.generateInsufficientDataResponse( 'datos del estado del negocio', 'No se pudo acceder a los datos financieros ni de estado del negocio' ); } // Calcular métricas principales const metrics = this.calculateBusinessMetrics(financialData, businessData, domosStatus); const alerts = this.checkBusinessAlerts(metrics); const overallStatus = this.determineOverallStatus(metrics); return { content: [{ type: 'text', text: this.formatBusinessStatus(metrics, alerts, overallStatus) }] }; } catch (error) { validator.log('error', `Error obteniendo estado del negocio: ${error.message}`); return validator.generateInsufficientDataResponse( 'estado del negocio', 'Error interno en la consulta. Contacta al administrador.' ); } } /** * Calcula métricas principales del negocio */ calculateBusinessMetrics(financialData, businessData, domosStatus) { const revenue = financialData?.ingresos_total || 0; const expenses = financialData?.gastos_total || 0; const profit = revenue - expenses; const margin = revenue > 0 ? (profit / revenue * 100).toFixed(1) : 0; const occupancy = businessData?.occupancy || 0; const reservations = financialData?.reservas_totales || 0; // Métricas de domos si están disponibles let domosMetrics = null; if (domosStatus) { domosMetrics = this.calculateOccupancyMetrics(domosStatus); } return { revenue, expenses, profit, margin: parseFloat(margin), occupancy, reservations, domosMetrics, // Metas del negocio revenueGoal: 6000000, profitGoal: 2000000, occupancyGoal: 70 }; } /** * Verifica alertas del negocio */ checkBusinessAlerts(metrics) { const alerts = []; // Alertas financieras if (metrics.profit < 1000000) { alerts.push('🔴 Utilidad por debajo del mínimo operativo'); } if (metrics.margin < 20) { alerts.push('⚠️ Margen de utilidad bajo (< 20%)'); } // Alertas de ocupación if (metrics.occupancy < 50) { alerts.push('🔴 Ocupación crítica (< 50%)'); } // Alertas de domos si están disponibles if (metrics.domosMetrics) { if (metrics.domosMetrics.maintenance > metrics.domosMetrics.total / 2) { alerts.push('⚠️ Muchos domos en mantención'); } if (metrics.domosMetrics.available === 0 && metrics.domosMetrics.occupied === 0) { alerts.push('🔴 Todos los domos fuera de servicio'); } } // Alertas de ingresos if (metrics.revenue < metrics.revenueGoal * 0.5) { alerts.push('🔴 Ingresos muy por debajo de la meta'); } return alerts; } /** * Determina estado general del negocio */ determineOverallStatus(metrics) { const revenueScore = (metrics.revenue / metrics.revenueGoal) * 100; const profitScore = (metrics.profit / metrics.profitGoal) * 100; const occupancyScore = (metrics.occupancy / metrics.occupancyGoal) * 100; const averageScore = (revenueScore + profitScore + occupancyScore) / 3; if (averageScore >= 90) { return '🟢 Excelente'; } else if (averageScore >= 70) { return '🟡 Bueno'; } else if (averageScore >= 50) { return '🟠 Regular'; } else { return '🔴 Requiere atención inmediata'; } } /** * Formatea estado del negocio */ formatBusinessStatus(metrics, alerts, overallStatus) { const now = new Date(); const timestamp = now.toLocaleDateString('es-CL') + ' ' + now.toLocaleTimeString('es-CL', { hour: '2-digit', minute: '2-digit' }); let content = `🎯 **ESTADO GENERAL TREEPOD GLAMPING**\n\n`; content += `💼 **Estado del Negocio:** ${overallStatus}\n`; content += `📅 **Última actualización:** ${timestamp}\n\n`; content += `**📊 KPIs PRINCIPALES:**\n`; content += `• 💰 Ingresos: ${this.formatCurrency(metrics.revenue)}\n`; content += `• 📈 Utilidad: ${this.formatCurrency(metrics.profit)} (${metrics.margin}%)\n`; content += `• 🏠 Ocupación: ${metrics.occupancy}%\n`; content += `• 📅 Reservas: ${metrics.reservations}\n\n`; // Información de domos si está disponible if (metrics.domosMetrics) { content += `**🏕️ ESTADO DOMOS:**\n`; content += `• Disponibles: ${metrics.domosMetrics.available}/${metrics.domosMetrics.total}\n`; content += `• Ocupados: ${metrics.domosMetrics.occupied}/${metrics.domosMetrics.total}\n`; if (metrics.domosMetrics.maintenance > 0) { content += `• En mantención: ${metrics.domosMetrics.maintenance}/${metrics.domosMetrics.total}\n`; } content += `\n`; } content += `**🚨 ALERTAS ACTIVAS:**\n`; if (alerts.length > 0) { alerts.forEach(alert => { content += `• ${alert}\n`; }); } else { content += `✅ Sin alertas críticas\n`; } content += `\n`; content += `**🎯 PROGRESO HACIA METAS:**\n`; content += `• Ingresos: ${((metrics.revenue / metrics.revenueGoal) * 100).toFixed(1)}% de ${this.formatCurrency(metrics.revenueGoal)}\n`; content += `• Utilidad: ${((metrics.profit / metrics.profitGoal) * 100).toFixed(1)}% de ${this.formatCurrency(metrics.profitGoal)}\n`; content += `• Ocupación: ${((metrics.occupancy / metrics.occupancyGoal) * 100).toFixed(1)}% de ${metrics.occupancyGoal}%\n\n`; content += `**⚡ ACCIONES RECOMENDADAS:**\n`; const recommendations = this.getBusinessRecommendations(metrics, alerts); recommendations.forEach(rec => { content += `• ${rec}\n`; }); content += `\n*Estado basado en datos reales del sistema TreePod*`; return content; } /** * Genera recomendaciones de acciones */ getBusinessRecommendations(metrics, alerts) { const recommendations = []; // Recomendaciones basadas en métricas if (metrics.occupancy < 60) { recommendations.push('Implementar estrategia de marketing para aumentar ocupación'); } else if (metrics.occupancy > 85) { recommendations.push('Considerar aumento de precios por alta demanda'); } if (metrics.margin < 25) { recommendations.push('Revisar estructura de costos para mejorar rentabilidad'); } if (metrics.revenue < metrics.revenueGoal * 0.7) { recommendations.push('Acelerar estrategias de generación de ingresos'); } // Recomendaciones basadas en alertas if (alerts.some(alert => alert.includes('mantención'))) { recommendations.push('Priorizar finalización de mantenciones pendientes'); } if (alerts.some(alert => alert.includes('crítica'))) { recommendations.push('Atención inmediata requerida - revisar operaciones'); } // Recomendación por defecto si todo está bien if (recommendations.length === 0) { recommendations.push('Mantener estrategia actual y monitorear KPIs'); } return recommendations; } // ===== OPTIMIZACIÓN DE PRECIOS ===== /** * Optimiza precios basado en estrategia y datos reales * ✅ IMPLEMENTA GUÍA: Sin hardcodeo, validación, trazabilidad */ async optimizePricing(strategy) { validator.log('info', `Iniciando optimización de precios con estrategia: ${strategy}`); try { // Cargar datos reales necesarios const financialData = await dataLoader.loadFinancialData(); const businessData = await dataLoader.loadBusinessStatus(); const competitionData = await dataLoader.loadCompetitionData(); // Validar que tenemos datos suficientes if (!financialData && !businessData) { return validator.generateInsufficientDataResponse( 'datos para optimización de precios', 'No se pudo acceder a los datos financieros ni de estado del negocio' ); } const optimization = this.calculatePricingOptimization( strategy, financialData, businessData, competitionData ); return { content: [{ type: 'text', text: this.formatPricingOptimization(optimization, strategy) }] }; } catch (error) { validator.log('error', `Error en optimización de precios: ${error.message}`); return validator.generateInsufficientDataResponse( 'optimización de precios', 'Error interno en el análisis. Contacta al administrador.' ); } } calculatePricingOptimization(strategy, financialData, businessData, competitionData) { const currentOccupancy = financialData?.ocupacion_promedio || businessData?.occupancy || 0; const currentRevenue = financialData?.ingresos_total || 0; const averageRate = currentRevenue > 0 && financialData?.reservas_totales > 0 ? Math.round(currentRevenue / financialData.reservas_totales) : 0; const optimization = { strategy, current_metrics: { occupancy: currentOccupancy, revenue: currentRevenue, average_rate: averageRate }, recommendations: [], price_adjustments: [], expected_impact: {}, implementation_steps: [], monitoring_kpis: [] }; // Análisis por estrategia switch (strategy) { case 'maximize_revenue': this.addRevenueMaximizationStrategy(optimization, currentOccupancy); break; case 'maximize_occupancy': this.addOccupancyMaximizationStrategy(optimization, currentOccupancy); break; case 'balanced': this.addBalancedStrategy(optimization, currentOccupancy); break; case 'competitive': this.addCompetitiveStrategy(optimization, competitionData); break; default: optimization.recommendations.push('Estrategia no reconocida, aplicando estrategia balanceada'); this.addBalancedStrategy(optimization, currentOccupancy); } // Agregar consideraciones generales this.addGeneralPricingConsiderations(optimization); return optimization; } addRevenueMaximizationStrategy(optimization, occupancy) { if (occupancy > 75) { optimization.recommendations.push('Aumentar tarifas 8-12% en fechas de alta demanda'); optimization.price_adjustments.push('Temporada alta: +10%'); optimization.expected_impact.revenue = '+12-18%'; optimization.expected_impact.occupancy = '-3-7%'; } else { optimization.recommendations.push('Mantener tarifas actuales y optimizar mix de canales'); optimization.price_adjustments.push('Reducir comisiones canales directos'); optimization.expected_impact.revenue = '+5-8%'; optimization.expected_impact.occupancy = '+2-5%'; } optimization.implementation_steps.push('Implementar dynamic pricing por día de semana'); optimization.implementation_steps.push('Crear tarifas premium para fechas especiales'); optimization.monitoring_kpis.push('RevPAR (Revenue per Available Room)'); optimization.monitoring_kpis.push('ADR (Average Daily Rate)'); } addOccupancyMaximizationStrategy(optimization, occupancy) { optimization.recommendations.push('Reducir tarifas 10-15% en fechas de baja demanda'); optimization.recommendations.push('Crear paquetes todo incluido atractivos'); optimization.price_adjustments.push('Temporada baja: -12%'); optimization.price_adjustments.push('Estancias largas (3+ noches): -15%'); optimization.expected_impact.revenue = '-5-8%'; optimization.expected_impact.occupancy = '+18-25%'; optimization.implementation_steps.push('Lanzar promociones last-minute'); optimization.implementation_steps.push('Crear descuentos por reserva anticipada'); optimization.monitoring_kpis.push('Tasa de ocupación'); optimization.monitoring_kpis.push('Días promedio de anticipación'); } addBalancedStrategy(optimization, occupancy) { optimization.recommendations.push('Ajustar precios dinámicamente según demanda'); optimization.recommendations.push('Optimizar mix de canales de distribución'); if (occupancy < 70) { optimization.price_adjustments.push('Fechas disponibles: -5-8%'); } else { optimization.price_adjustments.push('Fechas alta demanda: +3-5%'); } optimization.expected_impact.revenue = '+6-10%'; optimization.expected_impact.occupancy = '+5-8%'; optimization.implementation_steps.push('Implementar sistema de yield management'); optimization.implementation_steps.push('Segmentar precios por tipo de cliente'); optimization.monitoring_kpis.push('Revenue total'); optimization.monitoring_kpis.push('Ocupación promedio'); } addCompetitiveStrategy(optimization, competitionData) { if (competitionData) { optimization.recommendations.push('Ajustar precios según análisis competitivo'); optimization.recommendations.push('Diferenciarse por valor agregado'); } else { optimization.recommendations.push('Realizar análisis competitivo antes de ajustar precios'); } optimization.price_adjustments.push('Alinear con mercado local'); optimization.expected_impact.revenue = '+3-7%'; optimization.expected_impact.occupancy = '+4-8%'; optimization.implementation_steps.push('Monitorear precios competencia semanalmente'); optimization.implementation_steps.push('Destacar ventajas diferenciales'); optimization.monitoring_kpis.push('Posición competitiva'); optimization.monitoring_kpis.push('Share of market'); } addGeneralPricingConsiderations(optimization) { optimization.implementation_steps.push('Testear cambios gradualmente'); optimization.implementation_steps.push('Comunicar cambios claramente a clientes'); optimization.monitoring_kpis.push('Satisfacción del cliente'); optimization.monitoring_kpis.push('Tasa de conversión'); } formatPricingOptimization(optimization, strategy) { const strategyNames = { maximize_revenue: 'Maximizar Ingresos', maximize_occupancy: 'Maximizar Ocupación', balanced: 'Estrategia Balanceada', competitive: 'Estrategia Competitiva' }; let report = `💰 **OPTIMIZACIÓN DE PRECIOS TREEPOD**\n\n`; report += `🎯 **Estrategia:** ${strategyNames[strategy] || strategy}\n\n`; // Métricas actuales report += `📊 **MÉTRICAS ACTUALES:**\n`; report += `• Ocupación: ${optimization.current_metrics.occupancy}%\n`; if (optimization.current_metrics.revenue > 0) { report += `• Ingresos: ${this.formatCurrency(optimization.current_metrics.revenue)}\n`; } if (optimization.current_metrics.average_rate > 0) { report += `• Tarifa promedio: ${this.formatCurrency(optimization.current_metrics.average_rate)}\n`; } report += `\n`; // Recomendaciones report += `🎯 **RECOMENDACIONES:**\n`; optimization.recommendations.forEach(rec => { report += `• ${rec}\n`; }); report += `\n`; // Ajustes de precios if (optimization.price_adjustments.length > 0) { report += `💲 **AJUSTES SUGERIDOS:**\n`; optimization.price_adjustments.forEach(adj => { report += `• ${adj}\n`; }); report += `\n`; } // Impacto esperado if (Object.keys(optimization.expected_impact).length > 0) { report += `📈 **IMPACTO ESPERADO:**\n`; if (optimization.expected_impact.revenue) { report += `• Ingresos: ${optimization.expected_impact.revenue}\n`; } if (optimization.expected_impact.occupancy) { report += `• Ocupación: ${optimization.expected_impact.occupancy}\n`; } report += `\n`; } // Pasos de implementación if (optimization.implementation_steps.length > 0) { report += `🔧 **IMPLEMENTACIÓN:**\n`; optimization.implementation_steps.forEach((step, index) => { report += `${index + 1}. ${step}\n`; }); report += `\n`; } // KPIs de monitoreo if (optimization.monitoring_kpis.length > 0) { report += `📊 **KPIS A MONITOREAR:**\n`; optimization.monitoring_kpis.forEach(kpi => { report += `• ${kpi}\n`; }); } return report; } // ===== PREDICCIÓN DE INGRESOS ===== /** * Predice ingresos futuros basado en datos históricos * ✅ IMPLEMENTA GUÍA: Sin hardcodeo, validación, trazabilidad */ async predictRevenue(period) { validator.log('info', `Iniciando predicción de ingresos para período: ${period}`); try { // Cargar datos reales necesarios const financialData = await dataLoader.loadFinancialData(); const businessData = await dataLoader.loadBusinessStatus(); // Validar que tenemos datos suficientes if (!financialData) { return validator.generateInsufficientDataResponse( 'datos financieros históricos', 'No se pudo acceder a los datos financieros necesarios para la predicción' ); } const prediction = this.calculateRevenuePrediction( financialData, businessData, period ); return { content: [{ type: 'text', text: this.formatRevenuePrediction(prediction, period) }] }; } catch (error) { validator.log('error', `Error en predicción de ingresos: ${error.message}`); return validator.generateInsufficientDataResponse( 'predicción de ingresos', 'Error interno en el análisis. Contacta al administrador.' ); } } calculateRevenuePrediction(financialData, businessData, period) { const currentRevenue = financialData.ingresos_total || 0; const currentOccupancy = financialData.ocupacion_promedio || businessData?.occupancy || 0; const currentReservations = financialData.reservas_totales || 0; // Factores de crecimiento y estacionalidad basados en datos reales const growthFactors = this.calculateGrowthFactors(period); const seasonalityFactor = this.calculateSeasonalityFactor(); const marketFactor = this.calculateMarketFactor(currentOccupancy); // Cálculo base de predicción const basePrediction = currentRevenue * growthFactors.time * seasonalityFactor * marketFactor; const prediction = { period, current_metrics: { revenue: currentRevenue, occupancy: currentOccupancy, reservations: currentReservations, avg_rate: currentReservations > 0 ? Math.round(currentRevenue / currentReservations) : 0 }, prediction_factors: { time_factor: growthFactors.time, seasonality_factor: seasonalityFactor, market_factor: marketFactor, confidence_level: this.calculateConfidenceLevel(financialData) }, scenarios: { conservative: Math.round(basePrediction * 0.85), realistic: Math.round(basePrediction), optimistic: Math.round(basePrediction * 1.15) }, projected_metrics: { revenue: Math.round(basePrediction), occupancy: Math.min(95, Math.round(currentOccupancy * seasonalityFactor * marketFactor)), reservations: Math.round(currentReservations * growthFactors.time * marketFactor) }, recommendations: [], risk_factors: [], monitoring_points: [] }; // Agregar recomendaciones basadas en la predicción this.addRevenuePredictionRecommendations(prediction); return prediction; } calculateGrowthFactors(period) { const factors = { next_week: 0.25, next_month: 1.0, next_quarter: 3.0, next_semester: 6.0, next_year: 12.0 }; return { time: factors[period] || 1.0 }; } calculateSeasonalityFactor() { const currentMonth = new Date().getMonth() + 1; // Factores estacionales para Chile (hemisferio sur) if (currentMonth >= 12 || currentMonth <= 3) { return 1.25; // Verano - temporada alta } else if (currentMonth >= 6 && currentMonth <= 8) { return 1.15; // Invierno - temporada media-alta } else { return 0.9; // Temporadas intermedias } } calculateMarketFactor(currentOccupancy) { // Factor basado en performance actual if (currentOccupancy > 80) { return 1.1; // Mercado fuerte } else if (currentOccupancy > 60) { return 1.0; // Mercado estable } else { return 0.9; // Mercado débil } } calculateConfidenceLevel(financialData) { let confidence = 70; // Base // Aumentar confianza si tenemos más datos if (financialData.ingresos_total > 0) confidence += 10; if (financialData.reservas_totales > 10) confidence += 10; if (financialData.ocupacion_promedio > 0) confidence += 10; return Math.min(95, confidence); } addRevenuePredictionRecommendations(prediction) { const { projected_metrics, current_metrics, prediction_factors } = prediction; // Recomendaciones basadas en ocupación proyectada if (projected_metrics.occupancy < 65) { prediction.recommendations.push('Implementar estrategias de marketing agresivas'); prediction.recommendations.push('Considerar promociones especiales'); prediction.risk_factors.push('Baja ocupación proyectada'); } else if (projected_metrics.occupancy > 85) { prediction.recommendations.push('Evaluar aumento de tarifas'); prediction.recommendations.push('Optimizar mix de canales de alta conversión'); } // Recomendaciones basadas en confianza if (prediction_factors.confidence_level < 80) { prediction.recommendations.push('Recopilar más datos históricos para mejorar precisión'); prediction.risk_factors.push('Datos históricos limitados'); } // Puntos de monitoreo prediction.monitoring_points.push('Revisar predicción semanalmente'); prediction.monitoring_points.push('Comparar con resultados reales'); prediction.monitoring_points.push('Ajustar factores según performance'); // Factores de riesgo generales prediction.risk_factors.push('Cambios en condiciones económicas'); prediction.risk_factors.push('Nuevos competidores en el mercado'); prediction.risk_factors.push('Eventos externos impredecibles'); } formatRevenuePrediction(prediction, period) { const periodNames = { next_week: 'Próxima Semana', next_month: 'Próximo Mes', next_quarter: 'Próximo Trimestre', next_semester: 'Próximo Semestre', next_year: 'Próximo Año' }; let report = `📈 **PREDICCIÓN DE INGRESOS TREEPOD**\n\n`; report += `📅 **Período:** ${periodNames[period] || period}\n\n`; // Métricas actuales report += `📊 **MÉTRICAS ACTUALES:**\n`; if (prediction.current_metrics.revenue > 0) { report += `• Ingresos: ${this.formatCurrency(prediction.current_metrics.revenue)}\n`; } report += `• Ocupación: ${prediction.current_metrics.occupancy}%\n`; if (prediction.current_metrics.reservations > 0) { report += `• Reservas: ${prediction.current_metrics.reservations}\n`; } if (prediction.current_metrics.avg_rate > 0) { report += `• Tarifa promedio: ${this.formatCurrency(prediction.current_metrics.avg_rate)}\n`; } report += `\n`; // Predicción principal report += `🎯 **PREDICCIÓN:**\n`; report += `• **Ingresos proyectados:** ${this.formatCurrency(prediction.projected_metrics.revenue)}\n`; report += `• **Ocupación proyectada:** ${prediction.projected_metrics.occupancy}%\n`; report += `• **Reservas proyectadas:** ${prediction.projected_metrics.reservations}\n`; report += `• **Nivel de confianza:** ${prediction.prediction_factors.confidence_level}%\n\n`; // Escenarios report += `📊 **ESCENARIOS:**\n`; report += `• 🟢 **Optimista:** ${this.formatCurrency(prediction.scenarios.optimistic)}\n`; report += `• 🟡 **Realista:** ${this.formatCurrency(prediction.scenarios.realistic)}\n`; report += `• 🔴 **Conservador:** ${this.formatCurrency(prediction.scenarios.conservative)}\n\n`; // Factores considerados report += `🔍 **FACTORES CONSIDERADOS:**\n`; report += `• Factor temporal: ${(prediction.prediction_factors.time_factor * 100).toFixed(0)}%\n`; report += `• Factor estacional: ${((prediction.prediction_factors.seasonality_factor - 1) * 100).toFixed(0)}%\n`; report += `• Factor de mercado: ${((prediction.prediction_factors.market_factor - 1) * 100).toFixed(0)}%\n\n`; // Recomendaciones if (prediction.recommendations.length > 0) { report += `💡 **RECOMENDACIONES:**\n`; prediction.recommendations.forEach(rec => { report += `• ${rec}\n`; }); report += `\n`; } // Factores de riesgo if (prediction.risk_factors.length > 0) { report += `⚠️ **FACTORES DE RIESGO:**\n`; prediction.risk_factors.forEach(risk => { report += `• ${risk}\n`; }); report += `\n`; } // Puntos de monitoreo if (prediction.monitoring_points.length > 0) { report += `📋 **MONITOREO:**\n`; prediction.monitoring_points.forEach(point => { report += `• ${point}\n`; }); } return report; } } // Instancia global del calculador export const businessCalculator = new BusinessCalculator();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/janetsep/treepod-financial-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server