Skip to main content

Módulo Contact

src/app/modules/contact/

Gestiona los contactos externos del CRM (clientes que inician conversaciones).

Modelo — Contact​

CampoTipoDescripción
_idObjectIdID de MongoDB
tenantIdObjectId → TenantTenant propietario
externalIdstringID en el sistema origen (ej: WhatsApp ID)
usernamestringNombre del contacto
emailstringEmail (opcional, en minúsculas)
phonestringTeléfono (opcional)
isBlockedbooleanSi está bloqueado, no puede enviar mensajes
firstname, secondFirstname, lastName, secondLastNamestringNombre desglosado (opcional)
profilePicstringURL del avatar. Para Meta se descarga y se sube a avatars/<tenantId>/<contactId>.jpg
profilePicSourceUrl, profilePicSyncedAtstring / DateURL remota original y fecha de sincronización del avatar
originChannelTypestringCanal por el que entró por primera vez. Inmutable: no se pisa en reingresos
lastChannelTypestringÚltimo canal usado; se refresca en cada autenticación
labelsObjectId[] → LabelEtiquetas asignadas
isActivebooleanPor defecto true
metadataobject[]Datos adicionales libres
isOnline, lastSeen, lastDisconnectReasonboolean / Date / stringEstado de presencia del contacto en el socket
createdAtDate—
updatedAtDate—

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étodoRutaPermisoDescripción
POST/contacts/createCONTACT_CREATECrea un contacto
GET/contacts/get-allCONTACT_READLista; paginada si se envían page y limit
GET/contacts/get-one/:idCONTACT_READObtiene un contacto (con labels pobladas)
PATCH/contacts/update/:idCONTACT_UPDATEActualiza un contacto
DELETE/contacts/remove/:idCONTACT_DELETEElimina un contacto
PATCH/contacts/:id/assign-labelsCONTACT_UPDATEReemplaza 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ámetroTipoObligatorioValidaciónUsado en
idstringSíNinguna en el controlador; _id de MongoDBget-one/:id, update/:id, remove/:id, :id/assign-labels

CreateContactDto (body de POST /contacts/create)​

CampoTipoObligatorioValidacionesDescripción
externalIdstringSí@IsString, @MinLength(1), @MaxLength(100)ID en el sistema de origen; si ya existe en el tenant responde 409
usernamestringSí@IsString, @MinLength(1), @MaxLength(100)Nombre del contacto
phonestringNo@IsString, @Matches: + opcional y de 6 a 15 dígitosTeléfono en formato E.164 (se guarda como texto)
firstnamestringNo@IsString, @MaxLength(100)Primer nombre
secondFirstnamestringNo@IsString, @MaxLength(100)Segundo nombre
lastNamestringNo@IsString, @MaxLength(100)Apellido
secondLastNamestringNo@IsString, @MaxLength(100)Segundo apellido
emailstringNo@IsEmailEmail (se guarda en minúsculas)
profilePicstringNo@IsUrlURL del avatar
metadataobject[]No@IsArrayDatos adicionales libres
isActivebooleanNo@IsBooleanPor defecto true
isBlockedbooleanNo@IsBooleanPor defecto false
labelsstring[]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)​

CampoTipoObligatorioValidacionesDescripción
labelIdsstring[]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)​

CampoTipoObligatorioValidacionesDescripción
pagenumberNo@Type(() => Number), @IsInt, @Min(1)Página
limitnumberNo@Type(() => Number), @IsInt, @Min(1)Tamaño de página
usernamestringNo@IsStringRegex insensible a mayúsculas
firstnamestringNo@IsStringRegex insensible a mayúsculas
lastNamestringNo@IsStringRegex insensible a mayúsculas
emailstringNo@IsStringRegex insensible a mayúsculas
phonestringNo@IsStringVer la sección Filtro por teléfono
isActivebooleanNo@Transform ('true' o true produce true; ausente queda sin filtro), @IsBooleanFiltra por estado
isBlockedbooleanNoIgual que isActiveFiltra por bloqueo
labelsstring[]No@Transform (un valor suelto se vuelve arreglo), @IsArray, @IsString({ each: true })Contactos que tengan alguna de esas etiquetas ($in)

Relaciones​

  • ChatGateway — verifica isBlocked antes 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.