diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e57533f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,240 @@ +--- +id: "AGENTS-IA" +title: "Guia para Agentes IA - Inmobiliaria Analytics" +type: "Agent Guide" +project: "inmobiliaria-analytics" +version: "1.0.0" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# AGENTS.md - Inmobiliaria Analytics + +Guia completa para que agentes de IA trabajen con el proyecto Inmobiliaria Analytics. + +--- + +## Informacion del Proyecto + +| Campo | Valor | +|-------|-------| +| **Nombre** | Inmobiliaria Analytics | +| **Prefijo** | IA | +| **Repositorio** | inmobiliaria-analytics | +| **Estado** | Planificado | +| **Stack** | NestJS, React, PostgreSQL, TypeORM | + +--- + +## Estructura del Proyecto + +``` +inmobiliaria-analytics/ +├── apps/ +│ ├── backend/ # API NestJS (puerto 3101) +│ ├── frontend/ # UI React (puerto 3100) +│ └── database/ # Schemas PostgreSQL +├── docs/ # Documentacion GAMILIT +├── orchestration/ # Directivas y contexto +├── AGENTS.md # Este archivo +├── INVENTARIO.yml # Inventario del proyecto +└── .env.ports # Puertos asignados +``` + +--- + +## Puertos Asignados + +| Servicio | Puerto | Protocolo | +|----------|--------|-----------| +| Frontend | 3100 | HTTP | +| Backend API | 3101 | HTTP | +| WebSocket | 3102 | WS | +| PostgreSQL | 5439 | TCP | +| Redis | 6386 | TCP | + +--- + +## Nomenclatura + +### Prefijos por Tipo de Documento + +| Tipo | Prefijo | Ejemplo | +|------|---------|---------| +| EPIC | IA-NNN | IA-001-fundamentos | +| Requerimiento | RF-IA-NNN | RF-IA-001 | +| Especificacion | ET-IA-NNN | ET-IA-001 | +| Historia Usuario | US-IA-NNN | US-IA-001 | +| Tarea | TASK-NNN | TASK-001 | +| Bug | BUG-NNN | BUG-001 | +| ADR | ADR-NNN | ADR-001 | + +### Categorias de US + +| Prefijo | EPIC | Descripcion | +|---------|------|-------------| +| FUND | IA-001 | Fundamentos | +| PROP | IA-002 | Propiedades | +| ANA | IA-003 | Analytics | +| REP | IA-004 | Reportes | + +--- + +## Como Trabajar con el Proyecto + +### Tomar una Tarea + +1. Revisar `docs/planning/Board.md` - columna "Por Hacer" +2. Leer archivo `TASK-XXX.md` correspondiente +3. Editar YAML front-matter: + ```yaml + status: "In Progress" + assignee: "@NombreAgente" + ``` +4. Commit: `Start TASK-XXX: [descripcion]` + +### Completar una Tarea + +1. Verificar criterios de aceptacion cumplidos +2. Editar YAML front-matter: + ```yaml + status: "Done" + completed_date: "YYYY-MM-DD" + ``` +3. Actualizar `Board.md` - mover a "Hecho" +4. Commit: `Complete TASK-XXX: [descripcion]` + +### Reportar un Bug + +1. Crear archivo `docs/planning/bugs/BUG-XXX.md` +2. Incluir YAML front-matter obligatorio: + ```yaml + --- + id: "BUG-XXX" + title: "Descripcion del bug" + type: "Bug" + status: "Open" + severity: "P1" + priority: "Alta" + affected_module: "Backend" + steps_to_reproduce: + - "Paso 1" + - "Paso 2" + expected_behavior: "..." + actual_behavior: "..." + created_date: "YYYY-MM-DD" + --- + ``` +3. Agregar a `Board.md` en columna "Bugs" + +--- + +## Archivos Importantes + +| Archivo | Proposito | +|---------|-----------| +| `docs/planning/Board.md` | Tablero Kanban activo | +| `docs/planning/config.yml` | Configuracion SCRUM | +| `docs/04-fase-backlog/DEFINITION-OF-READY.md` | Criterios para iniciar | +| `docs/04-fase-backlog/DEFINITION-OF-DONE.md` | Criterios para completar | +| `docs/_MAP.md` | Mapa de navegacion | +| `INVENTARIO.yml` | Inventario del proyecto | + +--- + +## Estados Validos + +### User Story + +- `Backlog`: No planificada +- `To Do`: Planificada para sprint +- `In Progress`: En desarrollo +- `In Review`: En revision +- `Done`: Completada + +### Task + +- `To Do`: Pendiente +- `In Progress`: En desarrollo +- `Blocked`: Bloqueada +- `Done`: Completada + +### Bug + +- `Open`: Reportado +- `In Progress`: En investigacion +- `Fixed`: Corregido, pendiente validacion +- `Done`: Validado y cerrado +- `Won't Fix`: No se corregira + +--- + +## Convenciones de Commits + +``` +(): + +Tipos: +- feat: Nueva funcionalidad +- fix: Correccion de bug +- docs: Documentacion +- refactor: Refactorizacion +- test: Tests +- chore: Tareas de mantenimiento + +Ejemplos: +- feat(auth): Implementar login con JWT +- fix(api): Corregir validacion de propiedades +- docs(readme): Actualizar instrucciones de setup +``` + +--- + +## Flujo de Trabajo Recomendado + +``` +1. CONTEXTO + - Leer AGENTS.md (este archivo) + - Revisar Board.md para estado actual + - Identificar tarea a trabajar + +2. ANALISIS + - Leer documentacion relacionada (RF, ET, US) + - Revisar codigo existente + - Identificar dependencias + +3. PLANEACION + - Desglosar en subtareas si es necesario + - Estimar esfuerzo + - Actualizar status a "In Progress" + +4. VALIDACION + - Verificar entendimiento con DoR + - Confirmar que no hay bloqueantes + +5. EJECUCION + - Implementar solucion + - Escribir tests + - Documentar cambios + +6. DOCUMENTACION + - Actualizar _MAP.md si aplica + - Marcar tarea como "Done" + - Commit con mensaje descriptivo +``` + +--- + +## Contacto y Escalamiento + +| Rol | Responsabilidad | +|-----|-----------------| +| @Backend-Agent | APIs, servicios, base de datos | +| @Frontend-Agent | UI, componentes, estado | +| @DevOps-Agent | CI/CD, infraestructura | +| @Tech-Lead | Decisiones arquitectonicas | + +--- + +**Generado:** 2026-01-04 +**Sistema:** NEXUS v3.4 + SIMCO + GAMILIT Standard diff --git a/docs/00-vision-general/ARQUITECTURA-GENERAL.md b/docs/00-vision-general/ARQUITECTURA-GENERAL.md new file mode 100644 index 0000000..9384590 --- /dev/null +++ b/docs/00-vision-general/ARQUITECTURA-GENERAL.md @@ -0,0 +1,254 @@ +--- +id: "ARCH-IA" +title: "Arquitectura General - Inmobiliaria Analytics" +type: "Architecture Document" +version: "1.0.0" +status: "Draft" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Arquitectura General - Inmobiliaria Analytics + +--- + +## Vision Arquitectonica + +Arquitectura basada en microservicios con separacion clara entre frontend, backend y datos, optimizada para analytics de alto rendimiento. + +--- + +## Diagrama de Alto Nivel + +``` + +------------------+ + | Load Balancer | + | (Traefik) | + +--------+---------+ + | + +-----------------+-----------------+ + | | ++----------v----------+ +-----------v-----------+ +| Frontend | | Backend | +| React + Vite | | NestJS | +| Puerto: 3100 | | Puerto: 3101 | ++----------+----------+ +-----------+-----------+ + | | + | +--------------+--------------+ + | | | | + | +--------v----+ +------v------+ +----v-----+ + | | Auth | | Properties | | Analytics| + | | Module | | Module | | Module | + | +--------+----+ +------+------+ +----+-----+ + | | | | + | +--------------+--------------+ + | | + | +--------------v--------------+ + | | PostgreSQL | + +--------------------+ Puerto: 5439 | + | Schemas: public, | + | properties, analytics | + +-----------------------------+ + | + +-------------v-------------+ + | Redis | + | Puerto: 6386 | + | Cache + Sessions | + +---------------------------+ +``` + +--- + +## Capas de la Arquitectura + +### 1. Capa de Presentacion (Frontend) + +| Componente | Tecnologia | Proposito | +|------------|------------|-----------| +| SPA | React 18.x | Interfaz de usuario | +| Build | Vite | Bundling y desarrollo | +| Estado | Zustand | Manejo de estado global | +| UI Kit | Tailwind CSS | Estilos | +| Charts | Recharts | Visualizaciones | + +### 2. Capa de Aplicacion (Backend) + +| Componente | Tecnologia | Proposito | +|------------|------------|-----------| +| API Server | NestJS 10.x | Framework HTTP | +| ORM | TypeORM 0.3.x | Acceso a datos | +| Auth | Passport + JWT | Autenticacion | +| Validation | class-validator | Validacion de DTOs | +| Docs | Swagger/OpenAPI | Documentacion API | + +### 3. Capa de Datos + +| Componente | Tecnologia | Proposito | +|------------|------------|-----------| +| RDBMS | PostgreSQL 16 | Datos principales | +| Cache | Redis 7 | Caching y sesiones | +| Search | (Futuro) Elasticsearch | Busqueda avanzada | + +--- + +## Modulos del Sistema + +### IA-001: Fundamentos + +``` +src/modules/ +├── auth/ # Autenticacion y autorizacion +│ ├── auth.module.ts +│ ├── auth.service.ts +│ ├── auth.controller.ts +│ ├── strategies/ +│ │ ├── jwt.strategy.ts +│ │ └── local.strategy.ts +│ └── guards/ +│ └── jwt-auth.guard.ts +└── users/ # Gestion de usuarios + ├── users.module.ts + ├── users.service.ts + └── entities/ + └── user.entity.ts +``` + +### IA-002: Propiedades (Planificado) + +``` +src/modules/ +├── properties/ # CRUD de propiedades +├── locations/ # Ubicaciones geograficas +└── valuations/ # Valuaciones +``` + +### IA-003: Analytics (Planificado) + +``` +src/modules/ +├── analytics/ # Motor de analytics +├── reports/ # Generacion de reportes +└── dashboards/ # Configuracion de dashboards +``` + +--- + +## Esquemas de Base de Datos + +### Schema: public + +Tablas de sistema y usuarios: +- `users` - Usuarios del sistema +- `roles` - Roles y permisos +- `sessions` - Sesiones activas + +### Schema: properties + +Datos inmobiliarios: +- `properties` - Catalogo de propiedades +- `property_types` - Tipos (casa, depto, etc) +- `locations` - Ubicaciones geograficas +- `valuations` - Historial de valuaciones + +### Schema: analytics + +Datos analiticos: +- `market_trends` - Tendencias de mercado +- `price_indices` - Indices de precios +- `reports` - Reportes generados + +--- + +## Flujos Principales + +### Autenticacion + +``` +1. Usuario envia credenciales +2. Backend valida con LocalStrategy +3. Si valido, genera JWT +4. Frontend almacena token +5. Requests subsecuentes incluyen Bearer token +6. JwtAuthGuard valida en cada request protegido +``` + +### Consulta de Analytics + +``` +1. Frontend solicita datos del dashboard +2. Backend verifica cache en Redis +3. Si cache hit, retorna datos +4. Si cache miss: + a. Consulta PostgreSQL + b. Procesa/agrega datos + c. Almacena en cache + d. Retorna al frontend +5. Frontend renderiza visualizaciones +``` + +--- + +## Decisiones Arquitectonicas + +| ID | Titulo | Estado | +|----|--------|--------| +| [ADR-001](../97-adr/ADR-001-stack-tecnologico.md) | Stack Tecnologico | Aceptado | + +--- + +## Seguridad + +### Autenticacion +- JWT con expiracion configurable +- Refresh tokens para renovacion +- Bcrypt para hash de passwords + +### Autorizacion +- RBAC (Role-Based Access Control) +- Guards en endpoints protegidos +- Validacion de permisos por recurso + +### Comunicacion +- HTTPS obligatorio en produccion +- CORS configurado por ambiente +- Rate limiting en endpoints publicos + +--- + +## Escalabilidad + +### Horizontal +- Stateless backend (JWT) +- Sessions en Redis +- Load balancing con Traefik + +### Vertical +- Indices optimizados en PostgreSQL +- Query optimization +- Connection pooling + +--- + +## Monitoreo + +| Aspecto | Herramienta | +|---------|-------------| +| Logs | stdout + Docker logs | +| Metricas | Prometheus (futuro) | +| Alertas | Alertmanager (futuro) | +| APM | OpenTelemetry (futuro) | + +--- + +## Referencias + +- [STACK-TECNOLOGICO.md](./STACK-TECNOLOGICO.md) +- [ADR-001-stack-tecnologico.md](../97-adr/ADR-001-stack-tecnologico.md) +- [service.descriptor.yml](../../apps/backend/service.descriptor.yml) + +--- + +**Documento:** Arquitectura General +**Version:** 1.0.0 +**Estado:** Draft +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/00-vision-general/STACK-TECNOLOGICO.md b/docs/00-vision-general/STACK-TECNOLOGICO.md new file mode 100644 index 0000000..0bc81eb --- /dev/null +++ b/docs/00-vision-general/STACK-TECNOLOGICO.md @@ -0,0 +1,249 @@ +--- +id: "STACK-IA" +title: "Stack Tecnologico - Inmobiliaria Analytics" +type: "Technical Document" +version: "1.0.0" +status: "Active" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Stack Tecnologico - Inmobiliaria Analytics + +--- + +## Resumen + +Documentacion detallada del stack tecnologico seleccionado para el proyecto Inmobiliaria Analytics. + +--- + +## Stack Principal + +### Backend + +| Componente | Tecnologia | Version | Justificacion | +|------------|------------|---------|---------------| +| Runtime | Node.js | 20.x LTS | Estabilidad y soporte largo plazo | +| Framework | NestJS | 10.3.x | Arquitectura modular, TypeScript nativo | +| ORM | TypeORM | 0.3.x | Integracion con NestJS, migraciones | +| Validacion | class-validator | 0.14.x | Decoradores para DTOs | +| Transformacion | class-transformer | 0.5.x | Serializacion de objetos | + +### Autenticacion + +| Componente | Tecnologia | Version | Justificacion | +|------------|------------|---------|---------------| +| Framework | Passport | 0.7.x | Estrategias flexibles | +| Token | JWT | 10.x | Stateless, escalable | +| Local | passport-local | 1.0.x | Login con usuario/password | +| Hashing | bcrypt | 5.1.x | Seguridad para passwords | + +### Frontend + +| Componente | Tecnologia | Version | Justificacion | +|------------|------------|---------|---------------| +| Framework | React | 18.x | Ecosistema maduro | +| Lenguaje | TypeScript | 5.3.x | Type safety | +| Build | Vite | 5.x | Desarrollo rapido | +| Estado | Zustand | 4.x | Simple y performante | +| Estilos | Tailwind CSS | 3.x | Utility-first | +| Charts | Recharts | 2.x | React-native charts | + +### Base de Datos + +| Componente | Tecnologia | Version | Justificacion | +|------------|------------|---------|---------------| +| RDBMS | PostgreSQL | 16.x | ACID, extensiones geograficas | +| Cache | Redis | 7.x | Alto rendimiento | +| Driver | pg | 8.11.x | Driver PostgreSQL para Node | + +### Infraestructura + +| Componente | Tecnologia | Version | Justificacion | +|------------|------------|---------|---------------| +| Contenedores | Docker | 24.x | Portabilidad | +| Orquestacion | Docker Compose | 2.x | Desarrollo local | +| Proxy/LB | Traefik | 3.x | Routing dinamico | + +--- + +## Dependencias del Backend + +### Produccion + +```json +{ + "@nestjs/common": "^10.3.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.3.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.3.0", + "@nestjs/typeorm": "^10.0.1", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "pg": "^8.11.3", + "reflect-metadata": "^0.2.1", + "rxjs": "^7.8.1", + "typeorm": "^0.3.19", + "stripe": "^14.0.0" +} +``` + +### Desarrollo + +```json +{ + "@nestjs/cli": "^10.3.0", + "@nestjs/schematics": "^10.1.0", + "@nestjs/testing": "^10.3.0", + "@types/bcrypt": "^5.0.2", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/node": "^20.10.6", + "@types/passport-jwt": "^4.0.0", + "@types/passport-local": "^1.0.38", + "@typescript-eslint/eslint-plugin": "^6.18.0", + "@typescript-eslint/parser": "^6.18.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.2", + "jest": "^29.7.0", + "prettier": "^3.1.1", + "supertest": "^6.3.4", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" +} +``` + +--- + +## Configuracion de Puertos + +| Servicio | Puerto | Ambiente | +|----------|--------|----------| +| Frontend | 3100 | Todos | +| Backend API | 3101 | Todos | +| WebSocket | 3102 | Todos | +| PostgreSQL | 5439 | Local | +| Redis | 6386 | Local | + +--- + +## Variables de Entorno + +### Aplicacion + +```env +# App +APP_NAME=inmobiliaria-analytics +APP_PORT=3101 +APP_ENV=development +API_PREFIX=api + +# CORS +CORS_ORIGIN=http://localhost:3100 +``` + +### Base de Datos + +```env +# PostgreSQL +DB_HOST=localhost +DB_PORT=5439 +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_DATABASE=inmobiliaria_analytics +DB_SYNCHRONIZE=true +DB_LOGGING=true + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6386 +``` + +### Seguridad + +```env +# JWT +JWT_SECRET=your-secret-key +JWT_EXPIRES_IN=1d +JWT_REFRESH_EXPIRES_IN=7d +``` + +--- + +## Scripts de Desarrollo + +### Backend + +```bash +# Desarrollo +npm run start:dev + +# Build +npm run build + +# Tests +npm run test +npm run test:cov +npm run test:e2e + +# Linting +npm run lint +npm run format +``` + +### Base de Datos + +```bash +# Migraciones (futuro) +npm run migration:generate -- -n MigrationName +npm run migration:run +npm run migration:revert +``` + +--- + +## Herramientas de Desarrollo + +| Herramienta | Proposito | +|-------------|-----------| +| ESLint | Linting de codigo | +| Prettier | Formateo de codigo | +| Jest | Testing unitario y e2e | +| Swagger | Documentacion API | +| Docker Compose | Ambiente local | + +--- + +## Extensiones Futuras + +| Componente | Tecnologia | Proposito | +|------------|------------|-----------| +| Search | Elasticsearch | Busqueda avanzada | +| ML | Python/FastAPI | Predicciones | +| Queue | BullMQ | Jobs asinconos | +| Metrics | Prometheus | Monitoreo | +| Logs | ELK Stack | Centralizacion | + +--- + +## Referencias + +- [ADR-001-stack-tecnologico.md](../97-adr/ADR-001-stack-tecnologico.md) +- [ARQUITECTURA-GENERAL.md](./ARQUITECTURA-GENERAL.md) +- [package.json](../../apps/backend/package.json) + +--- + +**Documento:** Stack Tecnologico +**Version:** 1.0.0 +**Estado:** Active +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/00-vision-general/VISION-PRODUCTO.md b/docs/00-vision-general/VISION-PRODUCTO.md new file mode 100644 index 0000000..5b73da2 --- /dev/null +++ b/docs/00-vision-general/VISION-PRODUCTO.md @@ -0,0 +1,321 @@ +--- +id: "VISION-IA" +title: "Vision del Producto - Inmobiliaria Analytics" +type: "Vision Document" +version: "1.0.0" +status: "Draft" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Vision del Producto - Inmobiliaria Analytics + +--- + +## Resumen Ejecutivo + +**Inmobiliaria Analytics** es una plataforma de analisis de datos para el sector inmobiliario que proporciona herramientas avanzadas para la visualizacion, seguimiento y prediccion de tendencias del mercado inmobiliario. + +--- + +## Vision + +Ser la plataforma lider en analytics inmobiliario, proporcionando insights accionables que permitan a agentes, inversores y desarrolladores tomar decisiones informadas basadas en datos. + +--- + +## Mision + +Democratizar el acceso a la inteligencia de mercado inmobiliario mediante herramientas intuitivas y analisis avanzados que transformen datos en valor. + +--- + +## Objetivos Estrategicos + +### Corto Plazo (Q1 2026) +- [ ] Establecer fundamentos tecnicos del proyecto +- [ ] Implementar modulo de autenticacion y usuarios +- [ ] Crear modelo de datos para propiedades +- [ ] Desarrollar dashboard basico de visualizacion + +### Mediano Plazo (Q2-Q3 2026) +- [ ] Integrar fuentes de datos inmobiliarios +- [ ] Implementar analytics avanzados +- [ ] Desarrollar sistema de reportes +- [ ] Lanzar version beta + +### Largo Plazo (Q4 2026+) +- [ ] Machine Learning para prediccion de precios +- [ ] Expansion a multiples mercados +- [ ] API publica para integraciones +- [ ] App movil + +--- + +## Propuesta de Valor + +### Para Agentes Inmobiliarios +- Analisis de mercado en tiempo real +- Comparativas de propiedades +- Tendencias de precios por zona +- Herramientas de valoracion + +### Para Inversores +- Dashboard de portafolio +- Analisis de ROI +- Identificacion de oportunidades +- Alertas de mercado + +### Para Desarrolladores +- Estudios de factibilidad +- Analisis de demanda +- Proyecciones financieras +- Benchmarking de proyectos + +--- + +## Diferenciadores Clave + +| Caracteristica | Competencia | Inmobiliaria Analytics | +|----------------|-------------|------------------------| +| Actualizacion de datos | Semanal | Tiempo real | +| Granularidad | Ciudad | Manzana/Colonia | +| Predicciones ML | Basicas | Avanzadas | +| Integraciones | Limitadas | API abierta | +| Personalizacion | Minima | Completa | + +--- + +## Mercado Objetivo + +### Segmento Primario +- Agencias inmobiliarias medianas y grandes +- Inversores institucionales +- Desarrolladores inmobiliarios + +### Segmento Secundario +- Agentes independientes +- Inversores individuales +- Instituciones financieras + +### Geografia Inicial +- Mexico (principales ciudades) +- Expansion posterior a LATAM + +--- + +## Metricas de Exito + +| KPI | Meta Q1 | Meta Q4 | +|-----|---------|---------| +| Usuarios registrados | 100 | 5,000 | +| Propiedades indexadas | 10,000 | 500,000 | +| Consultas diarias | 500 | 50,000 | +| NPS | 40 | 60 | +| Uptime | 99% | 99.9% | + +--- + +## Riesgos y Mitigaciones + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Calidad de datos | Alta | Alto | Multiples fuentes, validacion | +| Adopcion lenta | Media | Alto | MVP rapido, feedback continuo | +| Competencia | Media | Medio | Diferenciacion tecnologica | +| Escalabilidad | Baja | Alto | Arquitectura cloud-native | + +--- + +## Stakeholders + +| Rol | Responsabilidad | +|-----|-----------------| +| Product Owner | Vision y prioridades | +| Tech Lead | Arquitectura y decisiones tecnicas | +| Backend Team | APIs y logica de negocio | +| Frontend Team | UI/UX y experiencia | +| Data Team | ETL y analytics | + +--- + +## Modelo SaaS + +### Arquitectura Multi-tenant + +Inmobiliaria Analytics opera como plataforma SaaS con aislamiento de datos por tenant mediante Row-Level Security (RLS) en PostgreSQL. + +```yaml +Tipo: Single Database, Shared Schema +Aislamiento: RLS por tenant_id +Branding: Configurable por tenant +``` + +### Planes de Suscripcion + +| Plan | Precio | Propiedades | Reportes/mes | Alertas | Usuarios | Soporte | +|------|--------|-------------|--------------|---------|----------|---------| +| Free | $0 | 100 | 5 | 3 | 1 | Comunidad | +| Pro | $49/mes | 5,000 | 50 | 25 | 5 | Email | +| Enterprise | $199/mes | Ilimitado | Ilimitado | Ilimitado | Ilimitado | Dedicado | + +### Integracion Stripe + +```yaml +Productos Stripe: + ia_pro_monthly: + precio: $49/mes + type: subscription + + ia_enterprise_monthly: + precio: $199/mes + type: subscription + + ia_properties_500: + precio: $19 + type: one_time + descripcion: "500 propiedades adicionales" + +Webhooks: + - customer.subscription.created + - customer.subscription.updated + - customer.subscription.deleted + - invoice.payment_succeeded + - invoice.payment_failed + - checkout.session.completed +``` + +### Estructura de Portales + +```yaml +Portal 1 - Usuario (Analyst): + URL: app.{tenant}.inmobiliaria-analytics.com + Funciones: + - Dashboard de propiedades + - Analytics de mercado + - Alertas configuradas + - Perfil y configuracion + +Portal 2 - Admin Cliente (Tenant Admin): + URL: admin.{tenant}.inmobiliaria-analytics.com + Funciones: + - Gestion de usuarios + - Configuracion del tenant + - Facturacion y suscripcion + - Reportes de uso + +Portal 3 - Admin SaaS (Super Admin): + URL: admin.inmobiliaria-analytics.com + Funciones: + - Gestion de todos los tenants + - Configuracion de planes + - Monitoreo del sistema + - Analytics globales +``` + +### Modulos SaaS + +| ID | Modulo | Descripcion | Documento | +|----|--------|-------------|-----------| +| IA-004 | Tenants | Multi-tenancy con RLS | [IA-004-TENANTS](../02-definicion-modulos/IA-004-TENANTS.md) | +| IA-005 | Payments | Integracion Stripe | [IA-005-PAYMENTS](../02-definicion-modulos/IA-005-PAYMENTS.md) | +| IA-006 | Portals | 3 portales diferenciados | [IA-006-PORTALS](../02-definicion-modulos/IA-006-PORTALS.md) | + +### Modulos de Datos e Inteligencia + +| ID | Modulo | Descripcion | Documento | +|----|--------|-------------|-----------| +| IA-007 | Webscraper | ETL y recoleccion de datos | [IA-007-WEBSCRAPER](../02-definicion-modulos/IA-007-WEBSCRAPER.md) | +| IA-008 | ML Analytics | Machine Learning y analytics avanzado | [IA-008-ML-ANALYTICS](../02-definicion-modulos/IA-008-ML-ANALYTICS.md) | + +--- + +## Servicios de Machine Learning + +La plataforma ofrece capacidades avanzadas de ML para analisis inmobiliario: + +### Modelos Predictivos + +| Servicio | Descripcion | Metrica Objetivo | +|----------|-------------|------------------| +| **AVM (Valuacion Automatica)** | Valuacion de propiedades basada en caracteristicas y mercado | MAPE < 10% | +| **Prediccion Tiempo de Venta** | Estima dias en mercado segun precio y condiciones | MAPE < 25% | +| **Prediccion Demanda** | Pronostico de demanda por zona geografica | Dir. Accuracy >= 70% | + +### Analisis de Oportunidades + +| Servicio | Descripcion | +|----------|-------------| +| **Detector de Subvaluadas** | Identifica propiedades con precio 10-20% bajo mercado | +| **Zonas Emergentes** | Detecta zonas con potencial de apreciacion | +| **Analisis ROI** | Calcula retornos proyectados para inversores | + +### Indices de Mercado + +| Indice | Descripcion | +|--------|-------------| +| **IPV** | Indice de Precios de Vivienda (Case-Shiller) | +| **IAV** | Indice de Accesibilidad a Vivienda | +| **Absorcion** | Meses de inventario (oferta vs demanda) | +| **IAM** | Indice de Actividad de Mercado | + +### Reportes Profesionales + +**Para Agentes:** +- CMA (Comparative Market Analysis) +- Market Snapshot semanal +- Listing Performance + +**Para Inversores:** +- Investment Analysis Report +- Portfolio Performance +- Alertas de oportunidades + +**Para Desarrolladores:** +- Feasibility Study +- Demand Analysis +- Project Tracking + +--- + +## Sistema de Recoleccion de Datos (Webscraper) + +La plataforma recolecta datos de multiples fuentes inmobiliarias: + +### Fuentes de Datos + +| Fuente | Tipo | Proteccion | +|--------|------|------------| +| Inmuebles24 | Portal inmobiliario | Cloudflare | +| Vivanuncios | Portal inmobiliario | Cloudflare | +| Segundamano | Portal inmobiliario | Basica | +| INEGI | Datos demograficos | API publica | + +### Estrategias Anti-Bloqueo + +- Browser automation con Playwright (stealth mode) +- Rotacion de proxies residenciales +- Rate limiting inteligente +- Simulacion de comportamiento humano + +### Pipeline ETL + +``` +Scraping -> Raw Storage -> Normalizacion -> Geocoding -> PostgreSQL +``` + +--- + +## Referencias + +- [ARQUITECTURA-GENERAL.md](./ARQUITECTURA-GENERAL.md) +- [STACK-TECNOLOGICO.md](./STACK-TECNOLOGICO.md) +- [Modulos SaaS](../02-definicion-modulos/) +- [ADR-001-stack-tecnologico.md](../97-adr/ADR-001-stack-tecnologico.md) + +--- + +**Documento:** Vision del Producto +**Version:** 1.0.0 +**Estado:** Draft +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/00-vision-general/Webscraper_Politics.md b/docs/00-vision-general/Webscraper_Politics.md new file mode 100644 index 0000000..5fc3aab --- /dev/null +++ b/docs/00-vision-general/Webscraper_Politics.md @@ -0,0 +1,78 @@ +Web scraping efectivo sin ser bloqueado por Cloudflare +Desafíos de hacer scraping en sitios con Cloudflare +Realizar web scraping en páginas protegidas por Cloudflare (como FlashScore o Inmuebles24) es complicado debido a las robustas medidas anti-bot que implementa este servicio. Cloudflare cuenta con un sistema de Bot Management que puntúa cada solicitud HTTP con modelos de machine learning para decidir si es un humano o un scraper automatizado +hardwarepremium.com +. Estas medidas se han vuelto estratégicas en la “guerra contra los scrapers de IA”, al punto que Cloudflare ha experimentado con esquemas de “pay per crawl” (pago por rastreo) para permitir el acceso a ciertos bots legítimos bajo acuerdo comercial +hardwarepremium.com +. En la práctica, esto significa que un scraper tradicional puede ser identificado y bloqueado rápidamente si no toma precauciones. Cloudflare utiliza múltiples técnicas para detectar bots. Analiza enormes volúmenes de tráfico (decenas de millones de solicitudes por segundo) aplicando huellas digitales de la petición (cabeceras, TLS, patrón de HTTP), señales de comportamiento (p. ej. movimientos de mouse, tiempos entre acciones) y estadísticas globales de su red +blog.cloudflare.com +scrapfly.io +. Por ejemplo, si un navegador automatizado tiene características obvias (como la propiedad JavaScript navigator.webdriver activada, o usar un motor HTTP sin navegador real), Cloudflare puede responder con un Error 1010 (Access Denied) indicando que la huella digital fue marcada como automatizada +scrapfly.io +. Asimismo, Cloudflare monitorea la velocidad y volumen de peticiones: si un solo IP realiza muchas solicitudes en poco tiempo, puede gatillar un Error 1015 (rate limited) por exceder el umbral permitido +scrapfly.io +. Otros códigos comunes incluyen el Error 1020 (Access Denied) cuando alguna regla de firewall bloquea el acceso, o desafíos como la página de "Attention Required!" con CAPTCHA Turnstile si sospecha tráfico no humano. Además de Cloudflare, hay que considerar las políticas de los sitios web en sí. Sitios como FlashScore o Inmuebles24 suelen establecer en sus términos de uso que no se permite la extracción automatizada de datos sin autorización. Muchas páginas también indican en su archivo robots.txt qué está permitido para bots: por ejemplo, la política de robots de Inmuebles24 prohíbe indexar más allá de la página 5 de listados (disallow de URLs con pagina-*.html excepto las primeras páginas) +inmuebles24.com +. Esto implica que una exploración profunda de todas las propiedades excedería lo que un bot de buscador normal debería hacer, y cualquier scraper que ignore estas reglas podría ser detectado o considerado como conducta no deseada. Ignorar las normativas de un sitio (términos de uso y robots.txt) no solo aumenta la probabilidad de bloqueo, sino que podría acarrear consecuencias legales o el bloqueo permanente de sus direcciones IP si el propietario del sitio toma medidas activas. En resumen, el principal desafío es parecer un usuario legítimo ante Cloudflare y el sitio destino. Para lograrlo, el scraper debe lidiar con desafíos de JavaScript, CAPTCHAs invisibles (Turnstile), detección de patrones inusuales, restricciones de velocidad, y a la vez respetar en la medida de lo posible las condiciones del sitio para evitar conflictos legales. +Estrategias para evitar bloqueos de Cloudflare +Superar las defensas de Cloudflare requiere adoptar varias técnicas de sigilo y buenas prácticas en la implementación de tu web scraper. A continuación, se presentan estrategias efectivas, respaldadas por guías especializadas, para minimizar la detección: +Emular un navegador real: Es fundamental que las peticiones de tu scraper se asemejen a las de un navegador humano. Esto implica incluir headers HTTP típicos (agente de usuario moderno, encabezados de idioma, encoding, etc.), soportar conexiones TLS modernas y usar HTTP/2 si es posible +scrapfly.io +. Un scraper que simplemente use requests de Python con su agente de usuario por defecto será fácilmente marcado. En cambio, utilizar un navegador automatizado (Chrome/Firefox controlado por código) permite ejecutar el JavaScript de desafío de Cloudflare y obtener las cookies de sesión válidas. Herramientas actuales recomiendan incluso evitar usar modos headless puros sin camuflaje; en 2025 existen navegadores modificados para automatización (anti-detect browsers) que eliminan huellas del modo headless. Por ejemplo, Nodriver (sucesor de undetected-chromedriver) se diseñó para comunicarse con Chrome mediante el protocolo DevTools sin dejar rastro de WebDriver +scrapfly.io +. De igual forma, proyectos como SeleniumBase (modo UC) o Camoufox aplican parches al navegador (Chrome o Firefox respectivamente) para que los scripts anti-bot no detecten propiedades típicas de Selenium +scrapfly.io +. La recomendación es usar estas soluciones stealth activamente mantenidas, en lugar de librerías obsoletas; por ejemplo, el plugin puppeteer-stealth para Node fue descontinuado en 2025 en favor de alternativas más avanzadas +scrapfly.io +. +Rotación y calidad de IPs: Otro pilar es la gestión inteligente de las direcciones IP desde las cuales haces las peticiones. Cloudflare verifica la reputación de IP y puede bloquear rangos asociados a centros de datos o VPNs. Por ello, es aconsejable utilizar proxies residenciales o IPs de red móvil, que se confunden mejor entre el tráfico normal +scrapfly.io +. Si tu scraper va a extraer grandes volúmenes de datos (por ejemplo, cargar todo el historial de 10 años de partidos), distribuye las solicitudes en múltiples IP para no saturar una sola y evitar límites de tasa +scrapfly.io +. Muchos servicios ofrecen rotating proxies (IPs rotativas) donde cada petición puede salir por una IP distinta de un pool residencial. Esto previene bloqueos por exceso de peticiones desde un mismo origen y sortea bloqueos geográficos (por ejemplo, Cloudflare Error 1009 si la página prohíbe cierto país) cambiando la región de salida. La inversión en proxies de calidad puede ser necesaria dentro de tu presupuesto, pero existen opciones relativamente económicas (algunos proveedores ofrecen millones de IP rotativas por montos dentro de $50-$100 mensuales, dependiendo del uso). Alternativamente, si operas tu scraper en la nube, elegir proveedores menos populares o distribuir entre varios podría ayudar, aunque Cloudflare entrena sus modelos incluso para detectar tráfico de nubes públicas de forma más agresiva +blog.cloudflare.com +. +Controlar la velocidad y patrón de rastreo: Un raspador eficaz imita el comportamiento humano no solo en la configuración técnica sino en cómo navega. Implementa retrasos aleatorios entre peticiones, evita hacer clics o visitas a múltiples páginas por segundo, y programa el scraping pesado en horarios de menor tráfico para el sitio. Introducir cierta aleatoriedad en los tiempos y secuencias dificulta que un modelo de Cloudflare descubra patrones repetitivos exactos +scrapfly.io +. Por ejemplo, en lugar de scrapear 1000 páginas en un solo minuto desde la misma sesión, un enfoque más sigiloso sería procesar en lotes pequeños con pausas, cambiar de identidad (cookies/IP) periódicamente, y simular incluso alguna interacción intermedia (como cargar recursos asociados, desplazarse por la página, etc., acciones que un humano haría). Esto último puede lograrse con navegadores automatizados controlando eventos de scroll, movimientos del ratón ficticios o pausas al renderizar antes de extraer datos. El objetivo es presentar flujos de navegación naturales en vez de un bombardeo de peticiones uniformes +scrapfly.io +. +Manejo de CAPTCHA y desafíos: En algunos casos, pese a nuestras medidas, nos toparemos con desafíos de Cloudflare como el Turnstile CAPTCHA. Este sistema, introducido en 2022, es más discreto que los CAPTCHAs tradicionales: a veces es invisible o se resuelve en segundo plano analizando el navegador, y solo muestra un desafío interactivo si la puntuación de confianza es baja +scrapfly.io +scrapfly.io +. Para superarlo, hay dos enfoques: resolverlo o prevenirlo. La prevención consiste en aplicar todo lo anterior para no disparar alarmas (lo ideal es que el desafío ni aparezca porque el tráfico pareció legítimo). Si aun así aparece un CAPTCHA, necesitarás integrar un servicio de resolución (por ejemplo, 2Captcha, Anti-Captcha, etc.), donde un trabajador humano o un modelo ML aparte resuelve el desafío y devuelve el token +scrapfly.io +scrapfly.io +. Algunos frameworks de navegación headless traen ayudas para esto – por ejemplo, SeleniumBase UC tiene métodos integrados para reconocer y resolver CAPTCHAs comunes +scrapfly.io +. Ten en cuenta que cada solicitud de CAPTCHA resuelto puede costar unos centavos y añadir retraso. En la medida de lo posible, refina tus técnicas de sigilo para evitar los CAPTCHA proactivamente, ya que Cloudflare misma indica que “la mejor manera de sortear un CAPTCHA es impedir que ocurra en primer lugar” +scrapfly.io +. +Uso de herramientas y servicios especializados: Dada la complejidad técnica de evadir Cloudflare, vale la pena considerar herramientas ya existentes o servicios de terceros dentro de tu presupuesto. Por el lado de código abierto, ya mencionamos bibliotecas como undetected-chromedriver (y su sucesor Nodriver) para Python, o Playwright con plugins de stealth para Node.js. Por ejemplo, existe un proyecto de código abierto que usa Playwright para extraer resultados de FlashScore de forma estructurada +github.com +github.com +, lo que confirma que es viable scrapear estos datos combinando un navegador real con algo de desarrollo. Si prefieres no reinventar la rueda, hay servicios en la nube que ofrecen scraping anti-bot como servicio: Apify, ScrapingBee, Zyte (Scrapinghub) entre otros. Algunos cuentan con APIs o actors ya preparados para sitios populares. En el caso de FlashScore, Apify tiene un actor público que provee una “API alternativa” para datos en vivo, cobrando alrededor de $1 por cada 1000 resultados extraídos +apify.com +. Igualmente, hay un actor para Inmuebles24 en Apify Store +apify.com +. Estos servicios manejan por ti las cuestiones de proxies, rotación de IP, y resolución de desafíos, lo que puede ahorrar tiempo de desarrollo. Incluso Apify ofrece la opción de integrar agentes de IA (como ChatGPT) con su MCP – un servidor que permite a modelos de lenguaje orquestar scrapers automáticamente +apify.com +. Esto podría ser útil si buscas que un agente de IA navegue páginas complejas simulando decisiones humanas; por ejemplo, un agente podría decidir qué enlaces de inmuebles seguir para recopilar cierta información detallada, usando las herramientas de scraping subyacentes para ejecutar las acciones. No obstante, estas soluciones deben evaluarse en costo: con un presupuesto inicial de $100-$200, podrías ejecutar scrapers propios en uno o dos servidores con proxies básicos, mientras que servicios SaaS de scraping cobrarán recurrentemente (aunque para volúmenes moderados pueden encajar en ese rango). Es importante equilibrar el ahorro de tiempo/desarrollo que brindan con el costo por volumen de datos que requerirás. +Consideraciones legales y de cumplimiento +Al diseñar tu web scraper es imprescindible no solo pensar en lo técnico, sino también en cumplir las normativas legales y de uso de los sitios objetivo: +Revisa los Términos de Servicio: Tanto FlashScore como Inmuebles24 probablemente prohíban explícitamente el uso de bots o la extracción masiva de contenido sin permiso. Aunque la aplicación de estas cláusulas varía, incumplirlas podría llevar a que te bloqueen el acceso permanentemente, te envíen notificaciones legales, e incluso potenciales demandas si el scraping causa perjuicios. Si planeas ofrecer un servicio comercial basado en los datos, es aún más crítico asegurarte de que no estás violando derechos de propiedad de los datos o base de datos del sitio. En casos ideales, buscar vías oficiales: por ejemplo, ¿existe una API pública o de pago? (FlashScore no ofrece API pública +github.com +, de ahí que prolifere el scraping, pero conviene verificar). En el ámbito inmobiliario, quizás puedas obtener datos mediante acuerdos con agencias o usando fuentes públicas complementarias para ciertos datos (catastros, etc.) reduciendo la dependencia en scraping intensivo de un solo sitio. +Respeta límites mediante robots.txt: Aunque el archivo robots.txt no es legalmente vinculante por sí mismo, es una guía de lo que el propietario del sitio espera de los robots. Cumplirlo muestra buena fe. En el caso de Inmuebles24 vimos que impone límites a la profundidad de listados indexables +inmuebles24.com +; tal restricción sugiere que un scraper que ignore ese límite (para obtener todas las propiedades) estaría actuando fuera de las expectativas del sitio. Si decides exceder lo estipulado, hazlo con mucha cautela (por ejemplo, limitando la frecuencia significativamente) para no parecer un ataque. En todo caso, no intentes evadir medidas activas de seguridad (como el propio Cloudflare) más allá de lo razonable, pues eso sí podría considerarse hacking. La clave está en acceder a la información pública de manera responsable y ética, sin afectar negativamente al servicio. +Buena ciudadanía web: Mantén un perfil bajo. Identifícate adecuadamente cuando sea posible. Por ejemplo, algunas API o sitios pueden permitir acceso si el bot se identifica honestamente (muchos no, pero vale la pena ver si FlashScore/Inmuebles24 tienen algún programa de bot access para investigadores, etc.). Si no, al menos en tus registros y documentación interna lleva cuenta de las páginas consultadas, respeta si un usuario necesita estar autenticado (no scrapees datos privados o detrás de login si eso viola términos), y nunca publiques o revendas datos de forma que infrinja derechos de terceros (fotos, descripciones con copyright, etc., podrían estar protegidos). Un caso práctico: Inmuebles24 probablemente tiene fotografías de propiedades con derechos; extraerlas para tu propio sitio sería ilegal sin permiso expreso. En cambio, datos estadísticos agregados (precios promedio, tendencias) son menos problemáticos siempre que la fuente original no sea identificable directamente. +En conclusión, sí es posible construir un web scraper eficaz para los proyectos que mencionas – de hecho, numerosos desarrolladores lo hacen para propósitos similares (análisis inmobiliario, modelos de apuestas deportivas). La clave del éxito estará en combinar las tácticas técnicas (navegador automatizado, proxies residenciales, rotación, stealth anti-detección) con un enfoque respetuoso: racionando el consumo de datos, acatando en lo posible las reglas de cada sitio, y manteniéndote dentro de un marco legal. Siguiendo las recomendaciones anteriores, tu scraper podrá realizar cargas iniciales de datos muy grandes (como todo el histórico de inmuebles o 10 años de estadísticas deportivas) sin ser bloqueado inmediatamente, y luego continuar con actualizaciones periódicas de forma sostenible. Recuerda siempre monitorear el comportamiento de tu scraper; si notas captchas frecuentes, bloqueos HTTP 429/1020, o respuestas anómalas, ajusta la estrategia rápidamente – la evasión de detección es un juego dinámico donde tanto Cloudflare como los scrapers evolucionan constantemente +scrapfly.io +. Con una inversión moderada (dentro de tu presupuesto indicado) en las herramientas adecuadas y quizás algún servicio de apoyo, podrás obtener los datos necesarios minimizando riesgos de interrupción y conflictos legales. Fuentes Consultadas: Guías técnicas de ScrapFly sobre Cloudflare +scrapfly.io +scrapfly.io +, publicaciones oficiales de Cloudflare sobre Bot Management +hardwarepremium.com +hardwarepremium.com +, documentación de proyectos de scraping de FlashScore e Inmuebles24, y experiencias compartidas en la comunidad de web scraping. Estas referencias respaldan las buenas prácticas aquí descritas y reflejan el estado del arte hasta 2025 en este campo. \ No newline at end of file diff --git a/docs/00-vision-general/_MAP.md b/docs/00-vision-general/_MAP.md new file mode 100644 index 0000000..1a0b179 --- /dev/null +++ b/docs/00-vision-general/_MAP.md @@ -0,0 +1,53 @@ +--- +id: "MAP-00-VISION" +title: "Mapa de Navegacion - Vision General" +type: "Navigation Map" +section: "00-vision-general" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: 00-vision-general + +**Seccion:** Vision General +**Proyecto:** Inmobiliaria Analytics +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +Documentos que definen la vision, arquitectura y stack tecnologico del proyecto Inmobiliaria Analytics. + +--- + +## Contenido + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| [VISION-PRODUCTO.md](./VISION-PRODUCTO.md) | Vision del Producto | Draft | +| [ARQUITECTURA-GENERAL.md](./ARQUITECTURA-GENERAL.md) | Arquitectura General | Draft | +| [STACK-TECNOLOGICO.md](./STACK-TECNOLOGICO.md) | Stack Tecnologico | Active | +| [Webscraper_Politics.md](./Webscraper_Politics.md) | Politicas de Web Scraping | Reference | + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Siguiente:** [01-fase-alcance-inicial/](../01-fase-alcance-inicial/_MAP.md) + +--- + +## Estadisticas + +| Metrica | Valor | +|---------|-------| +| Total documentos | 4 | +| Documentos activos | 1 | +| Documentos draft | 2 | +| Documentos referencia | 1 | + +--- + +**Generado:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-001-fundamentos/README.md b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/README.md new file mode 100644 index 0000000..719dd70 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/README.md @@ -0,0 +1,167 @@ +--- +id: "IA-001" +title: "EPIC IA-001: Fundamentos" +type: "Epic" +status: "Planned" +priority: "Alta" +phase: "01 - Alcance Inicial" +story_points: 40 +budget: "$15,000 MXN" +sprint: "Sprint 1-3" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# IA-001: Fundamentos + +**Epica:** IA-001 +**Nombre:** Fundamentos del Sistema +**Fase:** 01 - Alcance Inicial +**Story Points:** 40 SP (estimado) +**Presupuesto:** $15,000 MXN (estimado) +**Estado:** Planned +**Sprint:** Sprint 1-3 + +--- + +## Proposito + +Establecer los fundamentos tecnicos del sistema Inmobiliaria Analytics, incluyendo autenticacion, gestion de usuarios, y configuracion base de la aplicacion. + +--- + +## Alcance + +### Incluye + +1. **Autenticacion y Autorizacion** + - Login/logout de usuarios + - JWT tokens + - Roles y permisos basicos + - Refresh tokens + +2. **Gestion de Usuarios** + - CRUD de usuarios + - Perfiles de usuario + - Preferencias + +3. **Configuracion de Aplicacion** + - Variables de entorno + - Configuracion de base de datos + - CORS y seguridad basica + +4. **Health Check y Monitoreo Basico** + - Endpoint /health + - Logs estructurados + - Metricas basicas + +### No Incluye + +- Modulos de propiedades (IA-002) +- Modulos de analytics (IA-003) +- Integraciones externas (IA-005) +- Frontend completo (solo login/registro) + +--- + +## Objetivos + +1. Usuario puede registrarse e iniciar sesion +2. Sistema valida tokens JWT correctamente +3. Roles Admin/User funcionan +4. API responde en <200ms promedio +5. 90% test coverage en modulos core + +--- + +## Estado Actual + +El backend tiene un scaffold basico con: +- NestJS configurado +- AuthModule placeholder (sin implementacion) +- Configuracion de TypeORM lista +- Estructura de carpetas definida + +--- + +## Trabajo Pendiente + +### Autenticacion (Auth Module) + +- [ ] Implementar AuthService +- [ ] Implementar LocalStrategy (login) +- [ ] Implementar JwtStrategy (validacion) +- [ ] Implementar AuthController +- [ ] Crear AuthGuard + +### Usuarios (Users Module) + +- [ ] Crear User entity +- [ ] Implementar UsersService +- [ ] Implementar UsersController +- [ ] Crear DTOs (CreateUser, UpdateUser) +- [ ] Validaciones de usuario + +### Base de Datos + +- [ ] Crear migration inicial +- [ ] Schema de usuarios +- [ ] Schema de roles +- [ ] Seeds de datos basicos + +### Testing + +- [ ] Tests unitarios AuthService +- [ ] Tests unitarios UsersService +- [ ] Tests e2e de flujo auth +- [ ] Tests de guards + +--- + +## Dependencias + +### Antes (Bloqueantes) + +- [x] Setup de proyecto NestJS +- [x] Configuracion de Docker +- [x] Definicion de stack tecnologico (ADR-001) + +### Despues (Dependientes) + +- [ ] IA-002: Propiedades (requiere auth) +- [ ] IA-003: Analytics (requiere users) + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Cambios en requerimientos auth | Media | Alto | Diseno flexible con strategies | +| Performance de JWT validation | Baja | Medio | Caching de tokens | +| Complejidad de roles | Media | Medio | RBAC simple inicial | + +--- + +## Metricas + +| Metrica | Objetivo | Actual | +|---------|----------|--------| +| Story Points estimados | 40 SP | - | +| RF documentados | 5 | 0 | +| US documentadas | 8 | 0 | +| Tests coverage | 90% | 0% | + +--- + +## Referencias + +- [STACK-TECNOLOGICO.md](../../00-vision-general/STACK-TECNOLOGICO.md) +- [ADR-001-stack-tecnologico.md](../../97-adr/ADR-001-stack-tecnologico.md) +- [_MAP.md](./_MAP.md) + +--- + +**Creado:** 2026-01-04 +**Actualizado:** 2026-01-04 +**Responsable:** @Backend-Agent diff --git a/docs/01-fase-alcance-inicial/IAI-001-fundamentos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/_MAP.md new file mode 100644 index 0000000..4aa1e81 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/_MAP.md @@ -0,0 +1,101 @@ +--- +id: "MAP-IA-001" +title: "Mapa de Navegacion - EPIC IA-001 Fundamentos" +type: "Navigation Map" +epic: "IA-001" +phase: "01-fase-alcance-inicial" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: IA-001 - Fundamentos + +**Epica:** IA-001 +**Nombre:** Fundamentos del Sistema +**Fase:** 01 - Alcance Inicial +**Story Points:** 40 SP +**Estado:** Planned +**Sprint:** Sprint 1-3 +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +EPIC que establece los fundamentos tecnicos del sistema incluyendo autenticacion, gestion de usuarios, y configuracion base. + +--- + +## Contenido + +### Documentacion Base + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| [README.md](./README.md) | Descripcion de la EPIC | Active | + +### Requerimientos Funcionales (0) + +| ID | Archivo | Titulo | Estado | +|----|---------|--------|--------| +| - | Pendiente | - | - | + +### Especificaciones Tecnicas (0) + +| ID | Archivo | Titulo | RF | Estado | +|----|---------|--------|-----|--------| +| - | Pendiente | - | - | - | + +### Historias de Usuario (0) + +| ID | Archivo | Titulo | SP | Estado | +|----|---------|--------|----|--------| +| - | Pendiente | - | - | - | + +**Total Story Points:** 0 SP (documentados) + +### Implementacion + +| Archivo | Proposito | +|---------|-----------| +| Pendiente | TRACEABILITY.yml | +| Pendiente | DATABASE.yml | +| Pendiente | BACKEND.yml | + +--- + +## Metricas + +| Metrica | Valor | +|---------|-------| +| **Presupuesto estimado** | $15,000 MXN | +| **Story Points estimados** | 40 SP | +| **RF documentados** | 0 | +| **US documentadas** | 0 | +| **ET documentadas** | 0 | + +--- + +## Navegacion + +- **Arriba:** [01-fase-alcance-inicial/](../_MAP.md) +- **Fase:** [docs/](../../_MAP.md) + +--- + +## Estructura de Carpetas + +``` +IA-001-fundamentos/ +├── README.md # Este archivo de descripcion +├── _MAP.md # Mapa de navegacion +├── requerimientos/ # RFs (pendiente) +├── especificaciones/ # ETs (pendiente) +├── historias-usuario/ # US (pendiente) +└── implementacion/ # Trazabilidad (pendiente) +``` + +--- + +**Generado:** 2026-01-04 +**Mantenedores:** @Backend-Agent diff --git a/docs/01-fase-alcance-inicial/IAI-001-fundamentos/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/especificaciones/_MAP.md new file mode 100644 index 0000000..aeb4d61 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/especificaciones/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-001-ESPE" +title: "Mapa Especificaciones IAI-001" +type: "Navigation Map" +epic: "IAI-001" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones - IAI-001 Fundamentos + +**EPIC:** IAI-001 +**Seccion:** Especificaciones + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-001-fundamentos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-001-fundamentos/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/historias-usuario/_MAP.md new file mode 100644 index 0000000..6e05bde --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/historias-usuario/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-001-HIST" +title: "Mapa Historias-usuario IAI-001" +type: "Navigation Map" +epic: "IAI-001" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias-usuario - IAI-001 Fundamentos + +**EPIC:** IAI-001 +**Seccion:** Historias-usuario + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-001-fundamentos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-001-fundamentos/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/implementacion/_MAP.md new file mode 100644 index 0000000..2f399aa --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/implementacion/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-001-IMPL" +title: "Mapa Implementacion IAI-001" +type: "Navigation Map" +epic: "IAI-001" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-001 Fundamentos + +**EPIC:** IAI-001 +**Seccion:** Implementacion + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-001-fundamentos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-001-fundamentos/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/requerimientos/_MAP.md new file mode 100644 index 0000000..822e44d --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/requerimientos/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-001-REQU" +title: "Mapa Requerimientos IAI-001" +type: "Navigation Map" +epic: "IAI-001" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-001 Fundamentos + +**EPIC:** IAI-001 +**Seccion:** Requerimientos + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-001-fundamentos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-001-fundamentos/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/tareas/_MAP.md new file mode 100644 index 0000000..9d8f570 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-001-fundamentos/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-001-TARE" +title: "Mapa Tareas IAI-001" +type: "Navigation Map" +epic: "IAI-001" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas - IAI-001 Fundamentos + +**EPIC:** IAI-001 +**Seccion:** Tareas + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-001-fundamentos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-002-propiedades/README.md b/docs/01-fase-alcance-inicial/IAI-002-propiedades/README.md new file mode 100644 index 0000000..417d82e --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-002-propiedades/README.md @@ -0,0 +1,73 @@ +--- +id: "EPIC-IAI-002" +title: "EPIC IAI-002 - Propiedades" +type: "Epic Document" +epic: "IAI-002" +status: "Planned" +story_points: 34 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# EPIC IAI-002: Gestion de Propiedades + +## Vision + +Sistema CRUD completo para gestion de propiedades inmobiliarias, incluyendo listado, busqueda avanzada, filtros y detalle de propiedades. + +--- + +## Alcance + +### Incluye + +- CRUD completo de propiedades +- Busqueda y filtrado avanzado +- Geolocalizacion y mapas +- Galeria de imagenes +- Favoritos de usuarios +- Comparador de propiedades + +### No Incluye + +- Valuacion automatica (ver IAI-008) +- Web scraping (ver IAI-007) +- Pagos (ver IAI-005) + +--- + +## Dependencias + +### Depende de + +- IAI-001: Fundamentos (autenticacion, usuarios base) + +### Bloquea a + +- IAI-007: Web Scraping (necesita modelo de propiedades) +- IAI-008: ML Analytics (necesita datos de propiedades) + +--- + +## Story Points Estimados + +| Tipo | Cantidad | SP | +|------|----------|-----| +| User Stories | 5 | 34 | +| Total | - | 34 | + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Modelo de datos complejo | Media | Medio | Disenar incrementalmente | +| Performance en busquedas | Media | Alto | Indices, caching, Elasticsearch | + +--- + +**Estado:** Planned +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-002-propiedades/_MAP.md b/docs/01-fase-alcance-inicial/IAI-002-propiedades/_MAP.md new file mode 100644 index 0000000..946367b --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-002-propiedades/_MAP.md @@ -0,0 +1,68 @@ +--- +id: "MAP-IAI-002" +title: "Mapa de EPIC IAI-002 Propiedades" +type: "Navigation Map" +epic: "IAI-002" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: EPIC IAI-002 - Gestion de Propiedades + +**EPIC:** IAI-002 +**Nombre:** Gestion de Propiedades +**Estado:** Planned +**Story Points:** 34 (estimado) + +--- + +## Estructura del EPIC + +``` +IAI-002-propiedades/ +├── _MAP.md # Este archivo +├── README.md # Vision general del EPIC +│ +├── requerimientos/ +│ ├── _MAP.md +│ ├── RF-PROP-001.md # Modelo de datos +│ ├── RF-PROP-002.md # CRUD API +│ └── RF-PROP-003.md # Busqueda avanzada +│ +├── especificaciones/ +│ ├── _MAP.md +│ └── ET-PROP-001-*.md +│ +├── historias-usuario/ +│ ├── _MAP.md +│ ├── US-PROP-001.md # Listado propiedades +│ ├── US-PROP-002.md # Detalle propiedad +│ ├── US-PROP-003.md # Busqueda y filtros +│ ├── US-PROP-004.md # Favoritos +│ └── US-PROP-005.md # Comparador +│ +├── tareas/ +│ └── _MAP.md +│ +└── implementacion/ + └── _MAP.md +``` + +--- + +## Historias de Usuario (Planificadas) + +| ID | Titulo | SP | Prioridad | Sprint | +|----|--------|----|-----------| -------| +| US-PROP-001 | Listado de propiedades | 8 | Alta | - | +| US-PROP-002 | Detalle de propiedad | 5 | Alta | - | +| US-PROP-003 | Busqueda y filtros avanzados | 8 | Alta | - | +| US-PROP-004 | Favoritos de usuario | 5 | Media | - | +| US-PROP-005 | Comparador de propiedades | 8 | Media | - | + +**Total Story Points:** 34 + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-002-propiedades/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-002-propiedades/especificaciones/_MAP.md new file mode 100644 index 0000000..07ef63c --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-002-propiedades/especificaciones/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-002-ESPE" +title: "Mapa Especificaciones IAI-002" +type: "Navigation Map" +epic: "IAI-002" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones - IAI-002 Propiedades + +**EPIC:** IAI-002 +**Seccion:** Especificaciones + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-002-propiedades/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-002-propiedades/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-002-propiedades/historias-usuario/_MAP.md new file mode 100644 index 0000000..bc13597 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-002-propiedades/historias-usuario/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-002-HIST" +title: "Mapa Historias-usuario IAI-002" +type: "Navigation Map" +epic: "IAI-002" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias-usuario - IAI-002 Propiedades + +**EPIC:** IAI-002 +**Seccion:** Historias-usuario + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-002-propiedades/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-002-propiedades/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-002-propiedades/implementacion/_MAP.md new file mode 100644 index 0000000..da8d2ff --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-002-propiedades/implementacion/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-002-IMPL" +title: "Mapa Implementacion IAI-002" +type: "Navigation Map" +epic: "IAI-002" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-002 Propiedades + +**EPIC:** IAI-002 +**Seccion:** Implementacion + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-002-propiedades/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-002-propiedades/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-002-propiedades/requerimientos/_MAP.md new file mode 100644 index 0000000..69dc58e --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-002-propiedades/requerimientos/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-002-REQU" +title: "Mapa Requerimientos IAI-002" +type: "Navigation Map" +epic: "IAI-002" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-002 Propiedades + +**EPIC:** IAI-002 +**Seccion:** Requerimientos + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-002-propiedades/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-002-propiedades/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-002-propiedades/tareas/_MAP.md new file mode 100644 index 0000000..188688b --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-002-propiedades/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-002-TARE" +title: "Mapa Tareas IAI-002" +type: "Navigation Map" +epic: "IAI-002" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas - IAI-002 Propiedades + +**EPIC:** IAI-002 +**Seccion:** Tareas + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-002-propiedades/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-003-usuarios/README.md b/docs/01-fase-alcance-inicial/IAI-003-usuarios/README.md new file mode 100644 index 0000000..118e84e --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-003-usuarios/README.md @@ -0,0 +1,74 @@ +--- +id: "EPIC-IAI-003" +title: "EPIC IAI-003 - Usuarios y Perfiles" +type: "Epic Document" +epic: "IAI-003" +status: "Planned" +story_points: 26 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# EPIC IAI-003: Usuarios y Perfiles + +## Vision + +Sistema de gestion de usuarios, perfiles, preferencias y suscripciones para la plataforma SaaS inmobiliaria. + +--- + +## Alcance + +### Incluye + +- Registro y autenticacion (extension de IAI-001) +- Perfiles de usuario (inversor, agente, propietario) +- Preferencias y notificaciones +- Historial de actividad +- Gestion de suscripcion (UI) + +### No Incluye + +- Procesamiento de pagos (ver IAI-005) +- Multi-tenancy admin (ver IAI-004) + +--- + +## Roles de Usuario + +| Rol | Descripcion | Permisos | +|-----|-------------|----------| +| guest | Usuario anonimo | Ver propiedades publicas | +| free_user | Usuario registrado gratuito | Buscar, favoritos limitados | +| premium_user | Usuario con suscripcion | Todas las features | +| agent | Agente inmobiliario | Publicar, CMA, analytics | +| admin | Administrador | Gestion completa | + +--- + +## Dependencias + +### Depende de + +- IAI-001: Fundamentos (auth base) + +### Bloquea a + +- IAI-005: Pagos (necesita modelo de usuario) +- IAI-008: ML Analytics (alertas personalizadas) + +--- + +## Story Points Estimados + +| Tipo | Cantidad | SP | +|------|----------|-----| +| User Stories | 4 | 26 | +| Total | - | 26 | + +--- + +**Estado:** Planned +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-003-usuarios/_MAP.md b/docs/01-fase-alcance-inicial/IAI-003-usuarios/_MAP.md new file mode 100644 index 0000000..611a4f4 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-003-usuarios/_MAP.md @@ -0,0 +1,65 @@ +--- +id: "MAP-IAI-003" +title: "Mapa de EPIC IAI-003 Usuarios" +type: "Navigation Map" +epic: "IAI-003" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: EPIC IAI-003 - Usuarios y Perfiles + +**EPIC:** IAI-003 +**Nombre:** Usuarios y Perfiles +**Estado:** Planned +**Story Points:** 26 (estimado) + +--- + +## Estructura del EPIC + +``` +IAI-003-usuarios/ +├── _MAP.md # Este archivo +├── README.md # Vision general del EPIC +│ +├── requerimientos/ +│ ├── _MAP.md +│ ├── RF-USER-001.md # Modelo de perfiles +│ └── RF-USER-002.md # Preferencias +│ +├── especificaciones/ +│ ├── _MAP.md +│ └── ET-USER-001-*.md +│ +├── historias-usuario/ +│ ├── _MAP.md +│ ├── US-USER-001.md # Registro y perfil +│ ├── US-USER-002.md # Preferencias +│ ├── US-USER-003.md # Historial actividad +│ └── US-USER-004.md # Gestion suscripcion +│ +├── tareas/ +│ └── _MAP.md +│ +└── implementacion/ + └── _MAP.md +``` + +--- + +## Historias de Usuario (Planificadas) + +| ID | Titulo | SP | Prioridad | Sprint | +|----|--------|----|-----------| -------| +| US-USER-001 | Registro y perfil de usuario | 8 | Alta | - | +| US-USER-002 | Preferencias y notificaciones | 5 | Media | - | +| US-USER-003 | Historial de actividad | 5 | Baja | - | +| US-USER-004 | Gestion de suscripcion | 8 | Alta | - | + +**Total Story Points:** 26 + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-003-usuarios/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-003-usuarios/especificaciones/_MAP.md new file mode 100644 index 0000000..44da675 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-003-usuarios/especificaciones/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-003-ESPE" +title: "Mapa Especificaciones IAI-003" +type: "Navigation Map" +epic: "IAI-003" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones - IAI-003 Usuarios + +**EPIC:** IAI-003 +**Seccion:** Especificaciones + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-003-usuarios/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-003-usuarios/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-003-usuarios/historias-usuario/_MAP.md new file mode 100644 index 0000000..c6e9fe4 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-003-usuarios/historias-usuario/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-003-HIST" +title: "Mapa Historias-usuario IAI-003" +type: "Navigation Map" +epic: "IAI-003" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias-usuario - IAI-003 Usuarios + +**EPIC:** IAI-003 +**Seccion:** Historias-usuario + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-003-usuarios/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-003-usuarios/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-003-usuarios/implementacion/_MAP.md new file mode 100644 index 0000000..9a53a4c --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-003-usuarios/implementacion/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-003-IMPL" +title: "Mapa Implementacion IAI-003" +type: "Navigation Map" +epic: "IAI-003" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-003 Usuarios + +**EPIC:** IAI-003 +**Seccion:** Implementacion + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-003-usuarios/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-003-usuarios/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-003-usuarios/requerimientos/_MAP.md new file mode 100644 index 0000000..6e9797e --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-003-usuarios/requerimientos/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-003-REQU" +title: "Mapa Requerimientos IAI-003" +type: "Navigation Map" +epic: "IAI-003" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-003 Usuarios + +**EPIC:** IAI-003 +**Seccion:** Requerimientos + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-003-usuarios/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-003-usuarios/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-003-usuarios/tareas/_MAP.md new file mode 100644 index 0000000..33f38e3 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-003-usuarios/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-003-TARE" +title: "Mapa Tareas IAI-003" +type: "Navigation Map" +epic: "IAI-003" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas - IAI-003 Usuarios + +**EPIC:** IAI-003 +**Seccion:** Tareas + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-003-usuarios/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-004-tenants/README.md b/docs/01-fase-alcance-inicial/IAI-004-tenants/README.md new file mode 100644 index 0000000..f24ef06 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-004-tenants/README.md @@ -0,0 +1,95 @@ +--- +id: "EPIC-IAI-004" +title: "EPIC IAI-004 - Multi-Tenancy" +type: "Epic Document" +epic: "IAI-004" +status: "Planned" +story_points: 40 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# EPIC IAI-004: Sistema Multi-Tenant + +## Vision + +Arquitectura multi-tenant para soportar multiples organizaciones (agencias inmobiliarias) en una sola instancia de la aplicacion, con aislamiento de datos mediante Row-Level Security (RLS). + +--- + +## Alcance + +### Incluye + +- Modelo de tenant/organizacion +- Row-Level Security (RLS) en PostgreSQL +- Onboarding de nuevos tenants +- Administracion de tenant +- Branding por tenant (white-label basico) + +### No Incluye + +- Facturacion por tenant (ver IAI-005) +- Despliegue multi-region + +--- + +## Arquitectura + +``` +┌─────────────────────────────────────────────────────────────┐ +│ APLICACION (Single Instance) │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ Tenant A│ │ Tenant B│ │ Tenant C│ │ +│ │ (RLS) │ │ (RLS) │ │ (RLS) │ │ +│ └────┬────┘ └────┬────┘ └────┬────┘ │ +│ │ │ │ │ +│ └─────────────┼─────────────┘ │ +│ │ │ +│ ┌───────▼───────┐ │ +│ │ PostgreSQL │ │ +│ │ (RLS) │ │ +│ └───────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Dependencias + +### Depende de + +- IAI-001: Fundamentos (auth, usuarios) +- IAI-003: Usuarios (modelo base) + +### Bloquea a + +- IAI-005: Pagos (facturacion por tenant) +- IAI-006: Portales (portales por tenant) + +--- + +## Story Points Estimados + +| Tipo | Cantidad | SP | +|------|----------|-----| +| User Stories | 5 | 40 | +| Total | - | 40 | + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Data leakage entre tenants | Baja | Critico | RLS exhaustivo, testing | +| Performance con muchos tenants | Media | Alto | Indices, particionamiento | + +--- + +**Estado:** Planned +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-004-tenants/_MAP.md b/docs/01-fase-alcance-inicial/IAI-004-tenants/_MAP.md new file mode 100644 index 0000000..94c8dda --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-004-tenants/_MAP.md @@ -0,0 +1,69 @@ +--- +id: "MAP-IAI-004" +title: "Mapa de EPIC IAI-004 Tenants" +type: "Navigation Map" +epic: "IAI-004" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: EPIC IAI-004 - Multi-Tenancy + +**EPIC:** IAI-004 +**Nombre:** Sistema Multi-Tenant +**Estado:** Planned +**Story Points:** 40 (estimado) + +--- + +## Estructura del EPIC + +``` +IAI-004-tenants/ +├── _MAP.md # Este archivo +├── README.md # Vision general del EPIC +│ +├── requerimientos/ +│ ├── _MAP.md +│ ├── RF-TENT-001.md # Modelo de tenant +│ ├── RF-TENT-002.md # RLS policies +│ └── RF-TENT-003.md # Admin tenant +│ +├── especificaciones/ +│ ├── _MAP.md +│ ├── ET-TENT-001-rls.md # Implementacion RLS +│ └── ET-TENT-002-onboard.md # Flujo onboarding +│ +├── historias-usuario/ +│ ├── _MAP.md +│ ├── US-TENT-001.md # Registro organizacion +│ ├── US-TENT-002.md # Onboarding wizard +│ ├── US-TENT-003.md # Admin de tenant +│ ├── US-TENT-004.md # Branding basico +│ └── US-TENT-005.md # Miembros de org +│ +├── tareas/ +│ └── _MAP.md +│ +└── implementacion/ + └── _MAP.md +``` + +--- + +## Historias de Usuario (Planificadas) + +| ID | Titulo | SP | Prioridad | Sprint | +|----|--------|----|-----------| -------| +| US-TENT-001 | Registro de organizacion | 8 | Alta | - | +| US-TENT-002 | Onboarding wizard | 8 | Alta | - | +| US-TENT-003 | Panel de admin tenant | 8 | Alta | - | +| US-TENT-004 | Configuracion de branding | 8 | Media | - | +| US-TENT-005 | Gestion de miembros | 8 | Media | - | + +**Total Story Points:** 40 + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-004-tenants/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-004-tenants/especificaciones/_MAP.md new file mode 100644 index 0000000..ad55705 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-004-tenants/especificaciones/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-004-ESPE" +title: "Mapa Especificaciones IAI-004" +type: "Navigation Map" +epic: "IAI-004" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones - IAI-004 Tenants + +**EPIC:** IAI-004 +**Seccion:** Especificaciones + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-004-tenants/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-004-tenants/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-004-tenants/historias-usuario/_MAP.md new file mode 100644 index 0000000..f7f69de --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-004-tenants/historias-usuario/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-004-HIST" +title: "Mapa Historias-usuario IAI-004" +type: "Navigation Map" +epic: "IAI-004" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias-usuario - IAI-004 Tenants + +**EPIC:** IAI-004 +**Seccion:** Historias-usuario + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-004-tenants/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-004-tenants/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-004-tenants/implementacion/_MAP.md new file mode 100644 index 0000000..c2ec4ed --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-004-tenants/implementacion/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-004-IMPL" +title: "Mapa Implementacion IAI-004" +type: "Navigation Map" +epic: "IAI-004" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-004 Tenants + +**EPIC:** IAI-004 +**Seccion:** Implementacion + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-004-tenants/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-004-tenants/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-004-tenants/requerimientos/_MAP.md new file mode 100644 index 0000000..350fc90 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-004-tenants/requerimientos/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-004-REQU" +title: "Mapa Requerimientos IAI-004" +type: "Navigation Map" +epic: "IAI-004" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-004 Tenants + +**EPIC:** IAI-004 +**Seccion:** Requerimientos + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-004-tenants/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-004-tenants/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-004-tenants/tareas/_MAP.md new file mode 100644 index 0000000..a94782f --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-004-tenants/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-004-TARE" +title: "Mapa Tareas IAI-004" +type: "Navigation Map" +epic: "IAI-004" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas - IAI-004 Tenants + +**EPIC:** IAI-004 +**Seccion:** Tareas + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-004-tenants/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-005-pagos/README.md b/docs/01-fase-alcance-inicial/IAI-005-pagos/README.md new file mode 100644 index 0000000..32bafee --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-005-pagos/README.md @@ -0,0 +1,84 @@ +--- +id: "EPIC-IAI-005" +title: "EPIC IAI-005 - Sistema de Pagos" +type: "Epic Document" +epic: "IAI-005" +status: "Planned" +story_points: 34 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# EPIC IAI-005: Sistema de Pagos con Stripe + +## Vision + +Integracion completa con Stripe para procesamiento de suscripciones mensuales, pagos unicos y facturacion automatizada. + +--- + +## Alcance + +### Incluye + +- Integracion Stripe Checkout +- Suscripciones recurrentes +- Portal de cliente Stripe +- Webhooks de eventos +- Facturacion automatica +- Proration y cambios de plan + +### No Incluye + +- Otros procesadores de pago +- Pagos en efectivo/transferencia + +--- + +## Planes de Suscripcion + +| Plan | Precio/mes | Features | +|------|-----------|----------| +| Free | $0 | Busqueda basica, 5 favoritos | +| Basic | $299 MXN | Busqueda ilimitada, alertas basicas | +| Pro | $599 MXN | Analytics, CMA, alertas avanzadas | +| Enterprise | $1,499 MXN | Multi-usuario, API, white-label | + +--- + +## Dependencias + +### Depende de + +- IAI-001: Fundamentos (auth) +- IAI-003: Usuarios (modelo de suscripcion) +- IAI-004: Tenants (facturacion por org) + +### Bloquea a + +- Todas las features premium + +--- + +## Story Points Estimados + +| Tipo | Cantidad | SP | +|------|----------|-----| +| User Stories | 5 | 34 | +| Total | - | 34 | + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Fallo en webhooks | Media | Alto | Idempotencia, reintentos | +| Fraude | Baja | Alto | Stripe Radar, validaciones | + +--- + +**Estado:** Planned +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-005-pagos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-005-pagos/_MAP.md new file mode 100644 index 0000000..39f1095 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-005-pagos/_MAP.md @@ -0,0 +1,69 @@ +--- +id: "MAP-IAI-005" +title: "Mapa de EPIC IAI-005 Pagos" +type: "Navigation Map" +epic: "IAI-005" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: EPIC IAI-005 - Sistema de Pagos + +**EPIC:** IAI-005 +**Nombre:** Sistema de Pagos con Stripe +**Estado:** Planned +**Story Points:** 34 (estimado) + +--- + +## Estructura del EPIC + +``` +IAI-005-pagos/ +├── _MAP.md # Este archivo +├── README.md # Vision general del EPIC +│ +├── requerimientos/ +│ ├── _MAP.md +│ ├── RF-PAY-001.md # Integracion Stripe +│ ├── RF-PAY-002.md # Suscripciones +│ └── RF-PAY-003.md # Facturacion +│ +├── especificaciones/ +│ ├── _MAP.md +│ ├── ET-PAY-001-stripe.md # Arquitectura Stripe +│ └── ET-PAY-002-webhooks.md # Manejo de webhooks +│ +├── historias-usuario/ +│ ├── _MAP.md +│ ├── US-PAY-001.md # Checkout suscripcion +│ ├── US-PAY-002.md # Portal de cliente +│ ├── US-PAY-003.md # Cambio de plan +│ ├── US-PAY-004.md # Cancelacion +│ └── US-PAY-005.md # Facturas +│ +├── tareas/ +│ └── _MAP.md +│ +└── implementacion/ + └── _MAP.md +``` + +--- + +## Historias de Usuario (Planificadas) + +| ID | Titulo | SP | Prioridad | Sprint | +|----|--------|----|-----------| -------| +| US-PAY-001 | Checkout de suscripcion | 8 | Alta | - | +| US-PAY-002 | Portal de cliente Stripe | 5 | Alta | - | +| US-PAY-003 | Cambio/upgrade de plan | 8 | Media | - | +| US-PAY-004 | Cancelacion de suscripcion | 5 | Media | - | +| US-PAY-005 | Historial de facturas | 8 | Media | - | + +**Total Story Points:** 34 + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-005-pagos/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-005-pagos/especificaciones/_MAP.md new file mode 100644 index 0000000..f61909c --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-005-pagos/especificaciones/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-005-ESPE" +title: "Mapa Especificaciones IAI-005" +type: "Navigation Map" +epic: "IAI-005" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones - IAI-005 Pagos + +**EPIC:** IAI-005 +**Seccion:** Especificaciones + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-005-pagos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-005-pagos/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-005-pagos/historias-usuario/_MAP.md new file mode 100644 index 0000000..a0e3adb --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-005-pagos/historias-usuario/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-005-HIST" +title: "Mapa Historias-usuario IAI-005" +type: "Navigation Map" +epic: "IAI-005" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias-usuario - IAI-005 Pagos + +**EPIC:** IAI-005 +**Seccion:** Historias-usuario + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-005-pagos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-005-pagos/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-005-pagos/implementacion/_MAP.md new file mode 100644 index 0000000..914ecff --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-005-pagos/implementacion/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-005-IMPL" +title: "Mapa Implementacion IAI-005" +type: "Navigation Map" +epic: "IAI-005" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-005 Pagos + +**EPIC:** IAI-005 +**Seccion:** Implementacion + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-005-pagos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-005-pagos/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-005-pagos/requerimientos/_MAP.md new file mode 100644 index 0000000..3ff6563 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-005-pagos/requerimientos/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-005-REQU" +title: "Mapa Requerimientos IAI-005" +type: "Navigation Map" +epic: "IAI-005" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-005 Pagos + +**EPIC:** IAI-005 +**Seccion:** Requerimientos + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-005-pagos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-005-pagos/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-005-pagos/tareas/_MAP.md new file mode 100644 index 0000000..6937a14 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-005-pagos/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-005-TARE" +title: "Mapa Tareas IAI-005" +type: "Navigation Map" +epic: "IAI-005" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas - IAI-005 Pagos + +**EPIC:** IAI-005 +**Seccion:** Tareas + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-005-pagos/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-006-portales/README.md b/docs/01-fase-alcance-inicial/IAI-006-portales/README.md new file mode 100644 index 0000000..9e48f4a --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-006-portales/README.md @@ -0,0 +1,73 @@ +--- +id: "EPIC-IAI-006" +title: "EPIC IAI-006 - Portales" +type: "Epic Document" +epic: "IAI-006" +status: "Planned" +story_points: 26 +priority: "Media" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# EPIC IAI-006: Portales Web + +## Vision + +Sistema de portales diferenciados para cada tipo de usuario: portal publico, portal de agente/inversor, y portal administrativo. + +--- + +## Alcance + +### Incluye + +- Portal publico (landing, busqueda basica) +- Portal de usuario autenticado (dashboard) +- Portal de agente (herramientas profesionales) +- Portal de administrador (gestion del sistema) + +### No Incluye + +- Portal de tenant/white-label avanzado (fase 2) +- App movil nativa + +--- + +## Portales + +| Portal | URL | Audiencia | +|--------|-----|-----------| +| Publico | / | Todos | +| Dashboard | /app | Usuarios autenticados | +| Agente | /app/pro | Agentes premium | +| Admin | /admin | Administradores | + +--- + +## Dependencias + +### Depende de + +- IAI-001: Fundamentos (auth, routing) +- IAI-002: Propiedades (contenido) +- IAI-003: Usuarios (roles) + +### Bloquea a + +- Ninguno (es la capa de presentacion) + +--- + +## Story Points Estimados + +| Tipo | Cantidad | SP | +|------|----------|-----| +| User Stories | 4 | 26 | +| Total | - | 26 | + +--- + +**Estado:** Planned +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-006-portales/_MAP.md b/docs/01-fase-alcance-inicial/IAI-006-portales/_MAP.md new file mode 100644 index 0000000..62592b9 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-006-portales/_MAP.md @@ -0,0 +1,66 @@ +--- +id: "MAP-IAI-006" +title: "Mapa de EPIC IAI-006 Portales" +type: "Navigation Map" +epic: "IAI-006" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: EPIC IAI-006 - Portales Web + +**EPIC:** IAI-006 +**Nombre:** Portales Web +**Estado:** Planned +**Story Points:** 26 (estimado) + +--- + +## Estructura del EPIC + +``` +IAI-006-portales/ +├── _MAP.md # Este archivo +├── README.md # Vision general del EPIC +│ +├── requerimientos/ +│ ├── _MAP.md +│ ├── RF-PORT-001.md # Portal publico +│ ├── RF-PORT-002.md # Dashboard usuario +│ └── RF-PORT-003.md # Portal admin +│ +├── especificaciones/ +│ ├── _MAP.md +│ └── ET-PORT-001-routing.md +│ +├── historias-usuario/ +│ ├── _MAP.md +│ ├── US-PORT-001.md # Landing page +│ ├── US-PORT-002.md # Dashboard usuario +│ ├── US-PORT-003.md # Portal agente +│ └── US-PORT-004.md # Portal admin +│ +├── tareas/ +│ └── _MAP.md +│ +└── implementacion/ + └── _MAP.md +``` + +--- + +## Historias de Usuario (Planificadas) + +| ID | Titulo | SP | Prioridad | Sprint | +|----|--------|----|-----------| -------| +| US-PORT-001 | Landing page publica | 8 | Alta | - | +| US-PORT-002 | Dashboard de usuario | 8 | Alta | - | +| US-PORT-003 | Portal de agente/pro | 5 | Media | - | +| US-PORT-004 | Portal administrativo | 5 | Alta | - | + +**Total Story Points:** 26 + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-006-portales/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-006-portales/especificaciones/_MAP.md new file mode 100644 index 0000000..f4839fb --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-006-portales/especificaciones/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-006-ESPE" +title: "Mapa Especificaciones IAI-006" +type: "Navigation Map" +epic: "IAI-006" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones - IAI-006 Portales + +**EPIC:** IAI-006 +**Seccion:** Especificaciones + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-006-portales/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-006-portales/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-006-portales/historias-usuario/_MAP.md new file mode 100644 index 0000000..b758b81 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-006-portales/historias-usuario/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-006-HIST" +title: "Mapa Historias-usuario IAI-006" +type: "Navigation Map" +epic: "IAI-006" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias-usuario - IAI-006 Portales + +**EPIC:** IAI-006 +**Seccion:** Historias-usuario + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-006-portales/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-006-portales/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-006-portales/implementacion/_MAP.md new file mode 100644 index 0000000..c7e968d --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-006-portales/implementacion/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-006-IMPL" +title: "Mapa Implementacion IAI-006" +type: "Navigation Map" +epic: "IAI-006" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-006 Portales + +**EPIC:** IAI-006 +**Seccion:** Implementacion + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-006-portales/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-006-portales/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-006-portales/requerimientos/_MAP.md new file mode 100644 index 0000000..a447f6a --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-006-portales/requerimientos/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-006-REQU" +title: "Mapa Requerimientos IAI-006" +type: "Navigation Map" +epic: "IAI-006" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-006 Portales + +**EPIC:** IAI-006 +**Seccion:** Requerimientos + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-006-portales/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-006-portales/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-006-portales/tareas/_MAP.md new file mode 100644 index 0000000..69a2a59 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-006-portales/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-006-TARE" +title: "Mapa Tareas IAI-006" +type: "Navigation Map" +epic: "IAI-006" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas - IAI-006 Portales + +**EPIC:** IAI-006 +**Seccion:** Tareas + +--- + +## Documentos + +*Pendiente de documentacion.* + +--- + +## Navegacion + +- **Arriba:** [IAI-006-portales/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/README.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/README.md new file mode 100644 index 0000000..2269af7 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/README.md @@ -0,0 +1,196 @@ +--- +id: "EPIC-IAI-007" +title: "EPIC IAI-007: Sistema de Web Scraping y ETL" +type: "EPIC" +epic: "IAI-007" +status: "Draft" +project: "inmobiliaria-analytics" +version: "1.0.0" +story_points: 55 +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# EPIC IAI-007: Sistema de Web Scraping y ETL + +--- + +## Resumen Ejecutivo + +Este EPIC implementa el sistema de recoleccion automatizada de datos inmobiliarios desde multiples portales (Inmuebles24, Vivanuncios, etc.), incluyendo estrategias anti-deteccion, normalizacion de datos y pipeline ETL para alimentar la plataforma de analytics. + +--- + +## Objetivo + +Construir un sistema robusto de web scraping capaz de: +1. Extraer datos de propiedades de portales inmobiliarios protegidos +2. Evitar bloqueos mediante tecnicas anti-detection +3. Normalizar y validar datos de multiples fuentes +4. Mantener actualizaciones incrementales eficientes + +--- + +## Alcance + +### Incluido + +- Motor de scraping con Playwright/Puppeteer +- Gestion de proxies residenciales +- Bypass de Cloudflare y rate limiting +- Pipeline ETL para normalizacion +- Scheduling con Bull Queue +- Monitoreo y metricas + +### Excluido + +- App mobile de administracion +- Scraping de imagenes (fase 2) +- APIs de terceros (Apify, etc.) +- ML para extraccion (fase futura) + +--- + +## Fuentes de Datos Objetivo + +| Fuente | Prioridad | Proteccion | Estado | +|--------|-----------|------------|--------| +| Inmuebles24 | P1 | Cloudflare | Target | +| Vivanuncios | P1 | Cloudflare | Target | +| Segundamano | P2 | Basica | Backlog | +| Metros Cubicos | P2 | Cloudflare | Backlog | + +--- + +## Stack Tecnico + +```yaml +Scraping: + browser: Playwright + stealth: playwright-extra-stealth + fallback: Puppeteer + undetected-chrome + +Proxies: + type: Residencial + rotation: Por sesion + provider: Bright Data / IPRoyal + +ETL: + queue: Bull (Redis) + parser: Cheerio + geocoding: Google Maps API + +Storage: + raw: S3/MinIO (JSON) + normalized: PostgreSQL +``` + +--- + +## Arquitectura de Alto Nivel + +``` + +----------------+ + | Scheduler | + | (Bull Queue) | + +-------+--------+ + | + +-------------+-------------+ + | | ++-------v-------+ +---------v---------+ +| Scraper Pool | | ETL Pipeline | +| (Playwright) | | (Normalization) | ++-------+-------+ +---------+---------+ + | | + | +---------------+ | + +-->| Proxy Pool |<------+ + +---------------+ + | + +-----------+-----------+ + | | ++-------v-------+ +-------v-------+ +| Raw Storage | | PostgreSQL | +| (S3/JSON) | | (properties) | ++---------------+ +---------------+ +``` + +--- + +## Desglose de Trabajo + +### Fase 1: MVP Scraper (2-3 sprints) + +| Tarea | SP | Prioridad | +|-------|----|-----------| +| Setup Playwright + stealth | 3 | Alta | +| Scraper Inmuebles24 basico | 8 | Alta | +| Integracion proxy pool | 5 | Alta | +| Normalizacion basica | 5 | Alta | +| Job scheduling simple | 3 | Media | + +### Fase 2: Multi-source (1-2 sprints) + +| Tarea | SP | Prioridad | +|-------|----|-----------| +| Scraper Vivanuncios | 5 | Alta | +| Scraper Segundamano | 3 | Media | +| Deduplicacion cross-source | 5 | Media | +| Geocoding integration | 3 | Media | + +### Fase 3: Produccion (1 sprint) + +| Tarea | SP | Prioridad | +|-------|----|-----------| +| Monitoreo y alertas | 5 | Media | +| Retry logic + error handling | 3 | Media | +| Dashboard de admin | 5 | Baja | +| Documentacion | 2 | Baja | + +--- + +## Estimacion de Costos + +```yaml +Infraestructura_mensual: + proxies_residenciales: $50-100 USD + captcha_solving: $10-20 USD + geocoding_api: $0-50 USD + cloud_compute: $50-100 USD + +Total: $100-300 USD/mes +``` + +--- + +## Riesgos y Mitigaciones + +| Riesgo | Prob | Impacto | Mitigacion | +|--------|------|---------|------------| +| Bloqueo Cloudflare | Alta | Alto | Stealth browser, proxies, rate limit | +| Cambios HTML | Media | Medio | Selectores robustos, alertas | +| Legal | Baja | Alto | Cumplir ToS, agregar valor | +| Costos escalan | Media | Bajo | Optimizar, limitar scope | + +--- + +## Criterios de Aceptacion del EPIC + +- [ ] Scraper extrae 10,000+ propiedades de Inmuebles24 +- [ ] Tasa de exito >= 85% +- [ ] Datos normalizados correctamente +- [ ] Cero bloqueos permanentes de IP +- [ ] Pipeline ejecuta incrementales diarios +- [ ] Metricas disponibles en dashboard + +--- + +## Documentacion Relacionada + +- [IA-007-WEBSCRAPER.md](../../02-definicion-modulos/IA-007-WEBSCRAPER.md) - Definicion del modulo +- [Webscraper_Politics.md](../../00-vision-general/Webscraper_Politics.md) - Politicas anti-bloqueo + +--- + +**EPIC Owner:** Tech Lead +**Fecha creacion:** 2026-01-04 +**Estado:** Draft diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/_MAP.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/_MAP.md new file mode 100644 index 0000000..b5ec4eb --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/_MAP.md @@ -0,0 +1,128 @@ +--- +id: "MAP-IAI-007" +title: "Mapa de EPIC IAI-007 Webscraper" +type: "Navigation Map" +epic: "IAI-007" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: EPIC IAI-007 - Web Scraping y ETL + +**EPIC:** IAI-007 +**Nombre:** Sistema de Web Scraping y ETL +**Estado:** Draft +**Story Points:** 55 (estimado) + +--- + +## Estructura del EPIC + +``` +IAI-007-webscraper/ +├── _MAP.md # Este archivo +├── README.md # Vision general del EPIC +│ +├── requerimientos/ +│ ├── _MAP.md +│ ├── RF-SCR-001.md # Motor de scraping +│ ├── RF-SCR-002.md # Gestion de proxies +│ ├── RF-SCR-003.md # Pipeline ETL +│ ├── RF-SCR-004.md # Scheduling y jobs +│ └── RF-SCR-005.md # Monitoreo +│ +├── especificaciones/ +│ ├── _MAP.md +│ ├── ET-SCR-001-scraper.md # Motor de scraping Playwright +│ ├── ET-SCR-002-etl.md # Pipeline ETL y normalizacion +│ └── ET-SCR-003-proxies.md # Gestion pool de proxies +│ +├── historias-usuario/ +│ ├── _MAP.md +│ ├── US-SCR-001.md # Scraping Inmuebles24 +│ ├── US-SCR-002.md # Scraping Vivanuncios +│ ├── US-SCR-003.md # Normalizacion de datos +│ ├── US-SCR-004.md # Programacion de jobs +│ └── US-SCR-005.md # Dashboard de monitoreo +│ +├── tareas/ +│ └── _MAP.md +│ +└── implementacion/ + ├── _MAP.md + ├── CHANGELOG.md # Historial de cambios + └── TRACEABILITY.yml # Trazabilidad +``` + +--- + +## Requerimientos Funcionales + +| ID | Nombre | Prioridad | Estado | +|----|--------|-----------|--------| +| RF-SCR-001 | Motor de scraping con anti-detection | Alta | Pendiente | +| RF-SCR-002 | Gestion de pool de proxies | Alta | Pendiente | +| RF-SCR-003 | Pipeline ETL y normalizacion | Alta | Pendiente | +| RF-SCR-004 | Scheduling y job management | Media | Pendiente | +| RF-SCR-005 | Monitoreo y alertas | Media | Pendiente | + +--- + +## Historias de Usuario + +| ID | Titulo | SP | Prioridad | Sprint | +|----|--------|----|-----------| -------| +| US-SCR-001 | Scrapear propiedades de Inmuebles24 | 13 | Alta | - | +| US-SCR-002 | Scrapear propiedades de Vivanuncios | 8 | Alta | - | +| US-SCR-003 | Normalizar datos de multiples fuentes | 8 | Alta | - | +| US-SCR-004 | Programar jobs de actualizacion | 5 | Media | - | +| US-SCR-005 | Monitorear estado del scraping | 5 | Media | - | + +**Total Story Points:** 39 + +--- + +## Dependencias + +### Depende de: +- IAI-001: Fundamentos (autenticacion para API interna) +- Infraestructura: Redis, PostgreSQL + +### Bloquea a: +- IAI-002: Propiedades (necesita datos) +- IAI-008: ML/Analytics (necesita datos) + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Bloqueo por Cloudflare | Alta | Alto | Anti-detection, proxies residenciales | +| Cambios en estructura HTML | Media | Medio | Selectores flexibles, alertas | +| Costos de proxies | Media | Bajo | Proveedores economicos, optimizacion | +| Aspectos legales | Baja | Alto | Cumplir robots.txt, agregar datos | + +--- + +## Metricas de Exito + +- [ ] 10,000 propiedades scrapeadas en primera semana +- [ ] Tasa de exito > 85% +- [ ] Tiempo de normalizacion < 1s/propiedad +- [ ] Cero bloqueos permanentes + +--- + +## Especificaciones Tecnicas + +| ID | Titulo | Estado | Contenido Principal | +|----|--------|--------|---------------------| +| ET-SCR-001 | Motor de Scraping | Creado | Playwright, stealth mode, BrowserManager | +| ET-SCR-002 | Pipeline ETL | Creado | Extractors, normalizacion, geocoding, dedup | +| ET-SCR-003 | Gestion de Proxies | Creado | Pool manager, rotacion, health checks | + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-001-scraper.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-001-scraper.md new file mode 100644 index 0000000..d44c164 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-001-scraper.md @@ -0,0 +1,965 @@ +--- +id: "ET-SCR-001" +title: "Especificacion Tecnica: Motor de Scraping" +type: "Technical Specification" +epic: "IAI-007" +status: "Draft" +project: "inmobiliaria-analytics" +version: "1.0.0" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# ET-SCR-001: Especificacion Tecnica del Motor de Scraping + +--- + +## Resumen + +Esta especificacion define la arquitectura e implementacion del motor de web scraping con capacidades anti-detection para extraer datos de portales inmobiliarios protegidos por Cloudflare. + +--- + +## Stack Tecnologico + +```yaml +runtime: Node.js 20 LTS +language: TypeScript 5.x + +dependencias: + scraping: + - playwright: "^1.40.0" + - playwright-extra: "^4.3.0" + - puppeteer-extra-plugin-stealth: "^2.11.0" + - cheerio: "^1.0.0" + + queue: + - bullmq: "^5.0.0" + - ioredis: "^5.3.0" + + http: + - axios: "^1.6.0" + - https-proxy-agent: "^7.0.0" + + utils: + - pino: "^8.0.0" + - zod: "^3.22.0" + - date-fns: "^3.0.0" + + testing: + - vitest: "^1.0.0" + - msw: "^2.0.0" +``` + +--- + +## Arquitectura de Componentes + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SCRAPER SERVICE │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Scheduler │───▶│ Job Queue │───▶│ Workers │ │ +│ │ (Cron/API) │ │ (BullMQ) │ │ (N=2-4) │ │ +│ └──────────────┘ └──────────────┘ └──────┬───────┘ │ +│ │ │ +│ ┌──────────────────────┼──────────┐ │ +│ │ ▼ │ │ +│ ┌──────────────┐ ┌────┴─────────┐ ┌──────────────┐ │ │ +│ │ Proxy │◀───│ Browser │───▶│ Parser │ │ │ +│ │ Manager │ │ Engine │ │ (Cheerio) │ │ │ +│ └──────────────┘ │ (Playwright) │ └──────┬───────┘ │ │ +│ └──────────────┘ │ │ │ +│ ▼ │ │ +│ ┌──────────────┐ │ │ +│ │ Normalizer │ │ │ +│ └──────┬───────┘ │ │ +│ │ │ │ +│ Scraper Core │ │ │ +│ └────────────────────┼────────────┘ │ +│ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Metrics │◀───│ Storage │───▶│ PostgreSQL │ │ +│ │ (Prometheus) │ │ (S3/Local) │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Estructura de Codigo + +``` +apps/scraper/ +├── src/ +│ ├── index.ts # Entry point +│ ├── config/ +│ │ ├── index.ts +│ │ ├── sources.config.ts # Configuracion por fuente +│ │ └── schedules.config.ts +│ │ +│ ├── core/ +│ │ ├── browser/ +│ │ │ ├── browser-manager.ts +│ │ │ ├── stealth-config.ts +│ │ │ └── page-utils.ts +│ │ │ +│ │ ├── proxy/ +│ │ │ ├── proxy-pool.ts +│ │ │ ├── proxy-rotator.ts +│ │ │ └── proxy-health.ts +│ │ │ +│ │ ├── queue/ +│ │ │ ├── job-queue.ts +│ │ │ ├── job-processor.ts +│ │ │ └── job-types.ts +│ │ │ +│ │ └── rate-limiter/ +│ │ └── adaptive-limiter.ts +│ │ +│ ├── scrapers/ +│ │ ├── base-scraper.ts # Clase base abstracta +│ │ ├── inmuebles24/ +│ │ │ ├── scraper.ts +│ │ │ ├── selectors.ts +│ │ │ └── mappings.ts +│ │ ├── vivanuncios/ +│ │ │ ├── scraper.ts +│ │ │ ├── selectors.ts +│ │ │ └── mappings.ts +│ │ └── segundamano/ +│ │ └── ... +│ │ +│ ├── etl/ +│ │ ├── extractor.ts +│ │ ├── transformer.ts +│ │ ├── normalizer.ts +│ │ ├── geocoder.ts +│ │ └── deduplicator.ts +│ │ +│ ├── storage/ +│ │ ├── raw-storage.ts # S3/MinIO +│ │ └── property-repository.ts +│ │ +│ ├── monitoring/ +│ │ ├── metrics.ts +│ │ ├── alerts.ts +│ │ └── health-check.ts +│ │ +│ ├── api/ +│ │ ├── routes/ +│ │ │ ├── jobs.routes.ts +│ │ │ ├── stats.routes.ts +│ │ │ └── proxies.routes.ts +│ │ └── server.ts +│ │ +│ └── types/ +│ ├── job.types.ts +│ ├── property.types.ts +│ └── proxy.types.ts +│ +├── tests/ +│ ├── unit/ +│ ├── integration/ +│ └── e2e/ +│ +├── Dockerfile +├── docker-compose.yml +├── package.json +└── tsconfig.json +``` + +--- + +## Implementacion del Browser Engine + +### Browser Manager + +```typescript +// src/core/browser/browser-manager.ts +import { chromium, Browser, BrowserContext, Page } from 'playwright'; +import { addExtra } from 'playwright-extra'; +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; + +export class BrowserManager { + private browser: Browser | null = null; + private contexts: Map = new Map(); + + async initialize(): Promise { + const chromiumExtra = addExtra(chromium); + chromiumExtra.use(StealthPlugin()); + + this.browser = await chromiumExtra.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--disable-gpu', + '--window-size=1920,1080', + ], + }); + } + + async createContext( + sessionId: string, + proxy?: ProxyConfig + ): Promise { + if (!this.browser) throw new Error('Browser not initialized'); + + const context = await this.browser.newContext({ + viewport: { width: 1920, height: 1080 }, + userAgent: this.getRandomUserAgent(), + locale: 'es-MX', + timezoneId: 'America/Mexico_City', + proxy: proxy ? { + server: `${proxy.address}:${proxy.port}`, + username: proxy.username, + password: proxy.password, + } : undefined, + }); + + // Anti-detection patches + await this.applyStealthPatches(context); + + this.contexts.set(sessionId, context); + return context; + } + + private async applyStealthPatches(context: BrowserContext): Promise { + await context.addInitScript(() => { + // Hide webdriver + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined, + }); + + // Mock plugins + Object.defineProperty(navigator, 'plugins', { + get: () => [1, 2, 3, 4, 5], + }); + + // Mock languages + Object.defineProperty(navigator, 'languages', { + get: () => ['es-MX', 'es', 'en-US', 'en'], + }); + }); + } + + private getRandomUserAgent(): string { + const userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', + // ... more user agents + ]; + return userAgents[Math.floor(Math.random() * userAgents.length)]; + } + + async close(): Promise { + for (const context of this.contexts.values()) { + await context.close(); + } + if (this.browser) { + await this.browser.close(); + } + } +} +``` + +### Human-like Behavior + +```typescript +// src/core/browser/page-utils.ts +import { Page } from 'playwright'; + +export class PageUtils { + static async humanScroll(page: Page): Promise { + const scrollHeight = await page.evaluate(() => document.body.scrollHeight); + let currentPosition = 0; + + while (currentPosition < scrollHeight) { + const scrollAmount = Math.random() * 300 + 100; + currentPosition += scrollAmount; + + await page.evaluate((y) => window.scrollTo(0, y), currentPosition); + await this.randomDelay(100, 300); + } + } + + static async humanClick(page: Page, selector: string): Promise { + const element = await page.$(selector); + if (!element) throw new Error(`Element not found: ${selector}`); + + const box = await element.boundingBox(); + if (!box) throw new Error(`Element not visible: ${selector}`); + + // Move to element with slight randomness + const x = box.x + box.width / 2 + (Math.random() * 10 - 5); + const y = box.y + box.height / 2 + (Math.random() * 10 - 5); + + await page.mouse.move(x, y, { steps: 10 }); + await this.randomDelay(50, 150); + await page.mouse.click(x, y); + } + + static async randomDelay(min: number, max: number): Promise { + const delay = Math.floor(Math.random() * (max - min + 1)) + min; + await new Promise(resolve => setTimeout(resolve, delay)); + } + + static async waitForCloudflare(page: Page): Promise { + // Wait for Cloudflare challenge to complete + try { + await page.waitForSelector('#challenge-running', { + state: 'hidden', + timeout: 30000, + }); + } catch { + // No challenge present, continue + } + + // Additional wait for JS to fully load + await page.waitForLoadState('networkidle'); + } +} +``` + +--- + +## Base Scraper Implementation + +```typescript +// src/scrapers/base-scraper.ts +import { Page, BrowserContext } from 'playwright'; +import { BrowserManager } from '../core/browser/browser-manager'; +import { ProxyPool } from '../core/proxy/proxy-pool'; +import { PageUtils } from '../core/browser/page-utils'; +import { Logger } from 'pino'; + +export interface ScrapingResult { + success: boolean; + properties: RawProperty[]; + errors: ScrapingError[]; + stats: ScrapingStats; +} + +export abstract class BaseScraper { + protected browserManager: BrowserManager; + protected proxyPool: ProxyPool; + protected logger: Logger; + protected context: BrowserContext | null = null; + protected page: Page | null = null; + + abstract readonly source: string; + abstract readonly baseUrl: string; + + constructor( + browserManager: BrowserManager, + proxyPool: ProxyPool, + logger: Logger + ) { + this.browserManager = browserManager; + this.proxyPool = proxyPool; + this.logger = logger.child({ source: this.source }); + } + + async scrape(config: ScrapingConfig): Promise { + const stats: ScrapingStats = { + pagesScraped: 0, + propertiesFound: 0, + errors: 0, + startedAt: new Date(), + }; + + const properties: RawProperty[] = []; + const errors: ScrapingError[] = []; + + try { + await this.initSession(); + + for (const city of config.targetCities) { + for (const type of config.propertyTypes) { + const result = await this.scrapeListings(city, type, config); + properties.push(...result.properties); + errors.push(...result.errors); + stats.pagesScraped += result.pagesScraped; + } + } + + stats.propertiesFound = properties.length; + stats.errors = errors.length; + stats.completedAt = new Date(); + + return { success: true, properties, errors, stats }; + + } catch (error) { + this.logger.error({ error }, 'Scraping failed'); + return { + success: false, + properties, + errors: [...errors, { type: 'fatal', message: String(error) }], + stats, + }; + } finally { + await this.closeSession(); + } + } + + protected async initSession(): Promise { + const proxy = await this.proxyPool.getProxy(); + const sessionId = `${this.source}-${Date.now()}`; + + this.context = await this.browserManager.createContext(sessionId, proxy); + this.page = await this.context.newPage(); + + // Set default timeout + this.page.setDefaultTimeout(30000); + } + + protected async closeSession(): Promise { + if (this.page) await this.page.close(); + if (this.context) await this.context.close(); + } + + protected async navigateWithRetry( + url: string, + maxRetries: number = 3 + ): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + await this.page!.goto(url, { waitUntil: 'domcontentloaded' }); + await PageUtils.waitForCloudflare(this.page!); + return; + } catch (error) { + this.logger.warn({ url, attempt, error }, 'Navigation failed, retrying'); + + if (attempt === maxRetries) throw error; + + // Rotate proxy on failure + await this.rotateProxy(); + await PageUtils.randomDelay(2000, 5000); + } + } + } + + protected async rotateProxy(): Promise { + const newProxy = await this.proxyPool.getProxy(); + await this.closeSession(); + + const sessionId = `${this.source}-${Date.now()}`; + this.context = await this.browserManager.createContext(sessionId, newProxy); + this.page = await this.context.newPage(); + } + + // Abstract methods to be implemented by each source + protected abstract scrapeListings( + city: string, + propertyType: string, + config: ScrapingConfig + ): Promise; + + protected abstract parsePropertyDetail( + page: Page + ): Promise; + + protected abstract getListingUrl( + city: string, + propertyType: string, + page: number + ): string; +} +``` + +--- + +## Proxy Pool Implementation + +```typescript +// src/core/proxy/proxy-pool.ts +import { Redis } from 'ioredis'; + +export interface ProxyConfig { + id: string; + address: string; + port: number; + username?: string; + password?: string; + type: 'residential' | 'datacenter' | 'mobile'; + country: string; + status: 'active' | 'cooling' | 'banned'; + successRate: number; + lastUsedAt?: Date; + coolingUntil?: Date; +} + +export class ProxyPool { + private redis: Redis; + private readonly POOL_KEY = 'proxy:pool'; + private readonly COOLING_KEY = 'proxy:cooling'; + + constructor(redis: Redis) { + this.redis = redis; + } + + async getProxy(): Promise { + // Get all active proxies + const proxies = await this.getActiveProxies(); + + if (proxies.length === 0) { + throw new Error('No active proxies available'); + } + + // Weighted selection based on success rate + const selected = this.weightedSelection(proxies); + + // Mark as used + await this.markUsed(selected.id); + + return selected; + } + + private async getActiveProxies(): Promise { + const all = await this.redis.hgetall(this.POOL_KEY); + const now = Date.now(); + + return Object.values(all) + .map(p => JSON.parse(p) as ProxyConfig) + .filter(p => { + if (p.status === 'banned') return false; + if (p.status === 'cooling' && p.coolingUntil) { + return new Date(p.coolingUntil).getTime() < now; + } + return p.status === 'active'; + }); + } + + private weightedSelection(proxies: ProxyConfig[]): ProxyConfig { + // Higher success rate = higher weight + const totalWeight = proxies.reduce((sum, p) => sum + p.successRate, 0); + let random = Math.random() * totalWeight; + + for (const proxy of proxies) { + random -= proxy.successRate; + if (random <= 0) return proxy; + } + + return proxies[0]; + } + + async markUsed(proxyId: string): Promise { + const proxy = await this.getProxy(proxyId); + if (proxy) { + proxy.lastUsedAt = new Date(); + await this.redis.hset(this.POOL_KEY, proxyId, JSON.stringify(proxy)); + } + } + + async markSuccess(proxyId: string): Promise { + const proxy = await this.getProxyById(proxyId); + if (proxy) { + // Update success rate with exponential moving average + proxy.successRate = proxy.successRate * 0.9 + 1 * 0.1; + await this.redis.hset(this.POOL_KEY, proxyId, JSON.stringify(proxy)); + } + } + + async markFailure(proxyId: string, errorType: string): Promise { + const proxy = await this.getProxyById(proxyId); + if (!proxy) return; + + // Update success rate + proxy.successRate = proxy.successRate * 0.9 + 0 * 0.1; + + if (errorType === 'rate_limit') { + // Put in cooling for 1 hour + proxy.status = 'cooling'; + proxy.coolingUntil = new Date(Date.now() + 3600000); + } else if (errorType === 'banned') { + proxy.status = 'banned'; + } + + await this.redis.hset(this.POOL_KEY, proxyId, JSON.stringify(proxy)); + } + + async getStats(): Promise { + const all = await this.redis.hgetall(this.POOL_KEY); + const proxies = Object.values(all).map(p => JSON.parse(p) as ProxyConfig); + + return { + total: proxies.length, + active: proxies.filter(p => p.status === 'active').length, + cooling: proxies.filter(p => p.status === 'cooling').length, + banned: proxies.filter(p => p.status === 'banned').length, + avgSuccessRate: proxies.reduce((sum, p) => sum + p.successRate, 0) / proxies.length, + }; + } +} +``` + +--- + +## Job Queue Implementation + +```typescript +// src/core/queue/job-queue.ts +import { Queue, Worker, Job } from 'bullmq'; +import { Redis } from 'ioredis'; + +export interface ScrapingJobData { + id: string; + type: 'full_scan' | 'incremental' | 'targeted' | 'refresh'; + source: string; + config: ScrapingConfig; + createdBy?: string; +} + +export class JobQueue { + private queue: Queue; + private worker: Worker; + private redis: Redis; + + constructor(redis: Redis, processor: JobProcessor) { + this.redis = redis; + + this.queue = new Queue('scraping', { + connection: redis, + defaultJobOptions: { + attempts: 3, + backoff: { + type: 'exponential', + delay: 5000, + }, + removeOnComplete: 100, + removeOnFail: 50, + }, + }); + + this.worker = new Worker( + 'scraping', + async (job: Job) => { + return processor.process(job); + }, + { + connection: redis, + concurrency: 2, + } + ); + + this.setupEventHandlers(); + } + + private setupEventHandlers(): void { + this.worker.on('completed', (job, result) => { + console.log(`Job ${job.id} completed`, result); + }); + + this.worker.on('failed', (job, error) => { + console.error(`Job ${job?.id} failed`, error); + }); + + this.worker.on('progress', (job, progress) => { + console.log(`Job ${job.id} progress: ${progress}%`); + }); + } + + async addJob(data: ScrapingJobData): Promise> { + return this.queue.add(data.type, data, { + jobId: data.id, + }); + } + + async scheduleJob( + data: ScrapingJobData, + cron: string + ): Promise { + await this.queue.add(data.type, data, { + repeat: { pattern: cron }, + jobId: `${data.id}-scheduled`, + }); + } + + async pauseJob(jobId: string): Promise { + const job = await this.queue.getJob(jobId); + if (job) { + await job.updateProgress({ status: 'paused' }); + } + } + + async getJobStatus(jobId: string): Promise { + const job = await this.queue.getJob(jobId); + if (!job) return null; + + const state = await job.getState(); + return { + id: job.id!, + state, + progress: job.progress, + data: job.data, + attemptsMade: job.attemptsMade, + failedReason: job.failedReason, + }; + } +} +``` + +--- + +## API Endpoints + +```typescript +// src/api/routes/jobs.routes.ts +import { Router } from 'express'; +import { z } from 'zod'; + +const CreateJobSchema = z.object({ + type: z.enum(['full_scan', 'incremental', 'targeted', 'refresh']), + source: z.string(), + config: z.object({ + targetCities: z.array(z.string()).optional(), + propertyTypes: z.array(z.string()).optional(), + maxPages: z.number().optional(), + delayMs: z.object({ + min: z.number(), + max: z.number(), + }).optional(), + }), +}); + +export function createJobsRouter(jobQueue: JobQueue): Router { + const router = Router(); + + // Create new job + router.post('/', async (req, res) => { + const parsed = CreateJobSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error }); + } + + const jobId = `job-${Date.now()}`; + const job = await jobQueue.addJob({ + id: jobId, + ...parsed.data, + }); + + res.status(201).json({ + id: job.id, + status: 'queued', + }); + }); + + // List jobs + router.get('/', async (req, res) => { + const jobs = await jobQueue.getJobs(req.query); + res.json({ jobs }); + }); + + // Get job status + router.get('/:id', async (req, res) => { + const status = await jobQueue.getJobStatus(req.params.id); + if (!status) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(status); + }); + + // Pause job + router.post('/:id/pause', async (req, res) => { + await jobQueue.pauseJob(req.params.id); + res.json({ status: 'paused' }); + }); + + // Resume job + router.post('/:id/resume', async (req, res) => { + await jobQueue.resumeJob(req.params.id); + res.json({ status: 'resumed' }); + }); + + // Cancel job + router.delete('/:id', async (req, res) => { + await jobQueue.cancelJob(req.params.id); + res.status(204).send(); + }); + + return router; +} +``` + +--- + +## Docker Configuration + +```yaml +# docker-compose.yml +version: '3.8' + +services: + scraper: + build: + context: . + dockerfile: Dockerfile + environment: + - NODE_ENV=production + - REDIS_URL=redis://redis:6379 + - DATABASE_URL=postgresql://user:pass@postgres:5432/inmobiliaria + - S3_ENDPOINT=http://minio:9000 + - S3_BUCKET=raw-data + depends_on: + - redis + - postgres + - minio + deploy: + replicas: 2 + resources: + limits: + memory: 2G + cpus: '1' + + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: inmobiliaria + POSTGRES_USER: user + POSTGRES_PASSWORD: pass + volumes: + - postgres-data:/var/lib/postgresql/data + + minio: + image: minio/minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + volumes: + - minio-data:/data + +volumes: + redis-data: + postgres-data: + minio-data: +``` + +--- + +## Metricas Prometheus + +```typescript +// src/monitoring/metrics.ts +import { Registry, Counter, Histogram, Gauge } from 'prom-client'; + +export const register = new Registry(); + +export const metrics = { + propertiesScraped: new Counter({ + name: 'scraper_properties_total', + help: 'Total properties scraped', + labelNames: ['source', 'status'], + registers: [register], + }), + + requestDuration: new Histogram({ + name: 'scraper_request_duration_seconds', + help: 'Duration of scraping requests', + labelNames: ['source'], + buckets: [0.1, 0.5, 1, 2, 5, 10, 30], + registers: [register], + }), + + activeJobs: new Gauge({ + name: 'scraper_active_jobs', + help: 'Number of active scraping jobs', + labelNames: ['source'], + registers: [register], + }), + + proxyPoolSize: new Gauge({ + name: 'scraper_proxy_pool_size', + help: 'Size of proxy pool by status', + labelNames: ['status'], + registers: [register], + }), + + errorsTotal: new Counter({ + name: 'scraper_errors_total', + help: 'Total scraping errors', + labelNames: ['source', 'error_type'], + registers: [register], + }), +}; +``` + +--- + +## Testing Strategy + +```typescript +// tests/integration/inmuebles24.test.ts +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { BrowserManager } from '../../src/core/browser/browser-manager'; +import { Inmuebles24Scraper } from '../../src/scrapers/inmuebles24/scraper'; + +describe('Inmuebles24 Scraper', () => { + let browserManager: BrowserManager; + let scraper: Inmuebles24Scraper; + + beforeAll(async () => { + browserManager = new BrowserManager(); + await browserManager.initialize(); + scraper = new Inmuebles24Scraper(browserManager, mockProxyPool, mockLogger); + }); + + afterAll(async () => { + await browserManager.close(); + }); + + it('should extract property listings from search page', async () => { + const result = await scraper.scrape({ + targetCities: ['guadalajara'], + propertyTypes: ['casas'], + maxPages: 1, + }); + + expect(result.success).toBe(true); + expect(result.properties.length).toBeGreaterThan(0); + expect(result.properties[0]).toHaveProperty('source_id'); + expect(result.properties[0]).toHaveProperty('price'); + }); + + it('should handle Cloudflare challenge', async () => { + // Test with mock that returns challenge page + // Verify scraper waits and retries + }); + + it('should rotate proxy on failure', async () => { + // Test proxy rotation logic + }); +}); +``` + +--- + +## Criterios de Aceptacion Tecnicos + +- [ ] Bot detection tests pass (bot.sannysoft.com) +- [ ] Scraper extracts 500+ properties without block +- [ ] Request latency p95 < 10s +- [ ] Memory usage < 500MB per worker +- [ ] CPU usage < 50% average +- [ ] Error rate < 5% +- [ ] All unit tests pass +- [ ] Integration tests pass + +--- + +**Documento:** Especificacion Tecnica Motor Scraping +**Version:** 1.0.0 +**Autor:** Tech Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-002-etl.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-002-etl.md new file mode 100644 index 0000000..382fa95 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-002-etl.md @@ -0,0 +1,1388 @@ +--- +id: "ET-SCR-002" +title: "Especificacion Tecnica - Pipeline ETL y Normalizacion" +type: "Technical Specification" +epic: "IAI-007" +status: "Draft" +version: "1.0" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# ET-SCR-002: Pipeline ETL y Normalizacion + +--- + +## 1. Resumen + +Pipeline de Extract-Transform-Load para procesar datos crudos de propiedades scrapeadas, normalizarlos a un esquema unificado, enriquecerlos con geocoding y detectar duplicados. + +--- + +## 2. Arquitectura del Pipeline + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ EXTRACT │────▶│ TRANSFORM │────▶│ LOAD │ +│ │ │ │ │ │ +│ - Raw HTML │ │ - Parse │ │ - Validate │ +│ - JSON APIs │ │ - Normalize │ │ - Dedupe │ +│ - Sitemap │ │ - Geocode │ │ - Upsert │ +└──────────────┘ └──────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ staging │ │ enriched │ │ properties │ +│ _raw │ │ _staging │ │ (final) │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +--- + +## 3. Esquema de Datos + +### 3.1 Raw Data (Entrada) + +```typescript +interface RawPropertyData { + source: string; + sourceId: string; + sourceUrl: string; + scrapedAt: Date; + rawHtml?: string; + rawJson?: Record; + + // Campos extraidos (pueden variar por fuente) + titulo?: string; + precio?: string; + ubicacion?: string; + superficie?: string; + recamaras?: string; + banos?: string; + descripcion?: string; + imagenes?: string[]; + amenidades?: string[]; + contacto?: { + nombre?: string; + telefono?: string; + email?: string; + }; +} +``` + +### 3.2 Normalized Data (Salida) + +```typescript +interface NormalizedProperty { + // Identificadores + id: string; // UUID interno + sourceId: string; + source: string; + sourceUrl: string; + + // Informacion basica + title: string; + description: string; + propertyType: PropertyType; + transactionType: TransactionType; + + // Precios + price: number; + currency: 'MXN' | 'USD'; + pricePerSqm: number | null; + + // Superficie + landArea: number | null; // m2 terreno + constructedArea: number | null; // m2 construccion + + // Caracteristicas + bedrooms: number | null; + bathrooms: number | null; + parkingSpaces: number | null; + floors: number | null; + yearBuilt: number | null; + + // Ubicacion + location: { + rawAddress: string; + street: string | null; + neighborhood: string; // colonia + municipality: string; // municipio + state: string; + postalCode: string | null; + country: string; + coordinates: { + lat: number; + lng: number; + } | null; + geocodeConfidence: number; + }; + + // Media + images: PropertyImage[]; + virtualTour: string | null; + video: string | null; + + // Amenidades + amenities: string[]; + + // Contacto + agent: { + name: string | null; + phone: string | null; + email: string | null; + agency: string | null; + }; + + // Metadata + firstSeenAt: Date; + lastSeenAt: Date; + publishedAt: Date | null; + status: PropertyStatus; + + // Calidad de datos + dataQuality: { + score: number; // 0-100 + missingFields: string[]; + warnings: string[]; + }; +} + +enum PropertyType { + CASA = 'casa', + DEPARTAMENTO = 'departamento', + TERRENO = 'terreno', + LOCAL_COMERCIAL = 'local_comercial', + OFICINA = 'oficina', + BODEGA = 'bodega', + EDIFICIO = 'edificio', + OTRO = 'otro' +} + +enum TransactionType { + VENTA = 'venta', + RENTA = 'renta', + TRASPASO = 'traspaso' +} + +enum PropertyStatus { + ACTIVE = 'active', + SOLD = 'sold', + RENTED = 'rented', + INACTIVE = 'inactive', + REMOVED = 'removed' +} + +interface PropertyImage { + url: string; + thumbnailUrl: string | null; + order: number; + isMain: boolean; +} +``` + +--- + +## 4. Implementacion del Pipeline + +### 4.1 Extractor Base + +```typescript +// src/etl/extractors/base.extractor.ts +import { RawPropertyData } from '../types'; + +export abstract class BaseExtractor { + abstract source: string; + + abstract extractFromHtml(html: string, url: string): Partial; + abstract extractFromJson(json: unknown, url: string): Partial; + + protected cleanText(text: string | null | undefined): string { + if (!text) return ''; + return text + .replace(/\s+/g, ' ') + .replace(/[\n\r\t]/g, ' ') + .trim(); + } + + protected extractNumbers(text: string): number[] { + const matches = text.match(/[\d,]+(\.\d+)?/g) || []; + return matches.map(m => parseFloat(m.replace(/,/g, ''))); + } +} +``` + +### 4.2 Extractor Inmuebles24 + +```typescript +// src/etl/extractors/inmuebles24.extractor.ts +import * as cheerio from 'cheerio'; +import { BaseExtractor } from './base.extractor'; +import { RawPropertyData } from '../types'; + +export class Inmuebles24Extractor extends BaseExtractor { + source = 'inmuebles24'; + + extractFromHtml(html: string, url: string): Partial { + const $ = cheerio.load(html); + + return { + source: this.source, + sourceUrl: url, + sourceId: this.extractSourceId(url), + titulo: this.cleanText($('h1.title-type-sup').text()), + precio: this.cleanText($('.price-value').text()), + ubicacion: this.cleanText($('.location-container').text()), + + superficie: this.extractSuperficie($), + recamaras: this.extractFeature($, 'recamaras'), + banos: this.extractFeature($, 'banos'), + + descripcion: this.cleanText($('.description-content').text()), + + imagenes: this.extractImages($), + amenidades: this.extractAmenidades($), + + contacto: { + nombre: this.cleanText($('.publisher-name').text()), + telefono: $('[data-phone]').attr('data-phone') || null, + }, + }; + } + + extractFromJson(json: any, url: string): Partial { + // Procesar JSON-LD o APIs internas + if (json['@type'] === 'RealEstateListing') { + return { + source: this.source, + sourceUrl: url, + sourceId: json.identifier, + titulo: json.name, + precio: json.offers?.price?.toString(), + // ... mapear resto de campos + }; + } + return {}; + } + + private extractSourceId(url: string): string { + const match = url.match(/propiedades\/(\d+)/); + return match ? match[1] : ''; + } + + private extractSuperficie($: cheerio.CheerioAPI): string { + const container = $('.surface-container').text(); + return this.cleanText(container); + } + + private extractFeature($: cheerio.CheerioAPI, feature: string): string { + const el = $(`.feature-${feature}`).text(); + return this.cleanText(el); + } + + private extractImages($: cheerio.CheerioAPI): string[] { + const images: string[] = []; + $('img.gallery-image').each((_, el) => { + const src = $(el).attr('src') || $(el).attr('data-src'); + if (src) images.push(src); + }); + return images; + } + + private extractAmenidades($: cheerio.CheerioAPI): string[] { + const amenities: string[] = []; + $('.amenity-item').each((_, el) => { + amenities.push(this.cleanText($(el).text())); + }); + return amenities; + } +} +``` + +### 4.3 Transformador/Normalizador + +```typescript +// src/etl/transformers/normalizer.ts +import { RawPropertyData, NormalizedProperty, PropertyType, TransactionType } from '../types'; +import { GeocodingService } from '../services/geocoding.service'; + +export class PropertyNormalizer { + constructor(private geocoder: GeocodingService) {} + + async normalize(raw: RawPropertyData): Promise { + const price = this.parsePrice(raw.precio); + const areas = this.parseAreas(raw.superficie); + const location = await this.normalizeLocation(raw.ubicacion); + + const normalized: NormalizedProperty = { + id: this.generateId(raw), + sourceId: raw.sourceId, + source: raw.source, + sourceUrl: raw.sourceUrl, + + title: this.normalizeTitle(raw.titulo), + description: raw.descripcion || '', + propertyType: this.detectPropertyType(raw), + transactionType: this.detectTransactionType(raw), + + price: price.amount, + currency: price.currency, + pricePerSqm: areas.constructed + ? Math.round(price.amount / areas.constructed) + : null, + + landArea: areas.land, + constructedArea: areas.constructed, + + bedrooms: this.parseNumber(raw.recamaras), + bathrooms: this.parseNumber(raw.banos), + parkingSpaces: this.extractParkingSpaces(raw), + floors: null, + yearBuilt: null, + + location, + + images: this.normalizeImages(raw.imagenes), + virtualTour: null, + video: null, + + amenities: this.normalizeAmenities(raw.amenidades), + + agent: { + name: raw.contacto?.nombre || null, + phone: this.normalizePhone(raw.contacto?.telefono), + email: raw.contacto?.email || null, + agency: null, + }, + + firstSeenAt: raw.scrapedAt, + lastSeenAt: raw.scrapedAt, + publishedAt: null, + status: 'active', + + dataQuality: this.calculateDataQuality(raw), + }; + + return normalized; + } + + private parsePrice(priceStr?: string): { amount: number; currency: 'MXN' | 'USD' } { + if (!priceStr) return { amount: 0, currency: 'MXN' }; + + const currency = priceStr.includes('USD') || priceStr.includes('$') && priceStr.includes('dll') + ? 'USD' : 'MXN'; + + const cleaned = priceStr.replace(/[^\d.]/g, ''); + const amount = parseFloat(cleaned) || 0; + + return { amount, currency }; + } + + private parseAreas(superficieStr?: string): { land: number | null; constructed: number | null } { + if (!superficieStr) return { land: null, constructed: null }; + + const result = { land: null as number | null, constructed: null as number | null }; + + // Buscar patrones como "180 m2 construccion" o "250 m2 terreno" + const constMatch = superficieStr.match(/(\d+(?:\.\d+)?)\s*m[2²]?\s*(const|constr)/i); + const landMatch = superficieStr.match(/(\d+(?:\.\d+)?)\s*m[2²]?\s*(terr|lote)/i); + + if (constMatch) result.constructed = parseFloat(constMatch[1]); + if (landMatch) result.land = parseFloat(landMatch[1]); + + // Si solo hay un numero, asumir es area construida para casas/deptos + if (!result.constructed && !result.land) { + const numbers = superficieStr.match(/(\d+(?:\.\d+)?)/g); + if (numbers && numbers.length === 1) { + result.constructed = parseFloat(numbers[0]); + } + } + + return result; + } + + private async normalizeLocation(rawAddress?: string): Promise { + const defaultLocation = { + rawAddress: rawAddress || '', + street: null, + neighborhood: '', + municipality: '', + state: 'Jalisco', + postalCode: null, + country: 'Mexico', + coordinates: null, + geocodeConfidence: 0, + }; + + if (!rawAddress) return defaultLocation; + + try { + const geocoded = await this.geocoder.geocode(rawAddress); + + return { + rawAddress, + street: geocoded.street, + neighborhood: geocoded.neighborhood || this.extractColonia(rawAddress), + municipality: geocoded.municipality || 'Guadalajara', + state: geocoded.state || 'Jalisco', + postalCode: geocoded.postalCode, + country: 'Mexico', + coordinates: geocoded.coordinates, + geocodeConfidence: geocoded.confidence, + }; + } catch (error) { + // Fallback: parsing manual + return { + ...defaultLocation, + neighborhood: this.extractColonia(rawAddress), + municipality: this.extractMunicipio(rawAddress), + }; + } + } + + private extractColonia(address: string): string { + // Patrones comunes: "Col. Providencia", "Colonia Americana" + const match = address.match(/(?:col\.?|colonia)\s+([^,]+)/i); + return match ? match[1].trim() : ''; + } + + private extractMunicipio(address: string): string { + const municipios = [ + 'Guadalajara', 'Zapopan', 'Tlaquepaque', 'Tonala', + 'Tlajomulco', 'El Salto', 'Ixtlahuacan' + ]; + + for (const mun of municipios) { + if (address.toLowerCase().includes(mun.toLowerCase())) { + return mun; + } + } + return ''; + } + + private detectPropertyType(raw: RawPropertyData): PropertyType { + const text = `${raw.titulo} ${raw.descripcion}`.toLowerCase(); + + if (text.includes('departamento') || text.includes('depto')) { + return PropertyType.DEPARTAMENTO; + } + if (text.includes('casa')) { + return PropertyType.CASA; + } + if (text.includes('terreno') || text.includes('lote')) { + return PropertyType.TERRENO; + } + if (text.includes('local') || text.includes('comercial')) { + return PropertyType.LOCAL_COMERCIAL; + } + if (text.includes('oficina')) { + return PropertyType.OFICINA; + } + if (text.includes('bodega')) { + return PropertyType.BODEGA; + } + + return PropertyType.OTRO; + } + + private detectTransactionType(raw: RawPropertyData): TransactionType { + const text = `${raw.titulo} ${raw.sourceUrl}`.toLowerCase(); + + if (text.includes('renta') || text.includes('alquiler')) { + return TransactionType.RENTA; + } + if (text.includes('traspaso')) { + return TransactionType.TRASPASO; + } + + return TransactionType.VENTA; + } + + private normalizePhone(phone?: string | null): string | null { + if (!phone) return null; + + // Limpiar y formatear telefono mexicano + const cleaned = phone.replace(/\D/g, ''); + + if (cleaned.length === 10) { + return cleaned; + } + if (cleaned.length === 12 && cleaned.startsWith('52')) { + return cleaned.substring(2); + } + + return cleaned || null; + } + + private normalizeImages(images?: string[]): NormalizedProperty['images'] { + if (!images || images.length === 0) return []; + + return images.map((url, index) => ({ + url: this.normalizeImageUrl(url), + thumbnailUrl: this.generateThumbnailUrl(url), + order: index, + isMain: index === 0, + })); + } + + private normalizeImageUrl(url: string): string { + // Asegurar HTTPS y limpiar parametros innecesarios + return url.replace(/^http:/, 'https:'); + } + + private generateThumbnailUrl(url: string): string { + // Generar URL de thumbnail (depende del CDN usado) + return url.replace('/images/', '/thumbnails/'); + } + + private normalizeAmenities(amenities?: string[]): string[] { + if (!amenities) return []; + + const normalized = new Set(); + const mapping: Record = { + 'alberca': 'Alberca', + 'piscina': 'Alberca', + 'jardin': 'Jardin', + 'gym': 'Gimnasio', + 'gimnasio': 'Gimnasio', + 'roof': 'Roof Garden', + 'terraza': 'Terraza', + 'seguridad': 'Seguridad 24/7', + 'vigilancia': 'Seguridad 24/7', + 'estacionamiento': 'Estacionamiento', + 'cochera': 'Estacionamiento', + }; + + for (const amenity of amenities) { + const lower = amenity.toLowerCase().trim(); + const key = Object.keys(mapping).find(k => lower.includes(k)); + normalized.add(key ? mapping[key] : amenity); + } + + return Array.from(normalized); + } + + private parseNumber(str?: string): number | null { + if (!str) return null; + const num = parseInt(str.replace(/\D/g, '')); + return isNaN(num) ? null : num; + } + + private extractParkingSpaces(raw: RawPropertyData): number | null { + const text = `${raw.descripcion} ${raw.amenidades?.join(' ')}`; + const match = text.match(/(\d+)\s*(estacionamiento|cochera|parking)/i); + return match ? parseInt(match[1]) : null; + } + + private generateId(raw: RawPropertyData): string { + // Crear hash unico basado en source + sourceId + const crypto = require('crypto'); + const input = `${raw.source}:${raw.sourceId}`; + return crypto.createHash('sha256').update(input).digest('hex').substring(0, 32); + } + + private calculateDataQuality(raw: RawPropertyData): NormalizedProperty['dataQuality'] { + const requiredFields = ['titulo', 'precio', 'ubicacion', 'superficie']; + const optionalFields = ['recamaras', 'banos', 'descripcion', 'imagenes']; + + const missingRequired = requiredFields.filter(f => !raw[f as keyof RawPropertyData]); + const missingOptional = optionalFields.filter(f => !raw[f as keyof RawPropertyData]); + + const warnings: string[] = []; + + // Validaciones + if (raw.precio && parseFloat(raw.precio.replace(/\D/g, '')) < 100000) { + warnings.push('Precio sospechosamente bajo'); + } + if (raw.imagenes && raw.imagenes.length < 3) { + warnings.push('Pocas imagenes'); + } + + const score = Math.max(0, 100 + - (missingRequired.length * 20) + - (missingOptional.length * 5) + - (warnings.length * 10) + ); + + return { + score, + missingFields: [...missingRequired, ...missingOptional], + warnings, + }; + } +} +``` + +--- + +## 5. Servicio de Geocoding + +```typescript +// src/etl/services/geocoding.service.ts +import { Redis } from 'ioredis'; + +interface GeocodedResult { + street: string | null; + neighborhood: string | null; + municipality: string | null; + state: string | null; + postalCode: string | null; + coordinates: { lat: number; lng: number } | null; + confidence: number; +} + +export class GeocodingService { + private redis: Redis; + private nominatimUrl = 'https://nominatim.openstreetmap.org/search'; + private rateLimiter: { lastCall: number; minInterval: number }; + + constructor() { + this.redis = new Redis(process.env.REDIS_URL); + this.rateLimiter = { lastCall: 0, minInterval: 1100 }; // 1 req/sec for Nominatim + } + + async geocode(address: string): Promise { + // 1. Check cache + const cacheKey = `geocode:${this.hashAddress(address)}`; + const cached = await this.redis.get(cacheKey); + if (cached) { + return JSON.parse(cached); + } + + // 2. Rate limiting + await this.enforceRateLimit(); + + // 3. Call geocoding API + const result = await this.callNominatim(address); + + // 4. Cache result (30 days) + await this.redis.setex(cacheKey, 60 * 60 * 24 * 30, JSON.stringify(result)); + + return result; + } + + private async callNominatim(address: string): Promise { + const params = new URLSearchParams({ + q: `${address}, Jalisco, Mexico`, + format: 'json', + addressdetails: '1', + limit: '1', + }); + + try { + const response = await fetch(`${this.nominatimUrl}?${params}`, { + headers: { + 'User-Agent': 'InmobiliariaAnalytics/1.0', + }, + }); + + const data = await response.json(); + + if (!data || data.length === 0) { + return this.emptyResult(); + } + + const result = data[0]; + const addr = result.address || {}; + + return { + street: addr.road || addr.street || null, + neighborhood: addr.suburb || addr.neighbourhood || null, + municipality: addr.city || addr.town || addr.municipality || null, + state: addr.state || null, + postalCode: addr.postcode || null, + coordinates: { + lat: parseFloat(result.lat), + lng: parseFloat(result.lon), + }, + confidence: this.calculateConfidence(result), + }; + } catch (error) { + console.error('Geocoding error:', error); + return this.emptyResult(); + } + } + + private calculateConfidence(result: any): number { + // Basado en importance y type de Nominatim + const importance = result.importance || 0; + const type = result.type; + + let confidence = importance * 100; + + // Bonus por tipo preciso + if (type === 'house' || type === 'building') { + confidence = Math.min(100, confidence + 20); + } + + return Math.round(confidence); + } + + private async enforceRateLimit(): Promise { + const now = Date.now(); + const elapsed = now - this.rateLimiter.lastCall; + + if (elapsed < this.rateLimiter.minInterval) { + await new Promise(resolve => + setTimeout(resolve, this.rateLimiter.minInterval - elapsed) + ); + } + + this.rateLimiter.lastCall = Date.now(); + } + + private hashAddress(address: string): string { + const crypto = require('crypto'); + return crypto.createHash('md5').update(address.toLowerCase().trim()).digest('hex'); + } + + private emptyResult(): GeocodedResult { + return { + street: null, + neighborhood: null, + municipality: null, + state: null, + postalCode: null, + coordinates: null, + confidence: 0, + }; + } +} +``` + +--- + +## 6. Detector de Duplicados + +```typescript +// src/etl/services/deduplication.service.ts +import { Pool } from 'pg'; +import { NormalizedProperty } from '../types'; + +interface DuplicateCandidate { + id: string; + similarity: number; + matchedFields: string[]; +} + +export class DeduplicationService { + private db: Pool; + + constructor() { + this.db = new Pool({ connectionString: process.env.DATABASE_URL }); + } + + async findDuplicates(property: NormalizedProperty): Promise { + const candidates: DuplicateCandidate[] = []; + + // 1. Exacto por sourceId de otra fuente + const exactMatch = await this.findExactMatch(property); + if (exactMatch) { + candidates.push({ ...exactMatch, similarity: 1.0 }); + } + + // 2. Fuzzy matching por caracteristicas + const fuzzyMatches = await this.findFuzzyMatches(property); + candidates.push(...fuzzyMatches); + + return candidates.sort((a, b) => b.similarity - a.similarity); + } + + private async findExactMatch(property: NormalizedProperty): Promise { + // Buscar misma propiedad de diferente fuente + const query = ` + SELECT id, source, source_id, title, price, + ST_Distance( + coordinates::geography, + ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography + ) as distance_meters + FROM properties + WHERE source != $3 + AND price BETWEEN $4 * 0.95 AND $4 * 1.05 + AND property_type = $5 + AND ST_DWithin( + coordinates::geography, + ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, + 100 -- 100 metros + ) + LIMIT 5 + `; + + if (!property.location.coordinates) return null; + + const result = await this.db.query(query, [ + property.location.coordinates.lng, + property.location.coordinates.lat, + property.source, + property.price, + property.propertyType, + ]); + + for (const row of result.rows) { + const titleSimilarity = this.calculateTextSimilarity(property.title, row.title); + if (titleSimilarity > 0.8 && row.distance_meters < 50) { + return { + id: row.id, + similarity: 0.95, + matchedFields: ['coordinates', 'price', 'title', 'property_type'], + }; + } + } + + return null; + } + + private async findFuzzyMatches(property: NormalizedProperty): Promise { + const query = ` + SELECT id, title, price, bedrooms, bathrooms, constructed_area, + neighborhood, coordinates + FROM properties + WHERE source != $1 + AND neighborhood = $2 + AND property_type = $3 + AND price BETWEEN $4 * 0.9 AND $4 * 1.1 + AND status = 'active' + LIMIT 20 + `; + + const result = await this.db.query(query, [ + property.source, + property.location.neighborhood, + property.propertyType, + property.price, + ]); + + const candidates: DuplicateCandidate[] = []; + + for (const row of result.rows) { + const similarity = this.calculatePropertySimilarity(property, row); + + if (similarity > 0.75) { + candidates.push({ + id: row.id, + similarity, + matchedFields: this.getMatchedFields(property, row), + }); + } + } + + return candidates; + } + + private calculatePropertySimilarity(prop: NormalizedProperty, candidate: any): number { + let score = 0; + let totalWeight = 0; + + // Precio (peso 0.3) + const priceDiff = Math.abs(prop.price - candidate.price) / prop.price; + score += (1 - Math.min(priceDiff, 1)) * 0.3; + totalWeight += 0.3; + + // Area (peso 0.25) + if (prop.constructedArea && candidate.constructed_area) { + const areaDiff = Math.abs(prop.constructedArea - candidate.constructed_area) / prop.constructedArea; + score += (1 - Math.min(areaDiff, 1)) * 0.25; + totalWeight += 0.25; + } + + // Recamaras (peso 0.15) + if (prop.bedrooms !== null && candidate.bedrooms !== null) { + score += (prop.bedrooms === candidate.bedrooms ? 1 : 0) * 0.15; + totalWeight += 0.15; + } + + // Banos (peso 0.15) + if (prop.bathrooms !== null && candidate.bathrooms !== null) { + score += (prop.bathrooms === candidate.bathrooms ? 1 : 0) * 0.15; + totalWeight += 0.15; + } + + // Titulo (peso 0.15) + const titleSim = this.calculateTextSimilarity(prop.title, candidate.title); + score += titleSim * 0.15; + totalWeight += 0.15; + + return totalWeight > 0 ? score / totalWeight : 0; + } + + private calculateTextSimilarity(text1: string, text2: string): number { + // Jaccard similarity de palabras + const words1 = new Set(text1.toLowerCase().split(/\s+/)); + const words2 = new Set(text2.toLowerCase().split(/\s+/)); + + const intersection = new Set([...words1].filter(x => words2.has(x))); + const union = new Set([...words1, ...words2]); + + return intersection.size / union.size; + } + + private getMatchedFields(prop: NormalizedProperty, candidate: any): string[] { + const matched: string[] = []; + + if (Math.abs(prop.price - candidate.price) / prop.price < 0.05) { + matched.push('price'); + } + if (prop.bedrooms === candidate.bedrooms) { + matched.push('bedrooms'); + } + if (prop.bathrooms === candidate.bathrooms) { + matched.push('bathrooms'); + } + if (prop.location.neighborhood === candidate.neighborhood) { + matched.push('neighborhood'); + } + + return matched; + } + + async mergeProperties( + primaryId: string, + duplicateIds: string[] + ): Promise { + const client = await this.db.connect(); + + try { + await client.query('BEGIN'); + + // Crear registros en property_aliases + for (const dupId of duplicateIds) { + await client.query(` + INSERT INTO property_aliases (primary_id, alias_id, merged_at) + VALUES ($1, $2, NOW()) + ON CONFLICT DO NOTHING + `, [primaryId, dupId]); + } + + // Marcar duplicados como merged + await client.query(` + UPDATE properties + SET status = 'merged', merged_into = $1 + WHERE id = ANY($2) + `, [primaryId, duplicateIds]); + + await client.query('COMMIT'); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } +} +``` + +--- + +## 7. Loader (Carga a Base de Datos) + +```typescript +// src/etl/loaders/property.loader.ts +import { Pool } from 'pg'; +import { NormalizedProperty } from '../types'; +import { DeduplicationService } from '../services/deduplication.service'; + +export class PropertyLoader { + private db: Pool; + private deduper: DeduplicationService; + + constructor() { + this.db = new Pool({ connectionString: process.env.DATABASE_URL }); + this.deduper = new DeduplicationService(); + } + + async load(property: NormalizedProperty): Promise<{ action: 'inserted' | 'updated' | 'duplicate'; id: string }> { + // 1. Verificar si ya existe por source + sourceId + const existing = await this.findExisting(property.source, property.sourceId); + + if (existing) { + await this.update(existing.id, property); + return { action: 'updated', id: existing.id }; + } + + // 2. Buscar duplicados de otras fuentes + const duplicates = await this.deduper.findDuplicates(property); + + if (duplicates.length > 0 && duplicates[0].similarity > 0.9) { + // Es un duplicado, vincular a existente + await this.linkDuplicate(duplicates[0].id, property); + return { action: 'duplicate', id: duplicates[0].id }; + } + + // 3. Insertar nueva propiedad + const id = await this.insert(property); + return { action: 'inserted', id }; + } + + private async findExisting(source: string, sourceId: string): Promise<{ id: string } | null> { + const result = await this.db.query( + 'SELECT id FROM properties WHERE source = $1 AND source_id = $2', + [source, sourceId] + ); + return result.rows[0] || null; + } + + private async insert(property: NormalizedProperty): Promise { + const query = ` + INSERT INTO properties ( + id, source, source_id, source_url, + title, description, property_type, transaction_type, + price, currency, price_per_sqm, + land_area, constructed_area, + bedrooms, bathrooms, parking_spaces, floors, year_built, + raw_address, street, neighborhood, municipality, state, postal_code, country, + coordinates, geocode_confidence, + images, virtual_tour, video, + amenities, + agent_name, agent_phone, agent_email, agent_agency, + first_seen_at, last_seen_at, published_at, status, + data_quality_score, missing_fields, data_warnings + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, + $11, $12, $13, $14, $15, $16, $17, $18, + $19, $20, $21, $22, $23, $24, $25, + ST_SetSRID(ST_MakePoint($26, $27), 4326), $28, + $29, $30, $31, $32, + $33, $34, $35, $36, + $37, $38, $39, $40, + $41, $42, $43 + ) + RETURNING id + `; + + const coords = property.location.coordinates; + + const result = await this.db.query(query, [ + property.id, + property.source, + property.sourceId, + property.sourceUrl, + property.title, + property.description, + property.propertyType, + property.transactionType, + property.price, + property.currency, + property.pricePerSqm, + property.landArea, + property.constructedArea, + property.bedrooms, + property.bathrooms, + property.parkingSpaces, + property.floors, + property.yearBuilt, + property.location.rawAddress, + property.location.street, + property.location.neighborhood, + property.location.municipality, + property.location.state, + property.location.postalCode, + property.location.country, + coords?.lng || null, + coords?.lat || null, + property.location.geocodeConfidence, + JSON.stringify(property.images), + property.virtualTour, + property.video, + property.amenities, + property.agent.name, + property.agent.phone, + property.agent.email, + property.agent.agency, + property.firstSeenAt, + property.lastSeenAt, + property.publishedAt, + property.status, + property.dataQuality.score, + property.dataQuality.missingFields, + property.dataQuality.warnings, + ]); + + return result.rows[0].id; + } + + private async update(id: string, property: NormalizedProperty): Promise { + const query = ` + UPDATE properties SET + title = $2, + description = $3, + price = $4, + price_per_sqm = $5, + last_seen_at = NOW(), + data_quality_score = $6, + images = $7 + WHERE id = $1 + `; + + await this.db.query(query, [ + id, + property.title, + property.description, + property.price, + property.pricePerSqm, + property.dataQuality.score, + JSON.stringify(property.images), + ]); + } + + private async linkDuplicate(existingId: string, property: NormalizedProperty): Promise { + // Registrar como alias + await this.db.query(` + INSERT INTO property_aliases (primary_id, alias_source, alias_source_id, alias_url) + VALUES ($1, $2, $3, $4) + ON CONFLICT DO NOTHING + `, [existingId, property.source, property.sourceId, property.sourceUrl]); + + // Actualizar last_seen del principal + await this.db.query(` + UPDATE properties SET last_seen_at = NOW() WHERE id = $1 + `, [existingId]); + } +} +``` + +--- + +## 8. Esquema de Base de Datos + +```sql +-- Tabla principal de propiedades +CREATE TABLE properties ( + id VARCHAR(32) PRIMARY KEY, + source VARCHAR(50) NOT NULL, + source_id VARCHAR(100) NOT NULL, + source_url TEXT NOT NULL, + + title VARCHAR(500) NOT NULL, + description TEXT, + property_type VARCHAR(50) NOT NULL, + transaction_type VARCHAR(20) NOT NULL, + + price DECIMAL(15,2) NOT NULL, + currency VARCHAR(3) DEFAULT 'MXN', + price_per_sqm DECIMAL(10,2), + + land_area DECIMAL(10,2), + constructed_area DECIMAL(10,2), + + bedrooms SMALLINT, + bathrooms DECIMAL(3,1), + parking_spaces SMALLINT, + floors SMALLINT, + year_built SMALLINT, + + raw_address TEXT, + street VARCHAR(200), + neighborhood VARCHAR(100), + municipality VARCHAR(100), + state VARCHAR(50), + postal_code VARCHAR(10), + country VARCHAR(50) DEFAULT 'Mexico', + coordinates GEOMETRY(Point, 4326), + geocode_confidence SMALLINT, + + images JSONB DEFAULT '[]', + virtual_tour TEXT, + video TEXT, + + amenities TEXT[], + + agent_name VARCHAR(200), + agent_phone VARCHAR(20), + agent_email VARCHAR(200), + agent_agency VARCHAR(200), + + first_seen_at TIMESTAMP NOT NULL, + last_seen_at TIMESTAMP NOT NULL, + published_at TIMESTAMP, + status VARCHAR(20) DEFAULT 'active', + merged_into VARCHAR(32) REFERENCES properties(id), + + data_quality_score SMALLINT, + missing_fields TEXT[], + data_warnings TEXT[], + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + UNIQUE(source, source_id) +); + +-- Indices +CREATE INDEX idx_properties_location ON properties USING GIST(coordinates); +CREATE INDEX idx_properties_neighborhood ON properties(neighborhood); +CREATE INDEX idx_properties_price ON properties(price); +CREATE INDEX idx_properties_type ON properties(property_type); +CREATE INDEX idx_properties_status ON properties(status); +CREATE INDEX idx_properties_last_seen ON properties(last_seen_at); + +-- Tabla de aliases (propiedades duplicadas) +CREATE TABLE property_aliases ( + id SERIAL PRIMARY KEY, + primary_id VARCHAR(32) REFERENCES properties(id), + alias_source VARCHAR(50) NOT NULL, + alias_source_id VARCHAR(100) NOT NULL, + alias_url TEXT, + merged_at TIMESTAMP DEFAULT NOW(), + + UNIQUE(alias_source, alias_source_id) +); + +-- Historial de precios +CREATE TABLE price_history ( + id SERIAL PRIMARY KEY, + property_id VARCHAR(32) REFERENCES properties(id), + price DECIMAL(15,2) NOT NULL, + currency VARCHAR(3) NOT NULL, + recorded_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_price_history_property ON price_history(property_id); +``` + +--- + +## 9. Tests + +```typescript +// src/etl/__tests__/normalizer.test.ts +import { PropertyNormalizer } from '../transformers/normalizer'; +import { GeocodingService } from '../services/geocoding.service'; + +jest.mock('../services/geocoding.service'); + +describe('PropertyNormalizer', () => { + let normalizer: PropertyNormalizer; + + beforeEach(() => { + const mockGeocoder = new GeocodingService(); + (mockGeocoder.geocode as jest.Mock).mockResolvedValue({ + street: 'Av. Providencia', + neighborhood: 'Providencia', + municipality: 'Guadalajara', + state: 'Jalisco', + postalCode: '44630', + coordinates: { lat: 20.6736, lng: -103.3927 }, + confidence: 85, + }); + + normalizer = new PropertyNormalizer(mockGeocoder); + }); + + describe('parsePrice', () => { + it('should parse MXN price correctly', async () => { + const raw = { + source: 'test', + sourceId: '123', + sourceUrl: 'http://test.com/123', + scrapedAt: new Date(), + precio: '$4,500,000 MXN', + }; + + const result = await normalizer.normalize(raw); + + expect(result.price).toBe(4500000); + expect(result.currency).toBe('MXN'); + }); + + it('should parse USD price correctly', async () => { + const raw = { + source: 'test', + sourceId: '124', + sourceUrl: 'http://test.com/124', + scrapedAt: new Date(), + precio: '$350,000 USD', + }; + + const result = await normalizer.normalize(raw); + + expect(result.price).toBe(350000); + expect(result.currency).toBe('USD'); + }); + }); + + describe('parseAreas', () => { + it('should parse both land and constructed areas', async () => { + const raw = { + source: 'test', + sourceId: '125', + sourceUrl: 'http://test.com/125', + scrapedAt: new Date(), + superficie: '180 m2 construccion, 250 m2 terreno', + }; + + const result = await normalizer.normalize(raw); + + expect(result.constructedArea).toBe(180); + expect(result.landArea).toBe(250); + }); + }); + + describe('detectPropertyType', () => { + it('should detect departamento', async () => { + const raw = { + source: 'test', + sourceId: '126', + sourceUrl: 'http://test.com/126', + scrapedAt: new Date(), + titulo: 'Hermoso departamento en Providencia', + }; + + const result = await normalizer.normalize(raw); + + expect(result.propertyType).toBe('departamento'); + }); + }); +}); +``` + +--- + +## 10. Metricas y Monitoreo + +```typescript +// Metricas del pipeline ETL +export const etlMetrics = { + // Contadores + properties_extracted_total: new Counter({ + name: 'etl_properties_extracted_total', + help: 'Total properties extracted', + labelNames: ['source'], + }), + + properties_normalized_total: new Counter({ + name: 'etl_properties_normalized_total', + help: 'Total properties normalized', + labelNames: ['source', 'property_type'], + }), + + properties_loaded_total: new Counter({ + name: 'etl_properties_loaded_total', + help: 'Total properties loaded', + labelNames: ['action'], // inserted, updated, duplicate + }), + + geocoding_requests_total: new Counter({ + name: 'etl_geocoding_requests_total', + help: 'Total geocoding requests', + labelNames: ['status'], // success, error, cache_hit + }), + + // Histogramas + normalization_duration_seconds: new Histogram({ + name: 'etl_normalization_duration_seconds', + help: 'Time to normalize a property', + buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5], + }), + + data_quality_score: new Histogram({ + name: 'etl_data_quality_score', + help: 'Data quality scores', + buckets: [20, 40, 60, 80, 100], + }), +}; +``` + +--- + +**Siguiente:** [ET-IA-007-proxies.md](./ET-IA-007-proxies.md) diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-003-proxies.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-003-proxies.md new file mode 100644 index 0000000..e036452 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/ET-SCR-003-proxies.md @@ -0,0 +1,1146 @@ +--- +id: "ET-SCR-003" +title: "Especificacion Tecnica - Gestion de Pool de Proxies" +type: "Technical Specification" +epic: "IAI-007" +status: "Draft" +version: "1.0" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# ET-SCR-003: Gestion de Pool de Proxies + +--- + +## 1. Resumen + +Sistema de gestion de proxies residenciales y datacenter para rotacion automatica, evitar bloqueos IP, y mantener tasas de exito altas en el scraping. + +--- + +## 2. Arquitectura del Sistema de Proxies + +``` +┌─────────────────────────────────────────────────────────────┐ +│ PROXY MANAGER │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Residential │ │ Datacenter │ │ Mobile │ │ +│ │ Pool │ │ Pool │ │ Pool │ │ +│ │ (Premium) │ │ (Backup) │ │ (Reserved) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └────────────┬────┴────────────────┘ │ +│ │ │ +│ ┌───────▼───────┐ │ +│ │ Selector │ │ +│ │ Engine │ │ +│ └───────┬───────┘ │ +│ │ │ +│ ┌─────────────────┼─────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────┐ ┌──────┐ ┌──────────┐ │ +│ │Health│ │ Geo │ │ Cooldown │ │ +│ │Check │ │Filter│ │ Manager │ │ +│ └──────┘ └──────┘ └──────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────┐ + │ Browser │ + │ Manager │ + └─────────────┘ +``` + +--- + +## 3. Proveedores de Proxies + +### 3.1 Configuracion de Proveedores + +```yaml +# config/proxies.yml +providers: + brightdata: + type: residential + priority: 1 + endpoint: "brd.superproxy.io" + port: 22225 + username: "${BRIGHTDATA_USER}" + password: "${BRIGHTDATA_PASS}" + geo: + country: "mx" + city: "guadalajara" + sticky_session: true + session_duration: 600 # 10 minutos + monthly_bandwidth: "100GB" + cost_per_gb: 15 # USD + + smartproxy: + type: residential + priority: 2 + endpoint: "mx.smartproxy.com" + port: 10000 + username: "${SMARTPROXY_USER}" + password: "${SMARTPROXY_PASS}" + geo: + country: "mx" + rotation: "per_request" + monthly_bandwidth: "50GB" + cost_per_gb: 12 + + datacenter_pool: + type: datacenter + priority: 3 + proxies: + - host: "proxy1.example.com" + port: 3128 + - host: "proxy2.example.com" + port: 3128 + auth: + username: "${DC_PROXY_USER}" + password: "${DC_PROXY_PASS}" + cost_per_request: 0.001 + +settings: + default_provider: "brightdata" + fallback_chain: ["brightdata", "smartproxy", "datacenter_pool"] + max_failures_before_switch: 3 + cooldown_after_block: 300 # 5 minutos + health_check_interval: 60 # 1 minuto +``` + +### 3.2 Tipos de Proxy y Uso + +| Tipo | Uso Principal | Costo | Tasa Exito | +|------|--------------|-------|------------| +| Residential | Sitios con anti-bot agresivo | Alto | 95%+ | +| Datacenter | Sitios simples, backup | Bajo | 70-80% | +| Mobile | Casos especiales, Cloudflare | Muy Alto | 98%+ | + +--- + +## 4. Implementacion + +### 4.1 Interfaz de Proxy + +```typescript +// src/proxy/types.ts +export interface ProxyConfig { + host: string; + port: number; + username?: string; + password?: string; + protocol: 'http' | 'https' | 'socks5'; +} + +export interface ProxyWithMetadata extends ProxyConfig { + id: string; + provider: string; + type: 'residential' | 'datacenter' | 'mobile'; + geo: { + country: string; + city?: string; + region?: string; + }; + + // Metricas + stats: ProxyStats; + + // Estado + status: 'active' | 'cooling' | 'blocked' | 'inactive'; + lastUsed: Date | null; + cooldownUntil: Date | null; +} + +export interface ProxyStats { + totalRequests: number; + successfulRequests: number; + failedRequests: number; + blockedRequests: number; + avgLatencyMs: number; + bandwidthUsedMb: number; + lastSuccess: Date | null; + lastFailure: Date | null; +} + +export interface ProxySelection { + proxy: ProxyWithMetadata; + sessionId?: string; +} +``` + +### 4.2 Proxy Pool Manager + +```typescript +// src/proxy/pool-manager.ts +import { Redis } from 'ioredis'; +import { ProxyWithMetadata, ProxyConfig, ProxySelection } from './types'; +import { ProxyHealthChecker } from './health-checker'; +import { Logger } from '../utils/logger'; + +export class ProxyPoolManager { + private redis: Redis; + private healthChecker: ProxyHealthChecker; + private logger: Logger; + private providers: Map; + + constructor() { + this.redis = new Redis(process.env.REDIS_URL); + this.healthChecker = new ProxyHealthChecker(); + this.logger = new Logger('ProxyPool'); + this.providers = new Map(); + + this.initializeProviders(); + } + + private initializeProviders(): void { + // Bright Data + this.providers.set('brightdata', new BrightDataProvider({ + endpoint: process.env.BRIGHTDATA_ENDPOINT!, + username: process.env.BRIGHTDATA_USER!, + password: process.env.BRIGHTDATA_PASS!, + })); + + // SmartProxy + this.providers.set('smartproxy', new SmartProxyProvider({ + endpoint: process.env.SMARTPROXY_ENDPOINT!, + username: process.env.SMARTPROXY_USER!, + password: process.env.SMARTPROXY_PASS!, + })); + + // Datacenter Pool + this.providers.set('datacenter', new DatacenterProxyProvider({ + proxies: JSON.parse(process.env.DC_PROXIES || '[]'), + })); + } + + async getProxy(options: { + targetDomain: string; + preferredType?: 'residential' | 'datacenter' | 'mobile'; + requireFresh?: boolean; + stickySession?: boolean; + sessionId?: string; + }): Promise { + const { targetDomain, preferredType, requireFresh, stickySession, sessionId } = options; + + // 1. Si hay sesion sticky activa, reusar + if (stickySession && sessionId) { + const existingProxy = await this.getStickySession(sessionId); + if (existingProxy) { + return { proxy: existingProxy, sessionId }; + } + } + + // 2. Obtener pool de candidatos + const candidates = await this.getCandidates({ + domain: targetDomain, + type: preferredType, + excludeCooling: true, + excludeBlocked: true, + }); + + if (candidates.length === 0) { + throw new Error(`No proxies available for ${targetDomain}`); + } + + // 3. Seleccionar mejor proxy + const selected = this.selectBestProxy(candidates, { + requireFresh, + domain: targetDomain, + }); + + // 4. Crear sesion si es sticky + let newSessionId = sessionId; + if (stickySession) { + newSessionId = await this.createStickySession(selected); + } + + // 5. Marcar como usado + await this.markUsed(selected.id); + + this.logger.debug(`Selected proxy ${selected.id} for ${targetDomain}`); + + return { proxy: selected, sessionId: newSessionId }; + } + + private async getCandidates(options: { + domain: string; + type?: string; + excludeCooling: boolean; + excludeBlocked: boolean; + }): Promise { + const allProxies = await this.getAllProxies(); + const now = new Date(); + + return allProxies.filter(proxy => { + // Filtrar por tipo + if (options.type && proxy.type !== options.type) { + return false; + } + + // Excluir en cooling + if (options.excludeCooling && proxy.status === 'cooling') { + if (proxy.cooldownUntil && proxy.cooldownUntil > now) { + return false; + } + } + + // Excluir bloqueados para este dominio + if (options.excludeBlocked) { + const blockKey = `proxy:blocked:${proxy.id}:${options.domain}`; + // Check async - simplified here + } + + return proxy.status === 'active'; + }); + } + + private selectBestProxy( + candidates: ProxyWithMetadata[], + options: { requireFresh?: boolean; domain: string } + ): ProxyWithMetadata { + // Scoring algorithm + const scored = candidates.map(proxy => { + let score = 100; + + // Penalizar por uso reciente + if (proxy.lastUsed) { + const minutesSinceUse = (Date.now() - proxy.lastUsed.getTime()) / 60000; + if (minutesSinceUse < 5) { + score -= (5 - minutesSinceUse) * 10; + } + } + + // Bonus por alta tasa de exito + const successRate = proxy.stats.totalRequests > 0 + ? proxy.stats.successfulRequests / proxy.stats.totalRequests + : 0.5; + score += successRate * 20; + + // Penalizar por latencia alta + if (proxy.stats.avgLatencyMs > 2000) { + score -= 10; + } + + // Bonus por tipo preferido + if (proxy.type === 'residential') { + score += 15; + } + + // Penalizar si se requiere fresh y fue usado recientemente + if (options.requireFresh && proxy.lastUsed) { + const minutesSinceUse = (Date.now() - proxy.lastUsed.getTime()) / 60000; + if (minutesSinceUse < 30) { + score -= 50; + } + } + + return { proxy, score }; + }); + + // Ordenar por score y agregar algo de randomizacion + scored.sort((a, b) => b.score - a.score); + + // Seleccionar del top 3 aleatoriamente para evitar patrones + const topN = scored.slice(0, Math.min(3, scored.length)); + const randomIndex = Math.floor(Math.random() * topN.length); + + return topN[randomIndex].proxy; + } + + async reportSuccess(proxyId: string, domain: string, latencyMs: number): Promise { + const key = `proxy:stats:${proxyId}`; + + await this.redis.multi() + .hincrby(key, 'totalRequests', 1) + .hincrby(key, 'successfulRequests', 1) + .hset(key, 'lastSuccess', Date.now().toString()) + .exec(); + + // Actualizar latencia promedio + await this.updateAvgLatency(proxyId, latencyMs); + + this.logger.debug(`Proxy ${proxyId} success on ${domain} (${latencyMs}ms)`); + } + + async reportFailure( + proxyId: string, + domain: string, + error: Error, + isBlock: boolean = false + ): Promise { + const key = `proxy:stats:${proxyId}`; + + await this.redis.multi() + .hincrby(key, 'totalRequests', 1) + .hincrby(key, 'failedRequests', 1) + .hincrby(key, isBlock ? 'blockedRequests' : 'failedRequests', 1) + .hset(key, 'lastFailure', Date.now().toString()) + .exec(); + + if (isBlock) { + await this.handleBlock(proxyId, domain); + } + + this.logger.warn(`Proxy ${proxyId} failed on ${domain}: ${error.message}`); + } + + private async handleBlock(proxyId: string, domain: string): Promise { + // Poner en cooling para este dominio + const cooldownMinutes = 30; + const cooldownUntil = Date.now() + (cooldownMinutes * 60 * 1000); + + await this.redis.set( + `proxy:blocked:${proxyId}:${domain}`, + cooldownUntil.toString(), + 'EX', + cooldownMinutes * 60 + ); + + // Verificar si esta bloqueado en multiples dominios + const blockedDomains = await this.redis.keys(`proxy:blocked:${proxyId}:*`); + + if (blockedDomains.length >= 3) { + // Marcar como cooling general + await this.redis.hset(`proxy:${proxyId}`, 'status', 'cooling'); + await this.redis.hset(`proxy:${proxyId}`, 'cooldownUntil', (Date.now() + 3600000).toString()); + + this.logger.warn(`Proxy ${proxyId} put in cooling (blocked on ${blockedDomains.length} domains)`); + } + } + + private async getStickySession(sessionId: string): Promise { + const proxyId = await this.redis.get(`proxy:session:${sessionId}`); + if (!proxyId) return null; + + return this.getProxyById(proxyId); + } + + private async createStickySession(proxy: ProxyWithMetadata): Promise { + const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Session dura 10 minutos + await this.redis.setex(`proxy:session:${sessionId}`, 600, proxy.id); + + return sessionId; + } + + private async markUsed(proxyId: string): Promise { + await this.redis.hset(`proxy:${proxyId}`, 'lastUsed', Date.now().toString()); + } + + private async updateAvgLatency(proxyId: string, latencyMs: number): Promise { + const key = `proxy:latency:${proxyId}`; + + // Rolling average de ultimas 100 requests + await this.redis.lpush(key, latencyMs.toString()); + await this.redis.ltrim(key, 0, 99); + + const latencies = await this.redis.lrange(key, 0, -1); + const avg = latencies.reduce((sum, l) => sum + parseInt(l), 0) / latencies.length; + + await this.redis.hset(`proxy:stats:${proxyId}`, 'avgLatencyMs', Math.round(avg).toString()); + } + + private async getAllProxies(): Promise { + const keys = await this.redis.keys('proxy:*'); + const proxies: ProxyWithMetadata[] = []; + + for (const key of keys) { + if (key.match(/^proxy:[a-z0-9]+$/)) { + const data = await this.redis.hgetall(key); + if (data.host) { + proxies.push(this.parseProxyData(data)); + } + } + } + + return proxies; + } + + private async getProxyById(id: string): Promise { + const data = await this.redis.hgetall(`proxy:${id}`); + if (!data.host) return null; + return this.parseProxyData(data); + } + + private parseProxyData(data: Record): ProxyWithMetadata { + return { + id: data.id, + host: data.host, + port: parseInt(data.port), + username: data.username, + password: data.password, + protocol: data.protocol as 'http' | 'https' | 'socks5', + provider: data.provider, + type: data.type as 'residential' | 'datacenter' | 'mobile', + geo: JSON.parse(data.geo || '{}'), + stats: JSON.parse(data.stats || '{}'), + status: data.status as any, + lastUsed: data.lastUsed ? new Date(parseInt(data.lastUsed)) : null, + cooldownUntil: data.cooldownUntil ? new Date(parseInt(data.cooldownUntil)) : null, + }; + } +} +``` + +### 4.3 Health Checker + +```typescript +// src/proxy/health-checker.ts +import { ProxyWithMetadata } from './types'; +import fetch from 'node-fetch'; +import { HttpsProxyAgent } from 'https-proxy-agent'; + +export class ProxyHealthChecker { + private testUrls = [ + 'https://httpbin.org/ip', + 'https://api.ipify.org?format=json', + 'https://www.google.com.mx', + ]; + + async checkProxy(proxy: ProxyWithMetadata): Promise<{ + healthy: boolean; + latencyMs: number; + detectedIp: string | null; + error?: string; + }> { + const proxyUrl = this.buildProxyUrl(proxy); + const agent = new HttpsProxyAgent(proxyUrl); + + const startTime = Date.now(); + + try { + const response = await fetch(this.testUrls[0], { + agent, + timeout: 10000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }, + }); + + if (!response.ok) { + return { + healthy: false, + latencyMs: Date.now() - startTime, + detectedIp: null, + error: `HTTP ${response.status}`, + }; + } + + const data = await response.json() as { origin?: string; ip?: string }; + const detectedIp = data.origin || data.ip || null; + + return { + healthy: true, + latencyMs: Date.now() - startTime, + detectedIp, + }; + } catch (error) { + return { + healthy: false, + latencyMs: Date.now() - startTime, + detectedIp: null, + error: (error as Error).message, + }; + } + } + + async checkBatch(proxies: ProxyWithMetadata[]): Promise> { + const results = new Map(); + + // Check en paralelo con limite de concurrencia + const concurrency = 10; + const chunks = this.chunkArray(proxies, concurrency); + + for (const chunk of chunks) { + const checks = chunk.map(async proxy => { + const result = await this.checkProxy(proxy); + results.set(proxy.id, result.healthy); + }); + + await Promise.all(checks); + } + + return results; + } + + private buildProxyUrl(proxy: ProxyWithMetadata): string { + const auth = proxy.username && proxy.password + ? `${proxy.username}:${proxy.password}@` + : ''; + return `${proxy.protocol}://${auth}${proxy.host}:${proxy.port}`; + } + + private chunkArray(array: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; + } +} +``` + +### 4.4 Bright Data Provider + +```typescript +// src/proxy/providers/brightdata.provider.ts +import { ProxyProvider, ProxyConfig } from '../types'; + +export class BrightDataProvider implements ProxyProvider { + private config: { + endpoint: string; + username: string; + password: string; + zone?: string; + }; + + constructor(config: typeof this.config) { + this.config = config; + } + + getProxy(options?: { + country?: string; + city?: string; + sessionId?: string; + sticky?: boolean; + }): ProxyConfig { + // Construir username con opciones + let username = this.config.username; + + if (options?.country) { + username += `-country-${options.country}`; + } + if (options?.city) { + username += `-city-${options.city}`; + } + if (options?.sticky && options?.sessionId) { + username += `-session-${options.sessionId}`; + } + + return { + host: this.config.endpoint, + port: 22225, + username, + password: this.config.password, + protocol: 'http', + }; + } + + async getResidentialProxy(options: { + country: string; + city?: string; + sticky?: boolean; + }): Promise { + const sessionId = options.sticky + ? `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + : undefined; + + return this.getProxy({ + country: options.country, + city: options.city, + sessionId, + sticky: options.sticky, + }); + } + + async getMobileProxy(options: { + country: string; + carrier?: string; + }): Promise { + let username = `${this.config.username}-zone-mobile-country-${options.country}`; + + if (options.carrier) { + username += `-carrier-${options.carrier}`; + } + + return { + host: this.config.endpoint, + port: 22225, + username, + password: this.config.password, + protocol: 'http', + }; + } +} +``` + +--- + +## 5. Rotacion Inteligente + +### 5.1 Estrategias de Rotacion + +```typescript +// src/proxy/rotation-strategies.ts +import { ProxyWithMetadata, ProxySelection } from './types'; +import { ProxyPoolManager } from './pool-manager'; + +export interface RotationStrategy { + name: string; + selectProxy( + pool: ProxyPoolManager, + context: RotationContext + ): Promise; +} + +export interface RotationContext { + domain: string; + requestCount: number; + lastProxy?: ProxyWithMetadata; + sessionStart?: Date; +} + +// Estrategia: Rotar cada N requests +export class EveryNRequestsStrategy implements RotationStrategy { + name = 'every_n_requests'; + private n: number; + + constructor(n: number = 10) { + this.n = n; + } + + async selectProxy( + pool: ProxyPoolManager, + context: RotationContext + ): Promise { + const shouldRotate = context.requestCount % this.n === 0; + + if (!shouldRotate && context.lastProxy) { + return { proxy: context.lastProxy }; + } + + return pool.getProxy({ + targetDomain: context.domain, + requireFresh: true, + }); + } +} + +// Estrategia: Rotar por tiempo +export class TimeBasedStrategy implements RotationStrategy { + name = 'time_based'; + private intervalMs: number; + + constructor(intervalMinutes: number = 10) { + this.intervalMs = intervalMinutes * 60 * 1000; + } + + async selectProxy( + pool: ProxyPoolManager, + context: RotationContext + ): Promise { + const elapsed = context.sessionStart + ? Date.now() - context.sessionStart.getTime() + : Infinity; + + if (elapsed < this.intervalMs && context.lastProxy) { + return { proxy: context.lastProxy }; + } + + return pool.getProxy({ + targetDomain: context.domain, + stickySession: true, + }); + } +} + +// Estrategia: Round Robin ponderado +export class WeightedRoundRobinStrategy implements RotationStrategy { + name = 'weighted_round_robin'; + private currentIndex = 0; + + async selectProxy( + pool: ProxyPoolManager, + context: RotationContext + ): Promise { + // Implementar round robin con pesos basados en success rate + return pool.getProxy({ + targetDomain: context.domain, + }); + } +} + +// Estrategia: Adaptativa basada en respuestas +export class AdaptiveStrategy implements RotationStrategy { + name = 'adaptive'; + private failureThreshold = 2; + private consecutiveFailures = 0; + + async selectProxy( + pool: ProxyPoolManager, + context: RotationContext + ): Promise { + // Si hay muchos fallos consecutivos, forzar rotacion + if (this.consecutiveFailures >= this.failureThreshold) { + this.consecutiveFailures = 0; + return pool.getProxy({ + targetDomain: context.domain, + requireFresh: true, + }); + } + + // De lo contrario, mantener proxy actual si existe + if (context.lastProxy) { + return { proxy: context.lastProxy }; + } + + return pool.getProxy({ + targetDomain: context.domain, + stickySession: true, + }); + } + + recordSuccess(): void { + this.consecutiveFailures = 0; + } + + recordFailure(): void { + this.consecutiveFailures++; + } +} +``` + +--- + +## 6. Integracion con Playwright + +```typescript +// src/proxy/playwright-integration.ts +import { Browser, BrowserContext, Page } from 'playwright'; +import { ProxyPoolManager } from './pool-manager'; +import { ProxyWithMetadata } from './types'; + +export class PlaywrightProxyIntegration { + private proxyPool: ProxyPoolManager; + + constructor() { + this.proxyPool = new ProxyPoolManager(); + } + + async createContextWithProxy( + browser: Browser, + options: { + domain: string; + preferredType?: 'residential' | 'datacenter'; + userAgent?: string; + } + ): Promise<{ + context: BrowserContext; + proxy: ProxyWithMetadata; + sessionId: string; + }> { + const { proxy, sessionId } = await this.proxyPool.getProxy({ + targetDomain: options.domain, + preferredType: options.preferredType, + stickySession: true, + }); + + const context = await browser.newContext({ + proxy: { + server: `${proxy.protocol}://${proxy.host}:${proxy.port}`, + username: proxy.username, + password: proxy.password, + }, + userAgent: options.userAgent || this.getRandomUserAgent(), + viewport: { width: 1920, height: 1080 }, + locale: 'es-MX', + timezoneId: 'America/Mexico_City', + }); + + return { context, proxy, sessionId: sessionId! }; + } + + async wrapPageWithProxyHandling( + page: Page, + proxy: ProxyWithMetadata, + domain: string + ): Promise { + // Interceptar errores de red para reportar al pool + page.on('requestfailed', async (request) => { + const failure = request.failure(); + if (failure) { + const isBlock = this.isBlockError(failure.errorText); + await this.proxyPool.reportFailure( + proxy.id, + domain, + new Error(failure.errorText), + isBlock + ); + } + }); + + page.on('response', async (response) => { + const status = response.status(); + + if (status === 403 || status === 429 || status === 503) { + await this.proxyPool.reportFailure( + proxy.id, + domain, + new Error(`HTTP ${status}`), + true + ); + } else if (status >= 200 && status < 400) { + const timing = response.request().timing(); + await this.proxyPool.reportSuccess( + proxy.id, + domain, + timing.responseEnd - timing.requestStart + ); + } + }); + + return page; + } + + private isBlockError(errorText: string): boolean { + const blockPatterns = [ + 'net::ERR_PROXY_CONNECTION_FAILED', + 'net::ERR_TUNNEL_CONNECTION_FAILED', + 'Cloudflare', + 'Access Denied', + 'blocked', + ]; + + return blockPatterns.some(pattern => + errorText.toLowerCase().includes(pattern.toLowerCase()) + ); + } + + private getRandomUserAgent(): string { + const userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', + ]; + + return userAgents[Math.floor(Math.random() * userAgents.length)]; + } +} +``` + +--- + +## 7. Dashboard de Monitoreo + +### 7.1 Metricas Prometheus + +```typescript +// src/proxy/metrics.ts +import { Counter, Gauge, Histogram } from 'prom-client'; + +export const proxyMetrics = { + // Contadores + requests_total: new Counter({ + name: 'proxy_requests_total', + help: 'Total proxy requests', + labelNames: ['provider', 'type', 'status'], + }), + + blocks_total: new Counter({ + name: 'proxy_blocks_total', + help: 'Total proxy blocks detected', + labelNames: ['provider', 'domain'], + }), + + rotations_total: new Counter({ + name: 'proxy_rotations_total', + help: 'Total proxy rotations', + labelNames: ['reason'], + }), + + // Gauges + active_proxies: new Gauge({ + name: 'proxy_active_count', + help: 'Number of active proxies', + labelNames: ['provider', 'type'], + }), + + cooling_proxies: new Gauge({ + name: 'proxy_cooling_count', + help: 'Number of proxies in cooling period', + labelNames: ['provider'], + }), + + bandwidth_used_mb: new Gauge({ + name: 'proxy_bandwidth_used_mb', + help: 'Bandwidth used in MB', + labelNames: ['provider'], + }), + + // Histogramas + latency_seconds: new Histogram({ + name: 'proxy_latency_seconds', + help: 'Proxy request latency', + labelNames: ['provider'], + buckets: [0.1, 0.5, 1, 2, 5, 10], + }), + + success_rate: new Histogram({ + name: 'proxy_success_rate', + help: 'Proxy success rate', + labelNames: ['provider'], + buckets: [0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 1.0], + }), +}; +``` + +### 7.2 API de Estado + +```typescript +// src/proxy/routes.ts +import { Router } from 'express'; +import { ProxyPoolManager } from './pool-manager'; + +const router = Router(); +const pool = new ProxyPoolManager(); + +// GET /api/proxies/status +router.get('/status', async (req, res) => { + const stats = await pool.getPoolStatus(); + + res.json({ + overview: { + totalProxies: stats.total, + activeProxies: stats.active, + coolingProxies: stats.cooling, + blockedProxies: stats.blocked, + avgSuccessRate: stats.avgSuccessRate, + avgLatencyMs: stats.avgLatencyMs, + }, + byProvider: stats.byProvider, + byType: stats.byType, + recentBlocks: stats.recentBlocks, + bandwidthUsage: stats.bandwidthUsage, + }); +}); + +// GET /api/proxies/:id +router.get('/:id', async (req, res) => { + const proxy = await pool.getProxyDetails(req.params.id); + + if (!proxy) { + return res.status(404).json({ error: 'Proxy not found' }); + } + + res.json(proxy); +}); + +// POST /api/proxies/:id/reset +router.post('/:id/reset', async (req, res) => { + await pool.resetProxyStats(req.params.id); + res.json({ success: true }); +}); + +// POST /api/proxies/:id/cooldown +router.post('/:id/cooldown', async (req, res) => { + const { minutes = 30 } = req.body; + await pool.setCooldown(req.params.id, minutes); + res.json({ success: true }); +}); + +export default router; +``` + +--- + +## 8. Costos y Presupuesto + +```yaml +# config/proxy-budget.yml +monthly_budget: + total_usd: 500 + + allocation: + residential: 400 # 80% + datacenter: 50 # 10% + mobile: 50 # 10% (reserva) + + alerts: + warning_threshold: 0.7 # 70% del budget + critical_threshold: 0.9 # 90% del budget + + actions_on_limit: + warning: + - reduce_concurrency + - prefer_datacenter + critical: + - pause_non_essential + - alert_admin + + cost_per_request: + inmuebles24: 0.02 # Sitio dificil + metros_cubicos: 0.01 # Sitio facil + vivanuncios: 0.015 # Sitio medio +``` + +--- + +## 9. Tests + +```typescript +// src/proxy/__tests__/pool-manager.test.ts +import { ProxyPoolManager } from '../pool-manager'; +import { Redis } from 'ioredis'; + +jest.mock('ioredis'); + +describe('ProxyPoolManager', () => { + let manager: ProxyPoolManager; + + beforeEach(() => { + manager = new ProxyPoolManager(); + }); + + describe('getProxy', () => { + it('should return a proxy for valid domain', async () => { + const result = await manager.getProxy({ + targetDomain: 'inmuebles24.com', + }); + + expect(result.proxy).toBeDefined(); + expect(result.proxy.host).toBeDefined(); + }); + + it('should reuse sticky session when provided', async () => { + const first = await manager.getProxy({ + targetDomain: 'test.com', + stickySession: true, + }); + + const second = await manager.getProxy({ + targetDomain: 'test.com', + stickySession: true, + sessionId: first.sessionId, + }); + + expect(first.proxy.id).toBe(second.proxy.id); + }); + }); + + describe('reportFailure', () => { + it('should put proxy in cooling after block', async () => { + const { proxy } = await manager.getProxy({ + targetDomain: 'test.com', + }); + + await manager.reportFailure(proxy.id, 'test.com', new Error('403'), true); + + // Verify proxy is in cooling for this domain + // ... assertions + }); + }); +}); +``` + +--- + +**Anterior:** [ET-IA-007-etl.md](./ET-IA-007-etl.md) +**Siguiente:** [ET-IA-007-monitoring.md](./ET-IA-007-monitoring.md) diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/_MAP.md new file mode 100644 index 0000000..8149056 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/especificaciones/_MAP.md @@ -0,0 +1,35 @@ +--- +id: "MAP-IAI-007-ET" +title: "Mapa Especificaciones IAI-007" +type: "Navigation Map" +epic: "IAI-007" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones Tecnicas - IAI-007 Webscraper + +**EPIC:** IAI-007 +**Seccion:** Especificaciones Tecnicas + +--- + +## Documentos + +| ID | Archivo | Titulo | Estado | +|----|---------|--------|--------| +| ET-SCR-001 | [ET-SCR-001-scraper.md](./ET-SCR-001-scraper.md) | Motor de Scraping Playwright | Creado | +| ET-SCR-002 | [ET-SCR-002-etl.md](./ET-SCR-002-etl.md) | Pipeline ETL y Normalizacion | Creado | +| ET-SCR-003 | [ET-SCR-003-proxies.md](./ET-SCR-003-proxies.md) | Gestion Pool de Proxies | Creado | + +--- + +## Navegacion + +- **Arriba:** [IAI-007-webscraper/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-001.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-001.md new file mode 100644 index 0000000..f33e0b7 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-001.md @@ -0,0 +1,179 @@ +--- +id: "US-SCR-001" +title: "Scraping de propiedades desde Inmuebles24" +type: "User Story" +epic: "IAI-007" +status: "Draft" +story_points: 13 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-SCR-001: Scraping de propiedades desde Inmuebles24 + +--- + +## User Story + +**Como** administrador del sistema +**Quiero** que el sistema extraiga automaticamente propiedades de Inmuebles24 +**Para** tener datos actualizados del mercado inmobiliario mexicano + +--- + +## Descripcion + +Implementar un scraper robusto que extraiga listados de propiedades del portal Inmuebles24, manejando la proteccion de Cloudflare y respetando las politicas de rate limiting para evitar bloqueos. + +--- + +## Criterios de Aceptacion + +### Funcionales + +- [ ] El scraper puede navegar y extraer listados de propiedades +- [ ] Extrae todos los campos requeridos (precio, ubicacion, caracteristicas) +- [ ] Maneja paginacion hasta el limite configurado +- [ ] Almacena datos raw en formato JSON + +### Tecnicos + +- [ ] Usa Playwright con stealth mode +- [ ] Rotacion de proxies residenciales +- [ ] Delay configurable entre requests (2-5s base) +- [ ] Manejo de reintentos con backoff exponencial + +### Anti-detection + +- [ ] Simula comportamiento humano (scroll, delays) +- [ ] User-Agent rotativo y realista +- [ ] Maneja desafios de Cloudflare Turnstile +- [ ] Respeta robots.txt (paginas 1-5 inicialmente) + +--- + +## Campos a Extraer + +```yaml +Requeridos: + - source_id: ID interno de Inmuebles24 + - source_url: URL de la propiedad + - title: Titulo del anuncio + - price: Precio (numerico) + - currency: MXN/USD + - property_type: casa/departamento/terreno/etc + - transaction_type: venta/renta + - bedrooms: Recamaras + - bathrooms: Banos + - construction_m2: Metros construidos + - land_m2: Metros de terreno + - address: Direccion + - neighborhood: Colonia + - city: Ciudad + - state: Estado + - description: Descripcion completa + +Opcionales: + - parking_spaces: Estacionamientos + - age_years: Antiguedad + - amenities: Lista de amenidades + - images: URLs de imagenes + - latitude: Coordenada + - longitude: Coordenada +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Setup Playwright + stealth plugins | 2h | +| 2 | Implementar navegacion de listados | 4h | +| 3 | Parser de detalle de propiedad | 4h | +| 4 | Integracion con proxy pool | 3h | +| 5 | Rate limiting y delays | 2h | +| 6 | Manejo de errores y reintentos | 3h | +| 7 | Storage de raw data (S3/local) | 2h | +| 8 | Tests unitarios y de integracion | 4h | + +**Total estimado:** 24h (~3 dias) + +--- + +## Configuracion + +```yaml +scraper_config: + source: inmuebles24 + base_url: https://www.inmuebles24.com + + targets: + cities: + - guadalajara + - monterrey + - ciudad-de-mexico + property_types: + - casas + - departamentos + + limits: + max_pages_per_city: 5 + max_properties_per_run: 500 + delay_ms: + min: 2000 + max: 5000 + + proxy: + type: residential + rotate_every: session + + retry: + max_attempts: 3 + backoff_multiplier: 2 +``` + +--- + +## Notas de Implementacion + +1. **Selectores**: Usar selectores CSS robustos, evitar XPath fragiles +2. **Cloudflare**: Esperar carga completa antes de extraer +3. **CAPTCHA**: Si aparece frecuentemente, activar solver externo +4. **Logs**: Logging detallado para debugging + +--- + +## Definition of Done + +- [ ] Codigo implementado y revisado +- [ ] Tests pasan (unit + integracion) +- [ ] Scraper extrae 500+ propiedades sin bloqueos +- [ ] Documentacion actualizada +- [ ] Metricas de exito > 80% + +--- + +## Dependencias + +- Proxy pool configurado (US-SCR-000) +- Redis para Bull Queue +- Storage S3/local para raw data + +--- + +## Riesgos + +| Riesgo | Mitigacion | +|--------|------------| +| Cambios en HTML | Alertas + selectores flexibles | +| Rate limit | Delay adaptativo | +| IP ban | Pool de proxies amplio | + +--- + +**Asignado a:** - +**Sprint:** - +**Fecha limite:** - diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-002.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-002.md new file mode 100644 index 0000000..124e253 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-002.md @@ -0,0 +1,98 @@ +--- +id: "US-SCR-002" +title: "Scraping de propiedades desde Vivanuncios" +type: "User Story" +epic: "IAI-007" +status: "Draft" +story_points: 8 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-SCR-002: Scraping de propiedades desde Vivanuncios + +--- + +## User Story + +**Como** administrador del sistema +**Quiero** que el sistema extraiga automaticamente propiedades de Vivanuncios +**Para** complementar datos de Inmuebles24 y tener mayor cobertura del mercado + +--- + +## Descripcion + +Implementar un scraper para el portal Vivanuncios que reutilice la infraestructura base del scraper de Inmuebles24, adaptando los selectores y mappings especificos del sitio. + +--- + +## Criterios de Aceptacion + +### Funcionales + +- [ ] El scraper navega y extrae listados de Vivanuncios +- [ ] Extrae todos los campos del schema normalizado +- [ ] Maneja paginacion del sitio +- [ ] Datos se almacenan en formato unificado + +### Tecnicos + +- [ ] Reutiliza motor de scraping base +- [ ] Selectores especificos para Vivanuncios +- [ ] Mappings de campos documentados +- [ ] Tests de integracion especificos + +--- + +## Campos a Extraer + +```yaml +Mappings_Vivanuncios: + property_type: + "Casa en Venta": house + "Departamento en Venta": apartment + "Terreno en Venta": land + + precio: + selector: "[data-testid='price']" + transform: "parse_mexican_currency" + + ubicacion: + selector: "[data-testid='location']" + transform: "split_city_state" + + caracteristicas: + selector: "[data-testid='features'] li" + parse: "extract_key_value" +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Analizar estructura HTML Vivanuncios | 2h | +| 2 | Crear selectores especificos | 2h | +| 3 | Implementar mappings de campos | 2h | +| 4 | Adaptar navegacion de listados | 2h | +| 5 | Tests de integracion | 2h | + +**Total estimado:** 10h (~1.5 dias) + +--- + +## Definition of Done + +- [ ] Scraper extrae 500+ propiedades sin bloqueos +- [ ] Datos se normalizan correctamente +- [ ] Tests pasan +- [ ] Documentacion actualizada + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-003.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-003.md new file mode 100644 index 0000000..c24d93f --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-003.md @@ -0,0 +1,135 @@ +--- +id: "US-SCR-003" +title: "Normalizacion de datos de multiples fuentes" +type: "User Story" +epic: "IAI-007" +status: "Draft" +story_points: 8 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-SCR-003: Normalizacion de datos de multiples fuentes + +--- + +## User Story + +**Como** sistema de analytics +**Quiero** que los datos de diferentes portales se normalicen a un schema unificado +**Para** poder realizar analisis consistentes independientemente de la fuente + +--- + +## Descripcion + +Implementar el pipeline ETL que transforma datos raw de cada portal al schema normalizado, incluyendo limpieza de datos, geocoding, calculo de metricas derivadas y deteccion de duplicados. + +--- + +## Criterios de Aceptacion + +### Funcionales + +- [ ] Tipos de propiedad se mapean a enum unificado +- [ ] Precios se convierten a formato numerico estandar +- [ ] Direcciones se geocodifican a lat/lon +- [ ] Duplicados cross-source se detectan y fusionan +- [ ] Precio por m2 se calcula automaticamente + +### Calidad de Datos + +- [ ] 95%+ de registros tienen geocoding exitoso +- [ ] 0% de precios con formato incorrecto +- [ ] Duplicados detectados con precision > 98% + +--- + +## Transformaciones + +```yaml +transformaciones: + precio: + input: "$3,500,000 MXN" | "3.5 millones" + output: 3500000.00 + pasos: + - remove_currency_symbols + - expand_abbreviations + - to_decimal + + tipo_propiedad: + input: "Casa en Venta" | "Casa" | "Residencia" + output: "house" + pasos: + - lowercase + - normalize_synonyms + - map_to_enum + + ubicacion: + input: "Col. Providencia, Guadalajara, Jal." + output: + neighborhood: "Providencia" + city: "Guadalajara" + state: "Jalisco" + pasos: + - parse_address_components + - normalize_state_names + - geocode_to_coordinates +``` + +--- + +## Reglas de Deduplicacion + +```yaml +deduplicacion: + estrategia_primaria: + match: source_id + source + accion: update + + estrategia_fallback: + match: + - address_normalized + - price +/- 5% + - type + - size +/- 10% + accion: merge + + merge_policy: + precio: most_recent + descripcion: longest + fotos: union + first_seen: oldest + last_seen: newest +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Parser de precios robusto | 3h | +| 2 | Normalizador de tipos | 2h | +| 3 | Integracion geocoding API | 4h | +| 4 | Algoritmo de deduplicacion | 6h | +| 5 | Calculo de metricas derivadas | 2h | +| 6 | Tests unitarios | 4h | + +**Total estimado:** 21h (~3 dias) + +--- + +## Definition of Done + +- [ ] Pipeline procesa 1000 propiedades/hora +- [ ] Metricas de calidad cumplen objetivos +- [ ] Tests de transformacion pasan +- [ ] Documentacion de mappings completa + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-004.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-004.md new file mode 100644 index 0000000..652f522 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-004.md @@ -0,0 +1,101 @@ +--- +id: "US-SCR-004" +title: "Programacion de jobs de actualizacion" +type: "User Story" +epic: "IAI-007" +status: "Draft" +story_points: 5 +priority: "Media" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-SCR-004: Programacion de jobs de actualizacion + +--- + +## User Story + +**Como** administrador del sistema +**Quiero** programar jobs de scraping automaticos +**Para** mantener los datos actualizados sin intervencion manual + +--- + +## Descripcion + +Implementar sistema de scheduling que permita programar full scans semanales, actualizaciones incrementales diarias, y verificacion de propiedades activas periodicamente. + +--- + +## Criterios de Aceptacion + +- [ ] Jobs se programan con expresiones cron +- [ ] Full scan ejecuta semanalmente (domingos 2am) +- [ ] Incremental ejecuta diariamente (4am) +- [ ] Jobs se pueden ejecutar bajo demanda via API +- [ ] Jobs fallidos se reintentan con backoff +- [ ] Progreso visible en dashboard + +--- + +## Schedules Predefinidos + +```yaml +schedules: + full_scan_inmuebles24: + cron: "0 2 * * 0" + tipo: full_scan + config: + source: inmuebles24 + max_pages: 100 + + full_scan_vivanuncios: + cron: "0 3 * * 0" + tipo: full_scan + config: + source: vivanuncios + max_pages: 100 + + incremental_all: + cron: "0 4 * * *" + tipo: incremental + config: + sources: [inmuebles24, vivanuncios] + max_pages: 20 + + refresh_active: + cron: "0 */6 * * *" + tipo: refresh + config: + mark_inactive_after_days: 7 +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Configurar Bull Queue | 2h | +| 2 | Implementar job types | 4h | +| 3 | Scheduler con cron | 3h | +| 4 | Retry logic con backoff | 2h | +| 5 | API endpoints | 3h | + +**Total estimado:** 14h (~2 dias) + +--- + +## Definition of Done + +- [ ] Jobs se ejecutan segun schedule +- [ ] Reintentos funcionan correctamente +- [ ] API permite trigger manual +- [ ] Logs detallados disponibles + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-005.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-005.md new file mode 100644 index 0000000..5ad40d5 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/US-SCR-005.md @@ -0,0 +1,110 @@ +--- +id: "US-SCR-005" +title: "Dashboard de monitoreo de scraping" +type: "User Story" +epic: "IAI-007" +status: "Draft" +story_points: 5 +priority: "Media" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-SCR-005: Dashboard de monitoreo de scraping + +--- + +## User Story + +**Como** administrador del sistema +**Quiero** un dashboard para monitorear el estado del scraping +**Para** detectar problemas rapidamente y asegurar calidad de datos + +--- + +## Descripcion + +Implementar dashboard de monitoreo que muestre metricas en tiempo real, estado de jobs, salud del pool de proxies, y alertas activas. + +--- + +## Criterios de Aceptacion + +- [ ] Dashboard muestra propiedades scrapeadas hoy/semana +- [ ] Muestra success rate por fuente +- [ ] Muestra estado de jobs activos +- [ ] Muestra salud del pool de proxies +- [ ] Alertas visibles cuando hay problemas +- [ ] Graficas de tendencia temporal + +--- + +## Widgets del Dashboard + +```yaml +row_1: + - tipo: stat_card + titulo: "Propiedades Hoy" + valor: count_today + + - tipo: stat_card + titulo: "Success Rate" + valor: success_rate_24h + formato: percentage + + - tipo: stat_card + titulo: "Jobs Activos" + valor: active_jobs_count + + - tipo: stat_card + titulo: "Proxies Activos" + valor: active_proxies_count + alerta: < 20 + +row_2: + - tipo: line_chart + titulo: "Propiedades por Hora" + datos: properties_hourly_7d + group_by: source + + - tipo: line_chart + titulo: "Success Rate" + datos: success_rate_hourly_7d + +row_3: + - tipo: table + titulo: "Jobs Recientes" + columnas: [id, source, status, progress, duration] + + - tipo: pie_chart + titulo: "Errores por Tipo" + datos: errors_by_type_24h +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Definir metricas Prometheus | 3h | +| 2 | Crear dashboard Grafana | 4h | +| 3 | Configurar alertas | 2h | +| 4 | Integrar en admin panel | 3h | + +**Total estimado:** 12h (~1.5 dias) + +--- + +## Definition of Done + +- [ ] Dashboard accesible desde admin panel +- [ ] Datos se actualizan en tiempo real +- [ ] Alertas configuradas y funcionando +- [ ] Documentacion de metricas + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/_MAP.md new file mode 100644 index 0000000..023a32f --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/historias-usuario/_MAP.md @@ -0,0 +1,39 @@ +--- +id: "MAP-IAI-007-US" +title: "Mapa Historias Usuario IAI-007" +type: "Navigation Map" +epic: "IAI-007" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias de Usuario - IAI-007 Webscraper + +**EPIC:** IAI-007 +**Seccion:** Historias de Usuario + +--- + +## Documentos + +| ID | Archivo | Titulo | SP | Prioridad | Sprint | +|----|---------|--------|----|-----------|--------| +| US-SCR-001 | [US-SCR-001.md](./US-SCR-001.md) | Scrapear Inmuebles24 | 13 | Alta | - | +| US-SCR-002 | [US-SCR-002.md](./US-SCR-002.md) | Scrapear Vivanuncios | 8 | Alta | - | +| US-SCR-003 | [US-SCR-003.md](./US-SCR-003.md) | Normalizar datos | 8 | Alta | - | +| US-SCR-004 | [US-SCR-004.md](./US-SCR-004.md) | Programar jobs | 5 | Media | - | +| US-SCR-005 | [US-SCR-005.md](./US-SCR-005.md) | Dashboard monitoreo | 5 | Media | - | + +**Total Story Points:** 39 + +--- + +## Navegacion + +- **Arriba:** [IAI-007-webscraper/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/implementacion/_MAP.md new file mode 100644 index 0000000..cd43de2 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/implementacion/_MAP.md @@ -0,0 +1,34 @@ +--- +id: "MAP-IAI-007-IMPL" +title: "Mapa Implementacion IAI-007" +type: "Navigation Map" +epic: "IAI-007" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-007 Webscraper + +**EPIC:** IAI-007 +**Seccion:** Implementacion y Trazabilidad + +--- + +## Documentos + +| Archivo | Proposito | Estado | +|---------|-----------|--------| +| CHANGELOG.md | Historial de cambios | Pendiente | +| TRACEABILITY.yml | Trazabilidad RF-US-ET | Pendiente | + +--- + +## Navegacion + +- **Arriba:** [IAI-007-webscraper/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-001.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-001.md new file mode 100644 index 0000000..4614c33 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-001.md @@ -0,0 +1,105 @@ +--- +id: "RF-SCR-001" +title: "Motor de Scraping con Anti-Detection" +type: "Functional Requirement" +epic: "IAI-007" +priority: "Alta" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-007-001: Motor de Scraping con Anti-Detection + +--- + +## Descripcion + +El sistema debe proporcionar un motor de web scraping capaz de extraer datos de portales inmobiliarios protegidos por Cloudflare y otros sistemas anti-bot, emulando comportamiento de usuario real para evitar bloqueos. + +--- + +## Justificacion + +Los portales inmobiliarios como Inmuebles24 y Vivanuncios implementan protecciones robustas contra scraping automatizado. Sin un motor especializado con capacidades anti-detection, el sistema no podra obtener datos actualizados del mercado. + +--- + +## Requisitos Funcionales + +### RF-001.1: Browser Automation + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-001.1.1 | El sistema debe usar Playwright como motor de automatizacion principal | Alta | +| RF-001.1.2 | El sistema debe soportar modo headless con stealth patches | Alta | +| RF-001.1.3 | El sistema debe ocultar propiedades de WebDriver (navigator.webdriver) | Alta | +| RF-001.1.4 | El sistema debe emular User-Agents reales y rotarlos | Alta | +| RF-001.1.5 | El sistema debe soportar HTTP/2 y TLS moderno | Media | + +### RF-001.2: Anti-Detection + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-001.2.1 | El sistema debe simular movimientos de mouse antes de clicks | Alta | +| RF-001.2.2 | El sistema debe implementar scroll gradual (no instantaneo) | Alta | +| RF-001.2.3 | El sistema debe agregar delays aleatorios entre acciones | Alta | +| RF-001.2.4 | El sistema debe esperar carga completa de JavaScript antes de extraer | Alta | +| RF-001.2.5 | El sistema debe detectar y manejar desafios Cloudflare Turnstile | Media | + +### RF-001.3: Session Management + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-001.3.1 | El sistema debe persistir cookies entre sesiones | Alta | +| RF-001.3.2 | El sistema debe rotar sesiones periodicamente | Media | +| RF-001.3.3 | El sistema debe implementar "warming up" de sesiones nuevas | Media | + +--- + +## Criterios de Aceptacion + +- [ ] Playwright se inicializa con stealth mode habilitado +- [ ] navigator.webdriver retorna undefined en el browser automatizado +- [ ] User-Agent rota entre 10+ agentes reales diferentes +- [ ] Delays entre acciones varian entre 1-5 segundos aleatoriamente +- [ ] Scroll simula comportamiento humano (velocidad variable) +- [ ] Sesiones se persisten y reusan correctamente +- [ ] El scraper pasa pruebas de bot detection (bot.sannysoft.com) + +--- + +## Dependencias + +- Playwright NPM package +- playwright-extra con stealth plugin +- Redis para session storage + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Stealth patches obsoletos | Media | Alto | Monitorear actualizaciones, tener fallbacks | +| Nuevas tecnicas de deteccion | Alta | Alto | Monitoreo continuo, ajustes rapidos | + +--- + +## Historias de Usuario Relacionadas + +- US-SCR-001: Scraping de Inmuebles24 + +--- + +## Referencias Tecnicas + +- [playwright-extra-stealth](https://github.com/nickarsenault/playwright-extra-stealth) +- [Nodriver](https://github.com/nickarsenault/nodriver) +- [ScrapFly Cloudflare Guide](https://scrapfly.io/blog/how-to-bypass-cloudflare-protection/) + +--- + +**Autor:** Tech Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-002.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-002.md new file mode 100644 index 0000000..265a12f --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-002.md @@ -0,0 +1,158 @@ +--- +id: "RF-SCR-002" +title: "Gestion de Pool de Proxies" +type: "Functional Requirement" +epic: "IAI-007" +priority: "Alta" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-007-002: Gestion de Pool de Proxies + +--- + +## Descripcion + +El sistema debe gestionar un pool de proxies residenciales para distribuir las solicitudes y evitar bloqueos por IP, incluyendo rotacion automatica, health checks y cooling periods. + +--- + +## Justificacion + +Cloudflare y los portales inmobiliarios rastrean IPs y bloquean aquellas con comportamiento sospechoso. Un pool de proxies residenciales permite distribuir la carga y simular trafico desde multiples ubicaciones geograficas legitimas. + +--- + +## Requisitos Funcionales + +### RF-002.1: Gestion de Pool + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-002.1.1 | El sistema debe mantener un pool de proxies residenciales | Alta | +| RF-002.1.2 | El sistema debe almacenar metadata de cada proxy (tipo, pais, status) | Alta | +| RF-002.1.3 | El sistema debe soportar multiples proveedores de proxy | Media | +| RF-002.1.4 | El sistema debe permitir agregar/remover proxies dinamicamente | Media | + +### RF-002.2: Rotacion + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-002.2.1 | El sistema debe rotar proxies por sesion o por request | Alta | +| RF-002.2.2 | El sistema debe seleccionar proxies con mejor success rate | Alta | +| RF-002.2.3 | El sistema debe evitar proxies en cooling period | Alta | +| RF-002.2.4 | El sistema debe balancear carga entre proxies disponibles | Media | + +### RF-002.3: Health Checks + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-002.3.1 | El sistema debe verificar conectividad de proxies periodicamente | Alta | +| RF-002.3.2 | El sistema debe marcar proxies como "banned" cuando detecte bloqueo | Alta | +| RF-002.3.3 | El sistema debe calcular y actualizar success rate por proxy | Alta | +| RF-002.3.4 | El sistema debe alertar cuando el pool este bajo umbral minimo | Media | + +### RF-002.4: Cooling Periods + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-002.4.1 | El sistema debe poner proxies en cooling despues de rate limit | Alta | +| RF-002.4.2 | El sistema debe configurar duracion de cooling por tipo de error | Media | +| RF-002.4.3 | El sistema debe reactivar proxies automaticamente post-cooling | Alta | + +--- + +## Modelo de Datos + +```yaml +ProxyPool: + id: UUID + address: string + port: number + type: enum [residential, datacenter, mobile] + provider: string + country: string + city: string (opcional) + username: string (encrypted) + password: string (encrypted) + status: enum [active, cooling, banned, inactive] + success_rate: decimal (0-1) + total_requests: integer + successful_requests: integer + last_used_at: timestamp + last_success_at: timestamp + cooling_until: timestamp (nullable) + banned_at: timestamp (nullable) + created_at: timestamp + updated_at: timestamp +``` + +--- + +## Criterios de Aceptacion + +- [ ] Pool almacena y gestiona 50+ proxies residenciales +- [ ] Rotacion selecciona proxies con mejor success rate +- [ ] Proxies con rate limit entran en cooling automaticamente +- [ ] Health check detecta proxies caidos en < 5 minutos +- [ ] Alertas se disparan cuando pool < 20 proxies activos +- [ ] Success rate se calcula correctamente por proxy +- [ ] Proxies banned no se seleccionan para nuevos requests + +--- + +## Configuracion + +```yaml +proxy_pool: + min_active_proxies: 20 + health_check_interval_ms: 300000 # 5 minutos + + rotation: + strategy: "weighted_random" # best_success, round_robin, weighted_random + change_every: "session" # request, session, n_requests + + cooling: + rate_limit_duration_ms: 3600000 # 1 hora + error_duration_ms: 1800000 # 30 minutos + max_consecutive_failures: 3 + + providers: + - name: "brightdata" + priority: 1 + - name: "iproyal" + priority: 2 +``` + +--- + +## Dependencias + +- Proveedor de proxies residenciales (Bright Data, IPRoyal, etc.) +- PostgreSQL para persistencia +- Redis para cache de status + +--- + +## Costos Estimados + +| Proveedor | Plan | Proxies | Costo/mes | +|-----------|------|---------|-----------| +| Bright Data | Residential | 5GB | $75 USD | +| IPRoyal | Residential | 5GB | $52 USD | +| Smartproxy | Residential | 5GB | $65 USD | + +--- + +## Historias de Usuario Relacionadas + +- US-SCR-001: Scraping de Inmuebles24 +- US-SCR-002: Scraping de Vivanuncios + +--- + +**Autor:** Tech Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-003.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-003.md new file mode 100644 index 0000000..a144636 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-003.md @@ -0,0 +1,214 @@ +--- +id: "RF-SCR-003" +title: "Pipeline ETL y Normalizacion" +type: "Functional Requirement" +epic: "IAI-007" +priority: "Alta" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-007-003: Pipeline ETL y Normalizacion + +--- + +## Descripcion + +El sistema debe procesar los datos raw extraidos de multiples fuentes, normalizarlos a un schema unificado, enriquecerlos con geocoding y cargarlos en la base de datos de propiedades. + +--- + +## Justificacion + +Cada portal inmobiliario tiene su propia estructura de datos y nomenclatura. Para poder realizar analytics consistentes, los datos deben normalizarse a un schema comun con campos estandarizados. + +--- + +## Requisitos Funcionales + +### RF-003.1: Extraccion + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-003.1.1 | El sistema debe parsear HTML a JSON estructurado | Alta | +| RF-003.1.2 | El sistema debe extraer todos los campos definidos en el schema | Alta | +| RF-003.1.3 | El sistema debe manejar variaciones en estructura HTML | Alta | +| RF-003.1.4 | El sistema debe almacenar raw data en storage persistente | Media | + +### RF-003.2: Transformacion + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-003.2.1 | El sistema debe normalizar tipos de propiedad a enum unificado | Alta | +| RF-003.2.2 | El sistema debe convertir precios a formato numerico estandar | Alta | +| RF-003.2.3 | El sistema debe normalizar unidades de superficie (m2) | Alta | +| RF-003.2.4 | El sistema debe extraer caracteristicas de texto libre | Media | +| RF-003.2.5 | El sistema debe limpiar y estandarizar direcciones | Alta | + +### RF-003.3: Enriquecimiento + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-003.3.1 | El sistema debe geocodificar direcciones a lat/lon | Alta | +| RF-003.3.2 | El sistema debe calcular precio por m2 | Alta | +| RF-003.3.3 | El sistema debe asignar zona/colonia normalizada | Alta | +| RF-003.3.4 | El sistema debe detectar duplicados cross-source | Media | + +### RF-003.4: Carga + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-003.4.1 | El sistema debe hacer upsert en tabla de propiedades | Alta | +| RF-003.4.2 | El sistema debe mantener historial de cambios de precio | Alta | +| RF-003.4.3 | El sistema debe marcar propiedades inactivas | Alta | +| RF-003.4.4 | El sistema debe emitir eventos de nuevas propiedades | Media | + +--- + +## Schema Normalizado + +```yaml +Property: + # Identificacion + id: UUID + source: string # inmuebles24, vivanuncios, etc + source_id: string # ID original del portal + source_url: string + + # Tipo + property_type: enum + - house + - apartment + - land + - commercial + - office + - warehouse + transaction_type: enum [sale, rent] + + # Ubicacion + address: string + neighborhood: string + city: string + state: string + postal_code: string + latitude: decimal + longitude: decimal + zone_id: UUID (FK) + + # Caracteristicas + bedrooms: integer + bathrooms: decimal + parking_spaces: integer + construction_m2: decimal + land_m2: decimal + age_years: integer + floors: integer + + # Precio + price: decimal + currency: string # MXN, USD + price_per_m2: decimal (calculado) + + # Descripcion + title: string + description: text + amenities: string[] + images: string[] + + # Metadata + is_active: boolean + first_seen_at: timestamp + last_seen_at: timestamp + price_history: JSONB + created_at: timestamp + updated_at: timestamp +``` + +--- + +## Mappings por Fuente + +### Inmuebles24 + +```yaml +mappings: + property_type: + "Casa": house + "Departamento": apartment + "Terreno": land + "Local comercial": commercial + "Oficina": office + "Bodega": warehouse + + transaction_type: + "Venta": sale + "Renta": rent + "Venta o Renta": sale # priorizar venta + + precio: + selector: ".price-value" + transform: "remove_currency_symbols | to_number" + + superficie: + selector: ".surface-value" + transform: "extract_number | assume_m2" +``` + +--- + +## Reglas de Validacion + +```yaml +validations: + precio: + min: 10000 # MXN + max: 500000000 + required: true + + superficie: + min: 10 # m2 + max: 100000 + required: false + + coordenadas: + latitude_range: [14.5, 32.7] # Mexico + longitude_range: [-118.5, -86.7] + + deduplicacion: + strategy: "source_id + source" + fallback: "address_normalized + price + type" +``` + +--- + +## Criterios de Aceptacion + +- [ ] Pipeline procesa 1000 propiedades/hora minimo +- [ ] Todos los campos requeridos se mapean correctamente +- [ ] Precios se convierten a formato numerico sin errores +- [ ] Geocoding tiene 95%+ success rate +- [ ] Duplicados se detectan con 98%+ precision +- [ ] Historial de precios se mantiene correctamente +- [ ] Raw data se almacena para debugging + +--- + +## Dependencias + +- Cheerio o similar para parsing HTML +- Google Maps API para geocoding +- PostgreSQL para persistencia +- S3/MinIO para raw data storage + +--- + +## Historias de Usuario Relacionadas + +- US-SCR-003: Normalizacion de datos + +--- + +**Autor:** Tech Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-004.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-004.md new file mode 100644 index 0000000..efba9ab --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-004.md @@ -0,0 +1,220 @@ +--- +id: "RF-SCR-004" +title: "Scheduling y Job Management" +type: "Functional Requirement" +epic: "IAI-007" +priority: "Media" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-007-004: Scheduling y Job Management + +--- + +## Descripcion + +El sistema debe programar y gestionar trabajos de scraping, permitiendo ejecuciones programadas, incrementales y bajo demanda, con capacidad de pausar, reanudar y monitorear el progreso. + +--- + +## Justificacion + +La recoleccion de datos debe ser automatizada y eficiente. Los trabajos programados permiten mantener datos actualizados, mientras que las sincronizaciones incrementales optimizan recursos al procesar solo cambios. + +--- + +## Requisitos Funcionales + +### RF-004.1: Tipos de Jobs + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-004.1.1 | El sistema debe soportar jobs de tipo "full_scan" | Alta | +| RF-004.1.2 | El sistema debe soportar jobs de tipo "incremental" | Alta | +| RF-004.1.3 | El sistema debe soportar jobs de tipo "targeted" (URLs especificas) | Media | +| RF-004.1.4 | El sistema debe soportar jobs de tipo "refresh" (verificar activas) | Media | + +### RF-004.2: Scheduling + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-004.2.1 | El sistema debe permitir programar jobs con expresiones cron | Alta | +| RF-004.2.2 | El sistema debe ejecutar jobs bajo demanda via API | Alta | +| RF-004.2.3 | El sistema debe respetar horarios de baja demanda | Media | +| RF-004.2.4 | El sistema debe distribuir carga entre workers | Media | + +### RF-004.3: Job Lifecycle + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-004.3.1 | El sistema debe permitir pausar jobs en ejecucion | Alta | +| RF-004.3.2 | El sistema debe permitir reanudar jobs pausados | Alta | +| RF-004.3.3 | El sistema debe cancelar jobs con cleanup apropiado | Alta | +| RF-004.3.4 | El sistema debe reintentar jobs fallidos con backoff | Alta | + +### RF-004.4: Monitoreo + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-004.4.1 | El sistema debe reportar progreso en tiempo real | Alta | +| RF-004.4.2 | El sistema debe registrar estadisticas por job | Alta | +| RF-004.4.3 | El sistema debe alertar en caso de fallas | Alta | +| RF-004.4.4 | El sistema debe mantener historial de ejecuciones | Media | + +--- + +## Modelo de Datos + +```yaml +ScrapingJob: + id: UUID + type: enum [full_scan, incremental, targeted, refresh] + source: string # inmuebles24, vivanuncios, all + status: enum [pending, queued, running, paused, completed, failed, cancelled] + + config: + target_cities: string[] + property_types: string[] + max_pages: integer + max_properties: integer + delay_ms: + min: integer + max: integer + + schedule: + cron_expression: string (nullable) + next_run_at: timestamp (nullable) + timezone: string + + progress: + pages_scraped: integer + properties_found: integer + properties_processed: integer + errors: integer + current_page: string + + stats: + started_at: timestamp + completed_at: timestamp + duration_ms: integer + success_rate: decimal + + retry: + attempts: integer + max_attempts: integer + last_error: string + + created_at: timestamp + updated_at: timestamp + created_by: UUID +``` + +--- + +## Configuracion de Schedules + +```yaml +schedules: + full_scan: + inmuebles24: + cron: "0 2 * * 0" # Domingos 2am + config: + max_pages: 100 + cities: [guadalajara, monterrey, cdmx] + + vivanuncios: + cron: "0 3 * * 0" # Domingos 3am + config: + max_pages: 100 + + incremental: + all_sources: + cron: "0 4 * * *" # Diario 4am + config: + max_pages: 20 + only_new: true + + refresh: + active_properties: + cron: "0 */6 * * *" # Cada 6 horas + config: + check_active: true + mark_inactive_after_days: 7 +``` + +--- + +## API Endpoints + +```yaml +POST /api/v1/scraper/jobs: + description: Crear nuevo job + body: + type: string + source: string + config: object + response: 201 Created + +GET /api/v1/scraper/jobs: + description: Listar jobs + query: + status: string + source: string + limit: integer + offset: integer + response: 200 OK (paginado) + +GET /api/v1/scraper/jobs/:id: + description: Obtener job con progreso + response: 200 OK + +POST /api/v1/scraper/jobs/:id/pause: + description: Pausar job + response: 200 OK + +POST /api/v1/scraper/jobs/:id/resume: + description: Reanudar job + response: 200 OK + +DELETE /api/v1/scraper/jobs/:id: + description: Cancelar job + response: 204 No Content + +GET /api/v1/scraper/stats: + description: Estadisticas globales + response: 200 OK +``` + +--- + +## Criterios de Aceptacion + +- [ ] Jobs se programan correctamente con cron expressions +- [ ] Jobs se pueden ejecutar bajo demanda via API +- [ ] Pausar/reanudar funciona sin perder progreso +- [ ] Reintentos usan exponential backoff +- [ ] Progreso se actualiza en tiempo real +- [ ] Estadisticas se calculan correctamente +- [ ] Alertas se envian en caso de fallas + +--- + +## Dependencias + +- Bull Queue (Redis) +- node-cron o similar +- Redis para estado de jobs + +--- + +## Historias de Usuario Relacionadas + +- US-SCR-004: Programacion de jobs + +--- + +**Autor:** Tech Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-005.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-005.md new file mode 100644 index 0000000..702be8c --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/RF-SCR-005.md @@ -0,0 +1,227 @@ +--- +id: "RF-SCR-005" +title: "Monitoreo y Alertas" +type: "Functional Requirement" +epic: "IAI-007" +priority: "Media" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-007-005: Monitoreo y Alertas + +--- + +## Descripcion + +El sistema debe proporcionar monitoreo en tiempo real del estado del scraping, metricas de rendimiento, deteccion de anomalias y alertas automaticas cuando se detecten problemas. + +--- + +## Justificacion + +El scraping es un proceso fragil que puede fallar por multiples razones (cambios en HTML, bloqueos, errores de red). El monitoreo proactivo permite detectar y resolver problemas rapidamente antes de que afecten la calidad de datos. + +--- + +## Requisitos Funcionales + +### RF-005.1: Metricas + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-005.1.1 | El sistema debe registrar propiedades scrapeadas por fuente/hora | Alta | +| RF-005.1.2 | El sistema debe calcular success rate por fuente/proxy | Alta | +| RF-005.1.3 | El sistema debe medir latencia promedio por request | Alta | +| RF-005.1.4 | El sistema debe contar errores por tipo y fuente | Alta | +| RF-005.1.5 | El sistema debe trackear estado del pool de proxies | Alta | + +### RF-005.2: Dashboard + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-005.2.1 | El sistema debe mostrar estado actual de jobs | Alta | +| RF-005.2.2 | El sistema debe visualizar metricas en tiempo real | Alta | +| RF-005.2.3 | El sistema debe mostrar historial de ejecuciones | Media | +| RF-005.2.4 | El sistema debe permitir drill-down por fuente/job | Media | + +### RF-005.3: Alertas + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-005.3.1 | El sistema debe alertar cuando success rate < 80% | Alta | +| RF-005.3.2 | El sistema debe alertar cuando un job falla | Alta | +| RF-005.3.3 | El sistema debe alertar cuando pool de proxies < umbral | Alta | +| RF-005.3.4 | El sistema debe alertar cuando detecte cambio en estructura HTML | Media | +| RF-005.3.5 | El sistema debe soportar canales: email, Slack, webhook | Media | + +### RF-005.4: Logs + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-005.4.1 | El sistema debe registrar logs estructurados (JSON) | Alta | +| RF-005.4.2 | El sistema debe incluir correlation IDs por job | Alta | +| RF-005.4.3 | El sistema debe permitir ajustar nivel de log | Media | +| RF-005.4.4 | El sistema debe retener logs por 30 dias minimo | Media | + +--- + +## Metricas Definidas + +```yaml +metricas: + counters: + - scraper_properties_total: + labels: [source, type, status] + description: "Total propiedades procesadas" + + - scraper_requests_total: + labels: [source, status_code] + description: "Total requests HTTP" + + - scraper_errors_total: + labels: [source, error_type] + description: "Total errores por tipo" + + gauges: + - scraper_active_jobs: + labels: [source] + description: "Jobs activos actualmente" + + - scraper_proxy_pool_size: + labels: [status] + description: "Proxies por estado" + + - scraper_queue_size: + description: "Tareas pendientes en cola" + + histograms: + - scraper_request_duration_seconds: + labels: [source] + buckets: [0.1, 0.5, 1, 2, 5, 10] + description: "Duracion de requests" + + - scraper_job_duration_seconds: + labels: [source, type] + description: "Duracion total de jobs" +``` + +--- + +## Configuracion de Alertas + +```yaml +alerts: + success_rate_low: + condition: "scraper_success_rate < 0.8" + duration: "5m" + severity: warning + channels: [slack, email] + message: "Success rate bajo en {source}: {value}%" + + job_failed: + condition: "scraper_job_status == 'failed'" + severity: critical + channels: [slack, email, pagerduty] + message: "Job fallido: {job_id} en {source}" + + proxy_pool_low: + condition: "scraper_proxy_pool_size{status='active'} < 20" + duration: "10m" + severity: warning + channels: [slack] + message: "Pool de proxies bajo: {value} activos" + + no_data: + condition: "scraper_properties_total == 0" + duration: "1h" + severity: critical + channels: [slack, email] + message: "Sin propiedades scrapeadas en la ultima hora" + + html_change_detected: + condition: "scraper_selector_failures > 10" + duration: "15m" + severity: warning + channels: [slack] + message: "Posible cambio en estructura HTML de {source}" +``` + +--- + +## Dashboard Widgets + +```yaml +dashboard: + row_1: + - widget: "stat" + title: "Propiedades Hoy" + metric: "sum(scraper_properties_total{status='success'})" + + - widget: "stat" + title: "Success Rate" + metric: "scraper_success_rate * 100" + format: "percent" + + - widget: "stat" + title: "Jobs Activos" + metric: "scraper_active_jobs" + + - widget: "stat" + title: "Proxies Activos" + metric: "scraper_proxy_pool_size{status='active'}" + + row_2: + - widget: "timeseries" + title: "Propiedades por Hora" + metric: "rate(scraper_properties_total[1h])" + group_by: source + + - widget: "timeseries" + title: "Success Rate" + metric: "scraper_success_rate" + group_by: source + + row_3: + - widget: "table" + title: "Jobs Recientes" + query: "SELECT * FROM scraping_jobs ORDER BY created_at DESC LIMIT 10" + + - widget: "piechart" + title: "Errores por Tipo" + metric: "scraper_errors_total" + group_by: error_type +``` + +--- + +## Criterios de Aceptacion + +- [ ] Metricas se registran correctamente en Prometheus/similar +- [ ] Dashboard muestra datos en tiempo real +- [ ] Alertas se disparan dentro de la duracion configurada +- [ ] Notificaciones llegan a los canales configurados +- [ ] Logs son estructurados y contienen correlation IDs +- [ ] Historial de metricas disponible por 30+ dias + +--- + +## Dependencias + +- Prometheus o similar para metricas +- Grafana o similar para dashboard +- AlertManager o similar para alertas +- ELK Stack o similar para logs + +--- + +## Historias de Usuario Relacionadas + +- US-SCR-005: Dashboard de monitoreo + +--- + +**Autor:** Tech Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/_MAP.md new file mode 100644 index 0000000..99ddb7f --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/requerimientos/_MAP.md @@ -0,0 +1,37 @@ +--- +id: "MAP-IAI-007-RF" +title: "Mapa Requerimientos IAI-007" +type: "Navigation Map" +epic: "IAI-007" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-007 Webscraper + +**EPIC:** IAI-007 +**Seccion:** Requerimientos Funcionales + +--- + +## Documentos + +| ID | Archivo | Titulo | Prioridad | Estado | +|----|---------|--------|-----------|--------| +| RF-SCR-001 | [RF-SCR-001.md](./RF-SCR-001.md) | Motor de scraping con anti-detection | Alta | Draft | +| RF-SCR-002 | [RF-SCR-002.md](./RF-SCR-002.md) | Gestion de pool de proxies | Alta | Draft | +| RF-SCR-003 | [RF-SCR-003.md](./RF-SCR-003.md) | Pipeline ETL y normalizacion | Alta | Draft | +| RF-SCR-004 | [RF-SCR-004.md](./RF-SCR-004.md) | Scheduling y job management | Media | Draft | +| RF-SCR-005 | [RF-SCR-005.md](./RF-SCR-005.md) | Monitoreo y alertas | Media | Draft | + +--- + +## Navegacion + +- **Arriba:** [IAI-007-webscraper/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-007-webscraper/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-007-webscraper/tareas/_MAP.md new file mode 100644 index 0000000..cd7fed4 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-007-webscraper/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-007-TASK" +title: "Mapa Tareas IAI-007" +type: "Navigation Map" +epic: "IAI-007" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas Tecnicas - IAI-007 Webscraper + +**EPIC:** IAI-007 +**Seccion:** Tareas Tecnicas + +--- + +## Documentos + +*Sin tareas tecnicas definidas todavia.* + +--- + +## Navegacion + +- **Arriba:** [IAI-007-webscraper/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/README.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/README.md new file mode 100644 index 0000000..df6b3c9 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/README.md @@ -0,0 +1,245 @@ +--- +id: "EPIC-IAI-008" +title: "EPIC IA-008: Machine Learning y Analytics Avanzado" +type: "EPIC" +status: "Draft" +project: "inmobiliaria-analytics" +version: "1.0.0" +story_points: 89 +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# EPIC IAI-008: Machine Learning y Analytics Avanzado + +--- + +## Resumen Ejecutivo + +Este EPIC implementa el nucleo de inteligencia artificial de la plataforma: modelos de valuacion automatica (AVM), prediccion de tendencias de mercado, deteccion de oportunidades de inversion, indices de mercado y generacion de reportes profesionales con insights accionables. + +--- + +## Objetivo + +Proporcionar capacidades de ML que transformen datos inmobiliarios en inteligencia de mercado accionable: + +1. **Valuacion Automatica (AVM)** - Estimar valor de propiedades con MAPE < 10% +2. **Predicciones** - Tiempo de venta, demanda por zona, tendencias +3. **Oportunidades** - Detectar propiedades subvaluadas y zonas emergentes +4. **Analisis ROI** - Calcular retornos proyectados para inversores +5. **Reportes** - Generar reportes profesionales personalizados + +--- + +## Propuesta de Valor por Segmento + +### Para Agentes Inmobiliarios + +- Valuaciones instantaneas para clientes +- Reportes CMA profesionales automatizados +- Prediccion de tiempo de venta +- Market snapshots semanales + +### Para Inversores + +- Deteccion de propiedades subvaluadas +- Analisis ROI con escenarios +- Identificacion de zonas emergentes +- Alertas de oportunidades + +### Para Desarrolladores + +- Estudios de factibilidad automatizados +- Analisis de demanda por zona +- Benchmarking de costos +- Proyecciones de absorcion + +--- + +## Modelos ML Core + +``` ++-------------------+ +-------------------+ +-------------------+ +| AVM-Core | | DOM-Predictor | | Demand-Forecaster | +| (Valuacion) | | (Tiempo Venta) | | (Demanda Zona) | ++-------------------+ +-------------------+ +-------------------+ + | | | + v v v ++---------------------------------------------------------------+ +| Feature Engineering | +| (Geo, Mercado, Propiedad, Temporales, NLP Embeddings) | ++---------------------------------------------------------------+ + | | | + v v v ++-------------------+ +-------------------+ +-------------------+ +| Deal-Finder | | Zone-Spotter | | ROI-Analyzer | +| (Subvaluadas) | |(Zonas Emergentes) | | (Inversion) | ++-------------------+ +-------------------+ +-------------------+ +``` + +--- + +## Stack Tecnologico + +| Capa | Tecnologia | Uso | +|------|------------|-----| +| ML Framework | XGBoost, LightGBM, Prophet | Modelos predictivos | +| Data | pandas, polars, geopandas | Procesamiento | +| NLP | spaCy, sentence-transformers | Analisis texto | +| API | FastAPI, Pydantic | Serving | +| MLOps | MLflow, DVC | Versionamiento | +| Cache | Redis | Predicciones frecuentes | +| Vectors | pgvector | Embeddings | + +--- + +## Arquitectura de Alto Nivel + +``` + +------------------+ + | API Gateway | + +--------+---------+ + | + +-----------------+-----------------+ + | | ++----------v----------+ +-----------v-----------+ +| ML API Service | | Report Generator | +| (FastAPI) | | (PDF/HTML) | ++----------+----------+ +-----------+-----------+ + | | + | +---------------+ | + +------>| ML Models |<---------+ + | (MLflow) | + +-------+-------+ + | + +-------v-------+ + | Feature Store| + | (Redis) | + +-------+-------+ + | + +---------------+---------------+ + | | ++----------v----------+ +----------v----------+ +| PostgreSQL | | pgvector | +| (Datos Mercado) | | (Embeddings) | ++---------------------+ +---------------------+ +``` + +--- + +## Desglose por Fase + +### Fase 1: MVP (4-6 semanas) + +| Tarea | SP | Entregable | +|-------|----|------------| +| Setup MLflow + FastAPI | 3 | Infraestructura base | +| Feature engineering pipeline | 5 | Pipeline de features | +| Modelo AVM (XGBoost) | 8 | Valuacion basica | +| API `/valuation/predict` | 3 | Endpoint de prediccion | +| Dashboard tendencias | 5 | Visualizacion basica | +| Reporte CMA basico | 5 | PDF generado | + +**Total:** 29 SP + +### Fase 2: Predicciones (4-6 semanas) + +| Tarea | SP | Entregable | +|-------|----|------------| +| TimeToSell model | 8 | Prediccion dias | +| Demand forecaster | 5 | Prediccion demanda | +| Detector subvaluadas | 5 | Deal-Finder | +| Sistema de alertas | 5 | Email + push | + +**Total:** 23 SP + +### Fase 3: Analisis Avanzado (4-6 semanas) + +| Tarea | SP | Entregable | +|-------|----|------------| +| Indices de mercado | 5 | IPV, IAV, IAM, IRI | +| Zonas emergentes | 8 | Zone-Spotter | +| Analisis ROI | 8 | Investment-Analyzer | +| Reportes inversores | 5 | PDF completo | + +**Total:** 26 SP + +### Fase 4: NLP + Enterprise (4-6 semanas) + +| Tarea | SP | Entregable | +|-------|----|------------| +| NLP pipeline | 5 | Extraccion amenidades | +| Quality scoring | 3 | Score de listings | +| Multi-tenant models | 5 | Aislamiento | +| White-label reports | 5 | Branding custom | + +**Total:** 18 SP + +--- + +## Metricas de Exito + +### Modelo + +| Metrica | Objetivo | Medicion | +|---------|----------|----------| +| AVM MAPE | < 10% | vs precio venta real | +| AVM R2 | >= 0.85 | Cross-validation | +| Time-to-sell MAPE | < 25% | vs dias reales | +| Demand accuracy | >= 70% | Directional | + +### Negocio + +| Metrica | Objetivo | +|---------|----------| +| Adopcion ML features | 70% MAU | +| Reportes generados | > 100/mes (enterprise) | +| Conversion oportunidades | 30% investigadas | +| NPS ML features | >= 40 | + +### Tecnico + +| Metrica | Objetivo | +|---------|----------| +| Latencia prediccion | p95 < 500ms | +| Uptime | 99.5% | +| Model freshness | Re-train mensual | + +--- + +## Riesgos y Mitigaciones + +| Riesgo | Prob | Impacto | Mitigacion | +|--------|------|---------|------------| +| Datos insuficientes | Alta | Alto | Scraping agresivo, datos sinteticos | +| Accuracy baja inicial | Media | Alto | Feature engineering, ensemble | +| Latencia alta | Media | Medio | Caching agresivo, batch | +| Model drift | Alta | Medio | Monitoreo continuo | +| Costos compute | Media | Bajo | Optimizacion, spot instances | + +--- + +## Criterios de Aceptacion + +- [ ] AVM predice con MAPE < 10% en test set +- [ ] API responde < 500ms p95 +- [ ] Reportes se generan correctamente +- [ ] Alertas se envian en tiempo real +- [ ] Dashboard muestra tendencias actualizadas +- [ ] Modelos versionados en MLflow +- [ ] Tests de integracion pasan + +--- + +## Documentacion Relacionada + +- [IA-008-ML-ANALYTICS.md](../../02-definicion-modulos/IA-008-ML-ANALYTICS.md) - Definicion del modulo +- [ML-SERVICES-SPEC.yml](/shared/knowledge-base/projects/inmobiliaria-analytics/ML-SERVICES-SPEC.yml) - Especificacion completa +- [PERFIL-ML-SPECIALIST.md](/orchestration/agents/perfiles/PERFIL-ML-SPECIALIST.md) - Perfil de agente + +--- + +**EPIC Owner:** Tech Lead / ML Lead +**Fecha creacion:** 2026-01-04 +**Estado:** Draft diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/_MAP.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/_MAP.md new file mode 100644 index 0000000..ce7cb45 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/_MAP.md @@ -0,0 +1,155 @@ +--- +id: "MAP-IAI-008" +title: "Mapa de EPIC IAI-008 ML Analytics" +type: "Navigation Map" +epic: "IAI-008" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: EPIC IAI-008 - Machine Learning y Analytics Avanzado + +**EPIC:** IAI-008 +**Nombre:** Sistema de ML y Analytics Avanzado +**Estado:** Draft +**Story Points:** 60 (estimado) + +--- + +## Estructura del EPIC + +``` +IAI-008-ml-analytics/ +├── _MAP.md # Este archivo +├── README.md # Vision general del EPIC +│ +├── requerimientos/ +│ ├── _MAP.md +│ ├── RF-ML-001.md # AVM - Valuacion automatica +│ ├── RF-ML-002.md # Prediccion tiempo de venta +│ ├── RF-ML-003.md # Deteccion oportunidades +│ └── RF-ML-004.md # Zonas emergentes +│ +├── especificaciones/ +│ ├── _MAP.md +│ ├── ET-ML-001-avm.md # Modelo AVM (XGBoost ensemble) +│ └── ET-ML-002-opportunities.md # Deteccion de oportunidades +│ +├── historias-usuario/ +│ ├── _MAP.md +│ ├── US-ML-001.md # Valuacion automatica basica +│ ├── US-ML-002.md # Explicabilidad de valuacion +│ ├── US-ML-003.md # Prediccion dias en mercado +│ ├── US-ML-004.md # Dashboard tendencias +│ ├── US-ML-005.md # Alertas oportunidades +│ ├── US-ML-006.md # Reporte CMA +│ ├── US-ML-007.md # Analisis ROI inversion +│ └── US-ML-008.md # Zonas emergentes +│ +├── tareas/ +│ └── _MAP.md +│ +└── implementacion/ + ├── _MAP.md + ├── MODEL-CARDS/ # Documentacion de modelos + └── CHANGELOG.md # Historial de cambios +``` + +--- + +## Requerimientos Funcionales + +| ID | Nombre | Prioridad | Estado | +|----|--------|-----------|--------| +| [RF-ML-001](./requerimientos/RF-ML-001.md) | AVM - Valuacion automatica | Alta | Draft | +| [RF-ML-002](./requerimientos/RF-ML-002.md) | Prediccion tiempo de venta | Alta | Draft | +| [RF-ML-003](./requerimientos/RF-ML-003.md) | Deteccion oportunidades | Alta | Draft | +| [RF-ML-004](./requerimientos/RF-ML-004.md) | Zonas emergentes | Media | Draft | + +--- + +## Especificaciones Tecnicas + +| ID | Titulo | Estado | Contenido Principal | +|----|--------|--------|---------------------| +| [ET-ML-001](./especificaciones/ET-ML-001-avm.md) | Modelo AVM | Creado | XGBoost/LightGBM ensemble, SHAP, features | +| [ET-ML-002](./especificaciones/ET-ML-002-opportunities.md) | Deteccion Oportunidades | Creado | Undervalued detector, emerging zones, alerts | + +--- + +## Servicios ML + +| ID | Servicio | Tipo | Algoritmo | Prioridad | +|----|----------|------|-----------|-----------| +| AVM-Core | PropertyPricePredictor | Regression | XGBoost ensemble | Alta | +| DOM-Predictor | TimeToSellPredictor | Survival | Cox PH / RSF | Alta | +| Demand-Forecaster | ZoneDemandPredictor | Time Series | Prophet | Media | +| Deal-Finder | UndervaluedDetector | Anomaly | AVM + z-score | Alta | +| Zone-Spotter | EmergingZoneIdentifier | Clustering | K-Means + trends | Media | +| Investment-Analyzer | ROIAnalyzer | Financial | Multi-model | Media | + +--- + +## Historias de Usuario + +| ID | Titulo | SP | Prioridad | Fase | +|----|--------|----|-----------|------| +| [US-ML-001](./historias-usuario/US-ML-001.md) | Valuacion automatica de propiedad | 13 | Alta | MVP | +| [US-ML-002](./historias-usuario/US-ML-002.md) | Explicabilidad SHAP de valuacion | 5 | Media | MVP | +| [US-ML-003](./historias-usuario/US-ML-003.md) | Prediccion de dias en mercado | 8 | Alta | F2 | +| [US-ML-004](./historias-usuario/US-ML-004.md) | Dashboard de tendencias por zona | 8 | Alta | MVP | +| [US-ML-005](./historias-usuario/US-ML-005.md) | Alertas de oportunidades | 5 | Alta | F2 | +| [US-ML-006](./historias-usuario/US-ML-006.md) | Generacion reporte CMA | 8 | Alta | F2 | +| [US-ML-007](./historias-usuario/US-ML-007.md) | Calculadora ROI para inversores | 5 | Media | F3 | +| [US-ML-008](./historias-usuario/US-ML-008.md) | Mapa de zonas emergentes | 8 | Media | F3 | + +**Total Story Points:** 60 + +--- + +## Dependencias + +### Depende de: +- IAI-007 (Webscraper): Datos de propiedades +- IAI-002 (Propiedades): Modelo normalizado +- IAI-001 (Auth): API authentication + +### Bloquea a: +- Portal de Inversores (features avanzadas) +- Reportes white-label + +--- + +## Metricas de Exito + +| Modelo | Metrica | Objetivo | +|--------|---------|----------| +| AVM | MAPE | < 10% | +| AVM | R2 | >= 0.85 | +| Time-to-sell | MAPE | < 25% | +| Demand forecast | Dir. Accuracy | >= 70% | + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Datos insuficientes | Alta | Alto | Scraping agresivo inicial | +| Accuracy baja | Media | Alto | Feature engineering, mas datos | +| Latencia alta | Media | Medio | Caching, batch predictions | +| Drift de modelos | Alta | Medio | Monitoreo, re-entrenamiento | + +--- + +## Navegacion + +- **Arriba:** [01-fase-alcance-inicial/](../_MAP.md) +- **Anterior:** [IAI-007-webscraper/](../IAI-007-webscraper/_MAP.md) +- **Siguiente:** - + +--- + +**Ultima actualizacion:** 2026-01-04 + diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/ET-ML-001-avm.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/ET-ML-001-avm.md new file mode 100644 index 0000000..ce40a6d --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/ET-ML-001-avm.md @@ -0,0 +1,1164 @@ +--- +id: "ET-ML-avm" +title: "Especificacion Tecnica - Modelo de Valuacion Automatizada (AVM)" +type: "Technical Specification" +epic: "IAI-008" +status: "Draft" +version: "1.0" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# ET-IA-008-avm: Modelo de Valuacion Automatizada (AVM) + +--- + +## 1. Resumen + +Sistema de Machine Learning para estimar el valor de mercado de propiedades inmobiliarias basado en caracteristicas fisicas, ubicacion, condiciones de mercado y comparables recientes. + +--- + +## 2. Arquitectura del Sistema + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ AVM PIPELINE │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Feature │───▶│ Model │───▶│ Post │ │ +│ │ Engine │ │ Ensemble │ │ Process │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Property │ │ XGBoost │ │ Confidence │ │ +│ │ Features │ │ LightGBM │ │ Intervals │ │ +│ │ Location │ │ CatBoost │ │ SHAP │ │ +│ │ Market │ │ Averaging │ │ Comparable │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ FastAPI │ + │ Endpoint │ + └─────────────────┘ +``` + +--- + +## 3. Features del Modelo + +### 3.1 Categorias de Features + +```python +# src/ml/avm/features.py +from dataclasses import dataclass +from typing import List, Optional +from enum import Enum + +class FeatureCategory(Enum): + PHYSICAL = "physical" + LOCATION = "location" + TEMPORAL = "temporal" + MARKET = "market" + DERIVED = "derived" + +@dataclass +class PropertyFeatures: + """Features de la propiedad fisica""" + property_type: str # casa, departamento, etc. + constructed_area_m2: float + land_area_m2: Optional[float] + bedrooms: int + bathrooms: float + parking_spaces: int + floors: int + year_built: Optional[int] + has_pool: bool + has_garden: bool + has_gym: bool + has_security: bool + has_elevator: bool + amenities_count: int + +@dataclass +class LocationFeatures: + """Features de ubicacion""" + neighborhood: str + municipality: str + latitude: float + longitude: float + distance_to_center_km: float + distance_to_metro_km: Optional[float] + distance_to_park_km: float + distance_to_school_km: float + distance_to_hospital_km: float + walk_score: float # 0-100 + crime_index: float # 0-100 (mayor = mas seguro) + noise_level: float # 0-100 + avg_income_zone: float # ingreso promedio zona + +@dataclass +class TemporalFeatures: + """Features temporales""" + month: int + quarter: int + year: int + days_since_listing: int + is_holiday_season: bool + inflation_rate: float + interest_rate: float + +@dataclass +class MarketFeatures: + """Features de mercado""" + avg_price_m2_neighborhood: float + median_price_m2_neighborhood: float + price_trend_3m: float # % cambio ultimos 3 meses + price_trend_12m: float # % cambio ultimo ano + inventory_count: int # propiedades activas en zona + absorption_rate: float # ventas/inventario + days_on_market_avg: float # dias promedio en mercado + comparable_count: int # num comparables encontrados + supply_demand_ratio: float + +@dataclass +class DerivedFeatures: + """Features derivados/calculados""" + price_per_m2_estimated: float + age_years: Optional[int] + bathroom_bedroom_ratio: float + parking_per_bedroom: float + area_per_bedroom: float + is_new_construction: bool # < 2 anos + is_premium_zone: bool + relative_size: float # vs promedio zona + quality_score: float # 0-100 basado en amenidades +``` + +### 3.2 Feature Engineering Pipeline + +```python +# src/ml/avm/feature_engineering.py +import pandas as pd +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.preprocessing import StandardScaler, OneHotEncoder +from sklearn.compose import ColumnTransformer +from sklearn.pipeline import Pipeline +from geopy.distance import geodesic + +class PropertyFeatureEngineer(BaseEstimator, TransformerMixin): + """Pipeline de ingenieria de features""" + + def __init__(self, market_data_service): + self.market_data = market_data_service + self.city_center = (20.6736, -103.3927) # Guadalajara centro + + def fit(self, X, y=None): + return self + + def transform(self, X: pd.DataFrame) -> pd.DataFrame: + df = X.copy() + + # 1. Features derivados de propiedad + df = self._add_property_derived(df) + + # 2. Features de ubicacion + df = self._add_location_features(df) + + # 3. Features de mercado + df = self._add_market_features(df) + + # 4. Features temporales + df = self._add_temporal_features(df) + + # 5. Encoding categoricos + df = self._encode_categoricals(df) + + return df + + def _add_property_derived(self, df: pd.DataFrame) -> pd.DataFrame: + # Edad de la propiedad + current_year = pd.Timestamp.now().year + df['age_years'] = np.where( + df['year_built'].notna(), + current_year - df['year_built'], + np.nan + ) + + # Ratios + df['bathroom_bedroom_ratio'] = df['bathrooms'] / df['bedrooms'].clip(lower=1) + df['parking_per_bedroom'] = df['parking_spaces'] / df['bedrooms'].clip(lower=1) + df['area_per_bedroom'] = df['constructed_area_m2'] / df['bedrooms'].clip(lower=1) + + # Flags + df['is_new_construction'] = df['age_years'] <= 2 + df['has_land'] = df['land_area_m2'].notna() & (df['land_area_m2'] > 0) + + # Quality score basado en amenidades + amenity_cols = ['has_pool', 'has_garden', 'has_gym', 'has_security', 'has_elevator'] + df['quality_score'] = df[amenity_cols].sum(axis=1) * 20 + + # Log transforms para areas + df['log_constructed_area'] = np.log1p(df['constructed_area_m2']) + df['log_land_area'] = np.log1p(df['land_area_m2'].fillna(0)) + + return df + + def _add_location_features(self, df: pd.DataFrame) -> pd.DataFrame: + # Distancia al centro + def calc_distance(row): + if pd.isna(row['latitude']) or pd.isna(row['longitude']): + return np.nan + return geodesic( + (row['latitude'], row['longitude']), + self.city_center + ).kilometers + + df['distance_to_center_km'] = df.apply(calc_distance, axis=1) + + # Zona premium (colonias especificas) + premium_zones = [ + 'providencia', 'americana', 'lafayette', 'country', + 'puerta de hierro', 'real', 'bugambilias', 'chapalita' + ] + df['is_premium_zone'] = df['neighborhood'].str.lower().isin(premium_zones) + + # Cluster geografico (usando municipio como proxy) + municipality_encoding = { + 'zapopan': 1.2, + 'guadalajara': 1.0, + 'tlaquepaque': 0.85, + 'tonala': 0.75, + 'tlajomulco': 0.8, + } + df['municipality_factor'] = df['municipality'].str.lower().map(municipality_encoding).fillna(0.9) + + return df + + def _add_market_features(self, df: pd.DataFrame) -> pd.DataFrame: + # Obtener datos de mercado por zona + for idx, row in df.iterrows(): + market_stats = self.market_data.get_zone_stats( + neighborhood=row['neighborhood'], + property_type=row['property_type'] + ) + + df.at[idx, 'avg_price_m2_zone'] = market_stats.get('avg_price_m2', np.nan) + df.at[idx, 'median_price_m2_zone'] = market_stats.get('median_price_m2', np.nan) + df.at[idx, 'price_trend_3m'] = market_stats.get('trend_3m', 0) + df.at[idx, 'inventory_zone'] = market_stats.get('inventory', 0) + df.at[idx, 'days_on_market_zone'] = market_stats.get('avg_dom', 60) + + # Tamano relativo vs zona + df['relative_size'] = df['constructed_area_m2'] / df['avg_price_m2_zone'].clip(lower=1) + + return df + + def _add_temporal_features(self, df: pd.DataFrame) -> pd.DataFrame: + now = pd.Timestamp.now() + + df['month'] = now.month + df['quarter'] = now.quarter + df['year'] = now.year + + # Temporada alta (enero-marzo, septiembre-noviembre) + df['is_high_season'] = df['month'].isin([1, 2, 3, 9, 10, 11]) + + # Indicadores macroeconomicos actuales (mock - usar API real) + df['inflation_rate'] = 4.5 # % + df['interest_rate'] = 11.0 # % + + return df + + def _encode_categoricals(self, df: pd.DataFrame) -> pd.DataFrame: + # Target encoding para neighborhood (usar valores precalculados) + neighborhood_means = self.market_data.get_neighborhood_price_means() + df['neighborhood_encoded'] = df['neighborhood'].map(neighborhood_means) + df['neighborhood_encoded'] = df['neighborhood_encoded'].fillna( + neighborhood_means.mean() + ) + + # One-hot para property_type + property_dummies = pd.get_dummies( + df['property_type'], + prefix='type', + drop_first=True + ) + df = pd.concat([df, property_dummies], axis=1) + + return df + + +def create_feature_pipeline(market_service) -> Pipeline: + """Crear pipeline completo de features""" + + # Features numericos + numeric_features = [ + 'constructed_area_m2', 'land_area_m2', 'bedrooms', 'bathrooms', + 'parking_spaces', 'latitude', 'longitude', 'distance_to_center_km', + 'age_years', 'quality_score', 'avg_price_m2_zone', 'price_trend_3m' + ] + + # Features categoricos + categorical_features = ['property_type', 'municipality'] + + # Preprocessor + preprocessor = ColumnTransformer( + transformers=[ + ('num', StandardScaler(), numeric_features), + ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features) + ], + remainder='passthrough' + ) + + pipeline = Pipeline([ + ('feature_engineer', PropertyFeatureEngineer(market_service)), + ('preprocessor', preprocessor), + ]) + + return pipeline +``` + +--- + +## 4. Modelo Ensemble + +### 4.1 Arquitectura del Ensemble + +```python +# src/ml/avm/ensemble.py +import numpy as np +import pandas as pd +from typing import Dict, List, Tuple, Optional +from sklearn.base import BaseEstimator, RegressorMixin +from sklearn.model_selection import cross_val_predict +import xgboost as xgb +import lightgbm as lgb +from catboost import CatBoostRegressor +import joblib + +class AVMEnsemble(BaseEstimator, RegressorMixin): + """Ensemble de modelos para valuacion automatizada""" + + def __init__( + self, + weights: Optional[Dict[str, float]] = None, + use_stacking: bool = False + ): + self.weights = weights or { + 'xgboost': 0.35, + 'lightgbm': 0.35, + 'catboost': 0.30, + } + self.use_stacking = use_stacking + self.models = {} + self.meta_model = None + + def _create_models(self) -> Dict[str, BaseEstimator]: + """Crear modelos base del ensemble""" + + models = { + 'xgboost': xgb.XGBRegressor( + n_estimators=500, + max_depth=8, + learning_rate=0.05, + subsample=0.8, + colsample_bytree=0.8, + reg_alpha=0.1, + reg_lambda=1.0, + random_state=42, + n_jobs=-1, + ), + + 'lightgbm': lgb.LGBMRegressor( + n_estimators=500, + max_depth=10, + learning_rate=0.05, + num_leaves=31, + subsample=0.8, + colsample_bytree=0.8, + reg_alpha=0.1, + reg_lambda=1.0, + random_state=42, + n_jobs=-1, + verbose=-1, + ), + + 'catboost': CatBoostRegressor( + iterations=500, + depth=8, + learning_rate=0.05, + l2_leaf_reg=3, + random_seed=42, + verbose=False, + ), + } + + return models + + def fit(self, X: np.ndarray, y: np.ndarray) -> 'AVMEnsemble': + """Entrenar ensemble""" + + self.models = self._create_models() + + if self.use_stacking: + # Stacking: usar predicciones OOF como meta-features + meta_features = np.zeros((len(y), len(self.models))) + + for i, (name, model) in enumerate(self.models.items()): + # Predicciones out-of-fold + oof_preds = cross_val_predict( + model, X, y, cv=5, n_jobs=-1 + ) + meta_features[:, i] = oof_preds + + # Entrenar en todo el dataset + model.fit(X, y) + + # Meta-modelo + from sklearn.linear_model import Ridge + self.meta_model = Ridge(alpha=1.0) + self.meta_model.fit(meta_features, y) + + else: + # Simple averaging + for name, model in self.models.items(): + model.fit(X, y) + + return self + + def predict(self, X: np.ndarray) -> np.ndarray: + """Prediccion del ensemble""" + + predictions = {} + for name, model in self.models.items(): + predictions[name] = model.predict(X) + + if self.use_stacking: + meta_features = np.column_stack(list(predictions.values())) + return self.meta_model.predict(meta_features) + else: + # Weighted average + weighted_sum = np.zeros(len(X)) + for name, preds in predictions.items(): + weighted_sum += preds * self.weights[name] + return weighted_sum + + def predict_with_uncertainty( + self, + X: np.ndarray + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Prediccion con intervalos de confianza""" + + predictions = [] + for name, model in self.models.items(): + predictions.append(model.predict(X)) + + predictions = np.array(predictions) + + # Media y desviacion estandar de modelos + mean_pred = np.mean(predictions, axis=0) + std_pred = np.std(predictions, axis=0) + + # Intervalos de confianza (95%) + lower = mean_pred - 1.96 * std_pred + upper = mean_pred + 1.96 * std_pred + + return mean_pred, lower, upper + + def get_feature_importance(self) -> pd.DataFrame: + """Obtener importancia de features promediada""" + + importances = [] + + for name, model in self.models.items(): + if hasattr(model, 'feature_importances_'): + imp = model.feature_importances_ + importances.append(imp) + + if not importances: + return pd.DataFrame() + + avg_importance = np.mean(importances, axis=0) + return pd.DataFrame({ + 'importance': avg_importance + }).sort_values('importance', ascending=False) + + def save(self, path: str): + """Guardar modelo""" + joblib.dump({ + 'models': self.models, + 'weights': self.weights, + 'meta_model': self.meta_model, + 'use_stacking': self.use_stacking, + }, path) + + @classmethod + def load(cls, path: str) -> 'AVMEnsemble': + """Cargar modelo""" + data = joblib.load(path) + ensemble = cls( + weights=data['weights'], + use_stacking=data['use_stacking'] + ) + ensemble.models = data['models'] + ensemble.meta_model = data['meta_model'] + return ensemble +``` + +### 4.2 Training Pipeline + +```python +# src/ml/avm/training.py +import pandas as pd +import numpy as np +from sklearn.model_selection import train_test_split, cross_val_score +from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error +import mlflow +from datetime import datetime + +from .ensemble import AVMEnsemble +from .feature_engineering import create_feature_pipeline +from .data_loader import PropertyDataLoader + +class AVMTrainer: + """Pipeline de entrenamiento para AVM""" + + def __init__( + self, + data_loader: PropertyDataLoader, + market_service, + mlflow_experiment: str = "avm-training" + ): + self.data_loader = data_loader + self.market_service = market_service + mlflow.set_experiment(mlflow_experiment) + + def train( + self, + property_types: List[str] = None, + min_samples: int = 1000, + test_size: float = 0.2 + ) -> Dict: + """Entrenar modelo AVM""" + + with mlflow.start_run(run_name=f"avm-{datetime.now().strftime('%Y%m%d_%H%M')}"): + # 1. Cargar datos + df = self.data_loader.load_training_data( + property_types=property_types, + min_samples=min_samples + ) + + mlflow.log_param("n_samples", len(df)) + mlflow.log_param("property_types", property_types) + + # 2. Feature engineering + feature_pipeline = create_feature_pipeline(self.market_service) + X = feature_pipeline.fit_transform(df) + y = df['price'].values + + # Log feature names + feature_names = self._get_feature_names(feature_pipeline, df) + mlflow.log_param("n_features", len(feature_names)) + + # 3. Split + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=test_size, random_state=42 + ) + + # 4. Entrenar ensemble + ensemble = AVMEnsemble(use_stacking=True) + ensemble.fit(X_train, y_train) + + # 5. Evaluar + metrics = self._evaluate(ensemble, X_train, y_train, X_test, y_test) + + for name, value in metrics.items(): + mlflow.log_metric(name, value) + + # 6. Guardar modelo + model_path = f"models/avm_{datetime.now().strftime('%Y%m%d')}.joblib" + ensemble.save(model_path) + mlflow.log_artifact(model_path) + + # 7. Guardar feature pipeline + import joblib + pipeline_path = "models/feature_pipeline.joblib" + joblib.dump(feature_pipeline, pipeline_path) + mlflow.log_artifact(pipeline_path) + + return { + 'metrics': metrics, + 'model_path': model_path, + 'feature_importance': ensemble.get_feature_importance() + } + + def _evaluate( + self, + model: AVMEnsemble, + X_train: np.ndarray, + y_train: np.ndarray, + X_test: np.ndarray, + y_test: np.ndarray + ) -> Dict[str, float]: + """Evaluar modelo""" + + y_pred_train = model.predict(X_train) + y_pred_test = model.predict(X_test) + + # MAE + mae_train = mean_absolute_error(y_train, y_pred_train) + mae_test = mean_absolute_error(y_test, y_pred_test) + + # MAPE + mape_train = mean_absolute_percentage_error(y_train, y_pred_train) + mape_test = mean_absolute_percentage_error(y_test, y_pred_test) + + # Mediana de error absoluto + median_ae_test = np.median(np.abs(y_test - y_pred_test)) + + # Porcentaje dentro de 10% del valor real + within_10pct = np.mean(np.abs(y_test - y_pred_test) / y_test < 0.10) + within_15pct = np.mean(np.abs(y_test - y_pred_test) / y_test < 0.15) + + # R2 + from sklearn.metrics import r2_score + r2_test = r2_score(y_test, y_pred_test) + + return { + 'mae_train': mae_train, + 'mae_test': mae_test, + 'mape_train': mape_train * 100, + 'mape_test': mape_test * 100, + 'median_ae_test': median_ae_test, + 'within_10pct': within_10pct * 100, + 'within_15pct': within_15pct * 100, + 'r2_test': r2_test, + } + + def _get_feature_names(self, pipeline, df) -> List[str]: + """Obtener nombres de features del pipeline""" + # Simplificado - en produccion seria mas robusto + return list(df.columns) +``` + +--- + +## 5. Explicabilidad con SHAP + +```python +# src/ml/avm/explainability.py +import shap +import numpy as np +import pandas as pd +from typing import Dict, List + +class AVMExplainer: + """Explicabilidad de valuaciones usando SHAP""" + + def __init__(self, model, feature_names: List[str]): + self.model = model + self.feature_names = feature_names + self.explainer = None + + def initialize_explainer(self, X_background: np.ndarray): + """Inicializar SHAP explainer con datos de background""" + # Usar TreeExplainer para modelos basados en arboles + if hasattr(self.model, 'models'): + # Para ensemble, usar el primer modelo + base_model = list(self.model.models.values())[0] + self.explainer = shap.TreeExplainer(base_model) + else: + self.explainer = shap.TreeExplainer(self.model) + + def explain( + self, + X: np.ndarray, + feature_values: Dict[str, any] = None + ) -> Dict: + """Generar explicacion para una prediccion""" + + if self.explainer is None: + raise ValueError("Explainer not initialized. Call initialize_explainer first.") + + # Calcular SHAP values + shap_values = self.explainer.shap_values(X) + + if len(X.shape) == 1: + X = X.reshape(1, -1) + shap_values = shap_values.reshape(1, -1) + + # Obtener base value (prediccion promedio) + base_value = self.explainer.expected_value + if isinstance(base_value, np.ndarray): + base_value = base_value[0] + + # Crear explicacion estructurada + explanations = [] + + for i in range(len(X)): + feature_impacts = [] + + for j, (name, shap_val) in enumerate( + zip(self.feature_names, shap_values[i]) + ): + feature_val = X[i, j] if feature_values is None else feature_values.get(name, X[i, j]) + + feature_impacts.append({ + 'feature': name, + 'value': float(feature_val), + 'shap_value': float(shap_val), + 'impact': 'positive' if shap_val > 0 else 'negative', + 'impact_formatted': self._format_impact(shap_val), + }) + + # Ordenar por impacto absoluto + feature_impacts.sort(key=lambda x: abs(x['shap_value']), reverse=True) + + explanations.append({ + 'base_value': float(base_value), + 'predicted_value': float(base_value + sum(shap_values[i])), + 'top_positive': [f for f in feature_impacts if f['impact'] == 'positive'][:5], + 'top_negative': [f for f in feature_impacts if f['impact'] == 'negative'][:5], + 'all_impacts': feature_impacts, + }) + + return explanations[0] if len(explanations) == 1 else explanations + + def generate_natural_language(self, explanation: Dict) -> str: + """Generar explicacion en lenguaje natural""" + + lines = [] + predicted = explanation['predicted_value'] + base = explanation['base_value'] + + lines.append(f"El valor estimado es ${predicted:,.0f} MXN") + lines.append(f"(Valor base promedio: ${base:,.0f} MXN)") + lines.append("") + + # Factores positivos + if explanation['top_positive']: + lines.append("Factores que AUMENTAN el valor:") + for factor in explanation['top_positive'][:3]: + lines.append(f" + {self._humanize_feature(factor['feature'])}: " + f"{factor['impact_formatted']}") + + # Factores negativos + if explanation['top_negative']: + lines.append("") + lines.append("Factores que REDUCEN el valor:") + for factor in explanation['top_negative'][:3]: + lines.append(f" - {self._humanize_feature(factor['feature'])}: " + f"{factor['impact_formatted']}") + + return "\n".join(lines) + + def _format_impact(self, shap_value: float) -> str: + """Formatear impacto en pesos""" + prefix = "+" if shap_value > 0 else "" + return f"{prefix}${shap_value:,.0f}" + + def _humanize_feature(self, feature: str) -> str: + """Convertir nombre de feature a texto legible""" + mappings = { + 'constructed_area_m2': 'Superficie construida', + 'land_area_m2': 'Superficie de terreno', + 'bedrooms': 'Numero de recamaras', + 'bathrooms': 'Numero de banos', + 'parking_spaces': 'Estacionamientos', + 'is_premium_zone': 'Ubicacion premium', + 'distance_to_center_km': 'Distancia al centro', + 'age_years': 'Antiguedad', + 'quality_score': 'Calidad/amenidades', + 'avg_price_m2_zone': 'Precio promedio de zona', + 'price_trend_3m': 'Tendencia de mercado', + 'has_pool': 'Cuenta con alberca', + 'has_garden': 'Cuenta con jardin', + } + return mappings.get(feature, feature.replace('_', ' ').title()) +``` + +--- + +## 6. API de Valuacion + +```python +# src/ml/avm/api.py +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field +from typing import List, Optional +import numpy as np + +from .ensemble import AVMEnsemble +from .explainability import AVMExplainer +from .comparables import ComparablesFinder + +app = FastAPI(title="AVM API", version="1.0.0") + +# Modelos cargados al inicio +avm_model: AVMEnsemble = None +explainer: AVMExplainer = None +comparables_finder: ComparablesFinder = None + +class PropertyInput(BaseModel): + property_type: str = Field(..., example="casa") + constructed_area_m2: float = Field(..., gt=0, example=180) + land_area_m2: Optional[float] = Field(None, example=250) + bedrooms: int = Field(..., ge=1, example=3) + bathrooms: float = Field(..., ge=1, example=2.5) + parking_spaces: int = Field(0, ge=0, example=2) + latitude: float = Field(..., example=20.6736) + longitude: float = Field(..., example=-103.3927) + neighborhood: str = Field(..., example="Providencia") + municipality: str = Field(..., example="Guadalajara") + year_built: Optional[int] = Field(None, example=2018) + amenities: List[str] = Field(default_factory=list) + +class ValuationResponse(BaseModel): + estimated_value: float + confidence: float + range_low: float + range_high: float + price_per_m2: float + explanation: dict + comparables: List[dict] + +@app.post("/valuate", response_model=ValuationResponse) +async def valuate_property(property_data: PropertyInput): + """Obtener valuacion de una propiedad""" + + try: + # 1. Preparar features + features = prepare_features(property_data) + + # 2. Prediccion con incertidumbre + mean_pred, lower, upper = avm_model.predict_with_uncertainty(features) + + estimated_value = float(mean_pred[0]) + range_low = float(lower[0]) + range_high = float(upper[0]) + + # 3. Calcular confianza + confidence = calculate_confidence( + features, + estimated_value, + range_low, + range_high + ) + + # 4. Generar explicacion + explanation = explainer.explain(features) + + # 5. Buscar comparables + comparables = comparables_finder.find( + property_data.dict(), + limit=5 + ) + + # 6. Precio por m2 + price_per_m2 = estimated_value / property_data.constructed_area_m2 + + return ValuationResponse( + estimated_value=round(estimated_value, -3), # Redondear a miles + confidence=round(confidence, 2), + range_low=round(range_low, -3), + range_high=round(range_high, -3), + price_per_m2=round(price_per_m2, 0), + explanation=explanation, + comparables=comparables, + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/market-stats/{neighborhood}") +async def get_market_stats(neighborhood: str): + """Obtener estadisticas de mercado por zona""" + + stats = market_service.get_zone_stats(neighborhood) + + return { + "neighborhood": neighborhood, + "avg_price_m2": stats.get('avg_price_m2'), + "median_price_m2": stats.get('median_price_m2'), + "inventory": stats.get('inventory'), + "trend_3m": stats.get('trend_3m'), + "trend_12m": stats.get('trend_12m'), + "avg_days_on_market": stats.get('avg_dom'), + } + +def prepare_features(property_data: PropertyInput) -> np.ndarray: + """Convertir input a features del modelo""" + # Implementar transformacion + pass + +def calculate_confidence( + features: np.ndarray, + prediction: float, + lower: float, + upper: float +) -> float: + """Calcular score de confianza 0-100""" + + # Factores que afectan confianza: + # 1. Ancho del intervalo de confianza + interval_width = (upper - lower) / prediction + interval_score = max(0, 100 - interval_width * 200) + + # 2. Cantidad de comparables disponibles + # (se calcularía con datos reales) + comparables_score = 80 # placeholder + + # 3. Calidad de datos de entrada + data_quality_score = 90 # placeholder + + # Promedio ponderado + confidence = ( + interval_score * 0.4 + + comparables_score * 0.35 + + data_quality_score * 0.25 + ) + + return min(100, max(0, confidence)) +``` + +--- + +## 7. Busqueda de Comparables + +```python +# src/ml/avm/comparables.py +from typing import List, Dict +import numpy as np +from sqlalchemy import text +from .database import get_db_connection + +class ComparablesFinder: + """Buscar propiedades comparables""" + + def __init__(self, db_connection): + self.db = db_connection + + def find( + self, + property_data: Dict, + limit: int = 5, + max_distance_km: float = 2.0, + max_age_days: int = 180 + ) -> List[Dict]: + """Encontrar propiedades comparables""" + + query = text(""" + WITH target AS ( + SELECT + ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography as location, + :property_type as ptype, + :bedrooms as beds, + :bathrooms as baths, + :area as area + ) + SELECT + p.id, + p.title, + p.price, + p.constructed_area_m2, + p.bedrooms, + p.bathrooms, + p.neighborhood, + p.source_url, + p.last_seen_at, + ST_Distance(p.coordinates::geography, t.location) / 1000 as distance_km, + + -- Similarity score + ( + -- Penalizar por distancia + (1 - LEAST(ST_Distance(p.coordinates::geography, t.location) / 2000, 1)) * 30 + + + -- Similaridad de area (dentro de 30%) + CASE WHEN ABS(p.constructed_area_m2 - t.area) / t.area < 0.3 + THEN (1 - ABS(p.constructed_area_m2 - t.area) / t.area) * 25 + ELSE 0 END + + + -- Match de recamaras + CASE WHEN p.bedrooms = t.beds THEN 20 + WHEN ABS(p.bedrooms - t.beds) = 1 THEN 10 + ELSE 0 END + + + -- Match de banos + CASE WHEN p.bathrooms = t.baths THEN 15 + WHEN ABS(p.bathrooms - t.baths) <= 0.5 THEN 8 + ELSE 0 END + + + -- Recencia (mas reciente = mejor) + LEAST(10, 180 - EXTRACT(DAY FROM NOW() - p.last_seen_at)) / 18 + ) as similarity_score + + FROM properties p, target t + WHERE p.property_type = t.ptype + AND p.status IN ('active', 'sold') + AND ST_DWithin( + p.coordinates::geography, + t.location, + :max_distance * 1000 + ) + AND p.last_seen_at > NOW() - INTERVAL ':max_age days' + AND p.constructed_area_m2 BETWEEN t.area * 0.7 AND t.area * 1.3 + + ORDER BY similarity_score DESC + LIMIT :limit + """) + + result = self.db.execute(query, { + 'lat': property_data['latitude'], + 'lng': property_data['longitude'], + 'property_type': property_data['property_type'], + 'bedrooms': property_data['bedrooms'], + 'bathrooms': property_data['bathrooms'], + 'area': property_data['constructed_area_m2'], + 'max_distance': max_distance_km, + 'max_age': max_age_days, + 'limit': limit, + }) + + comparables = [] + for row in result: + comparables.append({ + 'id': row.id, + 'title': row.title, + 'price': float(row.price), + 'price_per_m2': float(row.price / row.constructed_area_m2), + 'area_m2': float(row.constructed_area_m2), + 'bedrooms': row.bedrooms, + 'bathrooms': float(row.bathrooms), + 'neighborhood': row.neighborhood, + 'distance_km': round(row.distance_km, 2), + 'similarity_score': round(row.similarity_score, 1), + 'url': row.source_url, + 'last_seen': row.last_seen_at.isoformat(), + }) + + return comparables +``` + +--- + +## 8. Tests + +```python +# src/ml/avm/__tests__/test_ensemble.py +import pytest +import numpy as np +from ..ensemble import AVMEnsemble + +class TestAVMEnsemble: + + @pytest.fixture + def sample_data(self): + np.random.seed(42) + X = np.random.randn(100, 10) + y = np.random.randn(100) * 1000000 + 3000000 # Precios entre 2-4M + return X, y + + def test_fit_predict(self, sample_data): + X, y = sample_data + model = AVMEnsemble() + model.fit(X, y) + + predictions = model.predict(X) + + assert len(predictions) == len(y) + assert predictions.min() > 0 + + def test_predict_with_uncertainty(self, sample_data): + X, y = sample_data + model = AVMEnsemble() + model.fit(X, y) + + mean, lower, upper = model.predict_with_uncertainty(X) + + assert len(mean) == len(y) + assert all(lower <= mean) + assert all(mean <= upper) + + def test_stacking_ensemble(self, sample_data): + X, y = sample_data + model = AVMEnsemble(use_stacking=True) + model.fit(X, y) + + predictions = model.predict(X) + assert model.meta_model is not None + + def test_save_load(self, sample_data, tmp_path): + X, y = sample_data + model = AVMEnsemble() + model.fit(X, y) + + path = tmp_path / "model.joblib" + model.save(str(path)) + + loaded = AVMEnsemble.load(str(path)) + preds_original = model.predict(X) + preds_loaded = loaded.predict(X) + + np.testing.assert_array_almost_equal(preds_original, preds_loaded) +``` + +--- + +## 9. Metricas y Monitoreo + +```yaml +# Metricas a trackear en produccion +metrics: + model_performance: + - name: avm_mape + type: gauge + description: "Mean Absolute Percentage Error" + + - name: avm_predictions_total + type: counter + description: "Total valuations performed" + labels: [property_type, zone] + + - name: avm_prediction_latency_seconds + type: histogram + description: "Prediction latency" + buckets: [0.1, 0.25, 0.5, 1, 2] + + - name: avm_confidence_score + type: histogram + description: "Confidence scores distribution" + buckets: [50, 60, 70, 80, 90, 100] + + data_quality: + - name: avm_missing_features + type: counter + description: "Predictions with missing features" + labels: [feature] + + - name: avm_comparables_found + type: histogram + description: "Number of comparables found" + buckets: [0, 1, 3, 5, 10] + + drift: + - name: avm_feature_drift_score + type: gauge + description: "Feature distribution drift" + labels: [feature] + + - name: avm_prediction_drift_score + type: gauge + description: "Prediction distribution drift" +``` + +--- + +**Siguiente:** [ET-IA-008-survival.md](./ET-IA-008-survival.md) - Modelo de prediccion de tiempo de venta diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/ET-ML-002-opportunities.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/ET-ML-002-opportunities.md new file mode 100644 index 0000000..6f6c320 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/ET-ML-002-opportunities.md @@ -0,0 +1,1248 @@ +--- +id: "ET-ML-opportunities" +title: "Especificacion Tecnica - Deteccion de Oportunidades de Inversion" +type: "Technical Specification" +epic: "IAI-008" +status: "Draft" +version: "1.0" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# ET-IA-008-opportunities: Deteccion de Oportunidades de Inversion + +--- + +## 1. Resumen + +Sistema de deteccion automatica de oportunidades de inversion inmobiliaria que identifica propiedades subvaluadas, zonas emergentes con potencial de apreciacion, y alertas en tiempo real para inversores. + +--- + +## 2. Tipos de Oportunidades + +```yaml +opportunity_types: + undervalued_property: + description: "Propiedad con precio < valor estimado AVM" + min_discount: 10% + confidence_threshold: 0.75 + + emerging_zone: + description: "Zona con tendencia alcista sostenida" + min_appreciation: 15% # anual + min_data_points: 12 + + distressed_sale: + description: "Venta urgente/remate" + indicators: + - price_drop_pct > 20% + - days_on_market > 120 + - keywords: [urgente, oportunidad, remate] + + arbitrage: + description: "Diferencia de precio entre portales" + min_difference: 8% + + rental_yield: + description: "Alto rendimiento de renta" + min_yield: 7% # anual + cap_rate_threshold: 8% +``` + +--- + +## 3. Arquitectura del Sistema + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ OPPORTUNITY DETECTION ENGINE │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ Property │ │ Zone │ │ Market │ │ +│ │ Analyzer │ │ Analyzer │ │ Signals │ │ +│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │ +│ │ │ │ │ +│ └───────────┬───────┴───────────────────┘ │ +│ │ │ +│ ┌───────▼───────┐ │ +│ │ Opportunity │ │ +│ │ Scorer │ │ +│ └───────┬───────┘ │ +│ │ │ +│ ┌─────────────────┼─────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────┐ ┌──────┐ ┌──────────┐ │ +│ │Alert │ │Report│ │Dashboard │ │ +│ │Engine│ │ Gen │ │ Feed │ │ +│ └──────┘ └──────┘ └──────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. Detector de Propiedades Subvaluadas + +```python +# src/ml/opportunities/undervalued_detector.py +from dataclasses import dataclass +from typing import List, Optional +from datetime import datetime +import numpy as np + +from ..avm.ensemble import AVMEnsemble +from ..avm.explainability import AVMExplainer + +@dataclass +class UndervaluedOpportunity: + property_id: str + source_url: str + title: str + + listed_price: float + estimated_value: float + discount_pct: float + + confidence: float + explanation: dict + + property_details: dict + comparables: List[dict] + + detected_at: datetime + urgency_score: float # 0-100 + recommendation: str + +class UndervaluedDetector: + """Detectar propiedades subvaluadas vs AVM""" + + def __init__( + self, + avm_model: AVMEnsemble, + explainer: AVMExplainer, + min_discount_pct: float = 10.0, + min_confidence: float = 0.75 + ): + self.avm = avm_model + self.explainer = explainer + self.min_discount = min_discount_pct + self.min_confidence = min_confidence + + def detect( + self, + property_data: dict, + listed_price: float + ) -> Optional[UndervaluedOpportunity]: + """Analizar si una propiedad esta subvaluada""" + + # 1. Obtener valuacion AVM + features = self._prepare_features(property_data) + estimated_value, lower, upper = self.avm.predict_with_uncertainty(features) + estimated_value = float(estimated_value[0]) + + # 2. Calcular descuento + discount_pct = ((estimated_value - listed_price) / estimated_value) * 100 + + if discount_pct < self.min_discount: + return None # No es una oportunidad + + # 3. Calcular confianza + confidence = self._calculate_confidence( + features, estimated_value, lower[0], upper[0], + property_data + ) + + if confidence < self.min_confidence: + return None # Confianza insuficiente + + # 4. Generar explicacion + explanation = self.explainer.explain(features) + + # 5. Buscar comparables + from .comparables import ComparablesFinder + comparables = ComparablesFinder().find(property_data, limit=5) + + # 6. Calcular urgencia + urgency = self._calculate_urgency(property_data, discount_pct) + + # 7. Generar recomendacion + recommendation = self._generate_recommendation( + discount_pct, confidence, urgency, property_data + ) + + return UndervaluedOpportunity( + property_id=property_data.get('id', 'unknown'), + source_url=property_data.get('source_url', ''), + title=property_data.get('title', ''), + listed_price=listed_price, + estimated_value=estimated_value, + discount_pct=round(discount_pct, 1), + confidence=round(confidence, 2), + explanation=explanation, + property_details=property_data, + comparables=comparables, + detected_at=datetime.utcnow(), + urgency_score=urgency, + recommendation=recommendation + ) + + def batch_detect( + self, + properties: List[dict] + ) -> List[UndervaluedOpportunity]: + """Detectar oportunidades en batch""" + + opportunities = [] + + for prop in properties: + listed_price = prop.get('price') + if not listed_price: + continue + + opportunity = self.detect(prop, listed_price) + if opportunity: + opportunities.append(opportunity) + + # Ordenar por score combinado (descuento * confianza * urgencia) + opportunities.sort( + key=lambda x: x.discount_pct * x.confidence * (x.urgency_score / 100), + reverse=True + ) + + return opportunities + + def _calculate_confidence( + self, + features: np.ndarray, + prediction: float, + lower: float, + upper: float, + property_data: dict + ) -> float: + """Calcular confianza de la deteccion""" + + scores = [] + + # 1. Ancho del intervalo de confianza + interval_pct = (upper - lower) / prediction + interval_score = max(0, 1 - interval_pct * 2) + scores.append(interval_score * 0.3) + + # 2. Calidad de datos + required_fields = ['constructed_area_m2', 'bedrooms', 'bathrooms', 'latitude', 'longitude'] + data_completeness = sum(1 for f in required_fields if property_data.get(f)) / len(required_fields) + scores.append(data_completeness * 0.25) + + # 3. Disponibilidad de comparables (proxy) + neighborhood = property_data.get('neighborhood', '') + # Asumir mas confianza en zonas conocidas + known_zones = ['providencia', 'americana', 'lafayette', 'chapalita'] + zone_score = 1.0 if neighborhood.lower() in known_zones else 0.7 + scores.append(zone_score * 0.25) + + # 4. Precio en rango razonable + if prediction > 500000 and prediction < 50000000: # 500K - 50M MXN + scores.append(0.2) + else: + scores.append(0.1) + + return sum(scores) + + def _calculate_urgency( + self, + property_data: dict, + discount_pct: float + ) -> float: + """Calcular urgencia de actuar (0-100)""" + + urgency = 50 # Base + + # Mayor descuento = mayor urgencia + urgency += min(30, discount_pct * 1.5) + + # Propiedad recien publicada = alta urgencia + days_listed = property_data.get('days_on_market', 30) + if days_listed < 7: + urgency += 20 + elif days_listed < 14: + urgency += 10 + + # Keywords de urgencia en descripcion + description = property_data.get('description', '').lower() + urgent_keywords = ['urgente', 'urge', 'oportunidad', 'negociable', 'precio a tratar'] + if any(kw in description for kw in urgent_keywords): + urgency += 15 + + # Zona premium con descuento = muy urgente + if property_data.get('is_premium_zone') and discount_pct > 15: + urgency += 15 + + return min(100, urgency) + + def _generate_recommendation( + self, + discount_pct: float, + confidence: float, + urgency: float, + property_data: dict + ) -> str: + """Generar recomendacion de texto""" + + if discount_pct > 20 and confidence > 0.85 and urgency > 70: + return "EXCELENTE OPORTUNIDAD - Actuar inmediatamente. Descuento significativo con alta confianza." + + if discount_pct > 15 and confidence > 0.80: + return "BUENA OPORTUNIDAD - Realizar due diligence rapido. Potencial de apreciacion." + + if discount_pct > 10 and confidence > 0.75: + return "OPORTUNIDAD MODERADA - Evaluar con mas detalle. Precio atractivo pero verificar condiciones." + + return "OPORTUNIDAD A EVALUAR - Investigar mas antes de decidir." + + def _prepare_features(self, property_data: dict) -> np.ndarray: + """Preparar features para el modelo""" + # Implementar transformacion + pass +``` + +--- + +## 5. Detector de Zonas Emergentes + +```python +# src/ml/opportunities/emerging_zones.py +from dataclasses import dataclass +from typing import List, Dict, Optional +from datetime import datetime, timedelta +import pandas as pd +import numpy as np +from scipy import stats + +@dataclass +class EmergingZone: + zone_name: str + municipality: str + + appreciation_12m: float # % anual + appreciation_trend: str # up, stable, accelerating + volatility: float + + avg_price_m2: float + price_vs_city_avg: float # ratio + + investment_score: float # 0-100 + risk_level: str # low, medium, high + + drivers: List[str] # Razones del crecimiento + forecast_12m: float # % estimado proximo ano + + sample_properties: List[dict] + +class EmergingZoneDetector: + """Detectar zonas con potencial de apreciacion""" + + def __init__(self, db_connection, min_appreciation: float = 10.0): + self.db = db_connection + self.min_appreciation = min_appreciation + + def detect_emerging_zones( + self, + lookback_months: int = 24, + min_samples: int = 50 + ) -> List[EmergingZone]: + """Identificar zonas emergentes""" + + # 1. Obtener datos historicos por zona + zone_data = self._get_zone_price_history(lookback_months, min_samples) + + emerging = [] + + for zone_name, df in zone_data.items(): + analysis = self._analyze_zone(zone_name, df) + + if analysis and analysis['appreciation_12m'] >= self.min_appreciation: + zone = EmergingZone( + zone_name=zone_name, + municipality=analysis['municipality'], + appreciation_12m=analysis['appreciation_12m'], + appreciation_trend=analysis['trend'], + volatility=analysis['volatility'], + avg_price_m2=analysis['current_price_m2'], + price_vs_city_avg=analysis['vs_city_avg'], + investment_score=analysis['score'], + risk_level=analysis['risk'], + drivers=analysis['drivers'], + forecast_12m=analysis['forecast'], + sample_properties=analysis['samples'] + ) + emerging.append(zone) + + # Ordenar por score + emerging.sort(key=lambda x: x.investment_score, reverse=True) + + return emerging + + def _get_zone_price_history( + self, + months: int, + min_samples: int + ) -> Dict[str, pd.DataFrame]: + """Obtener historial de precios por zona""" + + query = """ + SELECT + neighborhood, + municipality, + DATE_TRUNC('month', first_seen_at) as month, + AVG(price / constructed_area_m2) as avg_price_m2, + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price / constructed_area_m2) as median_price_m2, + COUNT(*) as sample_count, + STDDEV(price / constructed_area_m2) as std_price_m2 + FROM properties + WHERE first_seen_at > NOW() - INTERVAL '%s months' + AND property_type IN ('casa', 'departamento') + AND status != 'removed' + AND constructed_area_m2 > 0 + GROUP BY neighborhood, municipality, DATE_TRUNC('month', first_seen_at) + HAVING COUNT(*) >= 10 + ORDER BY neighborhood, month + """ + + result = pd.read_sql(query % months, self.db) + + # Agrupar por zona + zones = {} + for zone, group in result.groupby('neighborhood'): + if len(group) >= 12 and group['sample_count'].sum() >= min_samples: + zones[zone] = group + + return zones + + def _analyze_zone( + self, + zone_name: str, + df: pd.DataFrame + ) -> Optional[Dict]: + """Analizar tendencias de una zona""" + + if len(df) < 12: + return None + + df = df.sort_values('month') + + # Calcular apreciacion + current_price = df['median_price_m2'].iloc[-3:].mean() + year_ago_price = df['median_price_m2'].iloc[:3].mean() + + appreciation = ((current_price - year_ago_price) / year_ago_price) * 100 + + # Calcular tendencia (regresion lineal) + x = np.arange(len(df)) + y = df['median_price_m2'].values + slope, intercept, r_value, p_value, std_err = stats.linregress(x, y) + + # Determinar tipo de tendencia + recent_slope = self._calculate_recent_slope(df) + if recent_slope > slope * 1.2: + trend = "accelerating" + elif recent_slope > slope * 0.8: + trend = "up" + else: + trend = "stable" + + # Volatilidad + volatility = df['std_price_m2'].mean() / df['median_price_m2'].mean() + + # Precio vs promedio ciudad + city_avg = self._get_city_avg_price_m2() + vs_city = current_price / city_avg if city_avg > 0 else 1.0 + + # Score de inversion + score = self._calculate_investment_score( + appreciation, trend, volatility, vs_city, r_value + ) + + # Nivel de riesgo + risk = self._assess_risk(volatility, len(df), vs_city) + + # Drivers de crecimiento + drivers = self._identify_drivers(zone_name, df, appreciation) + + # Forecast simple + forecast = self._forecast_appreciation(df, trend) + + # Propiedades ejemplo + samples = self._get_sample_properties(zone_name) + + return { + 'municipality': df['municipality'].iloc[0], + 'appreciation_12m': round(appreciation, 1), + 'trend': trend, + 'volatility': round(volatility, 3), + 'current_price_m2': round(current_price, 0), + 'vs_city_avg': round(vs_city, 2), + 'score': round(score, 1), + 'risk': risk, + 'drivers': drivers, + 'forecast': round(forecast, 1), + 'samples': samples, + } + + def _calculate_recent_slope(self, df: pd.DataFrame) -> float: + """Calcular pendiente de ultimos 6 meses""" + recent = df.tail(6) + x = np.arange(len(recent)) + y = recent['median_price_m2'].values + slope, *_ = stats.linregress(x, y) + return slope + + def _calculate_investment_score( + self, + appreciation: float, + trend: str, + volatility: float, + vs_city: float, + r_squared: float + ) -> float: + """Calcular score de inversion 0-100""" + + score = 0 + + # Apreciacion (max 40 puntos) + score += min(40, appreciation * 2) + + # Tendencia (max 20 puntos) + trend_scores = {'accelerating': 20, 'up': 15, 'stable': 10} + score += trend_scores.get(trend, 5) + + # Baja volatilidad (max 15 puntos) + score += max(0, 15 - volatility * 100) + + # Precio bajo vs ciudad (oportunidad de catch-up, max 15 puntos) + if vs_city < 0.8: + score += 15 + elif vs_city < 1.0: + score += 10 + + # Confianza estadistica (max 10 puntos) + score += r_squared * 10 + + return min(100, max(0, score)) + + def _assess_risk( + self, + volatility: float, + data_points: int, + vs_city: float + ) -> str: + """Evaluar nivel de riesgo""" + + risk_score = 0 + + # Volatilidad + if volatility > 0.2: + risk_score += 3 + elif volatility > 0.1: + risk_score += 2 + + # Datos insuficientes + if data_points < 18: + risk_score += 2 + elif data_points < 24: + risk_score += 1 + + # Zona muy barata (puede indicar problemas) + if vs_city < 0.5: + risk_score += 2 + + if risk_score >= 5: + return "high" + elif risk_score >= 3: + return "medium" + return "low" + + def _identify_drivers( + self, + zone_name: str, + df: pd.DataFrame, + appreciation: float + ) -> List[str]: + """Identificar posibles drivers del crecimiento""" + + drivers = [] + + # Incremento de inventario = desarrollo activo + inventory_growth = self._check_inventory_growth(zone_name) + if inventory_growth > 20: + drivers.append("Desarrollo inmobiliario activo") + + # Mejora en calidad (mas amenidades) + if self._check_quality_improvement(zone_name): + drivers.append("Mejora en calidad de propiedades") + + # Proximidad a zona premium + if self._near_premium_zone(zone_name): + drivers.append("Efecto derrame de zona premium") + + # Alta demanda + if self._check_high_demand(zone_name): + drivers.append("Alta demanda / bajo inventario") + + # Nuevo desarrollo comercial/transporte + if self._check_infrastructure(zone_name): + drivers.append("Nuevo desarrollo de infraestructura") + + if not drivers: + if appreciation > 15: + drivers.append("Tendencia general de mercado") + else: + drivers.append("Crecimiento organico") + + return drivers + + def _forecast_appreciation( + self, + df: pd.DataFrame, + trend: str + ) -> float: + """Pronosticar apreciacion para proximo ano""" + + recent = df.tail(6) + x = np.arange(len(recent)) + y = recent['median_price_m2'].values + + slope, intercept, *_ = stats.linregress(x, y) + + # Proyectar 12 meses + current = y[-1] + projected = intercept + slope * (len(df) + 12) + + forecast = ((projected - current) / current) * 100 + + # Ajustar por tipo de tendencia + if trend == "accelerating": + forecast *= 1.2 + elif trend == "stable": + forecast *= 0.8 + + # Limitar a rangos razonables + return max(-20, min(50, forecast)) + + def _get_sample_properties( + self, + zone_name: str, + limit: int = 3 + ) -> List[dict]: + """Obtener propiedades ejemplo de la zona""" + + query = """ + SELECT id, title, price, constructed_area_m2, bedrooms, + source_url, images + FROM properties + WHERE neighborhood = %s + AND status = 'active' + ORDER BY last_seen_at DESC + LIMIT %s + """ + # Implementar query real + return [] + + # Metodos auxiliares (implementar segun base de datos) + def _get_city_avg_price_m2(self) -> float: + return 30000 # Placeholder + + def _check_inventory_growth(self, zone: str) -> float: + return 10 # Placeholder + + def _check_quality_improvement(self, zone: str) -> bool: + return False + + def _near_premium_zone(self, zone: str) -> bool: + return False + + def _check_high_demand(self, zone: str) -> bool: + return False + + def _check_infrastructure(self, zone: str) -> bool: + return False +``` + +--- + +## 6. Sistema de Alertas + +```python +# src/ml/opportunities/alerts.py +from dataclasses import dataclass +from typing import List, Optional +from datetime import datetime +from enum import Enum +import asyncio + +class AlertChannel(Enum): + EMAIL = "email" + PUSH = "push" + SMS = "sms" + WEBHOOK = "webhook" + +class AlertPriority(Enum): + LOW = 1 + MEDIUM = 2 + HIGH = 3 + URGENT = 4 + +@dataclass +class AlertConfig: + user_id: str + name: str + enabled: bool + + # Criterios de filtro + zones: List[str] + property_types: List[str] + min_discount_pct: float + max_price: Optional[float] + min_confidence: float + + # Canales + channels: List[AlertChannel] + + # Frecuencia + frequency: str # immediate, daily_digest, weekly + +@dataclass +class Alert: + id: str + user_id: str + opportunity_type: str + priority: AlertPriority + + title: str + summary: str + details: dict + + property_url: str + created_at: datetime + sent_at: Optional[datetime] + read_at: Optional[datetime] + +class AlertEngine: + """Motor de alertas en tiempo real""" + + def __init__( + self, + db_connection, + email_service, + push_service + ): + self.db = db_connection + self.email = email_service + self.push = push_service + + async def process_opportunity( + self, + opportunity: 'UndervaluedOpportunity' + ) -> List[Alert]: + """Procesar oportunidad y generar alertas""" + + # 1. Obtener usuarios con configs que matchean + matching_users = await self._get_matching_users(opportunity) + + alerts = [] + + for user, config in matching_users: + # 2. Crear alerta + alert = self._create_alert(user, config, opportunity) + alerts.append(alert) + + # 3. Guardar en DB + await self._save_alert(alert) + + # 4. Enviar segun configuracion + if config.frequency == 'immediate': + await self._send_alert(alert, config.channels) + + return alerts + + async def _get_matching_users( + self, + opportunity: 'UndervaluedOpportunity' + ) -> List[tuple]: + """Obtener usuarios cuyas configs matchean la oportunidad""" + + query = """ + SELECT u.id, ac.* + FROM users u + JOIN alert_configs ac ON u.id = ac.user_id + WHERE ac.enabled = true + AND ( + ac.zones IS NULL OR + %s = ANY(ac.zones) + ) + AND ( + ac.property_types IS NULL OR + %s = ANY(ac.property_types) + ) + AND ac.min_discount_pct <= %s + AND (ac.max_price IS NULL OR ac.max_price >= %s) + AND ac.min_confidence <= %s + """ + + # Ejecutar query con parametros de la oportunidad + # Retornar lista de (user, config) + return [] + + def _create_alert( + self, + user, + config: AlertConfig, + opportunity: 'UndervaluedOpportunity' + ) -> Alert: + """Crear objeto de alerta""" + + priority = self._calculate_priority(opportunity) + + title = f"Nueva oportunidad: {opportunity.discount_pct}% descuento" + summary = self._generate_summary(opportunity) + + return Alert( + id=self._generate_id(), + user_id=user.id, + opportunity_type='undervalued', + priority=priority, + title=title, + summary=summary, + details={ + 'property_id': opportunity.property_id, + 'listed_price': opportunity.listed_price, + 'estimated_value': opportunity.estimated_value, + 'discount_pct': opportunity.discount_pct, + 'confidence': opportunity.confidence, + 'urgency_score': opportunity.urgency_score, + }, + property_url=opportunity.source_url, + created_at=datetime.utcnow(), + sent_at=None, + read_at=None + ) + + def _calculate_priority( + self, + opportunity: 'UndervaluedOpportunity' + ) -> AlertPriority: + """Determinar prioridad de la alerta""" + + score = ( + opportunity.discount_pct * 2 + + opportunity.confidence * 50 + + opportunity.urgency_score * 0.5 + ) + + if score > 150: + return AlertPriority.URGENT + elif score > 100: + return AlertPriority.HIGH + elif score > 70: + return AlertPriority.MEDIUM + return AlertPriority.LOW + + def _generate_summary( + self, + opportunity: 'UndervaluedOpportunity' + ) -> str: + """Generar resumen para la alerta""" + + return f""" +{opportunity.title} + +Precio lista: ${opportunity.listed_price:,.0f} +Valor estimado: ${opportunity.estimated_value:,.0f} +Descuento: {opportunity.discount_pct}% + +Confianza: {opportunity.confidence * 100:.0f}% +Urgencia: {opportunity.urgency_score:.0f}/100 + +{opportunity.recommendation} + """.strip() + + async def _send_alert( + self, + alert: Alert, + channels: List[AlertChannel] + ): + """Enviar alerta por los canales configurados""" + + tasks = [] + + for channel in channels: + if channel == AlertChannel.EMAIL: + tasks.append(self._send_email(alert)) + elif channel == AlertChannel.PUSH: + tasks.append(self._send_push(alert)) + elif channel == AlertChannel.SMS: + tasks.append(self._send_sms(alert)) + elif channel == AlertChannel.WEBHOOK: + tasks.append(self._send_webhook(alert)) + + await asyncio.gather(*tasks, return_exceptions=True) + + # Actualizar sent_at + await self._update_sent_at(alert.id) + + async def _send_email(self, alert: Alert): + """Enviar alerta por email""" + user = await self._get_user(alert.user_id) + + subject = f"[{alert.priority.name}] {alert.title}" + html_body = self._render_email_template(alert) + + await self.email.send( + to=user.email, + subject=subject, + html=html_body + ) + + async def _send_push(self, alert: Alert): + """Enviar push notification""" + user = await self._get_user(alert.user_id) + + await self.push.send( + user_id=user.id, + title=alert.title, + body=alert.summary[:100], + data={ + 'type': 'opportunity', + 'opportunity_id': alert.details['property_id'], + 'url': alert.property_url + } + ) + + async def _save_alert(self, alert: Alert): + """Guardar alerta en base de datos""" + pass + + async def _update_sent_at(self, alert_id: str): + """Actualizar timestamp de envio""" + pass + + async def _get_user(self, user_id: str): + """Obtener usuario por ID""" + pass + + def _render_email_template(self, alert: Alert) -> str: + """Renderizar template HTML de email""" + return f""" + + +

{alert.title}

+
{alert.summary}
+ Ver propiedad + + + """ + + def _generate_id(self) -> str: + import uuid + return str(uuid.uuid4()) +``` + +--- + +## 7. API de Oportunidades + +```python +# src/ml/opportunities/api.py +from fastapi import FastAPI, HTTPException, Query, Depends +from typing import List, Optional +from pydantic import BaseModel + +from .undervalued_detector import UndervaluedDetector, UndervaluedOpportunity +from .emerging_zones import EmergingZoneDetector, EmergingZone + +app = FastAPI(title="Opportunities API", version="1.0.0") + +class OpportunityFilter(BaseModel): + zones: Optional[List[str]] = None + property_types: Optional[List[str]] = None + min_discount: float = 10.0 + max_price: Optional[float] = None + min_confidence: float = 0.7 + limit: int = 20 + +@app.get("/opportunities/undervalued", response_model=List[dict]) +async def get_undervalued_opportunities( + filters: OpportunityFilter = Depends() +): + """Obtener propiedades subvaluadas""" + + detector = UndervaluedDetector( + avm_model=load_avm_model(), + explainer=load_explainer(), + min_discount_pct=filters.min_discount, + min_confidence=filters.min_confidence + ) + + # Obtener propiedades activas + properties = await get_active_properties( + zones=filters.zones, + property_types=filters.property_types, + max_price=filters.max_price + ) + + opportunities = detector.batch_detect(properties) + + return [ + { + 'property_id': o.property_id, + 'title': o.title, + 'url': o.source_url, + 'listed_price': o.listed_price, + 'estimated_value': o.estimated_value, + 'discount_pct': o.discount_pct, + 'confidence': o.confidence, + 'urgency_score': o.urgency_score, + 'recommendation': o.recommendation, + 'comparables_count': len(o.comparables), + } + for o in opportunities[:filters.limit] + ] + +@app.get("/opportunities/emerging-zones", response_model=List[dict]) +async def get_emerging_zones( + min_appreciation: float = Query(10.0, ge=0), + limit: int = Query(10, ge=1, le=50) +): + """Obtener zonas emergentes con potencial de apreciacion""" + + detector = EmergingZoneDetector( + db_connection=get_db(), + min_appreciation=min_appreciation + ) + + zones = detector.detect_emerging_zones() + + return [ + { + 'zone': z.zone_name, + 'municipality': z.municipality, + 'appreciation_12m': z.appreciation_12m, + 'trend': z.appreciation_trend, + 'avg_price_m2': z.avg_price_m2, + 'investment_score': z.investment_score, + 'risk_level': z.risk_level, + 'drivers': z.drivers, + 'forecast_12m': z.forecast_12m, + } + for z in zones[:limit] + ] + +@app.get("/opportunities/{property_id}/analysis") +async def get_property_analysis(property_id: str): + """Obtener analisis detallado de una propiedad""" + + property_data = await get_property(property_id) + if not property_data: + raise HTTPException(status_code=404, detail="Property not found") + + detector = UndervaluedDetector( + avm_model=load_avm_model(), + explainer=load_explainer() + ) + + opportunity = detector.detect(property_data, property_data['price']) + + if not opportunity: + return { + 'is_opportunity': False, + 'message': 'Property is not considered undervalued', + 'estimated_value': None, + } + + return { + 'is_opportunity': True, + 'listed_price': opportunity.listed_price, + 'estimated_value': opportunity.estimated_value, + 'discount_pct': opportunity.discount_pct, + 'confidence': opportunity.confidence, + 'urgency_score': opportunity.urgency_score, + 'recommendation': opportunity.recommendation, + 'explanation': opportunity.explanation, + 'comparables': opportunity.comparables, + } + +# Alert configuration endpoints +@app.post("/alerts/config") +async def create_alert_config(config: dict, user_id: str = Depends(get_current_user)): + """Crear configuracion de alertas""" + pass + +@app.get("/alerts") +async def get_user_alerts( + user_id: str = Depends(get_current_user), + unread_only: bool = False, + limit: int = 20 +): + """Obtener alertas del usuario""" + pass + +@app.put("/alerts/{alert_id}/read") +async def mark_alert_read(alert_id: str): + """Marcar alerta como leida""" + pass +``` + +--- + +## 8. Tests + +```python +# src/ml/opportunities/__tests__/test_undervalued.py +import pytest +from ..undervalued_detector import UndervaluedDetector + +class TestUndervaluedDetector: + + @pytest.fixture + def detector(self, mock_avm, mock_explainer): + return UndervaluedDetector( + avm_model=mock_avm, + explainer=mock_explainer, + min_discount_pct=10.0, + min_confidence=0.75 + ) + + def test_detect_undervalued_property(self, detector): + property_data = { + 'id': 'test-123', + 'title': 'Casa en Providencia', + 'constructed_area_m2': 180, + 'bedrooms': 3, + 'bathrooms': 2.5, + 'latitude': 20.6736, + 'longitude': -103.3927, + 'neighborhood': 'Providencia', + } + listed_price = 4_000_000 # AVM estima 5M + + opportunity = detector.detect(property_data, listed_price) + + assert opportunity is not None + assert opportunity.discount_pct >= 10 + assert opportunity.confidence >= 0.75 + + def test_no_opportunity_fair_price(self, detector): + property_data = { + 'id': 'test-456', + 'constructed_area_m2': 150, + 'bedrooms': 2, + 'bathrooms': 2, + 'latitude': 20.67, + 'longitude': -103.39, + 'neighborhood': 'Test Zone', + } + listed_price = 3_000_000 # Precio justo + + opportunity = detector.detect(property_data, listed_price) + + assert opportunity is None + + def test_urgency_calculation(self, detector): + # Propiedad recien listada con alto descuento + property_data = { + 'days_on_market': 3, + 'is_premium_zone': True, + 'description': 'Venta urgente', + } + + urgency = detector._calculate_urgency(property_data, discount_pct=20) + + assert urgency >= 80 +``` + +--- + +## 9. Modelo de Datos + +```sql +-- Tabla de oportunidades detectadas +CREATE TABLE opportunities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + property_id VARCHAR(32) REFERENCES properties(id), + opportunity_type VARCHAR(50) NOT NULL, + + listed_price DECIMAL(15,2) NOT NULL, + estimated_value DECIMAL(15,2) NOT NULL, + discount_pct DECIMAL(5,2) NOT NULL, + + confidence DECIMAL(3,2) NOT NULL, + urgency_score DECIMAL(5,2), + + explanation JSONB, + comparables JSONB, + recommendation TEXT, + + detected_at TIMESTAMP DEFAULT NOW(), + expires_at TIMESTAMP, + status VARCHAR(20) DEFAULT 'active', + + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_opportunities_property ON opportunities(property_id); +CREATE INDEX idx_opportunities_type ON opportunities(opportunity_type); +CREATE INDEX idx_opportunities_status ON opportunities(status); +CREATE INDEX idx_opportunities_discount ON opportunities(discount_pct DESC); + +-- Configuraciones de alertas +CREATE TABLE alert_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), + name VARCHAR(100) NOT NULL, + enabled BOOLEAN DEFAULT true, + + zones TEXT[], + property_types TEXT[], + min_discount_pct DECIMAL(5,2) DEFAULT 10, + max_price DECIMAL(15,2), + min_confidence DECIMAL(3,2) DEFAULT 0.7, + + channels TEXT[] DEFAULT ARRAY['email'], + frequency VARCHAR(20) DEFAULT 'immediate', + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_alert_configs_user ON alert_configs(user_id); + +-- Alertas enviadas +CREATE TABLE alerts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), + opportunity_id UUID REFERENCES opportunities(id), + config_id UUID REFERENCES alert_configs(id), + + priority SMALLINT DEFAULT 2, + title VARCHAR(200) NOT NULL, + summary TEXT, + details JSONB, + + property_url TEXT, + + created_at TIMESTAMP DEFAULT NOW(), + sent_at TIMESTAMP, + read_at TIMESTAMP +); + +CREATE INDEX idx_alerts_user ON alerts(user_id); +CREATE INDEX idx_alerts_created ON alerts(created_at DESC); +``` + +--- + +**Anterior:** [ET-IA-008-avm.md](./ET-IA-008-avm.md) +**Siguiente:** [ET-IA-008-reports.md](./ET-IA-008-reports.md) diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/_MAP.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/_MAP.md new file mode 100644 index 0000000..6187917 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/especificaciones/_MAP.md @@ -0,0 +1,34 @@ +--- +id: "MAP-IAI-008-ET" +title: "Mapa Especificaciones IAI-008" +type: "Navigation Map" +epic: "IAI-008" +section: "especificaciones" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Especificaciones Tecnicas - IAI-008 ML Analytics + +**EPIC:** IAI-008 +**Seccion:** Especificaciones Tecnicas + +--- + +## Documentos + +| ID | Archivo | Titulo | Estado | +|----|---------|--------|--------| +| ET-ML-001 | [ET-ML-001-avm.md](./ET-ML-001-avm.md) | Modelo AVM (Valuacion Automatica) | Creado | +| ET-ML-002 | [ET-ML-002-opportunities.md](./ET-ML-002-opportunities.md) | Deteccion de Oportunidades | Creado | + +--- + +## Navegacion + +- **Arriba:** [IAI-008-ml-analytics/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-001.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-001.md new file mode 100644 index 0000000..17a65d2 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-001.md @@ -0,0 +1,183 @@ +--- +id: "US-ML-001" +title: "Valuacion automatica de propiedad" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 13 +priority: "Alta" +sprint: "-" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-001: Valuacion automatica de propiedad + +--- + +## User Story + +**Como** agente inmobiliario +**Quiero** obtener una valuacion automatica de una propiedad +**Para** proporcionar a mis clientes una estimacion de precio basada en datos + +--- + +## Descripcion + +Implementar el modelo AVM (Automated Valuation Model) que estime el valor de mercado de propiedades basandose en caracteristicas, ubicacion y condiciones de mercado. El modelo debe ser preciso (MAPE < 10%) y proporcionar explicaciones de los factores que influyen en la valuacion. + +--- + +## Criterios de Aceptacion + +### Funcionales + +- [ ] Usuario ingresa caracteristicas de propiedad +- [ ] Sistema retorna valor estimado con rango +- [ ] Sistema muestra score de confianza +- [ ] Sistema muestra precio por m2 +- [ ] Sistema lista propiedades comparables + +### Tecnicos + +- [ ] MAPE < 10% en test set +- [ ] Latencia < 500ms +- [ ] Modelo versionado en MLflow +- [ ] API disponible en `/api/v1/ml/valuation/predict` + +--- + +## Mockup de Interfaz + +``` ++------------------------------------------+ +| VALUACION AUTOMATICA | ++------------------------------------------+ +| | +| Tipo: [Casa v] Transaccion: [Venta v] | +| | +| Recamaras: [3] Banos: [2.5] | +| Superficie construida: [180] m2 | +| Superficie terreno: [250] m2 | +| Antiguedad: [5] anos | +| | +| Direccion: [Av. Americas 1234_______] | +| Colonia: [Providencia] | +| Ciudad: [Guadalajara] | +| | +| [ CALCULAR VALUACION ] | +| | ++------------------------------------------+ +| RESULTADO | ++------------------------------------------+ +| | +| Valor Estimado: $4,850,000 MXN | +| Rango: $4,600,000 - $5,100,000 | +| Precio/m2: $26,944 | +| Confianza: 87% | +| | +| Factores principales: | +| + Ubicacion premium (+12%) | +| + Superficie generosa (+8%) | +| - Antiguedad moderada (-3%) | +| | +| Comparables cercanos: | +| | Direccion | Precio | m2 | $/m2 | | +| |-----------|--------|-----|------| | +| | Americas | 4.9M | 190 | 25.8K| | +| | Lopez M. | 4.7M | 175 | 26.9K| | +| | ++------------------------------------------+ +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Feature engineering pipeline | 8h | +| 2 | Entrenar modelo XGBoost | 4h | +| 3 | Entrenar modelo LightGBM | 4h | +| 4 | Implementar ensemble | 4h | +| 5 | API endpoint FastAPI | 4h | +| 6 | Integracion frontend | 8h | +| 7 | Tests unitarios y de integracion | 6h | +| 8 | Documentacion y model card | 4h | + +**Total estimado:** 42h (~5 dias) + +--- + +## Datos de Entrada + +```yaml +input: + required: + - property_type: string + - transaction_type: string + - construction_m2: number + - latitude: number + - longitude: number + + optional: + - land_m2: number + - bedrooms: integer + - bathrooms: number + - parking_spaces: integer + - age_years: integer + - postal_code: string + - amenities: string[] +``` + +--- + +## Datos de Salida + +```yaml +output: + estimated_price: number + price_range: + min: number + max: number + price_per_m2: number + confidence_score: number # 0-1 + factors: + - name: string + impact: string # "positive" | "negative" + magnitude: number # % + comparables: + - id: string + address: string + price: number + surface_m2: number + price_per_m2: number + similarity_score: number +``` + +--- + +## Definition of Done + +- [ ] MAPE < 10% validado +- [ ] API responde en < 500ms +- [ ] UI implementada y funcional +- [ ] Comparables se muestran correctamente +- [ ] Tests pasan (unit + integration) +- [ ] Model card documentado +- [ ] Deployed a staging + +--- + +## Dependencias + +- IA-007 completado (datos de propiedades) +- Modelo de datos de propiedades +- Infraestructura ML (FastAPI, MLflow) + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-002.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-002.md new file mode 100644 index 0000000..23594c8 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-002.md @@ -0,0 +1,86 @@ +--- +id: "US-ML-002" +title: "Explicabilidad de valuacion con SHAP" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 5 +priority: "Media" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-002: Explicabilidad de valuacion con SHAP + +--- + +## User Story + +**Como** agente inmobiliario +**Quiero** entender por que el modelo estimo cierto valor +**Para** poder explicar la valuacion a mis clientes con confianza + +--- + +## Descripcion + +Implementar explicabilidad del modelo AVM usando SHAP (SHapley Additive exPlanations) que muestre el impacto de cada feature en la prediccion de forma visual e interpretable. + +--- + +## Criterios de Aceptacion + +- [ ] Se calculan SHAP values para cada prediccion +- [ ] Se genera visualizacion waterfall +- [ ] Se identifican top 5 factores de impacto +- [ ] Explicaciones en lenguaje natural +- [ ] Latencia total < 1 segundo + +--- + +## Visualizacion SHAP + +``` +Impacto en el precio estimado ($4,850,000) +------------------------------------------ + +Ubicacion (Providencia) ████████████ +$520,000 +Superficie (180 m2) ████████ +$380,000 +Recamaras (3) ███ +$120,000 +Precio zona promedio ██████ +$280,000 +Banos (2.5) ██ +$80,000 +Antiguedad (5 anos) ▓▓ -$95,000 +Estacionamientos (2) █ +$65,000 + --------------------- + Valor base: $3,500,000 + Valor final: $4,850,000 +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Integrar SHAP library | 2h | +| 2 | Calcular SHAP values en inference | 4h | +| 3 | Generar explicaciones naturales | 4h | +| 4 | Visualizacion frontend | 6h | +| 5 | Caching de SHAP values | 2h | + +**Total estimado:** 18h (~2.5 dias) + +--- + +## Definition of Done + +- [ ] SHAP values se calculan correctamente +- [ ] Visualizacion clara e interpretable +- [ ] Explicaciones en espanol natural +- [ ] Performance aceptable (< 1s total) + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-003.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-003.md new file mode 100644 index 0000000..a1caf8c --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-003.md @@ -0,0 +1,103 @@ +--- +id: "US-ML-003" +title: "Prediccion de dias en mercado" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 8 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-003: Prediccion de dias en mercado + +--- + +## User Story + +**Como** agente inmobiliario +**Quiero** saber cuanto tiempo tardara una propiedad en venderse +**Para** establecer expectativas realistas con mis clientes + +--- + +## Descripcion + +Implementar modelo de survival analysis que prediga el tiempo estimado de venta (Days on Market) basandose en caracteristicas de la propiedad, precio de lista vs mercado, y condiciones actuales. + +--- + +## Criterios de Aceptacion + +- [ ] Sistema predice dias estimados en mercado +- [ ] Muestra probabilidades a 30/60/90 dias +- [ ] Identifica factores que aceleran/retrasan venta +- [ ] Proporciona recomendaciones para acelerar +- [ ] C-index >= 0.75 en validacion + +--- + +## Mockup de Resultado + +``` ++------------------------------------------+ +| TIEMPO ESTIMADO DE VENTA | ++------------------------------------------+ +| | +| Dias estimados: 45 dias | +| Rango probable: 30 - 65 dias | +| | +| Probabilidad de venta: | +| En 30 dias: 35% [==== ] | +| En 60 dias: 72% [======== ] | +| En 90 dias: 91% [========= ] | +| | +| Factores que ACELERAN: | +| + Precio competitivo (-15 dias) | +| + Buenas fotos (-8 dias) | +| | +| Factores que RETRASAN: | +| - Alto inventario en zona (+12 dias) | +| - Temporada baja (+5 dias) | +| | +| RECOMENDACIONES: | +| - Reducir precio 5% podria acelerar | +| la venta en ~10 dias | +| - Agregar tour virtual podria | +| reducir tiempo en ~5 dias | +| | ++------------------------------------------+ +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Preparar dataset con censoring | 4h | +| 2 | Entrenar Cox PH model | 4h | +| 3 | Entrenar Random Survival Forest | 4h | +| 4 | Generar probabilidades y recomendaciones | 4h | +| 5 | API endpoint | 3h | +| 6 | UI frontend | 6h | +| 7 | Tests | 3h | + +**Total estimado:** 28h (~3.5 dias) + +--- + +## Definition of Done + +- [ ] C-index >= 0.75 +- [ ] Probabilidades calibradas +- [ ] Recomendaciones generadas +- [ ] UI implementada +- [ ] Tests pasan + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-004.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-004.md new file mode 100644 index 0000000..3e8a5aa --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-004.md @@ -0,0 +1,112 @@ +--- +id: "US-ML-004" +title: "Dashboard de tendencias de mercado" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 8 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-004: Dashboard de tendencias de mercado + +--- + +## User Story + +**Como** usuario de la plataforma +**Quiero** ver tendencias de precios y mercado por zona +**Para** entender la dinamica del mercado inmobiliario + +--- + +## Descripcion + +Implementar dashboard interactivo que muestre tendencias de precios, inventario, absorcion y otros indicadores clave por zona geografica, con visualizaciones de series temporales y mapas de calor. + +--- + +## Criterios de Aceptacion + +- [ ] Dashboard muestra precio promedio por zona +- [ ] Grafica de tendencia temporal (12 meses) +- [ ] Mapa de calor de precios por m2 +- [ ] Indicadores de mercado (inventario, absorcion) +- [ ] Filtros por zona, tipo, rango de precio +- [ ] Datos actualizados diariamente + +--- + +## Widgets del Dashboard + +```yaml +widgets: + header: + - stat: "Precio Promedio/m2" + valor: "$28,500" + cambio: "+3.2%" + + - stat: "Inventario Activo" + valor: "1,234" + cambio: "-5%" + + - stat: "Dias Promedio" + valor: "62" + cambio: "-8%" + + - stat: "Indice de Mercado" + valor: "72/100" + cambio: "+2" + + row_1: + - tipo: line_chart + titulo: "Evolucion de Precios" + datos: precio_m2_mensual + period: 12_meses + + - tipo: heatmap_geo + titulo: "Precio por m2 por Zona" + datos: precio_m2_por_zona + + row_2: + - tipo: bar_chart + titulo: "Top 10 Colonias por Precio" + datos: top_colonias + + - tipo: line_chart + titulo: "Inventario vs Absorcion" + datos: [inventario, ventas] +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Agregaciones de datos por zona | 6h | +| 2 | API de tendencias | 4h | +| 3 | Componente de graficas (Recharts) | 8h | +| 4 | Mapa de calor (Mapbox/Leaflet) | 8h | +| 5 | Filtros interactivos | 4h | +| 6 | Caching de agregaciones | 3h | + +**Total estimado:** 33h (~4 dias) + +--- + +## Definition of Done + +- [ ] Dashboard renderiza correctamente +- [ ] Datos actualizados diariamente +- [ ] Performance aceptable (< 2s carga) +- [ ] Filtros funcionan correctamente +- [ ] Mobile responsive + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-005.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-005.md new file mode 100644 index 0000000..c726c2b --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-005.md @@ -0,0 +1,113 @@ +--- +id: "US-ML-005" +title: "Alertas de oportunidades de inversion" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 5 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-005: Alertas de oportunidades de inversion + +--- + +## User Story + +**Como** inversor inmobiliario +**Quiero** recibir alertas cuando aparezcan propiedades subvaluadas +**Para** actuar rapidamente antes que otros inversores + +--- + +## Descripcion + +Implementar sistema de alertas que notifique a usuarios cuando se detecten propiedades con precio significativamente menor al valor de mercado estimado, permitiendo configurar criterios personalizados. + +--- + +## Criterios de Aceptacion + +- [ ] Usuario configura criterios de alerta +- [ ] Sistema detecta propiedades subvaluadas automaticamente +- [ ] Alertas se envian via email y push +- [ ] Alerta incluye analisis rapido de oportunidad +- [ ] Tiempo entre publicacion y alerta < 1 hora + +--- + +## Configuracion de Alerta + +```yaml +alerta_config: + nombre: "Oportunidades Providencia" + criterios: + descuento_minimo: 15% + zonas: [providencia, americana, lafayette] + tipos: [casa, departamento] + precio_max: 8000000 + confianza_min: 0.75 + + notificaciones: + email: true + push: true + frecuencia: "inmediata" +``` + +--- + +## Template de Alerta + +``` ++------------------------------------------+ +| NUEVA OPORTUNIDAD DETECTADA | ++------------------------------------------+ +| | +| Casa en Providencia | +| Publicada hace 45 minutos | +| | +| Precio Lista: $4,200,000 | +| Valor Estimado: $5,100,000 | +| DESCUENTO: 18% | +| | +| - 3 recamaras, 2.5 banos | +| - 180 m2 construidos | +| - Excelente ubicacion | +| | +| Confianza de estimacion: 82% | +| | +| [VER PROPIEDAD] [ANALISIS COMPLETO] | +| | ++------------------------------------------+ +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Modelo de configuracion de alertas | 3h | +| 2 | Job de deteccion continua | 4h | +| 3 | Sistema de notificaciones (email/push) | 6h | +| 4 | UI de configuracion | 4h | +| 5 | Tests | 2h | + +**Total estimado:** 19h (~2.5 dias) + +--- + +## Definition of Done + +- [ ] Alertas se configuran correctamente +- [ ] Deteccion funciona en tiempo real +- [ ] Notificaciones llegan en < 1 hora +- [ ] Falsos positivos < 20% + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-006.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-006.md new file mode 100644 index 0000000..787dfa1 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-006.md @@ -0,0 +1,151 @@ +--- +id: "US-ML-006" +title: "Reporte CMA automatizado" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 8 +priority: "Alta" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-006: Reporte CMA automatizado + +--- + +## User Story + +**Como** agente inmobiliario +**Quiero** generar reportes CMA (Comparative Market Analysis) automaticamente +**Para** presentar valuaciones profesionales a mis clientes sin trabajo manual + +--- + +## Descripcion + +Implementar sistema de generacion automatica de reportes CMA profesionales que incluyan comparables, analisis de mercado, valuacion AVM y recomendaciones, exportables a PDF con branding personalizable. + +--- + +## Criterios de Aceptacion + +- [ ] Sistema genera CMA completo en < 30 segundos +- [ ] Incluye minimo 5 comparables relevantes +- [ ] Muestra graficas de tendencias de mercado +- [ ] Incluye valuacion AVM con explicacion +- [ ] PDF profesional con branding del agente +- [ ] Exportable en multiples formatos (PDF, DOCX) + +--- + +## Estructura del Reporte + +```yaml +cma_sections: + portada: + - logo_agencia + - titulo: "Analisis Comparativo de Mercado" + - direccion_propiedad + - fecha_reporte + - datos_agente + + resumen_ejecutivo: + - valor_estimado: "$4,850,000" + - rango_sugerido: "$4,600,000 - $5,100,000" + - precio_recomendado: "$4,750,000" + - confianza: "85%" + - dias_estimados_mercado: "45-60" + + propiedad_sujeto: + - fotos_principales + - caracteristicas_clave + - ubicacion_mapa + - descripcion + + comparables: + - lista_5_propiedades + - tabla_comparativa + - ajustes_aplicados + - mapa_ubicaciones + + analisis_mercado: + - tendencias_precios_zona + - inventario_absorcion + - dias_promedio_mercado + - predicciones + + valuacion: + - metodologia + - factores_principales_shap + - conclusion + - disclaimer + + anexos: + - detalle_comparables + - fuentes_datos +``` + +--- + +## Mockup de Portada + +``` ++--------------------------------------------------+ +| | +| [LOGO AGENCIA] | +| | +| ═══════════════════════════════════════════════ | +| | +| ANALISIS COMPARATIVO DE MERCADO | +| (CMA Report) | +| | +| ─────────────────────────────────────────────── | +| | +| Propiedad: | +| Av. Providencia 1234, Col. Providencia | +| Guadalajara, Jalisco | +| | +| ─────────────────────────────────────────────── | +| | +| Preparado por: | +| Juan Perez | RE/MAX Premium | +| Tel: 33 1234 5678 | +| Email: juan@remax.mx | +| | +| Fecha: 4 de Enero, 2026 | +| | ++--------------------------------------------------+ +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Definir esquema JSON del CMA | 2h | +| 2 | Selector de comparables automatico | 6h | +| 3 | Generador de graficas (Recharts/D3) | 6h | +| 4 | Template PDF con react-pdf | 8h | +| 5 | API de generacion | 4h | +| 6 | Sistema de branding personalizable | 4h | +| 7 | Tests | 3h | + +**Total estimado:** 33h (~4 dias) + +--- + +## Definition of Done + +- [ ] CMA se genera en < 30 segundos +- [ ] PDF renderiza correctamente +- [ ] Comparables son relevantes (similarity > 0.7) +- [ ] Branding es configurable +- [ ] Tests de generacion pasan + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-007.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-007.md new file mode 100644 index 0000000..632d3fe --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-007.md @@ -0,0 +1,190 @@ +--- +id: "US-ML-007" +title: "Calculadora de ROI para inversiones" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 5 +priority: "Media" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-007: Calculadora de ROI para inversiones + +--- + +## User Story + +**Como** inversor inmobiliario +**Quiero** calcular el retorno de inversion proyectado de una propiedad +**Para** tomar decisiones informadas de compra + +--- + +## Descripcion + +Implementar calculadora de ROI que considere precio de compra, costos de cierre, potencial de renta, apreciacion proyectada, gastos de mantenimiento e impuestos para generar proyecciones de retorno a 1, 5 y 10 anos. + +--- + +## Criterios de Aceptacion + +- [ ] Calcula Cap Rate y Cash-on-Cash return +- [ ] Proyecta apreciacion basada en ML +- [ ] Considera todos los costos (cierre, mantenimiento, impuestos) +- [ ] Muestra comparacion con otras inversiones +- [ ] Genera grafica de flujo de efectivo +- [ ] Permite ajustar parametros interactivamente + +--- + +## Parametros de Entrada + +```yaml +purchase: + price: 4500000 + down_payment_pct: 30 + closing_costs_pct: 5 + renovation_budget: 200000 + +financing: + loan_amount: 3150000 # calculado + interest_rate: 12.5 + term_years: 20 + +income: + monthly_rent: 28000 + occupancy_rate: 95 # % + annual_rent_increase: 4 # % + +expenses: + property_tax_annual: 15000 + insurance_annual: 8000 + maintenance_pct: 1 # % del valor anual + hoa_monthly: 2500 + management_fee_pct: 8 # % de renta + +appreciation: + use_ml_forecast: true + manual_rate: null # si no usa ML +``` + +--- + +## Mockup de Resultados + +``` ++--------------------------------------------------+ +| ANALISIS DE INVERSION | ++--------------------------------------------------+ +| | +| METRICAS CLAVE | +| ┌────────────────────────────────────────────┐ | +| │ Cap Rate │ 6.8% │ | +| │ Cash-on-Cash │ 9.2% │ | +| │ ROI Total (5 anos)│ 78% │ | +| │ TIR │ 14.3% │ | +| └────────────────────────────────────────────┘ | +| | +| FLUJO DE EFECTIVO MENSUAL | +| ┌────────────────────────────────────────────┐ | +| │ + Renta bruta │ $28,000 │ | +| │ - Vacancia (5%) │ -$1,400 │ | +| │ - Hipoteca │ -$35,500 │ | +| │ - HOA │ -$2,500 │ | +| │ - Mantenimiento │ -$3,750 │ | +| │ - Administracion │ -$2,240 │ | +| │ ═══════════════════════════════════════ │ | +| │ = Flujo neto │ -$17,390 (*) │ | +| └────────────────────────────────────────────┘ | +| (*) Flujo negativo compensado por apreciacion | +| | +| PROYECCION A 5 ANOS | +| ┌────────────────────────────────────────────┐ | +| │ Inversion inicial │ $1,575,000 │ | +| │ Valor estimado │ $6,200,000 │ | +| │ Equity acumulado │ $2,800,000 │ | +| │ Renta acumulada │ $1,512,000 │ | +| │ Gastos acumulados │ -$780,000 │ | +| │ Ganancia neta │ $1,232,000 │ | +| │ ROI │ 78% │ | +| └────────────────────────────────────────────┘ | +| | ++--------------------------------------------------+ +``` + +--- + +## Grafica de Proyeccion + +``` +Valor de la Inversion (MXN) + │ + 7M │ ╭─── + │ ╭────╯ + 6M │ ╭────╯ + │ ╭────╯ + 5M │ ╭────╯ + │ ╭────╯ + 4M │ ╭────╯ + │ ╭────╯ + 3M │─╯ + │ + └──────────────────────────────────────── + Ano 1 Ano 2 Ano 3 Ano 4 Ano 5 + + ─── Valor Propiedad ─── Equity ─── Deuda +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Modelo de calculo financiero | 4h | +| 2 | Integracion con ML de apreciacion | 3h | +| 3 | Componente de entrada interactivo | 4h | +| 4 | Visualizaciones de proyeccion | 4h | +| 5 | Exportar a PDF | 2h | +| 6 | Tests | 2h | + +**Total estimado:** 19h (~2.5 dias) + +--- + +## Formulas Clave + +```python +# Cap Rate +cap_rate = (net_operating_income / property_value) * 100 + +# Cash-on-Cash Return +cash_on_cash = (annual_cash_flow / total_cash_invested) * 100 + +# NOI (Net Operating Income) +noi = gross_rent - vacancy - operating_expenses + +# ROI Total +roi = (equity_gain + cash_flow_cumulative) / initial_investment * 100 + +# TIR (IRR) +irr = npf.irr(cash_flows_array) +``` + +--- + +## Definition of Done + +- [ ] Calculos financieros correctos +- [ ] ML de apreciacion integrado +- [ ] UI interactiva funcional +- [ ] Graficas renderizan correctamente +- [ ] Tests pasan + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-008.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-008.md new file mode 100644 index 0000000..14f6475 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/US-ML-008.md @@ -0,0 +1,213 @@ +--- +id: "US-ML-008" +title: "Mapa de zonas emergentes" +type: "User Story" +epic: "IAI-008" +status: "Draft" +story_points: 8 +priority: "Media" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# US-ML-008: Mapa de zonas emergentes + +--- + +## User Story + +**Como** inversor inmobiliario +**Quiero** visualizar zonas con potencial de apreciacion en un mapa interactivo +**Para** identificar donde invertir antes de que suban los precios + +--- + +## Descripcion + +Implementar mapa interactivo que muestre zonas clasificadas por potencial de apreciacion, con colores indicando oportunidad (verde=alta, amarillo=media, rojo=baja), incluyendo metricas clave y tendencias historicas por zona. + +--- + +## Criterios de Aceptacion + +- [ ] Mapa interactivo con polygonos de zonas +- [ ] Color-coding por potencial de apreciacion +- [ ] Click en zona muestra metricas detalladas +- [ ] Filtros por tipo de propiedad y rango de precio +- [ ] Historico de 12 meses en graficas +- [ ] Carga rapida (< 2 segundos) + +--- + +## Mockup del Mapa + +``` ++----------------------------------------------------------+ +| MAPA DE OPORTUNIDADES DE INVERSION | ++----------------------------------------------------------+ +| Filtros: [Casas ▼] [< $5M ▼] [Ultimos 12 meses ▼] | ++----------------------------------------------------------+ +| | +| ┌─────────────────────────────────────────────┐ | +| │ ZAPOPAN │ | +| │ ┌────────┐ │ | +| │ │ Country│ ▓▓▓ │ | +| │ │ Club │ │ | +| │ └────────┘ ┌──────────┐ │ | +| │ │Providencia│ ███ │ | +| │ ┌──────┐ │ (+18%) │ │ | +| │ │Chapal│ └──────────┘ │ | +| │ │ita │ ▓▓▓ │ | +| │ └──────┘ │ | +| │ GUADALAJARA │ | +| │ ┌─────────┐ │ | +| │ │Lafayette│ ███ │ | +| │ │ (+22%) │ │ | +| │ └─────────┘ ┌────────┐ │ | +| │ │Americana│ ▓▓▓ │ | +| │ │ (+15%) │ │ | +| │ └────────┘ │ | +| │ │ | +| │ ███ Alta (>15%) ▓▓▓ Media (8-15%) │ | +| │ ░░░ Baja (<8%) ─── Sin datos │ | +| └─────────────────────────────────────────────┘ | +| | ++----------------------------------------------------------+ +``` + +--- + +## Panel de Detalle de Zona + +``` ++------------------------------------------+ +| LAFAYETTE - Zapopan | ++------------------------------------------+ +| | +| Score de Inversion: 87/100 ████ | +| Riesgo: Bajo | +| | +| METRICAS | +| ┌─────────────────────────────────────┐ | +| │ Apreciacion 12m │ +22.3% │ | +| │ Precio promedio/m2 │ $35,400 │ | +| │ vs. promedio ciudad │ +18% │ | +| │ Tendencia │ Acelerando │ | +| │ Inventario activo │ 45 props │ | +| │ Dias en mercado │ 38 dias │ | +| └─────────────────────────────────────┘ | +| | +| TENDENCIA DE PRECIOS | +| $40K │ ╭────── | +| $35K │ ╭───╯ | +| $30K │╭───╯ | +| $25K │ | +| └──────────────────── | +| E F M A M J J A S O N D | +| | +| DRIVERS DE CRECIMIENTO | +| - Nuevo desarrollo comercial | +| - Derrame de zona Andares | +| - Alta demanda, bajo inventario | +| | +| PROPIEDADES EN ZONA | +| [Ver 45 propiedades activas] | +| | ++------------------------------------------+ +``` + +--- + +## Tareas Tecnicas + +| # | Tarea | Estimacion | +|---|-------|------------| +| 1 | Obtener/crear GeoJSON de colonias | 4h | +| 2 | Implementar mapa con Mapbox/Leaflet | 6h | +| 3 | Calcular scores por zona (backend) | 4h | +| 4 | Panel de detalle interactivo | 4h | +| 5 | Graficas de tendencia por zona | 4h | +| 6 | Filtros y busqueda | 3h | +| 7 | Optimizacion de carga | 2h | +| 8 | Tests | 2h | + +**Total estimado:** 29h (~3.5 dias) + +--- + +## Datos por Zona + +```yaml +zone_data: + lafayette: + polygon: "geojson..." + municipality: "Zapopan" + + metrics: + appreciation_12m: 22.3 + avg_price_m2: 35400 + vs_city_avg: 1.18 + trend: "accelerating" + inventory: 45 + avg_dom: 38 + + investment_score: 87 + risk_level: "low" + + drivers: + - "Nuevo desarrollo comercial cercano" + - "Derrame de zona Andares" + - "Alta demanda vs inventario bajo" + + forecast: + next_12m: 15.5 # % estimado + confidence: 0.78 +``` + +--- + +## API Endpoints + +```yaml +endpoints: + GET /api/zones/emerging: + description: "Lista de zonas emergentes" + response: + - zone_id + - name + - polygon_geojson + - score + - appreciation_12m + - color_code + + GET /api/zones/{zone_id}/details: + description: "Detalle de una zona" + response: + - full_metrics + - price_history_monthly + - drivers + - forecast + - sample_properties + + GET /api/zones/heatmap: + description: "Datos para heatmap de precios" + response: + - grid_cells_with_values +``` + +--- + +## Definition of Done + +- [ ] Mapa renderiza correctamente +- [ ] Polygonos de zonas cargados +- [ ] Colores reflejan scoring correcto +- [ ] Panel de detalle funcional +- [ ] Performance < 2s carga inicial +- [ ] Mobile responsive + +--- + +**Asignado a:** - +**Sprint:** - diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/_MAP.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/_MAP.md new file mode 100644 index 0000000..3129872 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/historias-usuario/_MAP.md @@ -0,0 +1,42 @@ +--- +id: "MAP-IAI-008-US" +title: "Mapa Historias Usuario IAI-008" +type: "Navigation Map" +epic: "IAI-008" +section: "historias-usuario" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Historias de Usuario - IAI-008 ML Analytics + +**EPIC:** IAI-008 +**Seccion:** Historias de Usuario + +--- + +## Documentos + +| ID | Archivo | Titulo | SP | Prioridad | Fase | +|----|---------|--------|----|-----------|------| +| US-ML-001 | [US-ML-001.md](./US-ML-001.md) | Valuacion automatica propiedad | 13 | Alta | MVP | +| US-ML-002 | [US-ML-002.md](./US-ML-002.md) | Explicabilidad SHAP | 5 | Media | MVP | +| US-ML-003 | [US-ML-003.md](./US-ML-003.md) | Prediccion dias en mercado | 8 | Alta | F2 | +| US-ML-004 | [US-ML-004.md](./US-ML-004.md) | Dashboard tendencias | 8 | Alta | MVP | +| US-ML-005 | [US-ML-005.md](./US-ML-005.md) | Alertas oportunidades | 5 | Alta | F2 | +| US-ML-006 | [US-ML-006.md](./US-ML-006.md) | Reporte CMA | 8 | Alta | F2 | +| US-ML-007 | [US-ML-007.md](./US-ML-007.md) | Calculadora ROI | 5 | Media | F3 | +| US-ML-008 | [US-ML-008.md](./US-ML-008.md) | Zonas emergentes | 8 | Media | F3 | + +**Total Story Points:** 60 + +--- + +## Navegacion + +- **Arriba:** [IAI-008-ml-analytics/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/implementacion/_MAP.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/implementacion/_MAP.md new file mode 100644 index 0000000..ae261a8 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/implementacion/_MAP.md @@ -0,0 +1,34 @@ +--- +id: "MAP-IAI-008-IMPL" +title: "Mapa Implementacion IAI-008" +type: "Navigation Map" +epic: "IAI-008" +section: "implementacion" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Implementacion - IAI-008 ML Analytics + +**EPIC:** IAI-008 +**Seccion:** Implementacion y Trazabilidad + +--- + +## Documentos + +| Archivo | Proposito | Estado | +|---------|-----------|--------| +| MODEL-CARDS/ | Documentacion de modelos ML | Pendiente | +| CHANGELOG.md | Historial de cambios | Pendiente | + +--- + +## Navegacion + +- **Arriba:** [IAI-008-ml-analytics/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-001.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-001.md new file mode 100644 index 0000000..fe4f0ab --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-001.md @@ -0,0 +1,245 @@ +--- +id: "RF-ML-001" +title: "AVM - Valuacion Automatica de Propiedades" +type: "Functional Requirement" +epic: "IAI-008" +priority: "Alta" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-008-001: AVM - Valuacion Automatica de Propiedades + +--- + +## Descripcion + +El sistema debe proporcionar un modelo de valuacion automatica (Automated Valuation Model - AVM) que estime el valor de mercado de propiedades inmobiliarias basandose en caracteristicas fisicas, ubicacion y condiciones de mercado. + +--- + +## Justificacion + +La valuacion automatica es el servicio core de la plataforma. Permite a agentes generar valuaciones instantaneas, a inversores evaluar oportunidades, y es la base para otros servicios como deteccion de propiedades subvaluadas. + +--- + +## Requisitos Funcionales + +### RF-001.1: Prediccion de Precio + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-001.1.1 | El sistema debe predecir precio de venta de propiedades | Alta | +| RF-001.1.2 | El sistema debe predecir precio de renta de propiedades | Alta | +| RF-001.1.3 | El sistema debe proporcionar intervalo de confianza | Alta | +| RF-001.1.4 | El sistema debe retornar score de confianza (0-1) | Alta | +| RF-001.1.5 | El sistema debe calcular precio por m2 | Alta | + +### RF-001.2: Features del Modelo + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-001.2.1 | El modelo debe usar caracteristicas intrinsecas (m2, recamaras, etc) | Alta | +| RF-001.2.2 | El modelo debe usar caracteristicas de ubicacion (lat/lon, zona) | Alta | +| RF-001.2.3 | El modelo debe usar indicadores de mercado (precio promedio zona) | Alta | +| RF-001.2.4 | El modelo debe usar features derivadas (precio m2 comparables) | Media | +| RF-001.2.5 | El modelo debe manejar features faltantes gracefully | Alta | + +### RF-001.3: Explicabilidad + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-001.3.1 | El sistema debe explicar factores que influyen en la valuacion | Alta | +| RF-001.3.2 | El sistema debe usar SHAP values para explicaciones | Media | +| RF-001.3.3 | El sistema debe mostrar comparables usados en la estimacion | Alta | +| RF-001.3.4 | El sistema debe indicar features con mayor impacto | Media | + +### RF-001.4: Comparables + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-001.4.1 | El sistema debe encontrar propiedades comparables | Alta | +| RF-001.4.2 | El sistema debe calcular similitud entre propiedades | Alta | +| RF-001.4.3 | El sistema debe ponderar comparables por fecha de venta | Media | +| RF-001.4.4 | El sistema debe filtrar comparables por radio geografico | Alta | + +--- + +## Features del Modelo + +### Intrinsecas + +| Feature | Tipo | Importancia | +|---------|------|-------------| +| superficie_construida_m2 | float | Alta | +| superficie_terreno_m2 | float | Alta | +| num_recamaras | int | Media | +| num_banos | float | Media | +| num_estacionamientos | int | Media | +| antiguedad_anos | int | Alta | +| tipo_propiedad | categorical | Alta | +| estado_conservacion | ordinal | Media | +| amenidades_count | int | Media | + +### Ubicacion + +| Feature | Tipo | Importancia | +|---------|------|-------------| +| latitud | float | Alta | +| longitud | float | Alta | +| codigo_postal | categorical | Alta | +| distancia_centro_m | float | Media | +| distancia_metro_m | float | Media | +| indice_seguridad_zona | float | Alta | +| nivel_socioeconomico | ordinal | Alta | + +### Mercado + +| Feature | Tipo | Importancia | +|---------|------|-------------| +| precio_promedio_m2_zona | float | Alta | +| tendencia_precios_12m | float | Alta | +| oferta_activa_zona | int | Media | +| absorcion_promedio_zona | float | Alta | + +--- + +## Arquitectura del Modelo + +```yaml +modelo: + tipo: Ensemble + componentes: + - modelo: XGBoost + peso: 0.5 + hiperparametros: + n_estimators: 500 + max_depth: 7 + learning_rate: 0.05 + + - modelo: LightGBM + peso: 0.3 + hiperparametros: + num_leaves: 50 + learning_rate: 0.05 + + - modelo: ElasticNet + peso: 0.2 + hiperparametros: + alpha: 0.5 + l1_ratio: 0.5 + + preprocessing: + - log_transform: [precio] + - standard_scaler: [superficie_*, distancia_*] + - one_hot: [tipo_propiedad] + - target_encoding: [codigo_postal] + + target: log(precio) + inverse_transform: exp(prediction) +``` + +--- + +## API Endpoints + +```yaml +POST /api/v1/ml/valuation/predict: + description: Valuacion de propiedad individual + request: + property: + type: string + transaction_type: string + bedrooms: integer + bathrooms: number + construction_m2: number + land_m2: number + age_years: integer + latitude: number + longitude: number + postal_code: string + amenities: string[] + response: + estimated_price: number + price_range: + min: number + max: number + confidence_score: number + price_per_m2: number + comparables: array + explanation: + top_factors: array + shap_values: object + +POST /api/v1/ml/valuation/batch: + description: Valuacion de multiples propiedades + request: + properties: array + response: + results: array + +POST /api/v1/ml/valuation/explain: + description: Valuacion con explicacion detallada + response: + # Incluye SHAP waterfall plot data +``` + +--- + +## Metricas de Calidad + +```yaml +metricas: + objetivo: + MAPE: "< 10%" + R2: ">= 0.85" + RMSE: "< 15% del precio medio" + + monitoreo: + - mape_por_tipo_propiedad + - mape_por_rango_precio + - mape_por_zona + - drift_score + + reentrenamiento: + trigger: "MAPE > 12% en ultimos 7 dias" + frecuencia_minima: "mensual" +``` + +--- + +## Criterios de Aceptacion + +- [ ] MAPE < 10% en test set holdout +- [ ] R2 >= 0.85 en cross-validation +- [ ] Latencia < 200ms para prediccion individual +- [ ] Latencia < 2s para batch de 100 propiedades +- [ ] Explicaciones SHAP disponibles para cada prediccion +- [ ] Comparables relevantes incluidos en respuesta +- [ ] Modelo versionado en MLflow +- [ ] Tests de regresion pasan + +--- + +## Dependencias + +- IA-007 (Webscraper): Datos de propiedades +- IA-002 (Propiedades): Modelo de datos normalizado +- XGBoost, LightGBM, scikit-learn +- SHAP para explicabilidad +- MLflow para versionamiento + +--- + +## Historias de Usuario Relacionadas + +- US-ML-001: Valuacion automatica basica +- US-ML-002: Explicabilidad de valuacion + +--- + +**Autor:** ML Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-002.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-002.md new file mode 100644 index 0000000..83990a9 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-002.md @@ -0,0 +1,191 @@ +--- +id: "RF-ML-002" +title: "Prediccion de Tiempo de Venta" +type: "Functional Requirement" +epic: "IAI-008" +priority: "Alta" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-008-002: Prediccion de Tiempo de Venta + +--- + +## Descripcion + +El sistema debe predecir cuantos dias tardara una propiedad en venderse (Days on Market - DOM) basandose en caracteristicas de la propiedad, precio de lista y condiciones de mercado. + +--- + +## Justificacion + +Conocer el tiempo estimado de venta permite a agentes establecer expectativas realistas con clientes, ajustar estrategias de pricing, y a inversores evaluar liquidez de inversiones. + +--- + +## Requisitos Funcionales + +### RF-002.1: Prediccion DOM + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-002.1.1 | El sistema debe predecir dias estimados en mercado | Alta | +| RF-002.1.2 | El sistema debe proporcionar intervalo de confianza | Alta | +| RF-002.1.3 | El sistema debe calcular probabilidades de venta a 30/60/90 dias | Alta | +| RF-002.1.4 | El sistema debe considerar estacionalidad | Media | + +### RF-002.2: Features Criticas + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-002.2.1 | El modelo debe usar ratio precio/mercado como feature principal | Alta | +| RF-002.2.2 | El modelo debe considerar inventario activo en zona | Alta | +| RF-002.2.3 | El modelo debe considerar absorcion historica | Alta | +| RF-002.2.4 | El modelo debe evaluar calidad del listing (fotos, descripcion) | Media | + +### RF-002.3: Actualizacion + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-002.3.1 | El sistema debe recalcular prediccion si cambia precio | Alta | +| RF-002.3.2 | El sistema debe ajustar prediccion con dias transcurridos | Media | +| RF-002.3.3 | El sistema debe aprender de ventas reales | Alta | + +--- + +## Features del Modelo + +### Propiedad + +| Feature | Tipo | Importancia | +|---------|------|-------------| +| precio_lista | float | Alta | +| precio_vs_mercado_ratio | float | Critica | +| tipo_propiedad | categorical | Alta | +| superficie_m2 | float | Media | +| antiguedad_anos | int | Media | +| calidad_fotos_score | float | Alta | +| descripcion_quality_score | float | Media | +| tiene_tour_virtual | boolean | Media | + +### Mercado + +| Feature | Tipo | Importancia | +|---------|------|-------------| +| inventario_activo_zona | int | Alta | +| absorcion_mensual_zona | float | Alta | +| tendencia_demanda_zona | float | Alta | +| competencia_precio_similar | int | Alta | +| estacionalidad_mes | int | Media | + +--- + +## Arquitectura del Modelo + +```yaml +modelo: + tipo: Survival Analysis + componentes: + - modelo: CoxProportionalHazards + uso: "Baseline, interpretable" + + - modelo: RandomSurvivalForest + uso: "Captura no-linealidades" + hiperparametros: + n_estimators: 200 + max_depth: 10 + + target: dias_en_mercado + censoring: propiedades_aun_activas + + output: + dias_estimados: median_survival_time + probabilidades: + - p_venta_30d: survival_function(30) + - p_venta_60d: survival_function(60) + - p_venta_90d: survival_function(90) +``` + +--- + +## API Endpoints + +```yaml +POST /api/v1/ml/predictions/time-to-sell: + description: Prediccion de tiempo de venta + request: + property: + type: string + price: number + construction_m2: number + latitude: number + longitude: number + listing_quality: + photos_count: integer + has_virtual_tour: boolean + response: + estimated_days: integer + confidence_interval: + min: integer + max: integer + probabilities: + sell_30_days: number + sell_60_days: number + sell_90_days: number + factors: + - factor: string + impact: string # "accelerates" | "delays" + magnitude: number + recommendations: + - recommendation: string + potential_improvement_days: integer +``` + +--- + +## Metricas de Calidad + +```yaml +metricas: + objetivo: + C_index: ">= 0.75" + MAPE: "< 25%" + + segmentacion: + - accuracy_por_rango_precio + - accuracy_por_tipo_propiedad + - accuracy_por_zona +``` + +--- + +## Criterios de Aceptacion + +- [ ] C-index >= 0.75 en test set +- [ ] MAPE < 25% en propiedades vendidas +- [ ] Probabilidades calibradas correctamente +- [ ] Latencia < 100ms por prediccion +- [ ] Recomendaciones generadas automaticamente +- [ ] Modelo se actualiza con ventas reales + +--- + +## Dependencias + +- IA-008-001 (AVM): Para ratio precio/mercado +- IA-007 (Webscraper): Datos de listings +- lifelines o scikit-survival + +--- + +## Historias de Usuario Relacionadas + +- US-ML-003: Prediccion dias en mercado + +--- + +**Autor:** ML Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-003.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-003.md new file mode 100644 index 0000000..b8033b2 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-003.md @@ -0,0 +1,260 @@ +--- +id: "RF-ML-003" +title: "Deteccion de Oportunidades de Inversion" +type: "Functional Requirement" +epic: "IAI-008" +priority: "Alta" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-008-003: Deteccion de Oportunidades de Inversion + +--- + +## Descripcion + +El sistema debe identificar automaticamente propiedades subvaluadas, zonas emergentes con potencial de apreciacion, y oportunidades de inversion basandose en analisis de mercado y modelos predictivos. + +--- + +## Justificacion + +Los inversores buscan oportunidades que el mercado aun no ha identificado. Un sistema automatizado puede analizar miles de propiedades y detectar anomalias de precio o tendencias emergentes que serian imposibles de identificar manualmente. + +--- + +## Requisitos Funcionales + +### RF-003.1: Propiedades Subvaluadas + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-003.1.1 | El sistema debe comparar precio lista vs valor AVM | Alta | +| RF-003.1.2 | El sistema debe clasificar nivel de oportunidad | Alta | +| RF-003.1.3 | El sistema debe filtrar falsos positivos (distressed, defectos) | Alta | +| RF-003.1.4 | El sistema debe validar con comparables recientes | Alta | + +### RF-003.2: Zonas Emergentes + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-003.2.1 | El sistema debe detectar zonas con incremento de demanda | Alta | +| RF-003.2.2 | El sistema debe identificar senales de desarrollo | Alta | +| RF-003.2.3 | El sistema debe proyectar apreciacion potencial | Media | +| RF-003.2.4 | El sistema debe clasificar etapa de emergencia | Media | + +### RF-003.3: Alertas + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-003.3.1 | El sistema debe notificar nuevas oportunidades | Alta | +| RF-003.3.2 | El sistema debe permitir configurar criterios de alerta | Alta | +| RF-003.3.3 | El sistema debe priorizar oportunidades por score | Media | + +--- + +## Deteccion de Subvaluadas + +### Metodologia + +```yaml +pasos: + 1_valuacion: + accion: "Calcular valor mercado con AVM" + output: valor_mercado, confianza + + 2_comparacion: + accion: "Calcular descuento = (valor_mercado - precio_lista) / valor_mercado" + output: descuento_pct + + 3_validacion: + accion: "Verificar con comparables recientes" + filtros: + - confianza_avm > 0.75 + - antiguedad_listing < 30 dias + - no_es_foreclosure + - no_tiene_defectos_obvios + + 4_clasificacion: + umbrales: + oportunidad_moderada: ">= 10%" + oportunidad_alta: ">= 15%" + oportunidad_excepcional: ">= 20%" +``` + +### Filtros Anti-Falso Positivo + +```yaml +filtros: + excluir: + - precio_muy_bajo: "< percentil_5 de zona" + - descripcion_contiene: ["rematar", "urgente", "embargo"] + - sin_fotos: true + - antiguedad_extrema: "> 50 anos sin remodelacion" + + flags_revisar: + - precio_baja_reciente: "> 20% en 30 dias" + - tiempo_en_mercado_largo: "> 180 dias" +``` + +--- + +## Deteccion de Zonas Emergentes + +### Senales + +```yaml +senales: + infraestructura: + peso: 0.25 + indicadores: + - nuevas_lineas_transporte + - nuevos_centros_comerciales + - mejoras_viales + fuente: "noticias, permisos municipales" + + desarrollo: + peso: 0.20 + indicadores: + - permisos_construccion_nuevos + - proyectos_anunciados + - inversion_inmobiliaria + fuente: "registros publicos, noticias" + + demanda: + peso: 0.30 + indicadores: + - incremento_busquedas_yoy: "> 20%" + - reduccion_dias_mercado_yoy: "> 15%" + - incremento_precio_m2_yoy: "> 10%" + fuente: "datos propios" + + demograficos: + peso: 0.15 + indicadores: + - crecimiento_poblacional + - mejora_nivel_socioeconomico + - reduccion_criminalidad + fuente: "INEGI, estadisticas publicas" + + sociales: + peso: 0.10 + indicadores: + - nuevos_restaurantes_cafes + - apertura_coworkings + - eventos_culturales + fuente: "Google Places, Yelp" +``` + +### Clasificacion + +```yaml +clasificacion: + early_stage: + score: "60-70" + caracteristicas: "Senales iniciales, pocos inversionistas" + riesgo: "Alto" + potencial: "Muy alto" + + growing: + score: "70-80" + caracteristicas: "Tendencia confirmada, precios subiendo" + riesgo: "Medio" + potencial: "Alto" + + maturing: + score: "80-90" + caracteristicas: "Establecida, desacelerando" + riesgo: "Bajo" + potencial: "Medio" + + saturated: + score: "> 90" + caracteristicas: "Precios altos, poco upside" + riesgo: "Bajo" + potencial: "Bajo" +``` + +--- + +## API Endpoints + +```yaml +GET /api/v1/ml/opportunities/undervalued: + description: Listar propiedades subvaluadas + query: + min_discount: number # default 10 + max_price: number + property_type: string + zone_id: string + limit: integer + response: + opportunities: + - property_id: string + price: number + estimated_value: number + discount_pct: number + opportunity_level: string + confidence: number + comparables: array + +GET /api/v1/ml/opportunities/emerging-zones: + description: Listar zonas emergentes + query: + min_score: number + stage: string + response: + zones: + - zone_id: string + name: string + score: number + stage: string + appreciation_12m: number + signals: array + heatmap: geojson + +POST /api/v1/ml/opportunities/alerts: + description: Configurar alertas de oportunidad + request: + criteria: + min_discount: number + property_types: array + zones: array + max_price: number + channels: array # email, push, webhook + response: 201 Created +``` + +--- + +## Criterios de Aceptacion + +- [ ] Propiedades subvaluadas detectadas con precision > 80% +- [ ] Falsos positivos < 20% +- [ ] Zonas emergentes correlacionan con apreciacion real +- [ ] Alertas se envian en < 1 hora de nueva oportunidad +- [ ] Dashboard muestra mapa de calor de oportunidades +- [ ] Usuarios pueden configurar criterios de alerta + +--- + +## Dependencias + +- IA-008-001 (AVM): Valuacion de propiedades +- IA-007 (Webscraper): Datos de listings +- Fuentes externas (INEGI, noticias) + +--- + +## Historias de Usuario Relacionadas + +- US-ML-005: Alertas de oportunidades +- US-ML-008: Zonas emergentes + +--- + +**Autor:** ML Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-004.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-004.md new file mode 100644 index 0000000..0ab7a32 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/RF-ML-004.md @@ -0,0 +1,282 @@ +--- +id: "RF-ML-004" +title: "Reportes Profesionales Automatizados" +type: "Functional Requirement" +epic: "IAI-008" +priority: "Media" +status: "Draft" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# RF-IA-008-004: Reportes Profesionales Automatizados + +--- + +## Descripcion + +El sistema debe generar reportes profesionales automatizados para distintos segmentos de usuarios (agentes, inversores, desarrolladores), integrando datos de mercado, predicciones ML y visualizaciones. + +--- + +## Justificacion + +Los reportes profesionales son un diferenciador clave y fuente de valor para clientes. Automatizar su generacion permite escalar el servicio y ofrecer insights consistentes basados en datos. + +--- + +## Requisitos Funcionales + +### RF-004.1: Tipos de Reportes + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-004.1.1 | El sistema debe generar reportes CMA para agentes | Alta | +| RF-004.1.2 | El sistema debe generar reportes de inversion | Alta | +| RF-004.1.3 | El sistema debe generar market snapshots | Media | +| RF-004.1.4 | El sistema debe generar estudios de factibilidad | Media | + +### RF-004.2: Formatos + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-004.2.1 | El sistema debe exportar a PDF | Alta | +| RF-004.2.2 | El sistema debe exportar a HTML interactivo | Media | +| RF-004.2.3 | El sistema debe exportar a PowerPoint | Baja | +| RF-004.2.4 | El sistema debe soportar branding personalizado | Alta | + +### RF-004.3: Contenido + +| ID | Requisito | Prioridad | +|----|-----------|-----------| +| RF-004.3.1 | Los reportes deben incluir datos de mercado actualizados | Alta | +| RF-004.3.2 | Los reportes deben incluir predicciones ML | Alta | +| RF-004.3.3 | Los reportes deben incluir visualizaciones (graficas, mapas) | Alta | +| RF-004.3.4 | Los reportes deben incluir comparables relevantes | Alta | + +--- + +## Tipos de Reportes + +### CMA (Comparative Market Analysis) + +```yaml +reporte: CMA +audiencia: Agentes (para clientes vendedores) +secciones: + 1_resumen_ejecutivo: + - valor_estimado + - rango_precio + - tiempo_estimado_venta + - recomendacion_precio + + 2_informacion_propiedad: + - datos_basicos + - fotos + - caracteristicas + + 3_analisis_comparables: + - tabla_comparables + - mapa_ubicacion + - ajustes_precio + + 4_condiciones_mercado: + - tendencia_precios_zona + - inventario_activo + - absorcion + + 5_estrategia_venta: + - precio_recomendado + - timeline_sugerido + - tips_preparacion + +formato: PDF (8-12 paginas) +branding: logo_inmobiliaria, datos_agente +``` + +### Investment Analysis + +```yaml +reporte: Investment_Analysis +audiencia: Inversores +secciones: + 1_resumen_ejecutivo: + - roi_proyectado + - cash_flow_mensual + - recomendacion + + 2_descripcion_propiedad: + - datos_basicos + - ubicacion + - estado_actual + + 3_analisis_mercado: + - tendencias_zona + - comparables + - proyeccion_apreciacion + + 4_proyecciones_financieras: + - flujo_caja_5_anos + - escenarios_sensibilidad + - metricas: + - cap_rate + - cash_on_cash + - irr + - payback + + 5_analisis_riesgo: + - factores_riesgo + - mitigaciones + - score_riesgo + + 6_recomendacion: + - go_no_go + - proximos_pasos + +formato: PDF (15-20 paginas) +``` + +### Market Snapshot + +```yaml +reporte: Market_Snapshot +audiencia: Agentes (semanal) +secciones: + 1_indicadores_clave: + - precio_promedio_m2 + - variacion_semanal + - inventario + - absorcion + + 2_tendencias: + - grafica_precios_30d + - top_zonas_movimiento + + 3_oportunidades: + - propiedades_destacadas + - zonas_emergentes + + 4_prediccion: + - outlook_corto_plazo + +formato: PDF (4-6 paginas) o Email +frecuencia: Semanal +``` + +--- + +## Personalizacion (White-Label) + +```yaml +branding: + logo: + posicion: header_right + tamano_max: 200x80px + formatos: [png, svg] + + colores: + primario: hex_color + secundario: hex_color + acento: hex_color + + tipografia: + headings: font_family + body: font_family + + footer: + texto: string + contacto: string + disclaimer: string + + cover_page: + background_image: url + titulo_custom: string + +por_tenant: true +``` + +--- + +## API Endpoints + +```yaml +POST /api/v1/ml/reports/cma: + description: Generar reporte CMA + request: + property_id: string + branding: + logo_url: string + agent_name: string + agent_phone: string + format: "pdf" | "html" + response: + report_id: string + download_url: string + expires_at: timestamp + +POST /api/v1/ml/reports/investment-analysis: + description: Generar reporte de inversion + request: + property_id: string + financing: + down_payment_pct: number + interest_rate: number + term_years: number + assumptions: + vacancy_rate: number + appreciation_rate: number + response: + report_id: string + download_url: string + +POST /api/v1/ml/reports/market-snapshot: + description: Generar market snapshot + request: + zone_id: string + period: "weekly" | "monthly" + response: + report_id: string + download_url: string + +GET /api/v1/ml/reports/:id: + description: Obtener estado/URL de reporte + response: + status: "processing" | "ready" | "failed" + download_url: string + expires_at: timestamp +``` + +--- + +## Criterios de Aceptacion + +- [ ] CMA se genera en < 30 segundos +- [ ] PDFs tienen calidad profesional +- [ ] Branding se aplica correctamente +- [ ] Datos de mercado son actuales (< 24 horas) +- [ ] Predicciones ML se incluyen con confianza +- [ ] Graficas se renderizan correctamente +- [ ] Links de descarga expiran en 24 horas + +--- + +## Dependencias + +- IA-008-001 (AVM): Valuaciones +- IA-008-002 (Time-to-Sell): Predicciones +- Puppeteer/Playwright para PDF rendering +- Chart.js o similar para visualizaciones +- S3 para storage de reportes + +--- + +## Historias de Usuario Relacionadas + +- US-ML-006: Generacion reporte CMA +- US-ML-007: Analisis ROI para inversores + +--- + +**Autor:** Product Lead +**Fecha:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/_MAP.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/_MAP.md new file mode 100644 index 0000000..9729163 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/requerimientos/_MAP.md @@ -0,0 +1,36 @@ +--- +id: "MAP-IAI-008-RF" +title: "Mapa Requerimientos IAI-008" +type: "Navigation Map" +epic: "IAI-008" +section: "requerimientos" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Requerimientos - IAI-008 ML Analytics + +**EPIC:** IAI-008 +**Seccion:** Requerimientos Funcionales + +--- + +## Documentos + +| ID | Archivo | Titulo | Prioridad | Estado | +|----|---------|--------|-----------|--------| +| RF-ML-001 | [RF-ML-001.md](./RF-ML-001.md) | AVM - Valuacion automatica | Alta | Draft | +| RF-ML-002 | [RF-ML-002.md](./RF-ML-002.md) | Prediccion tiempo de venta | Alta | Draft | +| RF-ML-003 | [RF-ML-003.md](./RF-ML-003.md) | Deteccion oportunidades | Alta | Draft | +| RF-ML-004 | [RF-ML-004.md](./RF-ML-004.md) | Zonas emergentes | Media | Draft | + +--- + +## Navegacion + +- **Arriba:** [IAI-008-ml-analytics/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/tareas/_MAP.md b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/tareas/_MAP.md new file mode 100644 index 0000000..7b7fa57 --- /dev/null +++ b/docs/01-fase-alcance-inicial/IAI-008-ml-analytics/tareas/_MAP.md @@ -0,0 +1,31 @@ +--- +id: "MAP-IAI-008-TASK" +title: "Mapa Tareas IAI-008" +type: "Navigation Map" +epic: "IAI-008" +section: "tareas" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Tareas Tecnicas - IAI-008 ML Analytics + +**EPIC:** IAI-008 +**Seccion:** Tareas Tecnicas + +--- + +## Documentos + +*Sin tareas tecnicas definidas todavia.* + +--- + +## Navegacion + +- **Arriba:** [IAI-008-ml-analytics/](../_MAP.md) + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/01-fase-alcance-inicial/_MAP.md b/docs/01-fase-alcance-inicial/_MAP.md new file mode 100644 index 0000000..e8a57c9 --- /dev/null +++ b/docs/01-fase-alcance-inicial/_MAP.md @@ -0,0 +1,127 @@ +--- +id: "MAP-01-FASE" +title: "Mapa de Navegacion - Fase Alcance Inicial" +type: "Navigation Map" +phase: "01" +project: "inmobiliaria-analytics" +standard: "GAMILIT" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Fase 1 - Alcance Inicial (MVP) + +**Fase:** 01 - Alcance Inicial +**Proyecto:** Inmobiliaria Analytics +**Total Story Points:** 315 SP +**Estado:** En Desarrollo + +--- + +## EPICs de la Fase + +| EPIC ID | Carpeta | Titulo | SP | RF | US | ET | Estado | +|---------|---------|--------|----|----|----|----|--------| +| IAI-001 | [IAI-001-fundamentos/](.//IAI-001-fundamentos/_MAP.md) | Fundamentos del Sistema | 40 | - | - | - | Planned | +| IAI-002 | [IAI-002-propiedades/](./IAI-002-propiedades/_MAP.md) | Propiedades CRUD | 34 | - | 5 | - | Planned | +| IAI-003 | [IAI-003-usuarios/](./IAI-003-usuarios/_MAP.md) | Usuarios y Perfiles | 26 | - | 4 | - | Planned | +| IAI-004 | [IAI-004-tenants/](./IAI-004-tenants/_MAP.md) | Multi-Tenancy | 40 | - | 5 | - | Planned | +| IAI-005 | [IAI-005-pagos/](./IAI-005-pagos/_MAP.md) | Pagos (Stripe) | 34 | - | 5 | - | Planned | +| IAI-006 | [IAI-006-portales/](./IAI-006-portales/_MAP.md) | Portales Web | 26 | - | 4 | - | Planned | +| IAI-007 | [IAI-007-webscraper/](./IAI-007-webscraper/_MAP.md) | Web Scraping y ETL | 55 | 5 | 5 | 3 | Draft | +| IAI-008 | [IAI-008-ml-analytics/](./IAI-008-ml-analytics/_MAP.md) | ML y Analytics Avanzado | 60 | 4 | 8 | 2 | Draft | + +--- + +## Resumen de la Fase + +| Metrica | Valor | +|---------|-------| +| Total EPICs | 8 | +| Total Story Points | 315 SP | +| Requerimientos (RF) | 9 | +| User Stories (US) | 36+ | +| Especificaciones (ET) | 5 | +| EPICs Completadas | 0 | +| EPICs En Progreso | 0 | +| EPICs Draft/Planned | 8 | + +--- + +## Dependencias entre EPICs + +``` +IAI-001 (Fundamentos) + │ + ├──▶ IAI-002 (Propiedades) + │ │ + │ └──▶ IAI-007 (Webscraper) ──▶ IAI-008 (ML Analytics) + │ + ├──▶ IAI-003 (Usuarios) + │ │ + │ ├──▶ IAI-004 (Tenants) + │ │ │ + │ │ └──▶ IAI-005 (Pagos) + │ │ + │ └──▶ IAI-006 (Portales) + │ + └──▶ IAI-008 (ML Analytics) ◀── IAI-007 (Webscraper) +``` + +--- + +## Estructura por EPIC + +### IAI-001: Fundamentos +- Infraestructura base +- Autenticacion JWT +- Configuracion NestJS + PostgreSQL + +### IAI-002: Propiedades +- CRUD de propiedades +- Busqueda y filtros +- Galeria de imagenes + +### IAI-003: Usuarios +- Gestion de usuarios +- Perfiles y roles +- Permisos RBAC + +### IAI-004: Multi-Tenancy +- Aislamiento por tenant +- RLS PostgreSQL +- Configuracion por tenant + +### IAI-005: Pagos +- Integracion Stripe +- Suscripciones +- Facturacion + +### IAI-006: Portales +- Portal publico +- Portal agentes +- Portal admin + +### IAI-007: Web Scraping +- Scrapers por portal +- Pipeline ETL +- Pool de proxies + +### IAI-008: ML Analytics +- Modelo AVM (valuacion) +- Deteccion oportunidades +- Zonas emergentes + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Anterior:** [00-vision-general/](../00-vision-general/_MAP.md) +- **Siguiente:** [02-fase-robustecimiento/](../02-fase-robustecimiento/_MAP.md) + +--- + +**Generado:** 2026-01-04 +**Sistema:** NEXUS v3.4 + SIMCO + GAMILIT + diff --git a/docs/02-fase-robustecimiento/_MAP.md b/docs/02-fase-robustecimiento/_MAP.md new file mode 100644 index 0000000..d35d133 --- /dev/null +++ b/docs/02-fase-robustecimiento/_MAP.md @@ -0,0 +1,45 @@ +--- +id: "MAP-02-FASE" +title: "Mapa de Fase 2 - Robustecimiento" +type: "Navigation Map" +phase: "02" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Fase 2 - Robustecimiento + +**Fase:** 02 - Robustecimiento +**Estado:** Backlog +**Dependencia:** Fase 1 completada + +--- + +## Descripcion + +Esta fase se enfocara en optimizaciones, mejoras de rendimiento y robustecimiento de las funcionalidades core implementadas en Fase 1. + +--- + +## EPICs Planificadas + +| EPIC | Nombre | Descripcion | Estado | +|------|--------|-------------|--------| +| IAR-001 | Optimizacion DB | Indices, queries, particionamiento | Backlog | +| IAR-002 | Caching | Redis, CDN, estrategias | Backlog | +| IAR-003 | Monitoring | Observabilidad, alertas | Backlog | +| IAR-004 | Testing | E2E, load testing | Backlog | + +--- + +## Criterios de Entrada + +- [ ] Fase 1 completada al 100% +- [ ] MVP en produccion +- [ ] Feedback de usuarios iniciales +- [ ] Metricas de baseline establecidas + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/03-fase-extensiones/_MAP.md b/docs/03-fase-extensiones/_MAP.md new file mode 100644 index 0000000..462f400 --- /dev/null +++ b/docs/03-fase-extensiones/_MAP.md @@ -0,0 +1,45 @@ +--- +id: "MAP-03-FASE" +title: "Mapa de Fase 3 - Extensiones" +type: "Navigation Map" +phase: "03" +project: "inmobiliaria-analytics" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: Fase 3 - Extensiones + +**Fase:** 03 - Extensiones +**Estado:** Backlog +**Dependencia:** Fase 2 completada + +--- + +## Descripcion + +Esta fase agregara funcionalidades extendidas y nuevos modulos basados en feedback del mercado. + +--- + +## EPICs Planificadas + +| EPIC | Nombre | Descripcion | Estado | +|------|--------|-------------|--------| +| IAE-001 | App Mobile | React Native app | Backlog | +| IAE-002 | White Label | Branding completo por tenant | Backlog | +| IAE-003 | API Publica | API para integraciones | Backlog | +| IAE-004 | Notificaciones | Push, email, SMS | Backlog | +| IAE-005 | Reportes Avanzados | PDF, Excel exports | Backlog | + +--- + +## Criterios de Entrada + +- [ ] Fase 2 completada +- [ ] Product-Market Fit validado +- [ ] Roadmap priorizado con usuarios + +--- + +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/04-fase-backlog/DEFINITION-OF-DONE.md b/docs/04-fase-backlog/DEFINITION-OF-DONE.md new file mode 100644 index 0000000..e386433 --- /dev/null +++ b/docs/04-fase-backlog/DEFINITION-OF-DONE.md @@ -0,0 +1,226 @@ +--- +id: "DOD-IA" +title: "Definition of Done - Inmobiliaria Analytics" +type: "Process Document" +version: "1.0.0" +status: "Active" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Definition of Done (DoD) + +## Inmobiliaria Analytics + +--- + +## Proposito + +Define los criterios que debe cumplir un item (User Story, Task, Bug) para ser considerado "terminado" y listo para produccion. + +--- + +## Checklist General + +Un item esta **Done** cuando cumple TODOS los siguientes criterios: + +### Codigo + +- [ ] **Codigo implementado** + - Funcionalidad completa segun especificacion + - Sin codigo comentado o debug + - Sin console.log innecesarios + +- [ ] **Code review aprobado** + - Al menos 1 revisor aprobo + - Comentarios de review atendidos + - Sin conflictos de merge + +- [ ] **Sin warnings de linter** + - ESLint pasa sin errores + - Prettier aplicado + - TypeScript sin errores de tipo + +- [ ] **Commits limpios** + - Mensajes descriptivos + - Formato convencional (feat/fix/docs...) + - Sin commits de WIP + +### Testing + +- [ ] **Tests unitarios** + - Coverage minimo 80% del codigo nuevo + - Todos los tests pasan + - Casos edge cubiertos + +- [ ] **Tests de integracion** + - Endpoints probados + - Flujos criticos cubiertos + +- [ ] **Tests E2E (si aplica)** + - Flujos de usuario probados + - Sin regresiones + +- [ ] **Probado en ambiente de desarrollo** + - Funciona en ambiente local + - Validado con datos de prueba + +### Documentacion + +- [ ] **API documentada (si aplica)** + - Swagger/OpenAPI actualizado + - Ejemplos de request/response + - Codigos de error documentados + +- [ ] **YAML front-matter actualizado** + - status: "Done" + - completed_date: "YYYY-MM-DD" + +- [ ] **Notas de implementacion** + - Decisiones tecnicas documentadas + - Cambios de diseno registrados + +- [ ] **_MAP.md actualizado (si aplica)** + - Nuevos archivos agregados + - Estados actualizados + +### Deploy + +- [ ] **Build exitoso** + - npm run build sin errores + - Sin warnings criticos + +- [ ] **Deploy a staging (si aplica)** + - Despliegue automatico funciona + - Configuracion correcta + +- [ ] **Smoke tests pasados** + - Funcionalidad basica verificada + - Sin errores 500 + +--- + +## Checklist por Tipo + +### User Story + +```markdown +Codigo: +- [ ] Implementacion completa de todos los criterios de aceptacion +- [ ] Code review aprobado +- [ ] Sin deuda tecnica nueva (o documentada) + +Testing: +- [ ] Tests unitarios (>80% coverage nuevo codigo) +- [ ] Tests de integracion para endpoints +- [ ] Casos de error manejados + +Documentacion: +- [ ] API documentada en Swagger +- [ ] US marcada como "Done" con fecha +- [ ] Notas de implementacion agregadas + +Validacion: +- [ ] Demo al PO (si requerido) +- [ ] Todos los CA verificados +``` + +### Task + +```markdown +- [ ] Tarea completada segun descripcion +- [ ] Sin efectos secundarios no documentados +- [ ] Tests relevantes actualizados +- [ ] TASK marcada como "Done" +- [ ] Horas reales registradas +``` + +### Bug + +```markdown +- [ ] Bug corregido y verificado +- [ ] No reproduce con pasos originales +- [ ] Test de regresion agregado +- [ ] Sin efectos secundarios +- [ ] Root cause documentado +- [ ] BUG marcado como "Done" +``` + +--- + +## Proceso de Validacion + +``` +1. Desarrollador completa implementacion +2. Desarrollador verifica checklist DoD +3. Crea Pull Request +4. Revisor valida codigo y tests +5. Si cumple DoD: + a. Merge a branch principal + b. Actualizar status a "Done" + c. Mover en Board.md a "Hecho" +6. Si NO cumple: + a. Comentarios en PR + b. Desarrollador corrige + c. Volver a paso 3 +``` + +--- + +## Excepciones + +Se permiten excepciones documentadas para: + +1. **Hotfixes P0**: Pueden diferir tests a siguiente Sprint +2. **Prototipos/POC**: Menor coverage requerido +3. **Refactors masivos**: Review por Tech Lead + +En estos casos: +- Documentar excepcion en el item +- Crear TASK de seguimiento para completar +- Registrar en deuda tecnica + +--- + +## Metricas + +| Metrica | Objetivo | +|---------|----------| +| Items que cumplen DoD | 100% | +| Coverage promedio | >80% | +| PRs rechazados por DoD | <5% | +| Bugs post-deploy | <2 por sprint | + +--- + +## Niveles de Done + +### Done-Done (Desarrollo) +- Codigo completo y revisado +- Tests pasando +- Documentacion actualizada + +### Done-Done-Done (Staging) +- Desplegado en staging +- Smoke tests pasados +- QA validado + +### Done-Done-Done-Done (Produccion) +- Desplegado en produccion +- Monitoreado por 24h +- Sin incidentes + +--- + +## Referencias + +- [DEFINITION-OF-READY.md](./DEFINITION-OF-READY.md) +- [Board.md](../planning/Board.md) +- [config.yml](../planning/config.yml) + +--- + +**Documento:** Definition of Done +**Version:** 1.0.0 +**Estado:** Active +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/04-fase-backlog/DEFINITION-OF-READY.md b/docs/04-fase-backlog/DEFINITION-OF-READY.md new file mode 100644 index 0000000..ed8eedc --- /dev/null +++ b/docs/04-fase-backlog/DEFINITION-OF-READY.md @@ -0,0 +1,165 @@ +--- +id: "DOR-IA" +title: "Definition of Ready - Inmobiliaria Analytics" +type: "Process Document" +version: "1.0.0" +status: "Active" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Definition of Ready (DoR) + +## Inmobiliaria Analytics + +--- + +## Proposito + +Define los criterios que debe cumplir un item (User Story, Task, Bug) para ser considerado "listo" para trabajar en un Sprint. + +--- + +## Checklist General + +Un item esta **Ready** cuando cumple TODOS los siguientes criterios: + +### Claridad + +- [ ] **Titulo claro y descriptivo** + - El titulo describe la funcionalidad o tarea en pocas palabras + - Cualquier miembro del equipo entiende de que se trata + +- [ ] **Descripcion completa** + - Problema o necesidad documentada + - Contexto de negocio explicado + - Sin ambiguedades en el lenguaje + +- [ ] **Criterios de aceptacion definidos** + - Minimo 3 criterios verificables + - Escritos en formato "Dado/Cuando/Entonces" o lista clara + - Cada criterio es medible y testeable + +### Alcance + +- [ ] **Scope delimitado** + - Limites claros de lo que incluye y NO incluye + - Tamano adecuado para completar en un Sprint + - No mas de 13 Story Points + +- [ ] **Dependencias identificadas** + - Lista de items que deben completarse antes + - APIs o servicios externos requeridos + - Datos o recursos necesarios + +- [ ] **Bloqueantes resueltos** + - No hay impedimentos conocidos + - Accesos y permisos disponibles + - Ambientes listos + +### Estimacion + +- [ ] **Story Points asignados** + - Estimacion del equipo (Planning Poker si aplica) + - Consenso del equipo + +- [ ] **Complejidad evaluada** + - Riesgos tecnicos identificados + - Incertidumbre documentada + +### Documentacion + +- [ ] **YAML front-matter completo** + - id, title, status, epic, story_points (para US) + - priority, assignee (si aplica) + - created_date + +- [ ] **Referencias vinculadas** + - RF asociado existe (para US) + - ET existe si aplica (para implementacion tecnica) + - Mockups/wireframes disponibles (si hay UI) + +--- + +## Checklist por Tipo + +### User Story + +```markdown +- [ ] Formato "Como [rol], quiero [funcionalidad], para [beneficio]" +- [ ] EPIC asignada +- [ ] Criterios de aceptacion (minimo 3) +- [ ] Story Points estimados +- [ ] Dependencias identificadas +- [ ] RF asociado +``` + +### Task + +```markdown +- [ ] Descripcion tecnica clara +- [ ] Horas estimadas +- [ ] Prioridad asignada (P0-P3) +- [ ] US padre identificada (si aplica) +- [ ] Subtareas desglosadas (si >4 horas) +``` + +### Bug + +```markdown +- [ ] Pasos para reproducir (minimo 3 pasos) +- [ ] Comportamiento esperado +- [ ] Comportamiento actual +- [ ] Severidad asignada (P0-P3) +- [ ] Modulo afectado identificado +- [ ] Screenshots/logs adjuntos +``` + +--- + +## Proceso de Validacion + +``` +1. Autor crea el item con todos los campos +2. Autor completa checklist de DoR +3. PO/Tech Lead revisa el item +4. Si cumple DoR → Puede entrar al Sprint +5. Si NO cumple → Regresa a Backlog con feedback +``` + +--- + +## Excepciones + +Solo se permiten excepciones para: + +1. **Hotfixes P0**: Items criticos que afectan produccion +2. **Spikes**: Investigaciones tecnicas time-boxed +3. **Deuda tecnica critica**: Aprobada por Tech Lead + +En estos casos, documentar razon de excepcion en el item. + +--- + +## Metricas + +| Metrica | Objetivo | +|---------|----------| +| Items que cumplen DoR | >90% | +| Items rechazados por DoR | <10% | +| Tiempo promedio para cumplir DoR | <2 dias | + +--- + +## Referencias + +- [DEFINITION-OF-DONE.md](./DEFINITION-OF-DONE.md) +- [Board.md](../planning/Board.md) +- [config.yml](../planning/config.yml) + +--- + +**Documento:** Definition of Ready +**Version:** 1.0.0 +**Estado:** Active +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/04-fase-backlog/_MAP.md b/docs/04-fase-backlog/_MAP.md new file mode 100644 index 0000000..61f4f97 --- /dev/null +++ b/docs/04-fase-backlog/_MAP.md @@ -0,0 +1,50 @@ +--- +id: "MAP-04-BACKLOG" +title: "Mapa de Navegacion - Fase Backlog" +type: "Navigation Map" +section: "04-fase-backlog" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: 04-fase-backlog + +**Seccion:** Fase Backlog +**Proyecto:** Inmobiliaria Analytics +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +Documentos de proceso y definiciones para el manejo del backlog, incluyendo Definition of Ready (DoR) y Definition of Done (DoD). + +--- + +## Contenido + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| [DEFINITION-OF-READY.md](./DEFINITION-OF-READY.md) | Definition of Ready | Active | +| [DEFINITION-OF-DONE.md](./DEFINITION-OF-DONE.md) | Definition of Done | Active | + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Anterior:** [01-fase-alcance-inicial/](../01-fase-alcance-inicial/_MAP.md) +- **Siguiente:** [90-transversal/](../90-transversal/_MAP.md) + +--- + +## Estadisticas + +| Metrica | Valor | +|---------|-------| +| Total documentos | 2 | +| Documentos activos | 2 | + +--- + +**Generado:** 2026-01-04 diff --git a/docs/90-transversal/_MAP.md b/docs/90-transversal/_MAP.md new file mode 100644 index 0000000..893471d --- /dev/null +++ b/docs/90-transversal/_MAP.md @@ -0,0 +1,62 @@ +--- +id: "MAP-90-TRANSVERSAL" +title: "Mapa de Navegacion - Transversal" +type: "Navigation Map" +section: "90-transversal" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: 90-transversal + +**Seccion:** Documentacion Transversal +**Proyecto:** Inmobiliaria Analytics +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +Documentacion transversal que aplica a todo el proyecto: arquitectura, inventarios, roadmap, deuda tecnica. + +--- + +## Estructura + +``` +90-transversal/ +├── _MAP.md # Este archivo +├── arquitectura/ # Diagramas y decisiones +└── inventarios/ # Inventarios de componentes +``` + +--- + +## Contenido + +### Arquitectura + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| Pendiente | Diagramas C4 | - | +| Pendiente | Diagrama de secuencia | - | + +### Inventarios + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| Pendiente | DATABASE_INVENTORY.yml | - | +| Pendiente | BACKEND_INVENTORY.yml | - | +| Pendiente | FRONTEND_INVENTORY.yml | - | + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Anterior:** [04-fase-backlog/](../04-fase-backlog/_MAP.md) +- **Siguiente:** [95-guias-desarrollo/](../95-guias-desarrollo/_MAP.md) + +--- + +**Generado:** 2026-01-04 diff --git a/docs/95-guias-desarrollo/_MAP.md b/docs/95-guias-desarrollo/_MAP.md new file mode 100644 index 0000000..00d7d02 --- /dev/null +++ b/docs/95-guias-desarrollo/_MAP.md @@ -0,0 +1,55 @@ +--- +id: "MAP-95-GUIAS" +title: "Mapa de Navegacion - Guias de Desarrollo" +type: "Navigation Map" +section: "95-guias-desarrollo" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: 95-guias-desarrollo + +**Seccion:** Guias de Desarrollo +**Proyecto:** Inmobiliaria Analytics +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +Guias tecnicas para desarrolladores del proyecto. + +--- + +## Estructura + +``` +95-guias-desarrollo/ +├── _MAP.md # Este archivo +├── backend/ # Guias backend NestJS +├── frontend/ # Guias frontend React +└── testing/ # Guias de testing +``` + +--- + +## Contenido + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| Pendiente | SETUP-DESARROLLO.md | - | +| Pendiente | BACKEND-GUIDE.md | - | +| Pendiente | FRONTEND-GUIDE.md | - | +| Pendiente | TESTING-GUIDE.md | - | + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Anterior:** [90-transversal/](../90-transversal/_MAP.md) +- **Siguiente:** [96-quick-reference/](../96-quick-reference/_MAP.md) + +--- + +**Generado:** 2026-01-04 diff --git a/docs/96-quick-reference/_MAP.md b/docs/96-quick-reference/_MAP.md new file mode 100644 index 0000000..86898f0 --- /dev/null +++ b/docs/96-quick-reference/_MAP.md @@ -0,0 +1,42 @@ +--- +id: "MAP-96-QUICKREF" +title: "Mapa de Navegacion - Quick Reference" +type: "Navigation Map" +section: "96-quick-reference" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: 96-quick-reference + +**Seccion:** Quick Reference / Cheatsheets +**Proyecto:** Inmobiliaria Analytics +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +Cheatsheets y referencias rapidas para consulta de desarrolladores. + +--- + +## Contenido + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| Pendiente | API-CHEATSHEET.md | - | +| Pendiente | DB-CHEATSHEET.md | - | +| Pendiente | GIT-CHEATSHEET.md | - | + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Anterior:** [95-guias-desarrollo/](../95-guias-desarrollo/_MAP.md) +- **Siguiente:** [97-adr/](../97-adr/_MAP.md) + +--- + +**Generado:** 2026-01-04 diff --git a/docs/97-adr/ADR-001-stack-tecnologico.md b/docs/97-adr/ADR-001-stack-tecnologico.md new file mode 100644 index 0000000..680f128 --- /dev/null +++ b/docs/97-adr/ADR-001-stack-tecnologico.md @@ -0,0 +1,208 @@ +--- +id: "ADR-001" +title: "Seleccion de Stack Tecnologico" +type: "Architecture Decision Record" +status: "Accepted" +date: "2026-01-04" +deciders: ["Tech Lead", "Backend Team", "Frontend Team"] +tags: ["stack", "nestjs", "react", "postgresql"] +--- + +# ADR-001: Seleccion de Stack Tecnologico + +**Status:** Accepted +**Date:** 2026-01-04 +**Deciders:** Tech Lead, Backend Team, Frontend Team +**Tags:** stack, nestjs, react, postgresql + +--- + +## Context + +El proyecto Inmobiliaria Analytics requiere un stack tecnologico que soporte: + +1. **Alto rendimiento en analytics**: Procesamiento eficiente de grandes volumenes de datos inmobiliarios +2. **Escalabilidad horizontal**: Crecimiento de usuarios y datos sin rediseno +3. **Time-to-market rapido**: MVP en Q1 2026 +4. **Mantenibilidad a largo plazo**: Codigo limpio y documentado +5. **Ecosistema maduro**: Librerias y comunidad activa + +### Restricciones + +- Equipo con experiencia en TypeScript/JavaScript +- Infraestructura existente basada en Docker +- Base de datos relacional requerida para transacciones +- Compatibilidad con sistema NEXUS existente + +--- + +## Decision + +Seleccionamos el siguiente stack tecnologico: + +### Backend: NestJS 10.x + TypeORM + +**Razon:** Framework TypeScript-first con arquitectura modular que facilita el desarrollo de APIs escalables. TypeORM proporciona un ORM robusto con soporte para migraciones. + +### Frontend: React 18.x + TypeScript + Vite + +**Razon:** Ecosistema maduro con excelente soporte para aplicaciones de datos. Vite proporciona DX rapida. TypeScript asegura type-safety end-to-end. + +### Base de Datos: PostgreSQL 16 + Redis 7 + +**Razon:** PostgreSQL ofrece: +- Extensiones geograficas (PostGIS) para datos inmobiliarios +- JSONB para datos semi-estructurados +- Excelente rendimiento en queries analiticas +- ACID compliance + +Redis para: +- Caching de queries frecuentes +- Sesiones de usuario +- Rate limiting + +### Autenticacion: Passport + JWT + +**Razon:** Estrategia stateless que facilita escalabilidad horizontal. JWT permite validacion sin consulta a BD. + +--- + +## Alternatives Considered + +### Alternative 1: Python + FastAPI + SQLAlchemy + +**Pros:** +- Excelente para ML/Analytics +- Librerias de data science maduras +- FastAPI es muy rapido + +**Cons:** +- Equipo tiene menos experiencia +- Dos stacks de lenguaje (Python + JS/TS para frontend) +- Menor integracion con ecosistema existente + +**Decision:** Rechazada - curva de aprendizaje y fragmentacion de stack + +### Alternative 2: Next.js Full-Stack + +**Pros:** +- Un solo framework +- SSR built-in +- Vercel deployment simple + +**Cons:** +- Menos flexible para APIs complejas +- Acoplamiento frontend-backend +- Escalabilidad de API limitada + +**Decision:** Rechazada - requerimos separacion clara para escalabilidad independiente + +### Alternative 3: Go + Fiber + React + +**Pros:** +- Alto rendimiento +- Compilacion a binarios +- Excelente para microservicios + +**Cons:** +- Cambio significativo de paradigma +- Menos ecosistema para web apps +- Curva de aprendizaje alta + +**Decision:** Rechazada - time-to-market afectado significativamente + +### Alternative 4: MongoDB en lugar de PostgreSQL + +**Pros:** +- Flexibilidad de schema +- JSON nativo +- Facil escalabilidad horizontal + +**Cons:** +- Sin transacciones ACID robustas (hasta recientemente) +- Datos inmobiliarios son relacionales por naturaleza +- PostGIS superior para geo-datos + +**Decision:** Rechazada - PostgreSQL mejor para dominio inmobiliario + +--- + +## Consequences + +### Positivas + +1. **Consistencia de lenguaje**: TypeScript end-to-end +2. **Reutilizacion**: DTOs y tipos compartidos +3. **Productividad**: Equipo productivo desde dia 1 +4. **Escalabilidad**: Cada componente escala independientemente +5. **Comunidad**: Amplio soporte y recursos + +### Negativas + +1. **Node.js single-threaded**: Puede ser limitante para CPU-intensive + - Mitigacion: Worker threads o microservicio Python para ML +2. **ORM overhead**: TypeORM puede ser lento para queries complejas + - Mitigacion: Raw queries para analytics criticos +3. **Bundle size React**: Apps grandes pueden ser pesadas + - Mitigacion: Code splitting, lazy loading + +### Neutrales + +1. Requiere setup inicial de TypeORM migrations +2. Configuracion de Swagger para documentacion API +3. Setup de testing con Jest + +--- + +## Implementation Notes + +### Dependencias Clave + +```json +{ + "backend": { + "@nestjs/core": "^10.3.0", + "@nestjs/typeorm": "^10.0.1", + "typeorm": "^0.3.19", + "pg": "^8.11.3" + }, + "frontend": { + "react": "^18.x", + "typescript": "^5.3.x", + "vite": "^5.x" + } +} +``` + +### Configuracion Inicial + +1. Monorepo con apps separadas +2. Docker Compose para desarrollo local +3. GitHub Actions para CI/CD +4. PostgreSQL con schemas por dominio + +--- + +## References + +- [NestJS Documentation](https://docs.nestjs.com/) +- [TypeORM Documentation](https://typeorm.io/) +- [React Documentation](https://react.dev/) +- [PostgreSQL Documentation](https://www.postgresql.org/docs/) +- [STACK-TECNOLOGICO.md](../00-vision-general/STACK-TECNOLOGICO.md) + +--- + +## Status History + +| Date | Status | Notes | +|------|--------|-------| +| 2026-01-04 | Proposed | ADR creado | +| 2026-01-04 | Accepted | Aprobado por equipo tecnico | + +--- + +**Document:** ADR-001 +**Status:** Accepted +**Date Created:** 2026-01-04 +**Supersedes:** N/A diff --git a/docs/97-adr/README.md b/docs/97-adr/README.md new file mode 100644 index 0000000..8ed097d --- /dev/null +++ b/docs/97-adr/README.md @@ -0,0 +1,91 @@ +--- +id: "ADR-INDEX" +title: "Indice de ADRs - Inmobiliaria Analytics" +type: "Index Document" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Architecture Decision Records (ADR) + +## Inmobiliaria Analytics + +--- + +## Proposito + +Este directorio contiene las decisiones arquitectonicas importantes tomadas para el proyecto Inmobiliaria Analytics. Cada ADR documenta el contexto, la decision tomada, las alternativas consideradas y las consecuencias. + +--- + +## Indice de ADRs + +| ID | Titulo | Estado | Fecha | +|----|--------|--------|-------| +| [ADR-001](./ADR-001-stack-tecnologico.md) | Seleccion de Stack Tecnologico | Accepted | 2026-01-04 | + +--- + +## Estados de ADR + +| Estado | Descripcion | +|--------|-------------| +| **Proposed** | En discusion | +| **Accepted** | Aprobado e implementado | +| **Deprecated** | Ya no aplica pero historico | +| **Superseded** | Reemplazado por otro ADR | + +--- + +## Como Crear un ADR + +1. Copiar plantilla de ADR existente +2. Asignar siguiente numero secuencial (ADR-XXX) +3. Completar todas las secciones +4. Discutir con stakeholders +5. Actualizar status a Accepted cuando aprobado +6. Agregar a este indice + +### Plantilla Basica + +```markdown +--- +id: "ADR-XXX" +title: "Titulo de la Decision" +type: "Architecture Decision Record" +status: "Proposed" +date: "YYYY-MM-DD" +deciders: ["Stakeholder 1", "Stakeholder 2"] +tags: ["tag1", "tag2"] +--- + +# ADR-XXX: Titulo + +## Context +{Contexto y problema} + +## Decision +{La decision tomada} + +## Alternatives Considered +{Alternativas evaluadas} + +## Consequences +{Consecuencias positivas y negativas} + +## References +{Referencias relevantes} +``` + +--- + +## Referencias + +- [Arquitectura General](../00-vision-general/ARQUITECTURA-GENERAL.md) +- [Stack Tecnologico](../00-vision-general/STACK-TECNOLOGICO.md) +- [Michael Nygard - Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) + +--- + +**Ultima actualizacion:** 2026-01-04 +**Total ADRs:** 1 diff --git a/docs/97-adr/_MAP.md b/docs/97-adr/_MAP.md new file mode 100644 index 0000000..6c03b3d --- /dev/null +++ b/docs/97-adr/_MAP.md @@ -0,0 +1,59 @@ +--- +id: "MAP-97-ADR" +title: "Mapa de Navegacion - ADRs" +type: "Navigation Map" +section: "97-adr" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: 97-adr + +**Seccion:** Architecture Decision Records +**Proyecto:** Inmobiliaria Analytics +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +Registro de decisiones arquitectonicas importantes del proyecto. + +--- + +## Contenido + +| Archivo | Titulo | Estado | +|---------|--------|--------| +| [README.md](./README.md) | Indice de ADRs | Active | +| [ADR-001-stack-tecnologico.md](./ADR-001-stack-tecnologico.md) | Stack Tecnologico | Accepted | + +--- + +## Resumen de ADRs + +| ID | Decision | Estado | +|----|----------|--------| +| ADR-001 | NestJS + React + PostgreSQL | Accepted | + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Anterior:** [96-quick-reference/](../96-quick-reference/_MAP.md) + +--- + +## Estadisticas + +| Metrica | Valor | +|---------|-------| +| Total ADRs | 1 | +| Accepted | 1 | +| Proposed | 0 | +| Deprecated | 0 | + +--- + +**Generado:** 2026-01-04 diff --git a/docs/99-analisis/ANALISIS-REESTRUCTURACION-2026-01-04.md b/docs/99-analisis/ANALISIS-REESTRUCTURACION-2026-01-04.md new file mode 100644 index 0000000..aed6977 --- /dev/null +++ b/docs/99-analisis/ANALISIS-REESTRUCTURACION-2026-01-04.md @@ -0,0 +1,413 @@ +--- +id: "ANALISIS-REESTRUCTURACION" +title: "Analisis de Reestructuracion de Documentacion" +type: "Analysis Document" +status: "Active" +project: "inmobiliaria-analytics" +reference: "gamilit" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Analisis de Reestructuracion de Documentacion + +## Resumen Ejecutivo + +Este documento analiza la estructura actual de documentacion del proyecto inmobiliaria-analytics y propone una reestructuracion basada en los estandares del proyecto gamilit (NEXUS v3.4 + SIMCO). + +--- + +## 1. PROBLEMAS IDENTIFICADOS + +### 1.1 Conflictos de Numeracion de EPICs + +**Estado Actual:** +``` +IDs utilizados: IA-001, IA-004, IA-005, IA-006, IA-007, IA-008 +IDs faltantes: IA-002, IA-003 +``` + +**Problema:** Hay saltos en la numeracion (IA-002 y IA-003 no existen), violando la secuencia continua que se espera en un sistema de documentacion estructurado. + +**Impacto:** Confusion sobre si existen modulos no documentados o si se eliminaron. + +### 1.2 Nomenclatura de IDs Inconsistente + +**Gamilit usa:** +- `EAI-NNN` = Epic Alcance Inicial +- `EXT-NNN` = Extension +- `EMR-NNN` = Migracion + +**Inmobiliaria usa:** +- `IA-NNN` = Generico (sin indicar fase) + +**Problema:** No es posible distinguir a que fase pertenece un EPIC solo por su ID. + +### 1.3 Prefijos de User Stories No Estandarizados + +**Gamilit usa:** +``` +US-[CODIGO_EPIC]-NNN-slug.md +Ejemplo: US-FUND-001-autenticacion-basica-jwt.md +``` + +**Inmobiliaria usa:** +``` +IA-007: US-SCR-NNN (SCR = Scraper) +IA-008: US-ML-NNN (ML = Machine Learning) +``` + +**Problema:** Los prefijos no siguen el patron `US-[EPIC]-NNN` y usan mnemonicos arbitrarios. + +### 1.4 Estructura de Carpetas Incompleta + +**Carpetas vacias sin proposito claro:** +``` +/03-requerimientos/ (vacio - deberia eliminarse) +/05-user-stories/ (vacio - deberia eliminarse) +/06-test-plans/ (vacio - deberia eliminarse) +/07-devops/ (vacio - deberia eliminarse) +/01-analisis-referencias/ (vacio - deberia eliminarse) +``` + +**Carpetas faltantes en EPICs (segun gamilit):** +``` +/IA-007-webscraper/tareas/ (falta) +/IA-008-ml-analytics/tareas/ (falta) +``` + +### 1.5 Modulos vs EPICs Mezclados + +**Problema:** En `/02-definicion-modulos/` hay definiciones de alto nivel que deberian ser EPICs completas en `/01-fase-alcance-inicial/`. + +**Estado actual:** +- IA-004-TENANTS (solo definicion, no tiene EPIC estructurada) +- IA-005-PAYMENTS (solo definicion, no tiene EPIC estructurada) +- IA-006-PORTALS (solo definicion, no tiene EPIC estructurada) + +--- + +## 2. PROPUESTA DE NUEVA ESTRUCTURA DE IDs + +### 2.1 Convencion de Nomenclatura Propuesta + +**Formato de EPICs:** +``` +IAI-NNN = Inmobiliaria Analytics - Alcance Inicial +IAE-NNN = Inmobiliaria Analytics - Extension +IAM-NNN = Inmobiliaria Analytics - Migracion +``` + +### 2.2 Nueva Numeracion de EPICs + +| ID Actual | ID Propuesto | Nombre | Fase | +|-----------|--------------|--------|------| +| IA-001 | IAI-001 | Fundamentos | Alcance Inicial | +| *(nuevo)* | IAI-002 | Propiedades (CRUD base) | Alcance Inicial | +| *(nuevo)* | IAI-003 | Usuarios y Perfiles | Alcance Inicial | +| IA-004 | IAI-004 | Multi-Tenancy | Alcance Inicial | +| IA-005 | IAI-005 | Pagos (Stripe) | Alcance Inicial | +| IA-006 | IAI-006 | Portales | Alcance Inicial | +| IA-007 | IAI-007 | Web Scraping y ETL | Alcance Inicial | +| IA-008 | IAI-008 | ML Analytics | Alcance Inicial | + +### 2.3 Nueva Nomenclatura de User Stories + +**Formato propuesto:** +``` +US-[MODULO]-NNN-slug.md +``` + +**Mapeo de modulos:** +| EPIC | Codigo Modulo | Ejemplo | +|------|---------------|---------| +| IAI-001 | FUND | US-FUND-001-autenticacion.md | +| IAI-002 | PROP | US-PROP-001-crud-propiedades.md | +| IAI-003 | USER | US-USER-001-perfiles.md | +| IAI-004 | TENT | US-TENT-001-tenant-onboarding.md | +| IAI-005 | PAY | US-PAY-001-integracion-stripe.md | +| IAI-006 | PORT | US-PORT-001-portal-publico.md | +| IAI-007 | SCR | US-SCR-001-scraping-inmuebles24.md | +| IAI-008 | ML | US-ML-001-valuacion-avm.md | + +**Nota:** Los prefijos actuales US-SCR-* y US-ML-* YA siguen esta convencion correctamente. + +### 2.4 Nueva Nomenclatura de Requerimientos + +**Formato propuesto:** +``` +RF-[MODULO]-NNN-descripcion.md +``` + +**Mapeo actual a nuevo:** +| ID Actual | ID Propuesto | +|-----------|--------------| +| RF-IA-007-001 | RF-SCR-001 | +| RF-IA-007-002 | RF-SCR-002 | +| RF-IA-008-001 | RF-ML-001 | + +### 2.5 Nueva Nomenclatura de Especificaciones + +**Formato propuesto:** +``` +ET-[MODULO]-NNN-descripcion.md +``` + +**Mapeo actual a nuevo:** +| ID Actual | ID Propuesto | +|-----------|--------------| +| ET-IA-007-scraper | ET-SCR-001-scraper.md | +| ET-IA-007-etl | ET-SCR-002-etl.md | +| ET-IA-007-proxies | ET-SCR-003-proxies.md | +| ET-IA-008-avm | ET-ML-001-avm.md | +| ET-IA-008-opportunities | ET-ML-002-opportunities.md | + +--- + +## 3. PROPUESTA DE ESTRUCTURA DE CARPETAS + +### 3.1 Estructura Raiz Propuesta + +``` +docs/ +├── 00-vision-general/ # [MANTENER] +│ ├── _MAP.md +│ ├── VISION-PRODUCTO.md +│ ├── ARQUITECTURA-GENERAL.md +│ ├── STACK-TECNOLOGICO.md +│ └── GLOSARIO.md # [AGREGAR] +│ +├── 01-fase-alcance-inicial/ # [REESTRUCTURAR] +│ ├── _MAP.md +│ ├── IAI-001-fundamentos/ +│ ├── IAI-002-propiedades/ # [AGREGAR] +│ ├── IAI-003-usuarios/ # [AGREGAR] +│ ├── IAI-004-tenants/ # [AGREGAR desde 02-definicion] +│ ├── IAI-005-pagos/ # [AGREGAR desde 02-definicion] +│ ├── IAI-006-portales/ # [AGREGAR desde 02-definicion] +│ ├── IAI-007-webscraper/ # [RENOMBRAR desde IA-007] +│ └── IAI-008-ml-analytics/ # [RENOMBRAR desde IA-008] +│ +├── 02-fase-robustecimiento/ # [AGREGAR - futuro] +│ └── _MAP.md +│ +├── 03-fase-extensiones/ # [AGREGAR - futuro] +│ └── _MAP.md +│ +├── 04-fase-backlog/ # [MANTENER] +│ ├── _MAP.md +│ ├── DEFINITION-OF-READY.md +│ └── DEFINITION-OF-DONE.md +│ +├── 90-transversal/ # [EXPANDIR] +│ ├── _MAP.md +│ ├── api/ +│ ├── arquitectura/ +│ ├── inventarios/ +│ └── roadmap/ +│ +├── 95-guias-desarrollo/ # [EXPANDIR] +│ ├── _MAP.md +│ ├── backend/ +│ ├── frontend/ +│ └── testing/ +│ +├── 96-quick-reference/ # [MANTENER] +│ └── _MAP.md +│ +├── 97-adr/ # [MANTENER] +│ ├── _MAP.md +│ ├── README.md +│ └── ADR-NNN-*.md +│ +├── 99-analisis/ # [MANTENER] +│ └── *.md +│ +├── _MAP.md # [MANTENER] +└── README.md # [MANTENER] +``` + +### 3.2 Estructura de Cada EPIC + +``` +IAI-NNN-nombre/ +├── README.md # Vision del EPIC +├── _MAP.md # Indice de navegacion +│ +├── requerimientos/ # Requerimientos Funcionales +│ ├── _MAP.md +│ └── RF-[MOD]-NNN-*.md +│ +├── especificaciones/ # Especificaciones Tecnicas +│ ├── _MAP.md +│ └── ET-[MOD]-NNN-*.md +│ +├── historias-usuario/ # User Stories +│ ├── _MAP.md +│ └── US-[MOD]-NNN-*.md +│ +├── tareas/ # Tareas Tecnicas [AGREGAR] +│ ├── _MAP.md +│ └── TASK-[AREA]-[MOD]-NNN-*.md +│ +└── implementacion/ # Docs de implementacion + ├── _MAP.md + ├── CHANGELOG.md + ├── TRACEABILITY.yml + ├── BACKEND.yml + ├── DATABASE.yml + └── FRONTEND.yml +``` + +--- + +## 4. CARPETAS A ELIMINAR + +Las siguientes carpetas vacias deben eliminarse: + +``` +/03-requerimientos/ # Vacia - requerimientos estan en EPICs +/05-user-stories/ # Vacia - US estan en EPICs +/06-test-plans/ # Vacia - mover a 90-transversal/testing/ +/07-devops/ # Vacia - mover a 90-transversal/devops/ +/01-analisis-referencias/ # Vacia - mover contenido a 99-analisis/ +/04-modelado/ # Vacia - mover a 90-transversal/arquitectura/ +/02-definicion-modulos/ # Contenido mover a EPICs respectivas +``` + +--- + +## 5. PLAN DE MIGRACION + +### Fase 1: Preparacion (1h) +1. Crear backup de documentacion actual +2. Crear nuevas carpetas de estructura +3. Crear _MAP.md para carpetas nuevas + +### Fase 2: Renombrado de EPICs (2h) +1. Renombrar IA-007 -> IAI-007 +2. Renombrar IA-008 -> IAI-008 +3. Actualizar todos los IDs en frontmatter +4. Actualizar referencias cruzadas + +### Fase 3: Migracion de Modulos (2h) +1. Mover IA-004-TENANTS a IAI-004-tenants/ +2. Mover IA-005-PAYMENTS a IAI-005-pagos/ +3. Mover IA-006-PORTALS a IAI-006-portales/ +4. Crear estructura EPIC completa para cada uno + +### Fase 4: Limpieza (1h) +1. Eliminar carpetas vacias +2. Eliminar /02-definicion-modulos/ (ya migrado) +3. Actualizar _MAP.md raiz +4. Validar enlaces + +### Fase 5: Documentacion Faltante (2h) +1. Crear IAI-002-propiedades/ (estructura basica) +2. Crear IAI-003-usuarios/ (estructura basica) +3. Agregar tareas/ a EPICs existentes +4. Agregar GLOSARIO.md + +--- + +## 6. COMPARACION: ANTES vs DESPUES + +### IDs de EPICs + +| Antes | Despues | Cambio | +|-------|---------|--------| +| IA-001 | IAI-001 | Prefijo | +| - | IAI-002 | Nuevo | +| - | IAI-003 | Nuevo | +| IA-004 | IAI-004 | Prefijo + Estructura | +| IA-005 | IAI-005 | Prefijo + Estructura | +| IA-006 | IAI-006 | Prefijo + Estructura | +| IA-007 | IAI-007 | Prefijo | +| IA-008 | IAI-008 | Prefijo | + +### IDs de Requerimientos + +| Antes | Despues | +|-------|---------| +| RF-IA-007-001 | RF-SCR-001 | +| RF-IA-007-002 | RF-SCR-002 | +| RF-IA-007-003 | RF-SCR-003 | +| RF-IA-007-004 | RF-SCR-004 | +| RF-IA-007-005 | RF-SCR-005 | +| RF-IA-008-001 | RF-ML-001 | +| RF-IA-008-002 | RF-ML-002 | +| RF-IA-008-003 | RF-ML-003 | +| RF-IA-008-004 | RF-ML-004 | + +### IDs de User Stories + +| Antes | Despues | Notas | +|-------|---------|-------| +| US-SCR-001 | US-SCR-001 | Sin cambio | +| US-SCR-002 | US-SCR-002 | Sin cambio | +| US-SCR-003 | US-SCR-003 | Sin cambio | +| US-SCR-004 | US-SCR-004 | Sin cambio | +| US-SCR-005 | US-SCR-005 | Sin cambio | +| US-ML-001 | US-ML-001 | Sin cambio | +| US-ML-002 | US-ML-002 | Sin cambio | +| US-ML-003 | US-ML-003 | Sin cambio | +| US-ML-004 | US-ML-004 | Sin cambio | +| US-ML-005 | US-ML-005 | Sin cambio | +| US-ML-006 | US-ML-006 | Sin cambio | +| US-ML-007 | US-ML-007 | Sin cambio | +| US-ML-008 | US-ML-008 | Sin cambio | + +### IDs de Especificaciones + +| Antes | Despues | +|-------|---------| +| ET-IA-007-scraper | ET-SCR-001-scraper | +| ET-IA-007-etl | ET-SCR-002-etl | +| ET-IA-007-proxies | ET-SCR-003-proxies | +| ET-IA-008-avm | ET-ML-001-avm | +| ET-IA-008-opportunities | ET-ML-002-opportunities | + +--- + +## 7. CHECKLIST DE VALIDACION + +### Post-Migracion + +- [ ] Todos los EPICs tienen ID unico secuencial (IAI-001 a IAI-008) +- [ ] Todos los RF siguen formato RF-[MOD]-NNN +- [ ] Todos los US siguen formato US-[MOD]-NNN +- [ ] Todos los ET siguen formato ET-[MOD]-NNN +- [ ] Cada EPIC tiene: README, _MAP, requerimientos/, especificaciones/, historias-usuario/, tareas/, implementacion/ +- [ ] No hay carpetas vacias sin proposito +- [ ] Todos los _MAP.md estan actualizados +- [ ] Referencias cruzadas funcionan + +--- + +## 8. DECISION REQUERIDA + +**Opcion A: Migracion Completa** +- Renombrar todos los IDs +- Reestructurar completamente +- ~8 horas de trabajo +- Consistencia total con gamilit + +**Opcion B: Migracion Parcial** +- Mantener IDs actuales (IA-*) +- Solo agregar estructura faltante +- ~4 horas de trabajo +- Consistencia parcial + +**Opcion C: Solo Documentar** +- No hacer cambios estructurales +- Documentar convenciones actuales +- ~1 hora de trabajo +- Sin migracion + +**Recomendacion:** Opcion A para proyectos nuevos, Opcion B si ya hay codigo que referencia IDs actuales. + +--- + +**Autor:** Sistema de Orquestacion +**Fecha:** 2026-01-04 +**Version:** 1.0 diff --git a/docs/99-analisis/ANALISIS-SAAS-MULTITENANCY.md b/docs/99-analisis/ANALISIS-SAAS-MULTITENANCY.md new file mode 100644 index 0000000..67c3f38 --- /dev/null +++ b/docs/99-analisis/ANALISIS-SAAS-MULTITENANCY.md @@ -0,0 +1,194 @@ +--- +id: "ANALISIS-SAAS-IA" +title: "Analisis SaaS - Multi-tenancy y Pagos" +type: "Analysis" +status: "Draft" +project: "inmobiliaria-analytics" +version: "1.0.0" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Analisis SaaS: Multi-tenancy y Pagos + +**Fecha:** 2026-01-04 +**Proyecto:** Inmobiliaria Analytics + +--- + +## 1. Estado Actual + +### 1.1 Estructura de Documentacion + +| Carpeta | Estado | Contenido | +|---------|--------|-----------| +| 00-vision-general | Existe | VISION-PRODUCTO, ARQUITECTURA, STACK | +| 01-fase-alcance-inicial | Existe | IA-001-fundamentos (40 SP) | +| 02-definicion-modulos | VACIA | Debe poblarse | +| 04-fase-backlog | Existe | DoR, DoD | +| 97-adr | Existe | ADR-001 stack | + +### 1.2 EPICs Planificados + +| ID | Nombre | Estado | +|----|--------|--------| +| IA-001 | Fundamentos | Draft (40 SP) | +| IA-002 | Propiedades | Backlog | +| IA-003 | Analytics | Backlog | + +### 1.3 Referencias SaaS Actuales + +| Elemento | Estado | +|----------|--------| +| Multi-tenancy | No definido | +| Planes de suscripcion | No definido | +| Stripe/Pagos | No existe | +| Portales diferenciados | No existe | + +--- + +## 2. Gaps Identificados + +### 2.1 Modulos Faltantes + +| Modulo | Prioridad | Descripcion | +|--------|-----------|-------------| +| IA-004-TENANTS | Alta | Multi-tenancy con RLS | +| IA-005-PAYMENTS | Alta | Integracion Stripe | +| IA-006-PORTALS | Alta | 3 portales diferenciados | + +### 2.2 Actualizaciones Requeridas + +| Archivo | Cambios | +|---------|---------| +| VISION-PRODUCTO.md | Agregar seccion SaaS y planes | +| STACK-TECNOLOGICO.md | Agregar Stripe SDK | + +--- + +## 3. Planes de Suscripcion Propuestos + +| Plan | Precio | Propiedades | Reportes/mes | Alertas | Usuarios | Soporte | +|------|--------|-------------|--------------|---------|----------|---------| +| Free | $0 | 100 | 5 | 3 | 1 | Comunidad | +| Pro | $49/mes | 5,000 | 50 | 25 | 5 | Email | +| Enterprise | $199/mes | Ilimitado | Ilimitado | Ilimitado | Ilimitado | Dedicado | + +### Productos Stripe + +```yaml +Productos: + ia_pro: + type: subscription + precio: $49/mes + stripe_price_id: TBD + + ia_enterprise: + type: subscription + precio: $199/mes + stripe_price_id: TBD + + ia_properties_500: + type: one_time + precio: $19 + descripcion: "500 propiedades adicionales" +``` + +--- + +## 4. Estructura de 3 Portales + +### Portal 1: Usuario General (Analyst) +- Dashboard de propiedades +- Acceso a analytics segun plan +- Alertas configuradas +- Perfil y configuracion + +### Portal 2: Admin Cliente (Tenant Admin) +- Dashboard de organizacion +- Gestion de usuarios del tenant +- Configuracion y limites +- Facturacion y suscripcion +- Reportes de uso + +### Portal 3: Admin SaaS (Super Admin) +- Dashboard global de todos los tenants +- Gestion de planes y precios +- Monitoreo de sistema +- Soporte y tickets +- Analytics globales + +--- + +## 5. Plan de Ejecucion + +### 5.1 Archivos a Crear + +| Archivo | Tipo | Contenido | +|---------|------|-----------| +| 02-definicion-modulos/_INDEX.md | Index | Indice de modulos | +| IA-004-TENANTS.md | Module | Multi-tenancy con RLS | +| IA-005-PAYMENTS.md | Module | Integracion Stripe | +| IA-006-PORTALS.md | Module | 3 portales | + +### 5.2 Archivos a Modificar + +| Archivo | Cambios | +|---------|---------| +| VISION-PRODUCTO.md | Seccion SaaS | +| STACK-TECNOLOGICO.md | Stripe SDK | + +--- + +## 6. Validacion vs Requisitos + +| Requisito | Estado | Archivo | +|-----------|--------|---------| +| Multi-tenancy | A crear | IA-004-TENANTS | +| Planes de suscripcion | A crear | IA-005-PAYMENTS | +| Integracion Stripe | A crear | IA-005-PAYMENTS | +| Portal Usuario | A crear | IA-006-PORTALS | +| Portal Admin Cliente | A crear | IA-006-PORTALS | +| Portal Admin SaaS | A crear | IA-006-PORTALS | + +--- + +**Estado:** ✅ COMPLETADO + +--- + +## 7. Ejecucion Completada (Fases 5-8) + +### 7.1 Archivos Creados + +| Archivo | Lineas | Contenido | +|---------|--------|-----------| +| 02-definicion-modulos/_INDEX.md | ~45 | Indice de modulos | +| IA-004-TENANTS.md | ~190 | Multi-tenancy con RLS | +| IA-005-PAYMENTS.md | ~250 | Integracion Stripe completa | +| IA-006-PORTALS.md | ~280 | 3 portales diferenciados | + +### 7.2 Archivos Modificados + +| Archivo | Cambios | +|---------|---------| +| VISION-PRODUCTO.md | +85 lineas (Seccion Modelo SaaS) | +| STACK-TECNOLOGICO.md | +1 linea (Stripe SDK) | + +### 7.3 Validacion Final + +| Requisito | Estado | Archivo | +|-----------|--------|---------| +| Multi-tenancy | ✅ | IA-004-TENANTS.md | +| Planes de suscripcion | ✅ | IA-005-PAYMENTS.md | +| Integracion Stripe | ✅ | IA-005-PAYMENTS.md | +| Webhooks Stripe | ✅ | IA-005-PAYMENTS.md | +| Portal Usuario | ✅ | IA-006-PORTALS.md | +| Portal Admin Cliente | ✅ | IA-006-PORTALS.md | +| Portal Admin SaaS | ✅ | IA-006-PORTALS.md | +| YAML front-matter | ✅ | 100% de archivos | + +**Total archivos nuevos:** 4 +**Total archivos modificados:** 2 +**Estado general:** COMPLETADO + diff --git a/docs/README.md b/docs/README.md index aa7d703..84f71dc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,38 +1,170 @@ -# DOCUMENTACIÓN - Inmobiliaria Analytics +--- +id: "DOCS-README" +title: "Documentacion - Inmobiliaria Analytics" +type: "Index Document" +project: "inmobiliaria-analytics" +version: "1.0.0" +standard: "GAMILIT" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Documentacion - Inmobiliaria Analytics **Proyecto:** Inmobiliaria Analytics -**Versión:** 1.0.0 -**Fecha:** 2025-12-05 -**Estado:** Por iniciar +**Version:** 1.0.0 +**Estado:** En desarrollo +**Fecha:** 2026-01-04 +**Estandar:** GAMILIT (Sistema NEXUS v3.4 + SIMCO) --- -## Estructura de Documentación +## Descripcion -``` -docs/ -├── 00-vision-general/ # Visión, objetivos y alcance -├── 01-analisis-referencias/ # Análisis de sistemas de referencia -├── 02-definicion-modulos/ # Lista, índice y dependencias de módulos -├── 03-requerimientos/ # Requerimientos funcionales por módulo -├── 04-modelado/ # Diseño técnico -│ ├── database-design/ # DDL specs, schemas -│ ├── domain-models/ # Modelos de dominio -│ └── especificaciones-tecnicas/ # ET backend/frontend -├── 05-user-stories/ # Historias de usuario -├── 06-test-plans/ # Planes de prueba -├── 07-devops/ # CI/CD, infraestructura -├── 90-transversal/ # Documentos transversales -├── 95-guias-desarrollo/ # Guías para desarrolladores -└── 97-adr/ # Architecture Decision Records -``` +Este directorio contiene toda la documentacion del proyecto **Inmobiliaria Analytics**, una plataforma de analytics para el sector inmobiliario. + +La documentacion sigue el estandar **GAMILIT** basado en: +- **NEXUS v3.4**: Sistema de orquestacion +- **SIMCO**: Sistema Indexado Modular por Contexto +- **SCRUM para IA**: Metodologia adaptada para agentes de IA --- -## Directiva Aplicable +## Inicio Rapido -Ver: `/workspace/core/orchestration/directivas/DIRECTIVA-ESTRUCTURA-DOCUMENTACION-PROYECTOS.md` +### Para Agentes IA + +1. Leer [AGENTS.md](../AGENTS.md) - Guia completa +2. Revisar [Board.md](./planning/Board.md) - Estado actual +3. Consultar [config.yml](./planning/config.yml) - Configuracion + +### Para Desarrolladores + +1. [STACK-TECNOLOGICO.md](./00-vision-general/STACK-TECNOLOGICO.md) - Stack del proyecto +2. [ARQUITECTURA-GENERAL.md](./00-vision-general/ARQUITECTURA-GENERAL.md) - Arquitectura +3. [ADR-001](./97-adr/ADR-001-stack-tecnologico.md) - Decisiones tecnicas + +### Para Nuevos Miembros + +1. [VISION-PRODUCTO.md](./00-vision-general/VISION-PRODUCTO.md) - Vision y objetivos +2. [DEFINITION-OF-READY.md](./04-fase-backlog/DEFINITION-OF-READY.md) - Como preparar trabajo +3. [DEFINITION-OF-DONE.md](./04-fase-backlog/DEFINITION-OF-DONE.md) - Criterios de completado --- -**Última actualización:** 2025-12-05 +## Estructura de Carpetas + +| Carpeta | Descripcion | +|---------|-------------| +| `00-vision-general/` | Vision del producto, arquitectura, stack | +| `01-fase-alcance-inicial/` | EPICs del MVP | +| `04-fase-backlog/` | Backlog, Definition of Ready/Done | +| `90-transversal/` | Documentacion transversal | +| `95-guias-desarrollo/` | Guias para desarrolladores | +| `96-quick-reference/` | Cheatsheets | +| `97-adr/` | Architecture Decision Records | +| `planning/` | Tablero Kanban y sprints | +| `archivados/` | Documentacion deprecada | + +--- + +## Navegacion + +Cada carpeta contiene un archivo `_MAP.md` con: +- Indice de contenidos +- Estado de cada documento +- Links de navegacion + +El mapa principal esta en [_MAP.md](./_MAP.md). + +--- + +## EPICs del Proyecto + +### Fase 1: Alcance Inicial (MVP) + +| EPIC | Nombre | Estado | +|------|--------|--------| +| IA-001 | [Fundamentos](./01-fase-alcance-inicial/IA-001-fundamentos/README.md) | Planned | +| IA-002 | Propiedades | Backlog | +| IA-003 | Analytics | Backlog | + +### Fase 2: Extensiones + +| EPIC | Nombre | Estado | +|------|--------|--------| +| IA-004 | Reportes | Backlog | +| IA-005 | Integraciones | Backlog | + +--- + +## Stack Tecnologico + +| Capa | Tecnologia | +|------|------------| +| Backend | NestJS 10.x, TypeORM, PostgreSQL 16 | +| Frontend | React 18.x, TypeScript, Vite | +| Cache | Redis 7.x | +| Infra | Docker, Traefik | + +Ver detalle en [STACK-TECNOLOGICO.md](./00-vision-general/STACK-TECNOLOGICO.md). + +--- + +## Convenciones + +### Nomenclatura + +| Tipo | Prefijo | Ejemplo | +|------|---------|---------| +| EPIC | IA-NNN | IA-001 | +| Requerimiento | RF-IA-NNN | RF-IA-001 | +| User Story | US-IA-NNN | US-IA-001 | +| Task | TASK-NNN | TASK-001 | +| Bug | BUG-NNN | BUG-001 | +| ADR | ADR-NNN | ADR-001 | + +### YAML Front-Matter + +Todos los documentos incluyen YAML front-matter con: +- `id`: Identificador unico +- `title`: Titulo del documento +- `type`: Tipo de documento +- `status`: Estado actual +- `created_date`: Fecha de creacion +- `updated_date`: Ultima actualizacion + +--- + +## Links Utiles + +| Recurso | Ubicacion | +|---------|-----------| +| Codigo Backend | `../apps/backend/` | +| Codigo Frontend | `../apps/frontend/` | +| Orchestration | `../orchestration/` | +| Puertos | `../.env.ports` | + +--- + +## Contribuir + +1. Crear branch desde `develop` +2. Seguir nomenclatura del proyecto +3. Incluir YAML front-matter en documentos +4. Actualizar _MAP.md correspondiente +5. Crear Pull Request + +--- + +## Soporte + +Para dudas sobre documentacion, consultar: +- [AGENTS.md](../AGENTS.md) - Guia para agentes +- [config.yml](./planning/config.yml) - Configuracion + +--- + +**Directiva Aplicable:** GAMILIT Standard +**Sistema:** NEXUS v3.4 + SIMCO +**Ultima actualizacion:** 2026-01-04 diff --git a/docs/_MAP.md b/docs/_MAP.md index 23d32b3..6678bc4 100644 --- a/docs/_MAP.md +++ b/docs/_MAP.md @@ -1,8 +1,20 @@ -# Mapa de Documentacion: inmobiliaria-analytics +--- +id: "MAP-DOCS-ROOT" +title: "Mapa de Documentacion - Inmobiliaria Analytics" +type: "Navigation Map" +project: "inmobiliaria-analytics" +standard: "GAMILIT" +system: "NEXUS v3.4 + SIMCO" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- -**Proyecto:** inmobiliaria-analytics -**Actualizado:** 2026-01-04 -**Generado por:** EPIC-008 adapt-simco.sh +# _MAP: Documentacion Inmobiliaria Analytics + +**Proyecto:** Inmobiliaria Analytics +**Estandar:** GAMILIT +**Sistema:** NEXUS v3.4 + SIMCO +**Ultima actualizacion:** 2026-01-04 --- @@ -10,31 +22,176 @@ ``` docs/ -├── _MAP.md # Este archivo (indice de navegacion) -├── 00-overview/ # Vision general del proyecto -├── 01-architecture/ # Arquitectura y decisiones (ADRs) -├── 02-specs/ # Especificaciones tecnicas -├── 03-api/ # Documentacion de APIs -├── 04-guides/ # Guias de desarrollo -└── 99-finiquito/ # Entregables cliente (si aplica) +├── _MAP.md # Este archivo - Indice principal +├── README.md # Introduccion a la documentacion +│ +├── 00-vision-general/ # Vision, arquitectura, stack +│ ├── _MAP.md +│ ├── VISION-PRODUCTO.md +│ ├── ARQUITECTURA-GENERAL.md +│ ├── STACK-TECNOLOGICO.md +│ └── Webscraper_Politics.md +│ +├── 01-fase-alcance-inicial/ # EPICs del MVP (8 EPICs) +│ ├── _MAP.md +│ ├── IAI-001-fundamentos/ +│ ├── IAI-002-propiedades/ +│ ├── IAI-003-usuarios/ +│ ├── IAI-004-tenants/ +│ ├── IAI-005-pagos/ +│ ├── IAI-006-portales/ +│ ├── IAI-007-webscraper/ +│ └── IAI-008-ml-analytics/ +│ +├── 02-fase-robustecimiento/ # Fase 2 (futuro) +│ └── _MAP.md +│ +├── 03-fase-extensiones/ # Fase 3 (futuro) +│ └── _MAP.md +│ +├── 04-fase-backlog/ # Backlog y definiciones +│ ├── _MAP.md +│ ├── DEFINITION-OF-READY.md +│ └── DEFINITION-OF-DONE.md +│ +├── 90-transversal/ # Documentacion transversal +│ └── _MAP.md +│ +├── 95-guias-desarrollo/ # Guias para developers +│ └── _MAP.md +│ +├── 96-quick-reference/ # Cheatsheets +│ └── _MAP.md +│ +├── 97-adr/ # Architecture Decision Records +│ ├── _MAP.md +│ ├── README.md +│ └── ADR-001-stack-tecnologico.md +│ +├── 99-analisis/ # Documentos de analisis +│ └── ANALISIS-REESTRUCTURACION-2026-01-04.md +│ +└── planning/ # Planificacion SCRUM + ├── _MAP.md + ├── Board.md + └── config.yml ``` -## Navegacion Rapida - -| Seccion | Descripcion | Estado | -|---------|-------------|--------| -| Overview | Vision general | - | -| Architecture | Decisiones arquitectonicas | - | -| Specs | Especificaciones tecnicas | - | -| API | Documentacion de endpoints | - | -| Guides | Guias de desarrollo | - | - -## Estadisticas - -- Total archivos en docs/: 1 -- Fecha de adaptacion: 2026-01-04 - --- -**Nota:** Este archivo fue generado automaticamente por EPIC-008. -Actualizar manualmente con la estructura real del proyecto. +## Navegacion Rapida + +### Por Seccion + +| Seccion | Descripcion | _MAP | +|---------|-------------|------| +| 00-vision-general | Vision y arquitectura del proyecto | [Ver](./00-vision-general/_MAP.md) | +| 01-fase-alcance-inicial | EPICs fundamentales (MVP) | [Ver](./01-fase-alcance-inicial/_MAP.md) | +| 02-fase-robustecimiento | Mejoras y optimizaciones | [Ver](./02-fase-robustecimiento/_MAP.md) | +| 03-fase-extensiones | Extensiones futuras | [Ver](./03-fase-extensiones/_MAP.md) | +| 04-fase-backlog | Backlog, DoR, DoD | [Ver](./04-fase-backlog/_MAP.md) | +| 90-transversal | Inventarios, arquitectura | [Ver](./90-transversal/_MAP.md) | +| 95-guias-desarrollo | Guias para desarrolladores | [Ver](./95-guias-desarrollo/_MAP.md) | +| 96-quick-reference | Cheatsheets | [Ver](./96-quick-reference/_MAP.md) | +| 97-adr | Decisiones arquitectonicas | [Ver](./97-adr/_MAP.md) | +| planning | Tablero Kanban y sprints | [Ver](./planning/_MAP.md) | + +--- + +## EPICs del Proyecto + +### Fase 1: Alcance Inicial + +| EPIC ID | Nombre | SP | RF | US | ET | Estado | +|---------|--------|----|----|----|----|--------| +| [IAI-001](./01-fase-alcance-inicial/IAI-001-fundamentos/_MAP.md) | Fundamentos | 40 | - | - | - | Planned | +| [IAI-002](./01-fase-alcance-inicial/IAI-002-propiedades/_MAP.md) | Propiedades | 34 | - | 5 | - | Planned | +| [IAI-003](./01-fase-alcance-inicial/IAI-003-usuarios/_MAP.md) | Usuarios y Perfiles | 26 | - | 4 | - | Planned | +| [IAI-004](./01-fase-alcance-inicial/IAI-004-tenants/_MAP.md) | Multi-Tenancy | 40 | - | 5 | - | Planned | +| [IAI-005](./01-fase-alcance-inicial/IAI-005-pagos/_MAP.md) | Pagos (Stripe) | 34 | - | 5 | - | Planned | +| [IAI-006](./01-fase-alcance-inicial/IAI-006-portales/_MAP.md) | Portales Web | 26 | - | 4 | - | Planned | +| [IAI-007](./01-fase-alcance-inicial/IAI-007-webscraper/_MAP.md) | Web Scraping y ETL | 55 | 5 | 5 | 3 | Draft | +| [IAI-008](./01-fase-alcance-inicial/IAI-008-ml-analytics/_MAP.md) | ML Analytics | 60 | 4 | 8 | 2 | Draft | + +**Total Story Points Fase 1:** 315 SP + +--- + +## Estadisticas de Documentacion + +| Metrica | Valor | +|---------|-------| +| **Total EPICs** | 8 | +| **EPICs documentadas** | 8 | +| **Requerimientos (RF)** | 9 | +| **Historias Usuario (US)** | 13+ | +| **Especificaciones (ET)** | 5 | +| **ADRs creados** | 1 | +| **Conformidad GAMILIT** | 100% | + +--- + +## Convenciones de Nomenclatura + +### IDs de EPICs +- **IAI-NNN** = Inmobiliaria Analytics - Alcance Inicial + +### IDs de Documentos +| Tipo | Formato | Ejemplo | +|------|---------|---------| +| Requerimientos | RF-[MOD]-NNN | RF-SCR-001 | +| User Stories | US-[MOD]-NNN | US-ML-001 | +| Especificaciones | ET-[MOD]-NNN | ET-SCR-001 | +| Navigation Maps | MAP-IAI-NNN | MAP-IAI-007 | + +### Codigos de Modulo +| Codigo | Modulo | EPIC | +|--------|--------|------| +| FUND | Fundamentos | IAI-001 | +| PROP | Propiedades | IAI-002 | +| USER | Usuarios | IAI-003 | +| TENT | Tenants | IAI-004 | +| PAY | Pagos | IAI-005 | +| PORT | Portales | IAI-006 | +| SCR | Scraper | IAI-007 | +| ML | ML Analytics | IAI-008 | + +--- + +## Archivos Clave + +| Archivo | Proposito | +|---------|-----------| +| [AGENTS.md](../AGENTS.md) | Guia para agentes IA | +| [INVENTARIO.yml](../INVENTARIO.yml) | Inventario del proyecto | +| [planning/Board.md](./planning/Board.md) | Tablero Kanban | +| [04-fase-backlog/DEFINITION-OF-READY.md](./04-fase-backlog/DEFINITION-OF-READY.md) | Criterios para iniciar | +| [04-fase-backlog/DEFINITION-OF-DONE.md](./04-fase-backlog/DEFINITION-OF-DONE.md) | Criterios para completar | + +--- + +## Dependencias entre EPICs + +``` +IAI-001 (Fundamentos) + │ + ├──▶ IAI-002 (Propiedades) + │ │ + │ └──▶ IAI-007 (Webscraper) ──▶ IAI-008 (ML Analytics) + │ + ├──▶ IAI-003 (Usuarios) + │ │ + │ ├──▶ IAI-004 (Tenants) + │ │ │ + │ │ └──▶ IAI-005 (Pagos) + │ │ + │ └──▶ IAI-006 (Portales) + │ + └──▶ IAI-008 (ML Analytics) ◀── IAI-007 (Webscraper) +``` + +--- + +**Generado:** 2026-01-04 +**Sistema:** NEXUS v3.4 + SIMCO + GAMILIT +**Mantenedores:** @Tech-Lead diff --git a/docs/planning/Board.md b/docs/planning/Board.md new file mode 100644 index 0000000..7411c67 --- /dev/null +++ b/docs/planning/Board.md @@ -0,0 +1,109 @@ +--- +id: "BOARD-IA" +title: "Tablero Kanban - Inmobiliaria Analytics" +type: "Planning Board" +project: "inmobiliaria-analytics" +sprint: 1 +sprint_goal: "Establecer fundamentos del proyecto" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# Board.md - Inmobiliaria Analytics + +**Sprint Actual:** Sprint 1 +**Fecha Inicio:** 2026-01-04 +**Duracion:** 10 dias +**Objetivo:** Establecer fundamentos del proyecto y documentacion base + +--- + +## Resumen del Sprint + +| Metrica | Valor | +|---------|-------| +| **Story Points Planificados** | 0 SP | +| **Story Points Completados** | 0 SP | +| **Velocidad Objetivo** | 30 SP | +| **Tareas Pendientes** | 0 | +| **Bugs Abiertos** | 0 | + +--- + +## Tablero Kanban + +### Backlog + +| ID | Titulo | Tipo | SP | Prioridad | +|----|--------|------|-----|-----------| +| - | Sin items en backlog | - | - | - | + +### Por Hacer (To Do) + +| ID | Titulo | Tipo | SP | Asignado | +|----|--------|------|-----|----------| +| - | Sin tareas planificadas | - | - | - | + +### En Progreso (In Progress) + +| ID | Titulo | Tipo | SP | Asignado | Inicio | +|----|--------|------|-----|----------|--------| +| - | Sin tareas en progreso | - | - | - | - | + +### Bloqueado + +| ID | Titulo | Bloqueante | Asignado | +|----|--------|------------|----------| +| - | Sin bloqueos | - | - | + +### En Revision (In Review) + +| ID | Titulo | Tipo | Revisor | +|----|--------|------|---------| +| - | Sin items en revision | - | - | + +### Hecho (Done) + +| ID | Titulo | Tipo | SP | Completado | +|----|--------|------|-----|------------| +| - | Sin items completados | - | - | - | + +--- + +## Bugs Abiertos + +| ID | Titulo | Severidad | Modulo | Estado | +|----|--------|-----------|--------|--------| +| - | Sin bugs reportados | - | - | - | + +--- + +## Notas del Sprint + +### Sprint 1 (2026-01-04) +- Proyecto en fase de planificacion +- Estructura de documentacion GAMILIT creada +- Backend con scaffold basico NestJS +- Frontend y Database pendientes de implementacion + +--- + +## Historico de Sprints + +| Sprint | Fecha | SP Completados | Items | Estado | +|--------|-------|----------------|-------|--------| +| Sprint 1 | 2026-01-04 | 0 | 0 | En Progreso | + +--- + +## Referencias + +- [AGENTS.md](../../AGENTS.md) - Guia para agentes +- [config.yml](./config.yml) - Configuracion del proyecto +- [DEFINITION-OF-READY.md](../04-fase-backlog/DEFINITION-OF-READY.md) +- [DEFINITION-OF-DONE.md](../04-fase-backlog/DEFINITION-OF-DONE.md) + +--- + +**Actualizado:** 2026-01-04 +**Proximo Review:** 2026-01-14 diff --git a/docs/planning/_MAP.md b/docs/planning/_MAP.md new file mode 100644 index 0000000..189c2bf --- /dev/null +++ b/docs/planning/_MAP.md @@ -0,0 +1,55 @@ +--- +id: "MAP-PLANNING" +title: "Mapa de Navegacion - Planning" +type: "Navigation Map" +section: "planning" +created_date: "2026-01-04" +updated_date: "2026-01-04" +--- + +# _MAP: planning + +**Seccion:** Planificacion Activa +**Proyecto:** Inmobiliaria Analytics +**Ultima actualizacion:** 2026-01-04 + +--- + +## Proposito + +Infraestructura SCRUM para planificacion y seguimiento de sprints. + +--- + +## Contenido + +| Archivo | Titulo | Proposito | +|---------|--------|-----------| +| [Board.md](./Board.md) | Tablero Kanban | Estado del sprint actual | +| [config.yml](./config.yml) | Configuracion | Config SCRUM del proyecto | + +### Carpetas + +| Carpeta | Proposito | +|---------|-----------| +| tasks/ | Tareas tecnicas activas | +| bugs/ | Bugs reportados | + +--- + +## Sprint Actual + +**Sprint:** 1 +**Fecha Inicio:** 2026-01-04 +**Objetivo:** Establecer fundamentos del proyecto + +--- + +## Navegacion + +- **Arriba:** [docs/](../_MAP.md) +- **Referencia:** [AGENTS.md](../../AGENTS.md) + +--- + +**Generado:** 2026-01-04 diff --git a/docs/planning/config.yml b/docs/planning/config.yml new file mode 100644 index 0000000..5624f54 --- /dev/null +++ b/docs/planning/config.yml @@ -0,0 +1,172 @@ +# Configuracion del Proyecto - Inmobiliaria Analytics +# Sistema: NEXUS v3.4 + SIMCO + GAMILIT Standard +# Fecha: 2026-01-04 + +project: + name: "Inmobiliaria Analytics" + version: "1.0.0" + repository: "inmobiliaria-analytics" + description: "Plataforma de analytics para el sector inmobiliario" + status: "planned" + +# Estados validos por tipo de documento +states: + user_story: + - "Backlog" + - "To Do" + - "In Progress" + - "In Review" + - "Done" + task: + - "To Do" + - "In Progress" + - "Blocked" + - "Done" + bug: + - "Open" + - "In Progress" + - "Fixed" + - "Done" + - "Won't Fix" + requirement: + - "Draft" + - "Approved" + - "Implemented" + - "Done" + +# Prioridades y SLAs +priorities: + - id: "P0" + name: "Critico" + sla_hours: 4 + color: "#dc2626" + - id: "P1" + name: "Alto" + sla_hours: 24 + color: "#ea580c" + - id: "P2" + name: "Medio" + sla_hours: 72 + color: "#ca8a04" + - id: "P3" + name: "Bajo" + sla_hours: 168 + color: "#16a34a" + +# Nomenclatura del proyecto +naming: + epic_prefix: "IA" + user_story: "US" + task: "TASK" + bug: "BUG" + requirement: "RF" + specification: "ET" + adr: "ADR" + +# Categorias de User Stories +us_categories: + - prefix: "FUND" + epic: "IA-001" + description: "Fundamentos" + - prefix: "PROP" + epic: "IA-002" + description: "Propiedades" + - prefix: "ANA" + epic: "IA-003" + description: "Analytics" + - prefix: "REP" + epic: "IA-004" + description: "Reportes" + - prefix: "INT" + epic: "IA-005" + description: "Integraciones" + +# Configuracion de Sprint +sprint: + duration_days: 10 + velocity_target: 30 + current_sprint: 1 + start_date: "2026-01-04" + +# Agentes asignados +agents: + - id: "@Backend-Agent" + specialization: "NestJS, TypeORM, APIs REST" + modules: ["auth", "properties", "analytics"] + - id: "@Frontend-Agent" + specialization: "React, TypeScript, UI/UX" + modules: ["dashboard", "reports", "admin"] + - id: "@Database-Agent" + specialization: "PostgreSQL, Redis, Migrations" + modules: ["schemas", "queries", "optimization"] + - id: "@DevOps-Agent" + specialization: "Docker, CI/CD, Monitoring" + modules: ["deployment", "infrastructure"] + +# Campos requeridos por tipo de documento +required_fields: + user_story: + - "id" + - "title" + - "status" + - "epic" + - "story_points" + - "created_date" + task: + - "id" + - "title" + - "status" + - "priority" + - "created_date" + bug: + - "id" + - "title" + - "status" + - "severity" + - "affected_module" + - "steps_to_reproduce" + - "created_date" + requirement: + - "id" + - "title" + - "status" + - "priority" + - "module" + - "epic" + - "created_date" + +# Metricas del proyecto +metrics: + total_epics: 1 + total_user_stories: 0 + total_requirements: 0 + total_tasks: 0 + total_bugs: 0 + compliance_target: 100 + +# Stack tecnologico +stack: + backend: + framework: "NestJS 10.x" + runtime: "Node.js 20.x" + orm: "TypeORM 0.3.x" + frontend: + framework: "React 18.x" + language: "TypeScript 5.x" + state: "Zustand" + database: + primary: "PostgreSQL 16" + cache: "Redis 7" + infrastructure: + container: "Docker" + orchestration: "Docker Compose" + proxy: "Traefik" + +# Metadata +metadata: + created: "2026-01-04" + updated: "2026-01-04" + maintainers: + - "@Tech-Lead" + standard: "GAMILIT" + system: "NEXUS v3.4" diff --git a/orchestration/00-guidelines/CONTEXTO-PROYECTO.md b/orchestration/00-guidelines/CONTEXTO-PROYECTO.md index 490819d..0e47da6 100644 --- a/orchestration/00-guidelines/CONTEXTO-PROYECTO.md +++ b/orchestration/00-guidelines/CONTEXTO-PROYECTO.md @@ -132,7 +132,7 @@ Este proyecto hereda directivas de: | Recurso | Path | |---------|------| | Core orchestration | `/home/isem/workspace-v1/core/orchestration/` | -| Catálogo global | `/home/isem/workspace-v1/core/catalog/` | +| Catálogo global | `/home/isem/workspace-v1/shared/catalog/` | --- *Contexto del proyecto - Sistema NEXUS + SIMCO v2.2.0* diff --git a/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md b/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md index e16cafc..7e2703d 100644 --- a/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md +++ b/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md @@ -83,7 +83,7 @@ Este proyecto utiliza las siguientes funcionalidades del catálogo core: |---------------|-------------------| | *(Por definir según necesidades del proyecto)* | - | -**Path catálogo:** `~/workspace/core/catalog/` +**Path catálogo:** `~/workspace/shared/catalog/` ## Uso para Subagentes diff --git a/orchestration/CONTEXT-MAP.yml b/orchestration/CONTEXT-MAP.yml new file mode 100644 index 0000000..2179eb7 --- /dev/null +++ b/orchestration/CONTEXT-MAP.yml @@ -0,0 +1,158 @@ +# CONTEXT-MAP: INMOBILIARIA-ANALYTICS +# Sistema: SIMCO - NEXUS v4.0 +# Propósito: Mapear contexto automático por nivel y tarea +# Versión: 1.0.0 +# Fecha: 2026-01-04 + +metadata: + proyecto: "inmobiliaria-analytics" + nivel: "STANDALONE" + version: "1.0.0" + ultima_actualizacion: "2026-01-04" + workspace_root: "/home/isem/workspace-v1" + project_root: "/home/isem/workspace-v1/projects/inmobiliaria-analytics" + +# ═══════════════════════════════════════════════════════════════════════════════ +# VARIABLES DEL PROYECTO (PRE-RESUELTAS) +# ═══════════════════════════════════════════════════════════════════════════════ + +variables: + # Identificación + PROJECT: "inmobiliaria-analytics" + PROJECT_NAME: "INMOBILIARIA-ANALYTICS" + PROJECT_LEVEL: "STANDALONE" + + # Paths principales + APPS_ROOT: "/home/isem/workspace-v1/projects/inmobiliaria-analytics/apps" + DOCS_ROOT: "/home/isem/workspace-v1/projects/inmobiliaria-analytics/docs" + ORCHESTRATION_PATH: "/home/isem/workspace-v1/projects/inmobiliaria-analytics/orchestration" + +# ═══════════════════════════════════════════════════════════════════════════════ +# ALIASES RESUELTOS +# ═══════════════════════════════════════════════════════════════════════════════ + +aliases: + # Directivas globales + "@SIMCO": "/home/isem/workspace-v1/orchestration/directivas/simco" + "@PRINCIPIOS": "/home/isem/workspace-v1/orchestration/directivas/principios" + "@PERFILES": "/home/isem/workspace-v1/orchestration/agents/perfiles" + "@CATALOG": "/home/isem/workspace-v1/shared/catalog" + + # Proyecto específico + "@APPS": "/home/isem/workspace-v1/projects/inmobiliaria-analytics/apps" + "@DOCS": "/home/isem/workspace-v1/projects/inmobiliaria-analytics/docs" + + # Inventarios + "@INVENTORY": "/home/isem/workspace-v1/projects/inmobiliaria-analytics/orchestration/inventarios" + +# ═══════════════════════════════════════════════════════════════════════════════ +# CONTEXTO POR NIVEL +# ═══════════════════════════════════════════════════════════════════════════════ + +contexto_por_nivel: + L0_sistema: + descripcion: "Principios fundamentales y perfil de agente" + tokens_estimados: 4500 + obligatorio: true + archivos: + - path: "/home/isem/workspace-v1/orchestration/directivas/principios/PRINCIPIO-CAPVED.md" + proposito: "Ciclo de vida de tareas" + tokens: 800 + - path: "/home/isem/workspace-v1/orchestration/directivas/principios/PRINCIPIO-DOC-PRIMERO.md" + proposito: "Documentación antes de código" + tokens: 500 + - path: "/home/isem/workspace-v1/orchestration/directivas/principios/PRINCIPIO-ANTI-DUPLICACION.md" + proposito: "Verificar catálogo antes de crear" + tokens: 600 + - path: "/home/isem/workspace-v1/orchestration/directivas/principios/PRINCIPIO-VALIDACION-OBLIGATORIA.md" + proposito: "Build/lint deben pasar" + tokens: 600 + - path: "/home/isem/workspace-v1/orchestration/directivas/principios/PRINCIPIO-ECONOMIA-TOKENS.md" + proposito: "Límites de contexto" + tokens: 500 + - path: "/home/isem/workspace-v1/orchestration/directivas/principios/PRINCIPIO-NO-ASUMIR.md" + proposito: "Preguntar si falta información" + tokens: 500 + - path: "/home/isem/workspace-v1/orchestration/referencias/ALIASES.yml" + proposito: "Resolución de @ALIAS" + tokens: 400 + + L1_proyecto: + descripcion: "Contexto específico de INMOBILIARIA-ANALYTICS" + tokens_estimados: 3000 + obligatorio: true + archivos: + - path: "/home/isem/workspace-v1/projects/inmobiliaria-analytics/orchestration/00-guidelines/CONTEXTO-PROYECTO.md" + proposito: "Variables y configuración del proyecto" + tokens: 1500 + - path: "/home/isem/workspace-v1/projects/inmobiliaria-analytics/orchestration/PROXIMA-ACCION.md" + proposito: "Estado actual y siguiente paso" + tokens: 500 + + L2_operacion: + descripcion: "SIMCO específicos según operación y dominio" + tokens_estimados: 2500 + archivos_por_operacion: + CREAR: + - "/home/isem/workspace-v1/orchestration/directivas/simco/SIMCO-CREAR.md" + MODIFICAR: + - "/home/isem/workspace-v1/orchestration/directivas/simco/SIMCO-MODIFICAR.md" + VALIDAR: + - "/home/isem/workspace-v1/orchestration/directivas/simco/SIMCO-VALIDAR.md" + DELEGAR: + - "/home/isem/workspace-v1/orchestration/directivas/simco/SIMCO-DELEGACION.md" + + L3_tarea: + descripcion: "Contexto específico de la tarea" + tokens_max: 8000 + dinamico: true + +# ═══════════════════════════════════════════════════════════════════════════════ +# INFORMACIÓN ESPECÍFICA DEL PROYECTO +# ═══════════════════════════════════════════════════════════════════════════════ + +info_proyecto: + tipo: "Plataforma de Análisis Inmobiliario" + estado: "En desarrollo" + version: "1.0" + + stack: + apps: "Aplicaciones de análisis inmobiliario" + docs: "Documentación del proyecto" + +# ═══════════════════════════════════════════════════════════════════════════════ +# VALIDACIÓN DE TOKENS +# ═══════════════════════════════════════════════════════════════════════════════ + +validacion_tokens: + limite_absoluto: 25000 + limite_seguro: 18000 + limite_alerta: 20000 + + presupuesto: + L0_sistema: 4500 + L1_proyecto: 3000 + L2_operacion: 2500 + L3_tarea_max: 8000 + total_base: 10000 + disponible_tarea: 8000 + +# ═══════════════════════════════════════════════════════════════════════════════ +# HERENCIA +# ═══════════════════════════════════════════════════════════════════════════════ + +herencia: + tipo: "STANDALONE" + hereda_de: + - "/home/isem/workspace-v1/orchestration/" + +# ═══════════════════════════════════════════════════════════════════════════════ +# BÚSQUEDA DE HISTÓRICO +# ═══════════════════════════════════════════════════════════════════════════════ + +busqueda_historico: + habilitado: true + ubicaciones: + - "/home/isem/workspace-v1/projects/inmobiliaria-analytics/orchestration/trazas/" + - "/home/isem/workspace-v1/orchestration/errores/REGISTRO-ERRORES.yml" + - "/home/isem/workspace-v1/shared/knowledge-base/lessons-learned/" diff --git a/orchestration/PROJECT-STATUS.md b/orchestration/PROJECT-STATUS.md new file mode 100644 index 0000000..d3ee0b3 --- /dev/null +++ b/orchestration/PROJECT-STATUS.md @@ -0,0 +1,85 @@ +# PROJECT STATUS - Inmobiliaria Analytics + +**Fecha:** 2026-01-07 +**Estado:** Planificacion +**Fase:** Pre-Desarrollo + +--- + +## Resumen Ejecutivo + +| Aspecto | Estado | Notas | +|---------|--------|-------| +| Vision | Pendiente | Por definir | +| Arquitectura | Pendiente | Por definir | +| Database | Pendiente | - | +| Backend | Pendiente | - | +| Frontend | Pendiente | - | +| Analytics | Pendiente | Core del proyecto | + +--- + +## Descripcion + +Plataforma de analitica para el sector inmobiliario. Incluye analisis de mercado, valuaciones, predicciones y dashboards. + +--- + +## Progreso + +| Fase | Estado | Notas | +|------|--------|-------| +| Planificacion | Pendiente | Alcance por definir | +| Documentacion | Pendiente | - | +| Desarrollo | Pendiente | - | +| Testing | Pendiente | - | +| Deploy | Pendiente | - | + +--- + +## Proximas Acciones + +1. Definir vision y alcance del proyecto +2. Documentar requerimientos funcionales +3. Disenar arquitectura tecnica +4. Identificar fuentes de datos inmobiliarios +5. Crear estructura base de proyecto + +--- + +## Stack Propuesto + +| Capa | Tecnologia | +|------|------------| +| Backend | Python/FastAPI o NestJS | +| Frontend | React + Vite | +| Database | PostgreSQL + PostGIS | +| ML | Python + scikit-learn | +| Analytics | Metabase/Grafana | +| Maps | Leaflet/Mapbox | + +--- + +## Funcionalidades Potenciales + +- Analisis de precios por zona +- Prediccion de valuaciones +- Comparables automaticos +- Dashboards por region +- Alertas de mercado +- Integracion con portales inmobiliarios + +--- + +## Riesgos + +| Riesgo | Probabilidad | Impacto | Mitigacion | +|--------|--------------|---------|------------| +| Acceso a datos | Alta | Alto | APIs de portales | +| Calidad de datos | Media | Alto | Limpieza y validacion | +| Cobertura geografica | Media | Medio | Enfoque por region | + +--- + +**Ultima actualizacion:** 2026-01-07 +**Actualizado por:** Orquestador diff --git a/orchestration/environment/ENVIRONMENT-INVENTORY.yml b/orchestration/environment/ENVIRONMENT-INVENTORY.yml new file mode 100644 index 0000000..3f8b383 --- /dev/null +++ b/orchestration/environment/ENVIRONMENT-INVENTORY.yml @@ -0,0 +1,183 @@ +# ============================================================================= +# ENVIRONMENT-INVENTORY.yml - INMOBILIARIA-ANALYTICS +# ============================================================================= +# Inventario de Entorno de Desarrollo +# Generado por: @PERFIL_DEVENV +# Estado: RESERVADO (pendiente de implementacion) +# ============================================================================= + +version: "1.0.0" +fecha_creacion: "2026-01-04" +fecha_actualizacion: "2026-01-04" +responsable: "@PERFIL_DEVENV" +estado: "RESERVADO" + +# ----------------------------------------------------------------------------- +# IDENTIFICACION DEL PROYECTO +# ----------------------------------------------------------------------------- + +proyecto: + nombre: "Inmobiliaria Analytics" + alias: "inmobiliaria" + nivel: "NIVEL_2A" + tipo: "standalone" + estado: "reservado" + descripcion: "Plataforma de analitica inmobiliaria" + +# ----------------------------------------------------------------------------- +# HERRAMIENTAS Y RUNTIME (PLANIFICADO) +# ----------------------------------------------------------------------------- + +herramientas: + runtime: + node: + version: "20.x" + requerido: true + notas: "Para backend NestJS y frontend React" + python: + version: "3.11" + requerido: false + notas: "Opcional para ML" + + package_managers: + npm: + version: "10.x" + requerido: true + + build_tools: + - nombre: "Vite" + version: "5.x" + uso: "Frontend build" + - nombre: "TypeScript" + version: "5.x" + uso: "Compilacion" + - nombre: "NestJS CLI" + version: "10.x" + uso: "Backend build" + +# ----------------------------------------------------------------------------- +# SERVICIOS Y PUERTOS (RESERVADOS) +# ----------------------------------------------------------------------------- + +servicios: + frontend: + nombre: "inmobiliaria-frontend" + framework: "React" + version: "18.x" + puerto: 3100 + ubicacion: "apps/frontend/" + url_local: "http://localhost:3100" + estado: "reservado" + + backend: + nombre: "inmobiliaria-backend" + framework: "NestJS" + version: "10.x" + puerto: 3101 + ubicacion: "apps/backend/" + url_local: "http://localhost:3101" + api_prefix: "/api/v1" + estado: "reservado" + +# ----------------------------------------------------------------------------- +# BASE DE DATOS (RESERVADO) +# ----------------------------------------------------------------------------- + +base_de_datos: + principal: + engine: "PostgreSQL" + version: "15" + host: "localhost" + puerto: 5439 + + ambientes: + development: + nombre: "inmobiliaria_development" + usuario: "inmobiliaria_dev" + password_ref: "DB_PASSWORD en .env" + + test: + nombre: "inmobiliaria_test" + usuario: "inmobiliaria_dev" + password_ref: "DB_PASSWORD en .env" + + schemas: + - nombre: "public" + descripcion: "Schema principal" + + conexion_ejemplo: "postgresql://inmobiliaria_dev:{password}@localhost:5439/inmobiliaria_development" + estado: "reservado - BD no creada aun" + + redis: + host: "localhost" + puerto: 6386 + uso: "cache, sessions" + estado: "reservado" + +# ----------------------------------------------------------------------------- +# VARIABLES DE ENTORNO (TEMPLATE) +# ----------------------------------------------------------------------------- + +variables_entorno: + archivo_ejemplo: ".env.example" + estado: "pendiente de crear" + + variables: + - nombre: "NODE_ENV" + descripcion: "Ambiente de ejecucion" + requerido: true + ejemplo: "development" + + - nombre: "PORT" + descripcion: "Puerto del servidor backend" + requerido: true + ejemplo: "3101" + + - nombre: "DATABASE_URL" + descripcion: "Connection string de PostgreSQL" + requerido: true + ejemplo: "postgresql://inmobiliaria_dev:password@localhost:5439/inmobiliaria_development" + + - nombre: "REDIS_URL" + descripcion: "Connection string de Redis" + requerido: true + ejemplo: "redis://localhost:6386" + + - nombre: "JWT_SECRET" + descripcion: "Secreto para JWT" + requerido: true + sensible: true + +# ----------------------------------------------------------------------------- +# NOTAS DE IMPLEMENTACION +# ----------------------------------------------------------------------------- + +notas_implementacion: | + ## Estado: RESERVADO + + Este proyecto tiene puertos y recursos reservados pero aun no esta implementado. + + ### Recursos Reservados: + - Puertos: 3100-3101 + - PostgreSQL: puerto 5439 + - Redis: puerto 6386 + - BD: inmobiliaria_development / inmobiliaria_dev + + ### Pasos para Activar: + 1. Crear estructura de proyecto + 2. Crear BD y usuario + 3. Configurar .env.example + 4. Actualizar estado a "desarrollo" + +# ----------------------------------------------------------------------------- +# REFERENCIAS +# ----------------------------------------------------------------------------- + +referencias: + perfil_devenv: "orchestration/agents/perfiles/PERFIL-DEVENV.md" + inventario_master: "orchestration/inventarios/DEVENV-MASTER-INVENTORY.yml" + inventario_puertos: "orchestration/inventarios/DEVENV-PORTS-INVENTORY.yml" + +# ============================================================================= +# FIN DE INVENTARIO +# =============================================================================