# Listas de contactos

## Introducción

### Descripción general

El servicio de Listas de contactos permite crear y administrar colecciones de contactos identificados por teléfono y/o correo electrónico. Cada lista pertenece a un propietario y puede contar con usuarios administradores adicionales que pueden tener acceso de solo lectura o de lectura y escritura. El servicio expone una API REST sobre HTTP y admite dos mecanismos de autenticación: sesión de perfil y token de lista.

### Problema que resuelve

Proporciona una fuente de datos centralizada y reutilizable de contactos que puede emplearse en procesos de difusión, campañas de mensajería y operaciones similares que requieran iterar o validar destinatarios. Elimina la necesidad de mantener listas de contactos en cada proceso individual.

### Casos de uso

- Administración de listas de destinatarios para difusiones por WhatsApp u otros canales.
- Validación de destinatarios contra listas de rebotes duros y listas de exclusión (desuscritos).
- Importación masiva de contactos desde sistemas externos.
- Consulta y filtrado de contactos por teléfono, correo, etiquetas o campos libres.
- Delegación de administración de una lista a otros perfiles sin ceder la propiedad.

### Alcance funcional

- CRUD completo sobre listas de contactos.
- CRUD completo sobre contactos de una lista.
- Gestión de usuarios administradores de lista (requiere ser propietario).
- Importación masiva de contactos mediante arreglo JSON en una sola solicitud.
- Filtrado de contactos por texto libre.
- Campo de metadatos extensibles (`extra`) en cada contacto.
- Filtrado de listas por propiedad exclusiva mediante parámetro de consulta.

---

## Autenticación

El servicio implementa dos mecanismos de autenticación mutuamente excluyentes. La lógica evalúa primero si existe una sesión activa; si no la hay, intenta autenticar por token de lista.

### Sesión autenticada

La sesión es establecida previamente por la capa de autorización del sistema (IDP). Se identifica mediante el campo `session/user/ids` del contexto HTTP. Cuando existe un `ids` válido en sesión, cualquier presentación de token en el encabezado `Authorization` es ignorada.

La sesión de perfil es el único mecanismo que permite operar sobre el listado global de listas (`GET /contacts/`) y sobre la administración de usuarios de lista.

### API Token

Cuando no hay sesión activa, el servicio intenta autenticar la solicitud mediante el token configurado en la lista (`apitoken`).

```http
Authorization: Bearer {apitoken}
```

Si el encabezado no incluye el prefijo `Bearer`, el valor completo se trata como el token directamente.

**Restricciones del API Token:**

- Solo es válido para operaciones sobre la lista específica indicada en la ruta (`list_guid` requerido).
- No puede utilizarse para listar todas las listas del propietario (`GET /contacts/`).
- No puede utilizarse para operaciones de administración de usuarios de lista.
- Otorga acceso equivalente al nivel de escritura (`write`), no permite operaciones reservadas al propietario, como eliminar la lista, eliminar contactos o administrar usuarios.
- Si el `apitoken` de la lista es nulo o vacío, la autenticación por token falla con `401`.

---

## Convenciones generales

### Formato de respuesta exitosa

Las respuestas exitosas siguen la estructura:

```json
{
    "success": true,
    "data": {}
}
```

El campo `data` puede ser un objeto (para operaciones de lectura individual y escritura) o un arreglo (para operaciones de listado). En operaciones de eliminación, `data` es `null`.

### Formato de error

```json
{
    "success": false,
    "code": 400,
    "message": "Descripción del error"
}
```

El campo `code` corresponde al código HTTP de error.

### Códigos HTTP utilizados

| Código | Situación |
|--------|-----------|
| 200 | Operación exitosa |
| 400 | Datos de entrada inválidos o faltantes |
| 401 | Sin autenticación o token inválido |
| 403 | Autenticado pero sin permiso para la operación |
| 404 | Recurso no encontrado |
| 500 | Error interno no controlado |

### GUIDs

Todos los recursos exponen un campo `guid` (UUID v4 generado por el servidor en la creación). Las rutas de acceso a recursos individuales utilizan este `guid`. Los campos `id` (clave primaria, entero autonumérico) son devueltos en las respuestas pero no se utilizan en las rutas de la API.

