El modelo de negocio de Nvito es servicio directo al cliente: una persona o empresa contacta a Nvito, contrata un paquete de servicios, y el equipo gestióna su evento. La entidad Client representa a la persona o empresa que contrata y paga por los servicios de evento.
| Problema | Solución |
|---|
| No se sabia quien contrato un evento | Cada evento puede vincularse a un Client |
| No habia datos de contacto del cliente | Campos email, teléfono, datos fiscales |
| No se podía rastrear el ciclo de vida comercial | Estados: PROSPECT -> ACTIVE -> COMPLETED |
| No habia asignación de responsable en el equipo | Campo assignedToId con FK a User |
| No se registraba como llego el cliente | Fuente de adquisicion (redes, referido, Google, etc.) |
| No habia datos fiscales para facturacion | RFC, razon social, direccion fiscal |
| Campo | Tipo | Requerido | Descripción |
|---|
id | UUID PK | Si | Identificador único |
organizationId | UUID FK | Si | Organización propietaria (tenant) |
name | VARCHAR(255) | Si | Nombre completo o nombre comercial |
email | VARCHAR(255) | No | Email de contacto (único por organización) |
phone | VARCHAR(20) | No | Telefono / WhatsApp |
type | ClientType | Si | INDIVIDUAL (default) o COMPANY |
status | ClientStatus | Si | Estado del ciclo de vida (default: PROSPECT) |
rfc | VARCHAR(13) | No | Registro Federal de Contribuyentes (Mexico) |
businessName | VARCHAR(255) | No | Razon social para facturacion |
billingAddress | VARCHAR(500) | No | Direccion fiscal |
acquisitionSource | AcquisitionSource | No | Como llego el cliente |
acquisitionDetail | VARCHAR(255) | No | Detalle de la fuente (nombre del referido, campana, etc.) |
notes | TEXT | No | Notas internas del equipo |
assignedToId | UUID FK | No | Responsable en el equipo (FK a User) |
createdBy | UUID FK | No | Usuario que creo el registro |
createdAt | TIMESTAMPTZ | Si | Fecha de creación |
updatedAt | TIMESTAMPTZ | Si | Ultima actualización |
deletedAt | TIMESTAMPTZ | No | Soft delete |
ClientType:
| Valor | Descripción |
|---|
INDIVIDUAL | Persona fisica |
COMPANY | Persona moral / empresa |
ClientStatus:
| Valor | Descripción |
|---|
PROSPECT | Cliente potencial, en etapa de cotizacion |
ACTIVE | Cliente con evento(s) activo(s) |
COMPLETED | Todos los eventos del cliente finalizaron |
INACTIVE | Cliente inactivo o dado de baja |
AcquisitionSource:
| Valor | Descripción |
|---|
SOCIAL_MEDIA | Redes sociales (Instagram, Facebook, TikTok) |
REFERRAL | Referido por otro cliente |
GOOGLE | Búsqueda en Google / SEO / Google Ads |
WEBSITE | Visita directa al sitio web |
DIRECT | Contacto directo (networking, evento presencial) |
OTHER | Otra fuente |
| Indice | Proposito |
|---|
(organization_id) | Clientes por organización |
(status) | Filtro por estado |
(type) | Filtro por tipo |
(assigned_to_id) | Clientes por responsable |
(organization_id, status) | Filtro compuesto eficiente |
(email, organization_id) — Un cliente no puede tener el mismo email dentro de la misma organización. Los valores NULL en email no participan en la unicidad (comportamiento nativo de PostgreSQL).
Ciclo de Vida del Cliente
ACTIVE→COMPLETEDeventos_finalizaron
COMPLETED→ACTIVEnuevo_evento
INACTIVE→PROSPECTretoma_contacto
PROSPECT→INACTIVEsin_actividad
| Transición | Trigger | Contexto |
|---|
PROSPECT -> ACTIVE | Se vincula un evento o se firma contrato | Manual por el equipo |
PROSPECT -> INACTIVE | No se concreto la venta | Manual por el equipo |
ACTIVE -> COMPLETED | Todos los eventos del cliente finalizaron | Manual por el equipo |
ACTIVE -> INACTIVE | Cancelación del servicio | Manual por el equipo |
COMPLETED -> ACTIVE | Nuevo evento contratado | Manual por el equipo |
INACTIVE -> PROSPECT | El cliente retoma contacto | Manual por el equipo |
Nota: Las transiciones de estado son manuales (el equipo de Nvito actualiza el estado). No hay automatizacion por ahora, ya que el ciclo comercial requiere intervención humana para determinar el momento adecuado de cada transición.
Cada cliente pertenece a una sola organización a traves del campo organizationId. Esto significa que:
- Cada organización tiene su propia base de clientes — Un mismo cliente real que trabaje con dos organizaciones distintas tendrá dos registros
Client independientes.
- Las queries filtran por organización — El service filtra automáticamente por
organizationId del usuario autenticado.
- El email es único por organización — El constraint
@@unique([email, organizationId]) permite que dos organizaciones tengan clientes con el mismo email.
Aislamiento Multi-Tenant por Organizacion
Organizacion ATenant aislado
Organizacion BTenant aislado
Juan PerezCliente
Maria LopezCliente
Juan PerezCliente (misma persona)
Pedro GarciaCliente sin evento
Boda PerezEvento activo
XV Anos LopezEvento activo
Cumpleanos PerezEvento activo
Cuando un Super Admin crea un cliente, debe seleccionar la organización destino mediante el OrganizationSelector. Para usuarios normales (Platform Admin, Org Owner, Org Admin), la organización se asigna automáticamente desde su contexto de autenticación.
La relación entre Client y Event es 1:N con FK nullable:
Modelo de Datos — Cliente y Evento
Client4 campos
namestring
emailstring
statusClientStatus
Event5 campos
namestring
dateDateTime
statusEventStatus
| Regla | Implementación |
|---|
| Un cliente puede tener múltiples eventos | Relación 1:N (Client.events[]) |
| Un evento puede no tener cliente asignado | clientId es nullable en Event |
| Eventos existentes no se rompen | La migración no modifica datos existentes |
| Eliminar un cliente no elimina sus eventos | onDelete: SetNull — el clientId pasa a NULL |
| El detalle del cliente muestra sus eventos | findOne incluye events con select básico |
La Fase 5 del plan (pendiente) conectara el selector de cliente directamente en:
- Formulario de creación de evento — Campo autocomplete para vincular un cliente existente
- Formulario de contrato — Auto-popular datos del cliente si el evento ya tiene uno vinculado
- Detalle del evento — Mostrar información del cliente vinculado
El campo assignedToId es una FK a User que representa al miembro del equipo responsable de la relación con el cliente.
Flujo de Asignacion de Responsable
Usuario Admin
ClientFormDialog
MemberSelector
API
Abrir formulario de cliente Renderizar selector de responsable GET /v1/members (miembros org)REST responsableId seleccionado POST /v1/clients { responsableId }REST Cliente creado con responsable Usuario Admin
ClientFormDialog
MemberSelector
API
- Solo se muestran usuarios activos de la organización correspondiente
- Al cambiar de organización (Super Admin), se limpia automáticamente el responsable seleccionado
- Si el usuario asignado es dado de baja, el
onDelete: SetNull en la FK pone assignedToId en NULL
- El componente
MemberSelector es reutilizable para otros módulos que necesiten seleccionar un miembro del equipo
Los datos fiscales son opcionales y estan pensados para el contexto fiscal mexicano (CFDI):
| Campo | Formato | Descripción |
|---|
rfc | Regex: /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/i | RFC de persona fisica (4 letras) o moral (3 letras) |
businessName | VARCHAR(255) | Razon social que aparece en la factura |
billingAddress | VARCHAR(500) | Domicilio fiscal |
La validación es básica (regex de formato). No incluye validación ante el SAT (Servicio de Administración Tributaria), que seria necesaria para un sistema de facturacion completo. Esto es suficiente para el MVP.
- INDIVIDUAL: RFC de 13 caracteres (4 letras + 6 digitos + 3 alfanumericos)
- COMPANY: RFC de 12 caracteres (3 letras + 6 digitos + 3 alfanumericos)
Los campos fiscales son opcionales independientemente del tipo de cliente, pero son particularmente relevantes para clientes de tipo COMPANY.
El campo acquisitionSource permite rastrear de donde viene cada cliente para medir la efectividad de los canales de marketing:
| Canal | Ejemplo de detalle |
|---|
SOCIAL_MEDIA | "Instagram - campana bodas 2026" |
REFERRAL | "Referido por Maria Lopez (Boda Lopez)" |
GOOGLE | "Google Ads - keyword: invitaciones digitales" |
WEBSITE | "Formulario de contacto en nvito.mx" |
DIRECT | "Expo Bodas CDMX Marzo 2026" |
OTHER | "Contacto por WhatsApp" |
El campo acquisitionDetail (VARCHAR 255) complementa la fuente con información específica como el nombre del referido, la campana publicitaria o el evento donde se conocio al cliente.
El campo notes (TEXT, max 2000 caracteres en validación frontend) permite al equipo registrar información relevante sobre el cliente: preferencias, historial de comunicaciones, acuerdos verbales, etc.
| Metodo | Ruta | Descripción | Permiso |
|---|
POST | /admin/clients | Crear cliente | AdminPanel |
GET | /admin/clients | Listar clientes (páginado) | AdminPanel |
GET | /admin/clients/:id | Detalle con eventos | AdminPanel |
PATCH | /admin/clients/:id | Actualizar cliente | AdminPanel |
DELETE | /admin/clients/:id | Soft delete | SuperAdmin |
| Guard | Nivel | Descripción |
|---|
ClerkAuthGuard | Clase | Autenticación JWT obligatoria |
AdminPanelGuard | Clase | Requiere Super Admin o Platform Admin |
SuperAdminGuard | Delete | Solo Super Admin puede eliminar clientes |
| Parametro | Tipo | Descripción |
|---|
page | number | Página (default: 1) |
limit | number | Registros por página (default: 10) |
search | string | Busca en name, email y phone (contains, insensitive) |
status | ClientStatus | Filtra por estado |
type | ClientType | Filtra por tipo |
assignedToId | UUID | Filtra por responsable |
organizationId | UUID | Filtra por organización (Super Admin) |
{
"data": [
{
"id": "uuid",
"name": "Juan Perez",
"email": "juan@example.com",
"type": "INDIVIDUAL",
"status": "ACTIVE",
"assignedTo": { "id": "uuid", "firstName": "Ana", "lastName": "Garcia" },
"_count": { "events": 2 }
}
],
"meta": {
"total": 45,
"page": 1,
"limit": 10,
"totalPages": 5
}
}
| Componente | Archivo | Descripción |
|---|
ClientsTable | components/admin/clients-table.tsx | Tabla principal con filtros, páginación y acciones |
ClientFormDialog | components/admin/client-form-dialog.tsx | Dialog para crear/editar con RHF + Zod |
ClientStatusBadge | components/admin/client-status-badge.tsx | Badge visual por estado |
ClientTypeBadge | components/admin/client-type-badge.tsx | Badge visual por tipo |
MemberSelector | components/organizations/member-selector.tsx | Selector de responsable del equipo |
OrganizationSelector | components/organizations/organization-selector.tsx | Selector de organización (Super Admin) |
| Hook | Tipo | Descripción |
|---|
useClients(params) | Query | Lista paginada con filtros |
useClient(id) | Query | Detalle de un cliente |
useCreateClient() | Mutation | Crear cliente + invalidar cache |
useUpdateClient() | Mutation | Actualizar cliente + invalidar cache |
useDeleteClient() | Mutation | Eliminar cliente + invalidar cache |
| Action | Validación | Revalidación |
|---|
createClientAction(input) | createClientSchema | /admin/clients |
updateClientAction(id, input) | updateClientSchema | /admin/clients |
deleteClientAction(id) | Solo UUID | /admin/clients |
| Campo | Regla |
|---|
name | Requerido, max 255 |
email | Formato email, max 255, opcional |
phone | Max 20, opcional |
type | Enum ClientType, opcional |
status | Enum ClientStatus, opcional |
rfc | Regex RFC mexicano, max 13, opcional |
businessName | Max 255, opcional |
billingAddress | Max 500, opcional |
acquisitionSource | Enum AcquisitionSource, opcional |
acquisitionDetail | Max 255, opcional |
notes | Max 2000, opcional |
assignedToId | UUID valido, opcional |
| Archivo | Descripción |
|---|
modules/clients/clients.controller.ts | Controller con endpoints CRUD |
modules/clients/clients.service.ts | Service con lógica de negocio |
modules/clients/dto/create-client.dto.ts | DTO de creación con class-validator |
modules/clients/dto/update-client.dto.ts | DTO de actualización (PartialType) |
modules/clients/clients.module.ts | Modulo NestJS |
prisma/schema.prisma | Modelo Client, enums, FK en Event |
| Archivo | Descripción |
|---|
lib/api/types/client.ts | Tipos TypeScript |
lib/api/services/clients.service.ts | Servicio API |
lib/hooks/queries/use-clients.ts | React Query hooks |
lib/validations/client.ts | Schema Zod |
lib/actions/clients.ts | Server Actions |
app/(dashboard)/admin/clients/page.tsx | Página principal |
components/admin/clients-table.tsx | Tabla de clientes |
components/admin/client-form-dialog.tsx | Formulario dialog |
components/organizations/member-selector.tsx | Selector de responsable |