# Esquema de Base de Datos

Base de datos: `lliria_properties` (MySQL 8.0, charset `utf8mb4_unicode_ci`)

## Tablas

### `properties`
Almacena todas las propiedades scrapeadas.

| Columna         | Tipo                 | Descripción                              |
| --------------- | -------------------- | ---------------------------------------- |
| `id`            | INT PK               | Auto-incremental                         |
| `title`         | VARCHAR(500)         | Título del anuncio                       |
| `price`         | DECIMAL(12,2)        | Precio en euros                          |
| `location`      | VARCHAR(500)         | Texto de ubicación original              |
| `municipality`  | VARCHAR(200)         | Municipio normalizado                    |
| `property_type` | VARCHAR(50)          | Tipo canónico (ver tipos)                |
| `rooms`         | TINYINT              | Número de habitaciones                   |
| `bathrooms`     | TINYINT              | Número de baños                          |
| `size_m2`       | DECIMAL(8,2)         | Superficie útil                          |
| `description`   | TEXT                 | Descripción completa                     |
| `image_url`     | VARCHAR(1000)        | URL de imagen principal                  |
| `url`           | VARCHAR(1000) UNIQUE | URL del anuncio (clave de deduplicación) |
| `source`        | VARCHAR(100)         | Nombre del portal (p.ej "Idealista")     |
| `lat`           | DECIMAL(10,7)        | Latitud (geocodificada)                  |
| `lng`           | DECIMAL(10,7)        | Longitud (geocodificada)                 |
| `score`         | DECIMAL(4,2)         | Puntuación 0–10                          |
| `price_per_m2`  | DECIMAL(8,2)         | Precio por m² calculado                  |
| `is_duplicate`  | TINYINT(1)           | 1 si es duplicado detectado              |
| `duplicate_of`  | INT                  | ID de la propiedad original              |
| `is_favorite`   | TINYINT(1)           | Marcado como favorito por el usuario     |
| `notes`         | TEXT                 | Notas personales del usuario             |
| `created_at`    | TIMESTAMP            | Primera vez scrapeada                    |
| `updated_at`    | TIMESTAMP            | Última actualización                     |

**Índices:** `idx_price`, `idx_municipality`, `idx_property_type`, `idx_source`, `idx_updated`

**Tipos canónicos de propiedad:**
`piso` · `ático` · `chalet` · `terreno` · `local` · `garaje` · `oficina` · `trastero` · `finca` · `casa`

---

### `portals`
Portales inmobiliarios configurados.

| Columna         | Tipo                | Descripción                             |
| --------------- | ------------------- | --------------------------------------- |
| `id`            | INT PK              | Auto-incremental                        |
| `name`          | VARCHAR(100)        | Nombre del portal                       |
| `slug`          | VARCHAR(100) UNIQUE | Identificador URL-friendly              |
| `scraper_class` | VARCHAR(100)        | Nombre exacto de la clase TypeScript    |
| `active`        | TINYINT(1)          | 1 = activo en scrapers, 0 = desactivado |
| `created_at`    | TIMESTAMP           | —                                       |

**Portales disponibles (20) — 6 activos:**

| #   | Portal          | Slug         | Clase                      | Activo |
| --- | --------------- | ------------ | -------------------------- | ------ |
| 1   | Fotocasa        | fotocasa     | FotocasaPlaywrightScraper  | ✅      |
| 2   | Globaliza       | globaliza    | GlobalizaScraper           | ✅      |
| 3   | Habitaclia      | habitaclia   | HabitacliaScraper          | ✅      |
| 4   | Idealista       | idealista    | IdealistaPlaywrightScraper | ✅      |
| 5   | Pisos.com       | pisos-com    | PisosScraper               | ✅      |
| 6   | ThinkSpain      | thinkspain   | ThinkSpainScraper          | ✅      |
| 7   | Casasapo        | casasapo     | CasasapoScraper            | ⬜      |
| 8   | Engel & Völkers | engelvolkers | EngelVolkersScraper        | ⬜      |
| 9   | ERA Spain       | era-spain    | EraSpainScraper            | ⬜      |
| 10  | Holprop         | holprop      | HolpropScraper             | ⬜      |
| 11  | Inmobiliaria.es | inmobiliaria | InmobiliariaScraper        | ⬜      |
| 12  | Kyero           | kyero        | KyeroScraper               | ⬜      |
| 13  | Milanuncios     | milanuncios  | MilanunciosScraper         | ⬜      |
| 14  | Nuroa           | nuroa        | NuroaScraper               | ⬜      |
| 15  | REMAX España    | remax        | RemaxScraper               | ⬜      |
| 16  | SpainHouses     | spainhouses  | SpainHousesScraper         | ⬜      |
| 17  | Tecnocasa       | tecnocasa    | TecnocasaScraper           | ⬜      |
| 18  | Trovit          | trovit       | TrovatScraper              | ⬜      |
| 19  | Vibbo           | vibbo        | VibboScraper               | ⬜      |
| 20  | Yaencontre      | yaencontre   | YaencontreScraper          | ⬜      |

---

### `zones`
Municipios/zonas de búsqueda.