### Permisos

El servicio define tres niveles de acceso sobre una lista:

| Nivel | Descripción |
|-------|-------------|
| `owner` | Propietario de la lista. Tiene acceso total: leer, escribir, eliminar la lista, eliminar contactos y administrar usuarios de lista. |
| `write` | Administrador con escritura. Puede leer y modificar la lista y sus contactos. No puede eliminar la lista, eliminar contactos ni administrar usuarios. |
| `default` | Administrador de solo lectura. Solo puede leer la lista y sus contactos. |


La autenticación por API Token equivale funcionalmente al nivel `write` en cuanto a qué operaciones permite, pero no puede ejecutar operaciones que requieran `owner`.

---

## Endpoints

El enrutamiento determina el grupo de operaciones según la presencia de los parámetros de ruta `list_guid`, `contact_guid` y `admin_guid`. El método `PUT` es tratado internamente como `PATCH`.

Todas las solicitudes se realizan a...

**URL BASE**: `https://agent.induxsoft.net/`

**Ejemplo**:

- **GET** `https://agent.induxsoft.net/contacts/` (Consultar listas accesibles por el perfil)
- **GET** `https://agent.induxsoft.net/contacts/{list_guid}/elements/` (Listar contactos)
- etc...

Ver [resumen de endpoints](#anexo-tabla-resumen-de-endpoints).

---

### GET /contacts/

Obtiene todas las listas accesibles para el perfil en sesión: aquellas de las que es propietario y aquellas en las que está registrado como administrador.

**URL:** `https://agent.induxsoft.net/contacts/`

**Autenticación:** Sesión requerida. No admite API Token.

**Parámetros de consulta**

| Parámetro | Tipo | Descripción |
|-----------|------|-------------|
| `--only-owner` | bool | Si es `true`, devuelve únicamente las listas de las que el perfil es propietario. Por defecto devuelve todas las accesibles. |

**Ejemplo de solicitud**

```http
GET https://agent.induxsoft.net/contacts/
Authorization: Bearer {ids}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": [
        {
            "id": 1,
            "guid": "a1b2c3d4...",
            "owner": "perfil_guid...",
            "name": "Clientes activos",
            "notes": "Lista para campaña de verano",
            "apitoken": "tok_xyz...",
            "access_level": "owner",
            "owner_name": "Juan García"
        }
    ]
}
```

**Campos de la respuesta**

| Campo | Descripción |
|-------|-------------|
| `id` | Clave primaria interna. |
| `guid` | Identificador global de la lista. |
| `owner` | Identificador global del propietario. |
| `name` | Nombre de la lista. |
| `notes` | Notas. |
| `apitoken` | Token de acceso. Devuelto si existe. |
| `access_level` | Nivel de acceso del perfil en sesión: `owner`, `write` o `default`. |
| `owner_name` | Nombre del propietario resuelto. |

---

### GET /contacts/{list_guid}/

Obtiene las propiedades de una lista específica.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/`

**Autenticación:** Sesión o API Token. Requiere acceso de lectura.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |

**Ejemplo de solicitud**

```http
GET https://agent.induxsoft.net/contacts/a1b2c3d4.../
Authorization: Bearer {ids_o_apitoken}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": {
        "id": 1,
        "guid": "a1b2c3d4...",
        "owner": "perfil_guid...",
        "name": "Clientes activos",
        "notes": "Lista para campaña de verano",
        "apitoken": "tok_xyz...",
        "access_level": "owner",
        "owner_name": "Juan García"
    }
}
```

---

### POST /contacts/

Crea una nueva lista de contactos. El propietario es el perfil en sesión.

**URL:** `https://agent.induxsoft.net/contacts/`

**Autenticación:** Sesión requerida.

**Body (JSON)**

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| `name` | string | Sí | Nombre de la lista. Máximo 32 caracteres efectivos tras recorte. |
| `notes` | string | No | Notas o descripción. |
| `apitoken` | string | No | Token de acceso. Si se omite, no se modifica (en la creación queda nulo). |

**Ejemplo de solicitud**

