Módulo Contact
src/app/modules/contact/
Gestiona los contactos externos del CRM (clientes que inician conversaciones).
Modelo — Contact
| Campo | Tipo | Descripción |
|---|---|---|
_id | ObjectId | ID de MongoDB |
tenantId | ObjectId → Tenant | Tenant propietario |
externalId | string | ID en el sistema origen (ej: WhatsApp ID) |
username | string | Nombre del contacto |
email | string | Email (opcional, en minúsculas) |
phone | string | Teléfono (opcional) |
isBlocked | boolean | Si está bloqueado, no puede enviar mensajes |
firstname, secondFirstname, lastName, secondLastName | string | Nombre desglosado (opcional) |
profilePic | string | URL del avatar. Para Meta se descarga y se sube a avatars/<tenantId>/<contactId>.jpg |
profilePicSourceUrl, profilePicSyncedAt | string / Date | URL remota original y fecha de sincronización del avatar |
originChannelType | string | Canal por el que entró por primera vez. Inmutable: no se pisa en reingresos |
lastChannelType | string | Último canal usado; se refresca en cada autenticación |
labels | ObjectId[] → Label | Etiquetas asignadas |
isActive | boolean | Por defecto true |
metadata | object[] | Datos adicionales libres |
isOnline, lastSeen, lastDisconnectReason | boolean / Date / string | Estado de presencia del contacto en el socket |
createdAt | Date | — |
updatedAt | Date | — |
Endpoints
Base: /api/v1/contacts. Controlador con @Auth() y PermissionsGuard; todo se filtra por el tenant del usuario. Las rutas siguen el patrón CRUD_ROUTES, y las operaciones con :id usan el _id de MongoDB.
| Método | Ruta | Permiso | Descripción |
|---|---|---|---|
POST | /contacts/create | CONTACT_CREATE | Crea un contacto |
GET | /contacts/get-all | CONTACT_READ | Lista; paginada si se envían page y limit |
GET | /contacts/get-one/:id | CONTACT_READ | Obtiene un contacto (con labels pobladas) |
PATCH | /contacts/update/:id | CONTACT_UPDATE | Actualiza un contacto |
DELETE | /contacts/remove/:id | CONTACT_DELETE | Elimina un contacto |
PATCH | /contacts/:id/assign-labels | CONTACT_UPDATE | Reemplaza las etiquetas del contacto |
Con paginación la respuesta incluye data.meta { total, page, limit }. La asignación de etiquetas no es POST /contacts/:id/labels: es PATCH /contacts/:id/assign-labels y sustituye la lista completa ($set sobre labels), no la amplía.
DTOs
Validaciones de class-validator. Los límites provienen de CONTACT_VALIDATION.
Parámetros de ruta
| Parámetro | Tipo | Obligatorio | Validación | Usado en |
|---|---|---|---|---|
id | string | Sí | Ninguna en el controlador; _id de MongoDB | get-one/:id, update/:id, remove/:id, :id/assign-labels |
CreateContactDto (body de POST /contacts/create)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
externalId | string | Sí | @IsString, @MinLength(1), @MaxLength(100) | ID en el sistema de origen; si ya existe en el tenant responde 409 |
username | string | Sí | @IsString, @MinLength(1), @MaxLength(100) | Nombre del contacto |
phone | string | No | @IsString, @Matches: + opcional y de 6 a 15 dígitos | Teléfono en formato E.164 (se guarda como texto) |
firstname | string | No | @IsString, @MaxLength(100) | Primer nombre |
secondFirstname | string | No | @IsString, @MaxLength(100) | Segundo nombre |
lastName | string | No | @IsString, @MaxLength(100) | Apellido |
secondLastName | string | No | @IsString, @MaxLength(100) | Segundo apellido |
email | string | No | @IsEmail | Email (se guarda en minúsculas) |
profilePic | string | No | @IsUrl | URL del avatar |
metadata | object[] | No | @IsArray | Datos adicionales libres |
isActive | boolean | No | @IsBoolean | Por defecto true |
isBlocked | boolean | No | @IsBoolean | Por defecto false |
labels | string[] | No | @IsArray, @IsMongoId({ each: true }) | _id de etiquetas |
tenantId lo toma el controlador del usuario autenticado.
UpdateContactDto (body de PATCH /contacts/update/:id)
Mismos campos que CreateContactDto, todos opcionales (@IsOptional). Diferencia: username solo valida @MaxLength(100) (sin @MinLength).
AssignLabelsDto (body de PATCH /contacts/:id/assign-labels)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
labelIds | string[] | Sí en la práctica | @IsArray, @IsMongoId({ each: true }) | _id de las etiquetas que quedarán asignadas; el tipo es opcional en TypeScript, pero @IsArray rechaza la ausencia, y el controlador usa [] si llegara nulo |
QueryContactDto / FilterContactDto (query de GET /contacts/get-all)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
page | number | No | @Type(() => Number), @IsInt, @Min(1) | Página |
limit | number | No | @Type(() => Number), @IsInt, @Min(1) | Tamaño de página |
username | string | No | @IsString | Regex insensible a mayúsculas |
firstname | string | No | @IsString | Regex insensible a mayúsculas |
lastName | string | No | @IsString | Regex insensible a mayúsculas |
email | string | No | @IsString | Regex insensible a mayúsculas |
phone | string | No | @IsString | Ver la sección Filtro por teléfono |
isActive | boolean | No | @Transform ('true' o true produce true; ausente queda sin filtro), @IsBoolean | Filtra por estado |
isBlocked | boolean | No | Igual que isActive | Filtra por bloqueo |
labels | string[] | No | @Transform (un valor suelto se vuelve arreglo), @IsArray, @IsString({ each: true }) | Contactos que tengan alguna de esas etiquetas ($in) |
Relaciones
- ChatGateway — verifica
isBlockedantes de permitir que el contacto envíe mensajes - TicketModule — cada ticket está asociado a un contacto
- LabelModule — los labels del contacto son referencias a
Label
Contactos de Meta (Instagram / Messenger)
El externalId lleva prefijo de canal: ig:<id> (Instagram) o fb:<id> (Facebook). Si Graph no devuelve perfil (errores 230, 9010, 100), el contacto se crea con nombre anónimo Instagram ······NNNNNN / Facebook ······NNNNNN (últimos 6 dígitos del id). Ver Meta.
Filtro por teléfono
GET /api/v1/contacts/get-all?phone=<número> (FilterContactDto.phone): se normaliza a solo dígitos y coincide con phone guardado como <dígitos> o +<dígitos>, o con externalId = <dígitos>@s.whatsapp.net. Siempre acotado al tenant.