Initial commit: proyecto ContabilidadSaPolar completo

This commit is contained in:
root
2026-08-19 21:30:23 +00:00
commit 0d5c6f9512
232 changed files with 26730 additions and 0 deletions
+370
View File
@@ -0,0 +1,370 @@
# Referencia de la API REST
## Formato General
Todas las respuestas siguen el formato `ApiResponse<T>`:
```json
{
"success": true,
"message": "OK",
"data": { ... },
"timestamp": "2026-07-04T12:00:00"
}
```
Los errores usan el mismo formato con `success: false` y mensaje descriptivo.
## Autenticación
### `POST /api/auth/login`
Iniciar sesión.
**Request body:**
```json
{
"username": "admin",
"password": "admin123"
}
```
**Response (200):**
```json
{
"success": true,
"message": "OK",
"data": {
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"tokenType": "Bearer",
"userId": 1,
"username": "admin",
"role": "ADMIN"
}
}
```
### `POST /api/auth/register`
Registrar nuevo usuario.
**Request body:**
```json
{
"username": "usuario1",
"email": "user@example.com",
"password": "password123",
"fullName": "Usuario Ejemplo",
"phone": "600123456",
"roleName": "GERENTE"
}
```
### `POST /api/auth/refresh`
Renovar token de acceso.
**Request body:**
```json
{
"refreshToken": "eyJhbGciOi..."
}
```
## Usuarios (solo ADMIN)
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/users` | Listar todos los usuarios |
| `GET` | `/api/users/{id}` | Obtener usuario por ID |
| `PUT` | `/api/users/{id}` | Actualizar usuario |
| `DELETE` | `/api/users/{id}` | Desactivar usuario (soft-delete) |
## Propiedades
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/properties` | Listar propiedades activas |
| `GET` | `/api/properties/tree` | Obtener propiedades raíz (sin padre) |
| `GET` | `/api/properties/{id}/children` | Obtener hijos de una propiedad |
| `GET` | `/api/properties/{id}` | Obtener propiedad por ID |
| `POST` | `/api/properties` | Crear propiedad |
| `PUT` | `/api/properties/{id}` | Actualizar propiedad |
| `PATCH` | `/api/properties/{id}/status` | Cambiar estado de propiedad |
| `GET` | `/api/properties/{id}/history` | Historial de cambios de estado |
| `DELETE` | `/api/properties/{id}` | Eliminar propiedad (soft-delete) |
| `GET` | `/api/properties/types` | Listar tipos de propiedad |
| `GET` | `/api/properties/statuses` | Listar estados de propiedad |
**Estructura de Property:**
```json
{
"id": 1,
"parent": null,
"group": { "id": 1, "name": "Residencial Centro" },
"type": { "id": 2, "name": "PISO", "description": "Vivienda..." },
"status": { "id": 2, "name": "ALQUILADO", "description": "..." },
"reference": "PIS-001",
"name": "Piso Centro",
"addressStreet": "Calle Mayor",
"addressNumber": "12",
"addressCity": "Madrid",
"addressPostalCode": "28001",
"addressProvince": "Madrid",
"cadastralRef": "1234567VK1234A",
"surfaceM2": 85.50,
"floor": "3",
"door": "A",
"rentalAmount": 850.00,
"active": true
}
```
## Conjuntos (Property Groups)
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/property-groups` | Listar todos los conjuntos |
| `GET` | `/api/property-groups/{id}` | Obtener conjunto por ID |
| `POST` | `/api/property-groups` | Crear conjunto |
| `PUT` | `/api/property-groups/{id}` | Actualizar conjunto |
| `DELETE` | `/api/property-groups/{id}` | Eliminar conjunto |
| `GET` | `/api/property-groups/{id}/properties` | Propiedades pertenecientes al conjunto |
**Estructura de PropertyGroup:**
```json
{
"id": 1,
"name": "Residencial Centro",
"addressStreet": "Calle Mayor",
"addressNumber": "10",
"addressCity": "Madrid",
"addressPostalCode": "28001",
"isActive": true
}
```
## Inquilinos
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/tenants?search=` | Listar/buscar inquilinos |
| `GET` | `/api/tenants/{id}` | Obtener inquilino por ID |
| `POST` | `/api/tenants` | Crear inquilino |
| `PUT` | `/api/tenants/{id}` | Actualizar inquilino |
| `DELETE` | `/api/tenants/{id}` | Eliminar inquilino (soft-delete) |
## Contratos
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/contracts?propertyId=&tenantId=` | Listar/filtrar contratos |
| `GET` | `/api/contracts/{id}` | Obtener contrato por ID |
| `POST` | `/api/contracts` | Crear contrato (auto: propiedad → ALQUILADO) |
| `PUT` | `/api/contracts/{id}` | Actualizar contrato |
| `POST` | `/api/contracts/{id}/terminate` | Terminar contrato (auto: propiedad → VACIO) |
## Recibos de Ingresos (Income Receipts)
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/income-receipts?propertyId=&contractId=&from=&to=&statusId=` | Listar/filtrar recibos de ingresos |
| `GET` | `/api/income-receipts/pending` | Total pendiente de cobro |
| `GET` | `/api/income-receipts/{id}` | Obtener recibo por ID |
| `POST` | `/api/income-receipts` | Crear recibo |
| `PUT` | `/api/income-receipts/{id}` | Actualizar recibo |
| `PATCH` | `/api/income-receipts/{id}/pay` | Registrar pago |
| `DELETE` | `/api/income-receipts/{id}` | Eliminar recibo |
## Plantillas de Gastos (Expense Templates)
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/expense-templates?propertyId=&categoryId=` | Listar/filtrar plantillas |
| `GET` | `/api/expense-templates/active` | Plantillas activas |
| `GET` | `/api/expense-templates/{id}` | Obtener plantilla por ID |
| `POST` | `/api/expense-templates` | Crear plantilla |
| `PUT` | `/api/expense-templates/{id}` | Actualizar plantilla |
| `PATCH` | `/api/expense-templates/{id}/toggle` | Activar/desactivar plantilla |
| `POST` | `/api/expense-templates/{id}/generate` | Generar recibo de gasto desde plantilla |
| `DELETE` | `/api/expense-templates/{id}` | Eliminar plantilla |
## Recibos de Gastos (Expense Receipts)
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/expense-receipts?propertyId=&templateId=&from=&to=&statusId=` | Listar/filtrar recibos de gastos |
| `GET` | `/api/expense-receipts/{id}` | Obtener recibo por ID |
| `POST` | `/api/expense-receipts` | Crear recibo manual |
| `PUT` | `/api/expense-receipts/{id}` | Actualizar recibo |
| `PATCH` | `/api/expense-receipts/{id}/pay` | Registrar pago |
| `DELETE` | `/api/expense-receipts/{id}` | Eliminar recibo |
## Incidencias
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/incidents?propertyId=&statusId=` | Listar/filtrar incidencias |
| `GET` | `/api/incidents/{id}` | Obtener incidencia por ID |
| `POST` | `/api/incidents` | Crear incidencia |
| `PATCH` | `/api/incidents/{id}/status` | Actualizar estado |
| `PATCH` | `/api/incidents/{id}/assign` | Asignar técnico |
| `PATCH` | `/api/incidents/{id}/schedule` | Programar reparación |
| `DELETE` | `/api/incidents/{id}` | Eliminar incidencia |
**Flujo de estados de incidencia:**
`SIN_REVISAR``TECNICO_AVISADO``REPARACION_PREVISTA``REPARADO`
`IGNORADO`
`ANULADO`
## Mantenimiento Programado
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/maintenance?propertyId=` | Listar mantenimientos |
| `GET` | `/api/maintenance/pending` | Mantenimientos pendientes |
| `GET` | `/api/maintenance/upcoming?from=&to=` | Próximos mantenimientos |
| `GET` | `/api/maintenance/{id}` | Obtener por ID |
| `POST` | `/api/maintenance` | Crear mantenimiento |
| `PUT` | `/api/maintenance/{id}` | Actualizar |
| `PATCH` | `/api/maintenance/{id}/complete` | Marcar completado |
| `DELETE` | `/api/maintenance/{id}` | Eliminar |
## Recibos
| Método | Ruta | Descripción | Rol |
|--------|------|-------------|-----|
| `GET` | `/api/receipts` | Listar recibos | ADMIN, GERENTE, CONTABLE |
| `GET` | `/api/receipts/{id}` | Obtener recibo | ADMIN, GERENTE, CONTABLE |
| `POST` | `/api/receipts/generate` | Generar recibo individual | ADMIN, GERENTE, CONTABLE |
| `POST` | `/api/receipts/generate-monthly` | Generar recibos mensuales | ADMIN |
| `GET` | `/api/receipts/{id}/pdf` | Descargar PDF | ADMIN, GERENTE, CONTABLE |
| `POST` | `/api/receipts/{id}/send-email` | Enviar por email | ADMIN, GERENTE, CONTABLE |
| `GET` | `/api/receipts/reports/monthly?year=&month=` | Reporte Excel mensual | ADMIN, GERENTE, CONTABLE |
### `POST /api/receipts/generate`
Genera un recibo individual para un contrato específico.
**Request body:**
```json
{
"contractId": 1,
"issueDate": "2026-07-01",
"dueDate": "2026-07-15",
"description": "Alquiler julio 2026"
}
```
### `POST /api/receipts/generate-monthly`
Genera recibos para todos los contratos activos cuyo `payment_day` coincida con el mes actual. Solo ADMIN.
### `GET /api/receipts/{id}/pdf`
Devuelve el PDF del recibo como `application/pdf` con header `Content-Disposition: attachment; filename="recibo-R-2026-00001.pdf"`.
### `POST /api/receipts/{id}/send-email`
Envía el recibo por email al inquilino con el PDF adjunto.
### `GET /api/receipts/reports/monthly?year=2026&month=7`
Descarga un informe Excel (`.xlsx`) con el resumen de recibos de ingresos, recibos de gastos y balance del mes.
## Notificaciones
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/notifications?unreadOnly=true` | Listar notificaciones del usuario |
| `GET` | `/api/notifications/unread-count` | Contar no leídas |
| `PATCH` | `/api/notifications/{id}/read` | Marcar como leída |
| `PATCH` | `/api/notifications/read-all` | Marcar todas como leídas |
## Dashboard
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/api/dashboard/summary` | Resumen general (contadores, YTD) |
| `GET` | `/api/dashboard/income-expense?year=2026` | Ingresos/gastos mensuales del año |
**Respuesta de `/summary`:**
```json
{
"success": true,
"data": {
"totalProperties": 10,
"rentedProperties": 5,
"activeContracts": 5,
"totalTenants": 8,
"openIncidents": 2,
"pendingMaintenance": 1,
"incomeYtd": 42500.00,
"expenseYtd": 12300.00,
"pendingIncome": 2850.00
}
}
```
## Documentos
| Método | Ruta | Descripción |
|--------|------|-------------|
| `POST` | `/api/documents/upload` | Subir archivo (multipart) |
| `GET` | `/api/documents/entity/{entityType}/{entityId}` | Documentos de una entidad |
| `GET` | `/api/documents/types-for-entity/{entityType}` | Tipos de documento permitidos para una entidad |
| `GET` | `/api/documents/search` | Búsqueda avanzada con filtros |
| `GET` | `/api/documents/{id}/download` | Descargar documento |
| `DELETE` | `/api/documents/{id}` | Eliminar documento |
| `GET` | `/api/documents/{id}/entities` | Ver entidades asociadas a un documento |
| `POST` | `/api/documents/{id}/entities` | Asociar documento a otra entidad |
| `DELETE` | `/api/documents/{id}/entities/{entityType}/{entityId}` | Desasociar documento de una entidad |
**Tipos de entidad soportados:** `PROPERTY`, `CONTRACT`, `TENANT`, `INCIDENT`, `MAINTENANCE`
**Upload params:** `file` (multipart), `entityType`, `entityId`, `documentTypeId`, `description` (opcional)
### `GET /api/documents/types-for-entity/{entityType}`
Devuelve los tipos de documento permitidos para una entidad, indicando cuáles son obligatorios.
**Response (200):**
```json
{
"success": true,
"data": [
{
"documentTypeId": 1,
"documentTypeName": "CONTRATO",
"canUpload": true,
"mustHave": true,
"description": "Documento principal del contrato de alquiler"
},
{
"documentTypeId": 10,
"documentTypeName": "OTRO",
"canUpload": true,
"mustHave": false,
"description": "Otros documentos del contrato"
}
]
}
```
## Códigos de Error
| Código | Significado |
|--------|-------------|
| 200 | OK |
| 201 | Creado |
| 400 | Bad Request (validación, datos incorrectos) |
| 401 | No autenticado |
| 403 | No autorizado (rol insuficiente) |
| 404 | Recurso no encontrado |
| 409 | Conflicto (duplicado) |
| 500 | Error interno del servidor |
## Documentación Interactiva (Swagger)
Disponible en `http://localhost:8080/swagger-ui.html` cuando el backend está corriendo. También se puede obtener el spec OpenAPI en `http://localhost:8080/api-docs`.
+155
View File
@@ -0,0 +1,155 @@
# Arquitectura del Sistema
## 1. Visión General
Sa Polar es un **monolito modular** con frontend separado. El backend Spring Boot expone una API RESTful que consume un frontend React. La base de datos MySQL se inicializa mediante un script SQL ejecutado en el arranque del contenedor.
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Cliente │────▶│ Frontend │────▶│ Backend │────▶│ MySQL 8 │
│ (Browser) │ │ (React 19) │ │ (Spring Boot)│ │ │
│ │◀────│ (Vite 8) │◀────│ (Java 21) │◀────│ │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
│ /api/* │ JPA/Hibernate
▼ ▼
Nginx (proxy) JWT Security
```
## 2. Componentes
### 2.1 Backend (Spring Boot 3.4.1)
El backend se organiza en **paquetes verticales** por dominio de negocio:
| Paquete | Responsabilidad |
|---------|----------------|
| `auth` | Autenticación JWT, login, registro, refresh |
| `user` | CRUD de usuarios, roles, permisos |
| `property` | Gestión de inmuebles (jerárquica), tipos, estados y conjuntos (PropertyGroup) |
| `tenant` | Gestión de inquilinos (personas físicas/jurídicas) |
| `contract` | Contratos de alquiler, estados, periodos de pago |
| `finance.income` | Recibos de ingresos (IncomeReceipt), cobros, categorías |
| `finance.expense` | Plantillas de gastos (ExpenseTemplate), recibos de gastos (ExpenseReceipt), categorías |
| `finance.receipt` | Recibos, PDF, email, reportes Excel, scheduler |
| `incident` | Incidencias, prioridades, asignación técnica |
| `maintenance` | Mantenimiento programado recurrente |
| `notification` | Notificaciones por usuario |
| `document` | Gestión de documentos adjuntos (polimórfico) |
| `dashboard` | Agregaciones y resúmenes |
| `config` | Seguridad, CORS, OpenAPI, almacenamiento |
| `common` | DTOs genéricos, excepciones, utilidades |
### 2.2 Frontend (React 19 + TypeScript 6)
Aplicación SPA con las siguientes capas:
| Capa | Descripción |
|------|-------------|
| `api/client.ts` | Instancia Axios con interceptor JWT y redirección 401 |
| `api/auth.ts` | Funciones de login y refresh |
| `api/resources.ts` | Funciones CRUD para cada recurso |
| `contexts/AuthContext.tsx` | Estado global de autenticación |
| `components/Layout.tsx` | Sidebar de navegación + contenido principal |
| `pages/*.tsx` | Páginas individuales (Login, Dashboard, Properties, etc.) |
| `types/api.ts` | Interfaces TypeScript para los DTOs |
### 2.3 Base de datos (MySQL 8)
Esquema gestionado mediante script SQL de inicialización (`db/init.sql`). Hibernate opera en modo `validate` para verificar que el mapeo JPA coincida con el esquema existente.
## 3. Seguridad
### 3.1 Autenticación JWT
1. El usuario envía credenciales a `POST /api/auth/login`
2. El servidor valida contra la base de datos y devuelve:
- `accessToken`: válido por 24 horas
- `refreshToken`: válido por 30 días
3. El frontend almacena los tokens en `localStorage`
4. Cada petición incluye `Authorization: Bearer <token>`
5. El `JwtAuthenticationFilter` extrae y valida el token en cada request
6. Si el token expira, el frontend usa `POST /api/auth/refresh` para obtener uno nuevo
### 3.2 Roles y permisos
| Rol | Acceso |
|-----|--------|
| `ADMIN` | Todos los endpoints, incluyendo gestión de usuarios |
| `GERENTE` | Propiedades, inquilinos, contratos, incidencias, mantenimiento, recibos de ingresos, plantillas de gastos, recibos de gastos, dashboard |
| `CONTABLE` | Recibos de ingresos, plantillas de gastos, recibos de gastos, dashboard, reportes |
| `VISUALIZADOR` | Autenticado (acceso básico de solo lectura según configuración) |
### 3.3 Seguridad adicional
- CSRF deshabilitado (API stateless)
- Sesiones sin estado (`SessionCreationPolicy.STATELESS`)
- CORS configurable mediante `app.cors.allowed-origins`
- Contraseñas almacenadas con BCrypt
## 4. Flujo de Datos
### 4.1 Autenticación
```
Browser Frontend Backend MySQL
│ │ │ │
│ login(user, pass) │ │ │
│──────────────────────▶│ POST /api/auth/login │ │
│ │───────────────────────▶│ │
│ │ │ SELECT user by email │
│ │ │───────────────────────▶│
│ │ │◀───────────────────────│
│ │ │ Verificar BCrypt hash │
│ │ │ Generar JWT tokens │
│ │◀───────────────────────│ │
│◀──────────────────────│ TokenResponse │ │
│ Guardar en localStorage │ │
```
### 4.2 Generación de recibos automáticos
```
Scheduler (cron: 0 0 6 1 * ?)
ReceiptService.generateMonthlyReceipts()
├── Buscar contratos ACTIVOS con payment_day = mes actual
├── Para cada contrato:
│ ├── Obtener siguiente número de serie (ReceiptSeries)
│ ├── Crear registro IncomeReceipt con receipt_number
│ ├── Generar PDF (PdfReceiptService)
│ └── Enviar email si el inquilino tiene email (EmailReceiptService)
└── Log de emails enviados (email_log)
```
## 5. Despliegue
### 5.1 Docker Compose
Tres servicios orquestados:
1. **mysql**: Imagen `mysql:8.0`, puerto `3307:3306`, volumen persistente, script init.sql
2. **backend**: Build multi-etapa (Maven + JRE), puerto `8080:8080`
3. **frontend**: Build multi-etapa (Node + Nginx), puerto `3000:3000`, proxy reverso `/api/` al backend
### 5.2 Entornos
| Entorno | Backend URL | Frontend URL | Propósito |
|---------|-------------|--------------|-----------|
| Desarrollo | `localhost:8080` | `localhost:5173` (Vite) | Desarrollo local |
| Producción | `localhost:8080` | `localhost:3000` (Nginx) | Docker compose |
## 6. Dependencias Externas
| Dependencia | Versión | Uso |
|-------------|---------|-----|
| Spring Boot | 3.4.1 | Framework principal |
| JJWT | 0.12.6 | Tokens JWT |
| SpringDoc OpenAPI | 2.7.0 | Documentación Swagger |
| iText | 8.0.5 | Generación de PDFs |
| Apache POI | 5.3.0 | Generación de Excel |
| MapStruct | 1.6.3 | Mapeo de DTOs (si se usa) |
| Lombok | 1.18.36 | Reducción de boilerplate |
| Flyway | - | Dependencia incluida pero deshabilitada |
+569
View File
@@ -0,0 +1,569 @@
# Esquema de Base de Datos
## Visión General
Base de datos MySQL 8 con 24 tablas. El esquema se inicializa mediante `db/init.sql` en el arranque del contenedor MySQL. Hibernate opera en modo `validate` para verificar que el mapeo JPA coincida con el esquema.
## Diagrama de Tablas
```
property_groups ──── properties ──── property_types
│ │ property_statuses
│ └── property_status_history
roles ──── users ──── user_permissions
├── tenants ──── tenant_types
├── contracts ──── contract_statuses
│ │ payment_periods
│ │
│ └── income_receipts ──── income_categories
│ │ income_statuses
│ │
│ └── email_log
├── expense_templates ──── expense_categories
│ expense_statuses
│ payment_periods
├── expense_receipts ──── expense_categories
│ expense_statuses
├── incidents ──── incident_statuses
│ incident_priorities
├── scheduled_maintenance ──── maintenance_periods
└── notifications ──── notification_types
documents ──── document_types ──── document_type_entity_allowed
receipt_series
```
## Tablas Detalladas
### 1. roles
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(30) UNIQUE | Nombre del rol |
| description | VARCHAR(255) | Descripción |
**Datos semilla:** ADMIN, GERENTE, CONTABLE, VISUALIZADOR
### 2. users
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| username | VARCHAR(50) UNIQUE | Nombre de usuario |
| email | VARCHAR(100) UNIQUE | Correo electrónico |
| password_hash | VARCHAR(255) | Hash BCrypt |
| full_name | VARCHAR(150) | Nombre completo |
| phone | VARCHAR(20) | Teléfono |
| role_id | INT FK → roles(id) | Rol del usuario |
| active | BOOLEAN | Usuario activo (soporta soft-delete) |
| created_at | DATETIME | Fecha de creación |
| updated_at | DATETIME | Fecha de modificación |
| last_login | DATETIME | Último inicio de sesión |
### 3. user_permissions
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| user_id | BIGINT FK → users(id) CASCADE | Usuario |
| permission | VARCHAR(50) | Permiso específico |
| granted | BOOLEAN | Concedido/denegado |
**Unique:** (user_id, permission)
### 4. property_types
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(50) UNIQUE | Tipo (EDIFICIO, PISO, etc.) |
| description | VARCHAR(255) | Descripción |
### 5. property_statuses
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(50) UNIQUE | Estado (DISPONIBLE, ALQUILADO, etc.) |
| description | VARCHAR(255) | Descripción |
### 6. properties
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| parent_id | BIGINT FK → properties(id) SET NULL | Propiedad padre (jerarquía) |
| group_id | BIGINT FK → property_groups(id) SET NULL | Conjunto al que pertenece |
| type_id | INT FK → property_types(id) | Tipo de propiedad |
| status_id | INT FK → property_statuses(id) | Estado actual |
| reference | VARCHAR(50) UNIQUE | Referencia interna |
| name | VARCHAR(200) | Nombre identificativo |
| description | TEXT | Descripción |
| address_street | VARCHAR(200) | Calle |
| address_number | VARCHAR(20) | Número |
| address_city | VARCHAR(100) | Ciudad |
| address_postal_code | VARCHAR(10) | Código postal |
| address_province | VARCHAR(100) | Provincia |
| cadastral_ref | VARCHAR(30) | Referencia catastral |
| surface_m2 | DECIMAL(10,2) | Superficie en m² |
| floor | VARCHAR(50) | Planta / Piso |
| door | VARCHAR(50) | Puerta |
| rental_amount | DECIMAL(12,2) | Importe de alquiler |
| rented_since | DATE | Alquilado desde |
| vacant_since | DATE | Vacío desde |
| occupied_since | DATE | Ocupado desde |
| notes | TEXT | Notas |
| active | BOOLEAN | Activo (soft-delete) |
| created_by | BIGINT FK → users(id) SET NULL | Creado por |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
**Índices:** parent, type, status, active
### 7. property_status_history
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| property_id | BIGINT FK → properties(id) CASCADE | Propiedad |
| status_id | INT FK → property_statuses(id) | Nuevo estado |
| changed_by | BIGINT FK → users(id) SET NULL | Quién cambió |
| changed_at | DATETIME | Cuándo |
| notes | VARCHAR(500) | Motivo del cambio |
### 8. property_groups
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| name | VARCHAR(200) | Nombre del conjunto |
| address_street | VARCHAR(200) | Calle |
| address_number | VARCHAR(20) | Número |
| address_city | VARCHAR(100) | Ciudad |
| address_postal_code | VARCHAR(10) | Código postal |
| is_active | BOOLEAN | Activo |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
**Relaciones:** Una propiedad puede pertenecer opcionalmente a un conjunto. Al eliminar un conjunto, las propiedades asociadas quedan con `group_id = NULL` (`ON DELETE SET NULL`).
### 9. tenant_types
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(30) UNIQUE | PERSONA_FISICA / PERSONA_JURIDICA |
### 10. tenants
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| tenant_type_id | INT FK → tenant_types(id) | Tipo de inquilino |
| fiscal_id | VARCHAR(20) UNIQUE | DNI/NIF/CIF |
| full_name | VARCHAR(200) | Nombre o razón social |
| business_name | VARCHAR(200) | Solo personas jurídicas |
| email | VARCHAR(100) | Correo electrónico |
| phone | VARCHAR(20) | Teléfono |
| address | VARCHAR(300) | Dirección |
| iban | VARCHAR(34) | IBAN para domiciliación |
| notes | TEXT | Notas |
| active | BOOLEAN | Activo (soft-delete) |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
### 11. contract_statuses
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(30) UNIQUE | ACTIVO, VENCIDO, RENOVADO, RESCINDIDO, ANULADO |
### 12. payment_periods
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(30) UNIQUE | MENSUAL, TRIMESTRAL, SEMESTRAL, ANUAL |
### 13. contracts
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| property_id | BIGINT FK → properties(id) | Propiedad |
| tenant_id | BIGINT FK → tenants(id) | Inquilino |
| status_id | INT FK → contract_statuses(id) | Estado |
| period_id | INT FK → payment_periods(id) DEFAULT 1 | Período de pago |
| contract_number | VARCHAR(50) UNIQUE | Número de contrato |
| start_date | DATE | Fecha de inicio |
| end_date | DATE | Fecha de fin |
| renewal_date | DATE | Fecha de renovación |
| rental_amount | DECIMAL(12,2) | Renta mensual |
| deposit_amount | DECIMAL(12,2) | Fianza |
| payment_day | INT DEFAULT 1 | Día de pago |
| payment_day_end | INT DEFAULT NULL | Día final del rango de pago (NULL = día único) |
| iban_charge | VARCHAR(34) | IBAN domiciliación |
| notes | TEXT | Notas |
| signed_at | DATE | Fecha de firma |
| terminated_at | DATE | Fecha de terminación |
| termination_cause | VARCHAR(500) | Causa de terminación |
| created_by | BIGINT FK → users(id) SET NULL | Creado por |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
### 14. document_types
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(50) UNIQUE | Tipo de documento |
**Tipos:** CONTRATO, ANEXO_CONTRATO, DNI_ARREENDATARIO, CIF_EMPRESA, FOTO_PROPIEDAD, FOTO_INCIDENCIA, FACTURA, JUSTIFICANTE_PAGO, CERTIFICADO, OTRO
### 16. documents
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| document_type_id | INT FK → document_types(id) | Tipo |
| original_name | VARCHAR(255) | Nombre original del archivo |
| stored_name | VARCHAR(255) | Nombre almacenado en disco (UUID + extensión) |
| mime_type | VARCHAR(100) | Tipo MIME |
| file_size | BIGINT | Tamaño en bytes |
| description | VARCHAR(500) | Descripción |
| uploaded_by | BIGINT FK → users(id) SET NULL | Subido por |
| uploaded_at | DATETIME | Fecha de subida |
**Relaciones:** Relación One-to-Many con `document_entities`.
### 17. document_entities (tabla pivote)
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| document_id | BIGINT FK → documents(id) CASCADE | Documento |
| entity_type | VARCHAR(30) | Tipo de entidad asociada |
| entity_id | BIGINT | ID de la entidad |
| created_at | DATETIME | Fecha de creación |
**Entidades soportadas:** PROPERTY, TENANT, CONTRACT, INCOME, EXPENSE, INCIDENT, MAINTENANCE
**Caso de uso:** Permite asociar un mismo documento a múltiples entidades. Por ejemplo, una fianza puede estar asociada tanto a un INCOME (cuando se recibe) como a un EXPENSE (cuando se devuelve).
**Índices:** UNIQUE(document_id, entity_type, entity_id), INDEX(entity_type, entity_id)
### 17. document_type_entity_allowed (tabla pivote)
Define qué tipos de documento pueden asociarse a qué tipos de entidades del sistema.
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| document_type_id | INT FK → document_types(id) | Tipo de documento |
| entity_type | VARCHAR(30) | Tipo de entidad (PROPERTY, TENANT, CONTRACT, INCOME, EXPENSE, INCIDENT, MAINTENANCE) |
| can_upload | BOOLEAN | Si el usuario puede subir este tipo en esta entidad |
| must_have | BOOLEAN | Si es obligatorio al crear/editar la entidad |
| description | VARCHAR(255) | Descripción del uso del documento |
| created_at | DATETIME | Fecha de creación |
**Relaciones permitidas:**
| Tipo Documento | Entidad | Obligatorio | Descripción |
|----------------|---------|-------------|-------------|
| CONTRATO | CONTRACT | Sí | Documento principal del contrato |
| CONTRATO | TENANT | No | Copia firmada por inquilino |
| CONTRATO | PROPERTY | No | Contrato asociado al inmueble |
| ANEXO_CONTRATO | CONTRACT | No | Anexos y modificaciones |
| DNI_ARRENDATARIO | TENANT | Sí | DNI del inquilino |
| CIF_EMPRESA | TENANT | No | Para personas jurídicas |
| FOTO_PROPIEDAD | PROPERTY | Sí | Fotos del inmueble |
| FOTO_INCIDENCIA | INCIDENT | No | Fotos de la incidencia |
| FACTURA | EXPENSE | Sí | Factura o justificante del gasto |
| JUSTIFICANTE_PAGO | INCOME | Sí | Justificante de pago |
| CERTIFICADO | TENANT | No | Certificados varios |
| CERTIFICADO | CONTRACT | No | Certificados asociados |
| OTRO | Todas | No | Otros documentos |
**Índices:** UNIQUE(document_type_id, entity_type), INDEX(entity_type)
### 18. income_categories
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| name | VARCHAR(100) | Nombre |
| description | VARCHAR(255) | Descripción |
| active | BOOLEAN | Activo |
**Categorías:** ALQUILER, FIANZA, GASTOS_COMUNIDAD, INTERESES_DEMORA, INDEMNIZACION, OTROS_INGRESOS
### 17. income_statuses
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(30) UNIQUE | PENDIENTE, PAGADO, VENCIDO, PARCIAL, ANULADO |
### 18. income_receipts
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| contract_id | BIGINT FK → contracts(id) SET NULL | Contrato asociado |
| property_id | BIGINT FK → properties(id) | Propiedad (NOT NULL) |
| tenant_id | BIGINT FK → tenants(id) SET NULL | Inquilino |
| bank_account_id | BIGINT FK → bank_accounts(id) SET NULL | Cuenta bancaria |
| is_domiciled | BOOLEAN DEFAULT FALSE | Domiciliado |
| category_id | BIGINT FK → income_categories(id) SET NULL | Categoría |
| status_id | INT FK → income_statuses(id) | Estado |
| period_label | VARCHAR(20) | Etiqueta de período (ej: "2026-07") |
| amount | DECIMAL(12,2) | Importe bruto |
| tax_withheld | DECIMAL(12,2) DEFAULT 0 | Retención IRPF |
| net_amount | DECIMAL(12,2) | Importe neto |
| issue_date | DATE | Fecha de emisión |
| due_date | DATE | Fecha de vencimiento |
| payment_date | DATE | Fecha de pago |
| payment_method | VARCHAR(30) | TRANSFERENCIA / EFECTIVO / BIZUM / RECIBO / TARJETA |
| description | VARCHAR(500) | Descripción |
| receipt_number | VARCHAR(50) | Número de recibo |
| notes | TEXT | Notas |
| created_by | BIGINT FK → users(id) SET NULL | Creado por |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
### 19. expense_categories
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| name | VARCHAR(100) | Nombre |
| description | VARCHAR(255) | Descripción |
| active | BOOLEAN | Activo |
### 20. expense_statuses
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(30) UNIQUE | PENDIENTE, PAGADO, VENCIDO, ANULADO |
### 21. expense_templates
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| property_id | BIGINT FK → properties(id) NULL | Propiedad asociada |
| property_group_id | BIGINT FK → property_groups(id) NULL | Conjunto asociado |
| bank_account_id | BIGINT FK → bank_accounts(id) NULL | Cuenta bancaria |
| is_domiciled | BOOLEAN DEFAULT FALSE | Domiciliado |
| category_id | BIGINT FK → expense_categories(id) SET NULL | Categoría |
| period_id | INT FK → payment_periods(id) | Periodicidad |
| payment_day | INT DEFAULT 1 | Día de pago |
| supplier_name | VARCHAR(200) | Proveedor |
| supplier_fiscal_id | VARCHAR(20) | NIF/CIF |
| amount | DECIMAL(12,2) NULL | Importe fijo (NULL = variable) |
| tax_amount | DECIMAL(12,2) | Importe impuestos |
| description | VARCHAR(500) | Concepto |
| notes | TEXT | Notas |
| is_variable | BOOLEAN DEFAULT FALSE | Importe variable (usuario rellena cada mes) |
| active | BOOLEAN DEFAULT TRUE | Template activo/inactivo |
| created_by | BIGINT FK → users(id) SET NULL | Creador |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
### 22. expense_receipts
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| template_id | BIGINT FK → expense_templates(id) SET NULL | Template origen (NULL = gasto manual) |
| property_id | BIGINT FK → properties(id) NULL | Propiedad asociada |
| property_group_id | BIGINT FK → property_groups(id) NULL | Conjunto asociado |
| bank_account_id | BIGINT FK → bank_accounts(id) NULL | Cuenta bancaria |
| is_domiciled | BOOLEAN DEFAULT FALSE | Domiciliado |
| category_id | BIGINT FK → expense_categories(id) SET NULL | Categoría |
| status_id | INT FK → expense_statuses(id) | Estado |
| supplier_name | VARCHAR(200) | Proveedor |
| supplier_fiscal_id | VARCHAR(20) | NIF/CIF |
| invoice_number | VARCHAR(50) | Número de factura |
| amount | DECIMAL(12,2) DEFAULT 0 | Base imponible |
| tax_amount | DECIMAL(12,2) DEFAULT 0 | Importe impuestos |
| total_amount | DECIMAL(12,2) | Total con impuestos |
| is_variable | BOOLEAN DEFAULT FALSE | Es gasto variable |
| previous_amount | DECIMAL(12,2) NULL | Importe del periodo anterior (para variables) |
| issue_date | DATE | Fecha de emisión |
| due_date | DATE | Fecha de vencimiento |
| payment_date | DATE | Fecha de pago |
| payment_method | VARCHAR(30) | Método de pago |
| description | VARCHAR(500) | Concepto |
| notes | TEXT | Notas |
| created_by | BIGINT FK → users(id) SET NULL | Creador |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
**Índices:** template, property, property_group, category, status, issue_date
### 23. incident_statuses
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(50) UNIQUE | SIN_REVISAR, TECNICO_AVISADO, REPARACION_PREVISTA, REPARADO, IGNORADO, ANULADO |
### 24. incident_priorities
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(20) UNIQUE | BAJA, MEDIA, ALTA, URGENTE |
### 25. incidents
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| property_id | BIGINT FK → properties(id) | Propiedad |
| status_id | INT FK → incident_statuses(id) | Estado |
| priority_id | INT FK → incident_priorities(id) | Prioridad |
| title | VARCHAR(200) | Título |
| description | TEXT | Descripción |
| reported_by | BIGINT FK → users(id) SET NULL | Reportado por |
| assigned_to | BIGINT FK → users(id) SET NULL | Asignado a |
| reported_at | DATETIME | Fecha de reporte |
| scheduled_date | DATE | Fecha prevista reparación |
| resolved_at | DATETIME | Fecha de resolución |
| resolution_notes | TEXT | Notas de resolución |
| cost_estimate | DECIMAL(12,2) | Coste estimado |
| final_cost | DECIMAL(12,2) | Coste final |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
### 26. maintenance_periods
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(30) UNIQUE | UNICA_VEZ, MENSUAL, TRIMESTRAL, etc. |
### 27. scheduled_maintenance
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| property_id | BIGINT FK → properties(id) | Propiedad |
| period_id | INT FK → maintenance_periods(id) | Periodicidad |
| title | VARCHAR(200) | Título |
| description | TEXT | Descripción |
| estimated_cost | DECIMAL(12,2) | Coste estimado |
| last_execution | DATE | Última ejecución |
| next_execution | DATE | Próxima ejecución |
| reminder_days_before | INT DEFAULT 30 | Días antes para recordatorio |
| responsible | VARCHAR(200) | Responsable |
| notes | TEXT | Notas |
| completed | BOOLEAN | Completado |
| completed_at | DATE | Fecha de finalización |
| completed_by | BIGINT FK → users(id) SET NULL | Completado por |
| created_by | BIGINT FK → users(id) SET NULL | Creado por |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
### 28. notification_types
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | INT PK | Auto-increment |
| name | VARCHAR(50) UNIQUE | INCIDENCIA_ABIERTA, MANTENIMIENTO_PROXIMO, RECIBO_VENCIDO, CONTRATO_PROXIMO_VENCER, CONTRATO_VENCIDO, SISTEMA |
### 29. notifications
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| user_id | BIGINT FK → users(id) CASCADE | Usuario destinatario |
| type_id | INT FK → notification_types(id) | Tipo |
| title | VARCHAR(200) | Título |
| body | TEXT | Cuerpo |
| entity_type | VARCHAR(30) | Tipo de entidad relacionada |
| entity_id | BIGINT | ID de entidad |
| sent_by_email | BOOLEAN | Enviado por email |
| read | BOOLEAN | Leído |
| read_at | DATETIME | Fecha de lectura |
| created_at | DATETIME | Creación |
### 30. receipt_series
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| series_name | VARCHAR(50) | Nombre de la serie |
| fiscal_year | INT | Año fiscal |
| last_number | INT DEFAULT 0 | Último número usado |
| prefix | VARCHAR(20) DEFAULT 'R-' | Prefijo del número |
| active | BOOLEAN | Serie activa |
| created_at | DATETIME | Creación |
| updated_at | DATETIME | Modificación |
**Unique:** (series_name, fiscal_year)
### 31. email_log
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | BIGINT PK | Auto-increment |
| income_receipt_id | BIGINT | ID del recibo de ingreso asociado |
| recipient_email | VARCHAR(200) | Email del destinatario |
| subject | VARCHAR(300) | Asunto |
| body | TEXT | Cuerpo del mensaje |
| success | BOOLEAN | Envío exitoso |
| error_message | TEXT | Mensaje de error si falló |
| sent_at | DATETIME | Fecha de envío |
## Notas Técnicas
- **Soft-delete:** Las tablas `users`, `properties`, `tenants` tienen columna `active` para borrado lógico.
- **Índices:** Las columnas más consultadas tienen índices (foreign keys, fechas, estados).
- **Charset:** `utf8mb4` con collation `utf8mb4_unicode_ci` para soporte completo de Unicode.
- **Motor:** Todas las tablas usan InnoDB para integridad referencial y transacciones.
- **Integridad referencial:** 32 constraints FK en total (incluyendo V5 para document_type_entity_allowed).
- **Al borrar el volumen de datos**, el script `init.sql` se ejecuta automáticamente al arrancar el contenedor MySQL.
- **Hibernate:** Opera con `ddl-auto: validate`. Cualquier cambio en las entidades JPA requiere actualizar `init.sql`.
## Codificación de caracteres (UTF-8)
Para evitar problemas con acentos y caracteres especiales (tildes, eñes, etc.) en todo el sistema:
- **Base de datos:** charset `utf8mb4` y collation `utf8mb4_unicode_ci`.
- **Conexión JDBC (`application.yml`):** la URL incluye `characterEncoding=UTF-8&connectionCollation=utf8mb4_unicode_ci` para forzar la lectura/escritura en UTF-8.
- **Respuestas HTTP (`application.yml`):** `server.servlet.encoding.charset=UTF-8` y `force=true` para que el header `Content-Type` siempre incluya `charset=UTF-8`.
- **Frontend (nginx):** directiva `charset utf-8;` en el bloque `server` del `Dockerfile.frontend`.
- **Datos de prueba (`seed.sql`):** el script incluye `SET NAMES utf8mb4;` al inicio para evitar corrupciones al insertar desde clientes con charset por defecto (latin1).
Si se ejecuta `seed.sql` manualmente desde un cliente MySQL, usar siempre `--default-character-set=utf8mb4` o ejecutar primero `SET NAMES utf8mb4;`.
## Datos de prueba (seed)
El archivo `backend/src/main/resources/db/seed.sql` contiene datos de demostración (usuarios adicionales, propiedades, inquilinos, contratos, recibos de ingresos, plantillas de gastos, recibos de gastos, incidencias, etc.).
**Para poblar la base de datos tras un `docker compose down -v`:**
```bash
# Copiar el script al contenedor
docker cp backend/src/main/resources/db/seed.sql sa-polar-mysql:/tmp/seed.sql
# Ejecutarlo forzando UTF-8
docker exec sa-polar-mysql bash -c "mysql -u root -proot --default-character-set=utf8mb4 sa_polar < /tmp/seed.sql"
```
> Las contraseñas de los usuarios de prueba (`admin`, `gerente`, `contable`) son todas `admin123` y solo válidas para desarrollo local.