```http
POST https://agent.induxsoft.net/contacts/
Authorization: Bearer {ids}
Content-Type: application/json

{
    "name": "Prospectos 2025",
    "notes": "Leads de feria",
    "apitoken": "mi-token-secreto"
}
```

**Ejemplo de respuesta**

Devuelve el objeto completo de la lista recién creada (equivalente a `GET https://agent.induxsoft.net/contacts/{list_guid}/`).

```json
{
    "success": true,
    "data": {
        "id": 7,
        "guid": "f9e8d7c6...",
        "owner": "perfil_guid...",
        "name": "Prospectos 2025",
        "notes": "Leads de feria",
        "apitoken": "mi-token-secreto",
        "access_level": "owner",
        "owner_name": "Juan García"
    }
}
```

---

### PUT|PATCH /contacts/{list_guid}/

Actualiza las propiedades de una lista existente.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/`

**Autenticación:** Sesión o API Token. Requiere acceso de escritura.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista a actualizar. |

**Body (JSON)**

Mismos campos que `POST https://agent.induxsoft.net/contacts/`.

**Ejemplo de solicitud**

```http
PATCH https://agent.induxsoft.net/contacts/a1b2c3d4.../
Authorization: Bearer {ids_o_apitoken}
Content-Type: application/json

{
    "name": "Prospectos 2025 (actualizado)",
    "notes": "Actualizado post-evento",
    "apitoken": ""
}
```

**Ejemplo de respuesta**

Devuelve el objeto actualizado de la lista (equivalente a `GET https://agent.induxsoft.net/contacts/{list_guid}/`).

**Consideraciones**

- Enviar `apitoken` con valor vacío (`""`) deshabilita la autenticación por token para esa lista.
- El campo `owner` no es modificable a través de este endpoint.

---

### DELETE /contacts/{list_guid}/

Elimina una lista de contactos.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/`

**Autenticación:** Sesión requerida. Solo el propietario.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista a eliminar. |

**Ejemplo de solicitud**

```http
DELETE https://agent.induxsoft.net/contacts/a1b2c3d4.../
Authorization: Bearer {ids}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": null
}
```

**Consideraciones**

La eliminación es física.

---

### GET /contacts/{list_guid}/elements/

Obtiene la lista de contactos de una lista, con soporte de filtro por texto.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/elements/`

**Autenticación:** Sesión o API Token. Requiere acceso de lectura.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |

**Parámetros de consulta**

| Parámetro | Tipo | Descripción |
|-----------|------|-------------|
| `text` | string | Texto de filtro. Si se proporciona, se aplica búsqueda exacta sobre `phone` y `email`, y búsqueda parcial (`LIKE %text%`) sobre `name` y `tags`. Si se omite o es vacío, devuelve todos los contactos. |

**Ejemplo de solicitud**

```http
GET https://agent.induxsoft.net/contacts/a1b2c3d4.../elements/?text=vip
Authorization: Bearer {ids_o_apitoken}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": [
        {
            "id": 10,
            "guid": "c1d2e3f4...",
            "name": "María López",
            "phone": "5212221234567",
            "email": "maria@ejemplo.com",
            "tags": "vip,confirmado",
            "extra": {
                "empresa": "Acme",
                "ciudad": "CDMX"
            }
        }
    ]
}
```

El campo `extra` se devuelve como objeto JSON, no como cadena.

---

### GET /contacts/{list_guid}/elements/{contact_guid}/

Obtiene la información de un contacto específico.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/elements/{contact_guid}/`

**Autenticación:** Sesión o API Token. Requiere acceso de lectura.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |
| `contact_guid` | GUID del contacto. |

**Ejemplo de solicitud**

```http
GET https://agent.induxsoft.net/contacts/a1b2c3d4.../elements/c1d2e3f4.../
Authorization: Bearer {ids_o_apitoken}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": {
        "id": 10,
        "guid": "c1d2e3f4...",
        "name": "María López",
        "phone": "5212221234567",
        "email": "maria@ejemplo.com",
        "tags": "vip,confirmado",
        "extra": {
            "empresa": "Acme"
        }
    }
}
```

---

### POST /contacts/{list_guid}/elements/

Agrega uno o múltiples contactos a una lista.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/elements/`