| Columna           | Tipo                | Descripción                                              |
| ----------------- | ------------------- | -------------------------------------------------------- |
| `id`              | INT PK              | Auto-incremental                                         |
| `name`            | VARCHAR(200)        | Nombre completo del municipio                            |
| `slug`            | VARCHAR(200) UNIQUE | Slug base (clave única para URLs de scrapers)            |
| `lat`             | DECIMAL(10,7)       | Latitud centro municipio                                 |
| `lng`             | DECIMAL(10,7)       | Longitud centro municipio                                |
| `radius_km`       | DECIMAL(5,2)        | Radio de búsqueda en km                                  |
| `active`          | TINYINT(1)          | 1 = incluido en scrapers                                 |
| `pisos_slug`      | VARCHAR(200)        | Slug para pisos.com (p.ej `pisos-lliria`)                |
| `thinkspain_slug` | VARCHAR(200)        | Slug para ThinkSpain (`NULL` = sin página en ThinkSpain) |
| `globaliza_slug`  | VARCHAR(200)        | Slug para Globaliza (`NULL` = sin página en Globaliza)   |
| `created_at`      | TIMESTAMP           | —                                                        |

**Restricción:** `UNIQUE KEY uq_zones_slug (slug)` — evita duplicados.

**Zonas activas (15):**

| Zona                 | slug              | pisos_slug                    | thinkspain_slug      | globaliza_slug  |
| -------------------- | ----------------- | ----------------------------- | -------------------- | --------------- |
| Llíria               | lliria            | pisos-lliria                  | lliria               | lliria          |
| Marines              | marines           | pisos-marines                 | marines              | marines         |
| Olocau               | olocau            | pisos-olocau                  | olocau               | olocau          |
| Náquera              | naquera           | pisos-naquera                 | naquera              | naquera         |
| Bétera               | betera            | pisos-betera                  | betera               | betera          |
| Villamarchante       | villamarchante    | pisos-villamarchante-valencia | —                    | vilamarxant     |
| Benaguasil           | benaguasil        | pisos-benaguasil              | benaguasil           | benaguasil      |
| La Pobla de Vallbona | pobla-de-vallbona | pisos-pobla-de-vallbona       | la-pobla-de-vallbona | pobla-vallbona  |
| L'Eliana             | eliana            | pisos-eliana                  | l-eliana             | l-eliana        |
| Riba-roja del Túria  | riba-roja         | pisos-riba-roja-de-turia      | riba-roja-de-turia   | riba-roja-turia |
| Chelva               | chelva            | pisos-chelva                  | chelva               | chelva          |
| Casinos              | casinos           | pisos-casinos                 | casinos              | casinos         |
| Moncofa              | moncofa           | pisos-moncofa                 | moncofa              | moncofa         |
| La Vall d'Uixó       | la-vall-duixo     | pisos-la-vall-duixo           | —                    | —               |
| Valencia             | valencia          | pisos-valencia                | —                    | valencia        |

> **Nota:** `—` indica que la zona no tiene página en ese portal o se excluye intencionalmente (p.ej. Valencia en Idealista y ThinkSpain por volumen elevado).

---

### `filters`
Filtros guardados por el usuario con opción de alertas por email.

| Columna         | Tipo         | Descripción                   |
| --------------- | ------------ | ----------------------------- |
| `id`            | INT PK       | —                             |
| `name`          | VARCHAR(200) | Nombre descriptivo del filtro |
| `min_price`     | DECIMAL      | Precio mínimo                 |
| `max_price`     | DECIMAL      | Precio máximo                 |
| `property_type` | VARCHAR(50)  | Tipo de propiedad canónico    |
| `municipality`  | VARCHAR(200) | Municipio                     |
| `min_rooms`     | TINYINT      | Habitaciones mínimas          |
| `min_size_m2`   | DECIMAL      | Superficie mínima             |
| `portal`        | VARCHAR(100) | Portal específico (opcional)  |
| `email_alerts`  | TINYINT(1)   | 1 = enviar alertas por email  |

---

### `price_history`
Historial de cambios de precio por propiedad.

| Columna       | Tipo          | Descripción                  |
| ------------- | ------------- | ---------------------------- |
| `id`          | INT PK        | —                            |
| `property_id` | INT           | FK implícita a properties.id |
| `price`       | DECIMAL(12,2) | Precio en esa fecha          |
| `recorded_at` | TIMESTAMP     | Momento del registro         |

**Índice:** `idx_property_price (property_id, recorded_at)`

---

### `scraping_log`
Log de ejecuciones del scraper.

| Columna       | Tipo        | Descripción                       |
| ------------- | ----------- | --------------------------------- |
| `id`          | INT PK      | —                                 |
| `started_at`  | TIMESTAMP   | Inicio del scraping               |
| `finished_at` | TIMESTAMP   | Fin del scraping                  |
| `total_found` | INT         | Propiedades encontradas           |
| `new_saved`   | INT         | Propiedades nuevas guardadas      |
| `errors`      | INT         | Errores producidos                |
| `status`      | VARCHAR(20) | `running` / `completed` / `error` |
| `details`     | TEXT        | Mensajes adicionales              |

---

### `users`
Usuarios del sistema (actualmente solo George/admin).

| Columna      | Tipo                | Descripción        |
| ------------ | ------------------- | ------------------ |
| `id`         | INT PK              | —                  |
| `username`   | VARCHAR(100) UNIQUE | Nombre de usuario  |
| `password`   | VARCHAR(255)        | Hash bcrypt        |
| `email`      | VARCHAR(255)        | Email para alertas |
| `created_at` | TIMESTAMP           | —                  |

---

### `sessions`
Sesiones de Express (gestionadas por `express-mysql-session`).
Creada automáticamente por la librería.

## Migraciones

Las migraciones se aplican automáticamente al arrancar la app en `src/config/db.ts` usando:
```sql
ALTER TABLE ... ADD COLUMN IF NOT EXISTS ...
```
Esto permite actualizar la BD sin perder datos.