**Autenticación:** Sesión o API Token. Requiere acceso de escritura.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |

**Body (JSON) — objeto único**

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| `phone` | string | Condicional | Teléfono. Requerido si `email` está vacío. |
| `email` | string | Condicional | Correo electrónico. Requerido si `phone` está vacío. |
| `name` | string | No | Nombre del contacto. Máximo 50 caracteres. |
| `tags` | string | No | Etiquetas separadas por comas. |
| `extra` | object o string JSON | No | Campos adicionales. Puede enviarse como objeto o como cadena JSON. |
| Campos adicionales | cualquiera | No | Cualquier campo no reconocido se incorpora automáticamente al objeto `extra`. |

**Body (JSON) — arreglo para importación masiva**

Se puede enviar un arreglo de objetos con la misma estructura anterior. Ver sección [Importación masiva de contactos](#importacion-masiva-de-contactos).

**Ejemplo de solicitud (objeto único)**

```http
POST https://agent.induxsoft.net/contacts/a1b2c3d4.../elements/
Authorization: Bearer {ids_o_apitoken}
Content-Type: application/json

{
    "phone": "5212221234567",
    "email": "nuevo@ejemplo.com",
    "name": "Carlos Reyes",
    "tags": "prospecto",
    "empresa": "Beta Corp"
}
```

En este ejemplo, `empresa` no es un campo estándar y se incorpora automáticamente a `extra`.

**Ejemplo de respuesta**

Devuelve el objeto del contacto recién creado.

```json
{
    "success": true,
    "data": {
        "id": 15,
        "guid": "e5f6g7h8...",
        "name": "Carlos Reyes",
        "phone": "5212221234567",
        "email": "nuevo@ejemplo.com",
        "tags": "prospecto",
        "extra": {
            "empresa": "Beta Corp"
        }
    }
}
```

---

### PUT|PATCH /contacts/{list_guid}/elements/{contact_guid}/

Actualiza un contacto existente.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/elements/{contact_guid}/`

**Autenticación:** Sesión o API Token. Requiere acceso de escritura.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |
| `contact_guid` | GUID del contacto a actualizar. |

**Body (JSON)**

Mismos campos que la creación. En actualización, la validación de `phone`/`email` solo aplica si ambos campos están presentes en el body y resultan vacíos simultáneamente.

**Ejemplo de solicitud**

```http
PATCH https://agent.induxsoft.net/contacts/a1b2c3d4.../elements/e5f6g7h8.../
Authorization: Bearer {ids_o_apitoken}
Content-Type: application/json

{
    "tags": "prospecto,contactado"
}
```

**Ejemplo de respuesta**

Devuelve el objeto actualizado del contacto.

**Consideraciones**

- En actualizaciones parciales, los campos no incluidos en el body no son modificados.
- Si `phone` se envía vacío (`""`), se establece como nulo en la base de datos.
- Si `email` se envía vacío (`""`), se establece como nulo en la base de datos.
- Los campos no reconocidos se incorporan a `extra`.

---

### DELETE /contacts/{list_guid}/elements/{contact_guid}/

Elimina un contacto de una lista.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/elements/{contact_guid}/`

**Autenticación:** Sesión requerida. Solo el propietario.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |
| `contact_guid` | GUID del contacto a eliminar. |

**Ejemplo de solicitud**

```http
DELETE https://agent.induxsoft.net/contacts/a1b2c3d4.../elements/e5f6g7h8.../
Authorization: Bearer {ids}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": null
}
```

La eliminación es física.

---

### GET /contacts/{list_guid}/admins/

Obtiene la lista de usuarios administradores de una lista.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/admins/`

**Autenticación:** Sesión requerida. Solo el propietario.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |

**Ejemplo de solicitud**

```http
GET https://agent.induxsoft.net/contacts/a1b2c3d4.../admins/
Authorization: Bearer {ids}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": [
        {
            "id": 3,
            "guid": "adm1guid...",
            "user": "perfil_guid...",
            "can_write": true,
            "user_name": "Ana Torres",
            "user_email": "ana@ejemplo.com",
            "user_phone": "5219991234567"
        }
    ]
}
```

---

### GET /contacts/{list_guid}/admins/{admin_guid}/

Obtiene la información de un administrador específico de una lista.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/admins/{admin_guid}/`

**Autenticación:** Sesión requerida. Solo el propietario.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |
| `admin_guid` | GUID del registro de administrador (`contact_list_usr.sys_guid`). |

**Ejemplo de solicitud**

```http
GET https://agent.induxsoft.net/contacts/a1b2c3d4.../admins/adm1guid.../
Authorization: Bearer {ids}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": {
        "id": 3,
        "guid": "adm1guid...",
        "user": "perfil_guid...",
        "can_write": true,
        "user_name": "Ana Torres",
        "user_email": "ana@ejemplo.com",
        "user_phone": "5219991234567"
    }
}
```

---

### POST /contacts/{list_guid}/admins/

Agrega un administrador a una lista.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/admins/`

**Autenticación:** Sesión requerida. Solo el propietario.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |

**Body (JSON)**

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| `user` | string | Sí | GUID, correo electrónico o número de teléfono móvil del perfil a agregar. |
| `can_write` | bool | No | `true` para acceso de escritura. `false` (predeterminado) para solo lectura. |

**Ejemplo de solicitud**

```http
POST https://agent.induxsoft.net/contacts/a1b2c3d4.../admins/
Authorization: Bearer {ids}
Content-Type: application/json

{
    "user": "ana@ejemplo.com",
    "can_write": true
}
```

**Ejemplo de respuesta**

Devuelve el objeto del administrador recién creado.

```json
{
    "success": true,
    "data": {
        "id": 3,
        "guid": "adm1guid...",
        "user": "perfil_guid...",
        "can_write": true,
        "user_name": "Ana Torres",
        "user_email": "ana@ejemplo.com",
        "user_phone": "5219991234567"
    }
}
```

---

### PUT|PATCH /contacts/{list_guid}/admins/{admin_guid}/

Actualiza los datos de un administrador de lista (principalmente el permiso `can_write`).

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/admins/{admin_guid}/`

**Autenticación:** Sesión requerida. Solo el propietario.

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |
| `admin_guid` | GUID del registro de administrador. |

**Body (JSON)**

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| `can_write` | bool | No | Nuevo nivel de acceso. |

**Ejemplo de solicitud**

```http
PATCH https://agent.induxsoft.net/contacts/a1b2c3d4.../admins/adm1guid.../
Authorization: Bearer {ids}
Content-Type: application/json

{
    "can_write": false
}
```

**Ejemplo de respuesta**

Devuelve el objeto actualizado del administrador.

---

### DELETE /contacts/{list_guid}/admins/{admin_guid}/

Elimina a un usuario de la lista de administradores.

**URL:** `https://agent.induxsoft.net/contacts/{list_guid}/admins/{admin_guid}/`

**Autenticación:** Sesión requerida. Solo el propietario (`RequireOwner`).

**Parámetros de ruta**

| Parámetro | Descripción |
|-----------|-------------|
| `list_guid` | GUID de la lista. |
| `admin_guid` | GUID del registro de administrador. |

**Ejemplo de solicitud**

```http
DELETE https://agent.induxsoft.net/contacts/a1b2c3d4.../admins/adm1guid.../
Authorization: Bearer {ids}
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": null
}
```

---

## Importación masiva de contactos

El endpoint `POST /contacts/{list_guid}/elements/` detecta automáticamente si el body es un objeto o un arreglo. Cuando recibe un arreglo JSON, procesa cada elemento de forma individual y devuelve un arreglo de resultados.

Los errores en elementos individuales son absorbidos: si un contacto del lote falla (por ejemplo, por duplicado), el error se descarta y el procesamiento continúa con el siguiente elemento. Los contactos creados exitosamente aparecen en la respuesta; los fallidos simplemente no se incluyen.

**Ejemplo de solicitud**

```http
POST https://agent.induxsoft.net/contacts/a1b2c3d4.../elements/
Authorization: Bearer {ids_o_apitoken}
Content-Type: application/json

[
    {
        "phone": "5212221230001",
        "name": "Contacto Uno",
        "tags": "importado"
    },
    {
        "phone": "5212221230002",
        "email": "dos@ejemplo.com",
        "name": "Contacto Dos"
    },
    {
        "email": "tres@ejemplo.com"
    }
]
```

**Ejemplo de respuesta**

```json
{
    "success": true,
    "data": [
        {
            "id": 20,
            "guid": "guid-uno...",
            "name": "Contacto Uno",
            "phone": "5212221230001",
            "email": null,
            "tags": "importado",
            "extra": {}
        },
        {
            "id": 21,
            "guid": "guid-dos...",
            "name": "Contacto Dos",
            "phone": "5212221230002",
            "email": "dos@ejemplo.com",
            "tags": null,
            "extra": {}
        },
        {
            "id": 22,
            "guid": "guid-tres...",
            "name": null,
            "phone": null,
            "email": "tres@ejemplo.com",
            "tags": null,
            "extra": {}
        }
    ]
}
```

Si todos los elementos fallan, la respuesta devuelve un arreglo vacío con `success: true`.

---

## Reglas de negocio

### Campos obligatorios

- Al crear una lista: `name` es requerido. El nombre se recorta a 32 caracteres y se eliminan espacios al inicio y final. Un nombre vacío tras el recorte produce error `400`.
- Al crear un contacto: al menos uno de `phone` o `email` debe ser no vacío.
- Al crear un administrador: el campo `user` es requerido y debe corresponder a un perfil de Induxsoft existente.

### Restricciones de duplicidad

- Nombres de lista: un propietario no puede tener dos listas con el mismo nombre (índice único `owner` + `name`).
- Contactos: dentro de una misma lista, no puede haber dos contactos con el mismo `phone` (índice único `list` + `phone`) ni con el mismo `email` (índice único `list` + `email`). Los intentos de crear duplicados resultarán en error de base de datos.
- Administradores: un perfil no puede ser administrador de la misma lista más de una vez (índice único `list` + `user`).

### Reglas de eliminación

- La eliminación de una lista es física.
- La eliminación de contactos es física.
- La eliminación de administradores es física.
- Solo el propietario puede eliminar una lista, un contacto o un administrador.

### Restricciones de permisos

| Operación | Nivel mínimo requerido |
|-----------|----------------------|
| Listar todas las listas propias | Sesión (propietario o administrador) |
| Leer propiedades de una lista | Sesión o API Token (cualquier nivel) |
| Crear lista | Sesión |
| Actualizar lista | Sesión o API Token (nivel `write` o propietario) |
| Eliminar lista | Sesión (solo propietario) |
| Listar contactos | Sesión o API Token (cualquier nivel) |
| Leer contacto | Sesión o API Token (cualquier nivel) |
| Crear contacto | Sesión o API Token (nivel `write` o propietario) |
| Actualizar contacto | Sesión o API Token (nivel `write` o propietario) |
| Eliminar contacto | Sesión (solo propietario) |
| Listar administradores | Sesión (solo propietario) |
| Leer administrador | Sesión (solo propietario) |
| Crear administrador | Sesión (solo propietario) |
| Actualizar administrador | Sesión (solo propietario) |
| Eliminar administrador | Sesión (solo propietario) |

### Validaciones de formato

- El campo `user` en administradores acepta GUID, correo electrónico o número de teléfono móvil. Si no hay coincidencia, error `404`.
- Los campos `phone` y `email` de contactos no tienen validación de formato en la capa de servicio; se almacenan tal como se reciben (con `trim`).
- `name` del contacto se recorta a 50 caracteres; `name` de la lista se recorta a 32 caracteres.

### Comportamientos especiales del campo `extra`

- Cualquier campo del body de un contacto que no sea `name`, `phone`, `email`, `tags` o `extra` se incorpora automáticamente al objeto `extra`.
- El campo `extra` puede enviarse como objeto JSON o como cadena JSON; ambas formas son aceptadas.
- Al leer contactos, `extra` siempre se devuelve como objeto (nunca como cadena). Si el valor almacenado es nulo o inválido, se devuelve `{}`.

### Actualización parcial de contactos

- Los campos no enviados en el body de una actualización no son modificados.
- Si `phone` o `email` se envían explícitamente como cadena vacía, el campo se establece como nulo en la base de datos.
- Si `name` o `tags` no están presentes en el body, no se modifican.

---

## Errores

| Código | Mensaje representativo | Causa |
|--------|------------------------|-------|
| 400 | El nombre de la lista es requerido. | `name` vacío o ausente al crear/actualizar una lista. |
| 400 | El contacto debe tener al menos teléfono o correo. | Ambos campos vacíos al crear o al actualizar con ambos campos presentes. |
| 400 | El perfil es requerido. | Campo `user` vacío al crear/actualizar un administrador. |
| 401 | No se proporciono el token de autenticación. | Header `Authorization` vacío o ausente sin sesión activa. |
| 401 | Se requiere identificador de lista para autenticación por token. | Intento de autenticación por token sin `list_guid` en la ruta. |
| 401 | No autorizado. | Token enviado no coincide con el `apitoken` de la lista. |
| 401 | Método de autenticación no implementado. | Header `Authorization` con prefijo distinto de `bearer`. |
| 401 | Esta operación requiere autenticación por sesión. | Operación que requiere sesión invocada sin sesión activa. |
| 403 | No se pudo determinar el perfil del usuario. | `uid` del perfil vacío en la sesión al verificar acceso. |
| 403 | Esta operación está reservada al propietario de la lista. | Operación `RequireOwner` ejecutada por un no propietario. |
| 403 | No tiene acceso a esta lista. | Perfil sin registro en `contact_list_usr` intentando acceder a una lista ajena. |
| 403 | No tiene permiso de escritura en esta lista. | Administrador de solo lectura intentando escribir. |
| 404 | Lista no encontrada. | `list_guid` no corresponde a ninguna lista activa. |
| 404 | Contacto no encontrado. | `contact_guid` no corresponde a ningún contacto en la lista indicada. |
| 404 | Administrador no encontrado. | `admin_guid` no corresponde a ningún administrador en la lista indicada. |
| 404 | Perfil no encontrado. | El valor de `user` no resuelve ningún perfil de Induxsoft. |
| 500 | (mensaje de excepción interna) | Error no controlado. |

---

## Anexo: Tabla resumen de endpoints

| Método | Ruta | Descripción | Permisos |
|--------|------|-------------|----------|
| GET | `/contacts/` | Listar listas accesibles por el perfil | Sesión |
| GET | `/contacts/{list_guid}/` | Leer propiedades de una lista | Sesión o Token |
| POST | `/contacts/` | Crear lista | Sesión |
| PUT|PATCH | `/contacts/{list_guid}/` | Actualizar lista | Sesión o Token (write) |
| DELETE | `/contacts/{list_guid}/` | Eliminar lista | Sesión (owner) |
| GET | `/contacts/{list_guid}/elements/` | Listar contactos (con filtro opcional) | Sesión o Token |
| GET | `/contacts/{list_guid}/elements/{contact_guid}/` | Leer contacto | Sesión o Token |
| POST | `/contacts/{list_guid}/elements/` | Crear contacto o importación masiva | Sesión o Token (write) |
| PUT|PATCH | `/contacts/{list_guid}/elements/{contact_guid}/` | Actualizar contacto | Sesión o Token (write) |
| DELETE | `/contacts/{list_guid}/elements/{contact_guid}/` | Eliminar contacto | Sesión (owner) |
| GET | `/contacts/{list_guid}/admins/` | Listar administradores | Sesión (owner) |
| GET | `/contacts/{list_guid}/admins/{admin_guid}/` | Leer administrador | Sesión (owner) |
| POST | `/contacts/{list_guid}/admins/` | Agregar administrador | Sesión (owner) |
| PUT|PATCH | `/contacts/{list_guid}/admins/{admin_guid}/` | Actualizar administrador | Sesión (owner) |
| DELETE | `/contacts/{list_guid}/admins/{admin_guid}/` | Eliminar administrador | Sesión (owner) |
