- Contexto de Negocio
- Modelo de Datos
- Ciclo de Vida del Cliente
- Tenant Isolation
- Relacion Client - Event
- Asignacion de Responsable
- Datos Fiscales
- Adquisicion y Seguimiento
- API y Permisos
- Frontend (Admin Panel)
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 gestiona su evento. La entidad Client representa a la persona o empresa que contrata y paga por los servicios de evento.
| Problema | Solucion |
|---|
| No se sabia quien contrato un evento | Cada evento puede vincularse a un Client |
| No habia datos de contacto del cliente | Campos email, telefono, datos fiscales |
| No se podia rastrear el ciclo de vida comercial | Estados: PROSPECT -> ACTIVE -> COMPLETED |
| No habia asignacion 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 | Descripcion |
|---|
id | UUID PK | Si | Identificador unico |
organizationId | UUID FK | Si | Organizacion propietaria (tenant) |
name | VARCHAR(255) | Si | Nombre completo o nombre comercial |
email | VARCHAR(255) | No | Email de contacto (unico por organizacion) |
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 creacion |
updatedAt | TIMESTAMPTZ | Si | Ultima actualizacion |
deletedAt | TIMESTAMPTZ | No | Soft delete |
ClientType:
| Valor | Descripcion |
|---|
INDIVIDUAL | Persona fisica |
COMPANY | Persona moral / empresa |
ClientStatus:
| Valor | Descripcion |
|---|
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 | Descripcion |
|---|
SOCIAL_MEDIA | Redes sociales (Instagram, Facebook, TikTok) |
REFERRAL | Referido por otro cliente |
GOOGLE | Busqueda 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 organizacion |
(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 organizacion. Los valores NULL en email no participan en la unicidad (comportamiento nativo de PostgreSQL).
| Transicion | 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 | Cancelacion 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 intervencion humana para determinar el momento adecuado de cada transicion.
Cada cliente pertenece a una sola organizacion a traves del campo organizationId. Esto significa que:
- Cada organizacion tiene su propia base de clientes — Un mismo cliente real que trabaje con dos organizaciones distintas tendra dos registros
Client independientes.
- Las queries filtran por organizacion — El service filtra automaticamente por
organizationId del usuario autenticado.
- El email es unico por organizacion — El constraint
@@unique([email, organizationId]) permite que dos organizaciones tengan clientes con el mismo email.
Cuando un Super Admin crea un cliente, debe seleccionar la organizacion destino mediante el OrganizationSelector. Para usuarios normales (Platform Admin, Org Owner, Org Admin), la organizacion se asigna automaticamente desde su contexto de autenticacion.
La relacion entre Client y Event es 1:N con FK nullable:
| Regla | Implementacion |
|---|
| Un cliente puede tener multiples eventos | Relacion 1:N (Client.events[]) |
| Un evento puede no tener cliente asignado | clientId es nullable en Event |
| Eventos existentes no se rompen | La migracion 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 basico |
La Fase 5 del plan (pendiente) conectara el selector de cliente directamente en:
- Formulario de creacion 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 informacion del cliente vinculado
El campo assignedToId es una FK a User que representa al miembro del equipo responsable de la relacion con el cliente.
- Solo se muestran usuarios activos de la organizacion correspondiente
- Al cambiar de organizacion (Super Admin), se limpia automaticamente 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 modulos que necesiten seleccionar un miembro del equipo
Los datos fiscales son opcionales y estan pensados para el contexto fiscal mexicano (CFDI):
| Campo | Formato | Descripcion |
|---|
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 validacion es basica (regex de formato). No incluye validacion ante el SAT (Servicio de Administracion 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 informacion especifica como el nombre del referido, la campana publicitaria o el evento donde se conocio al cliente.
El campo notes (TEXT, max 2000 caracteres en validacion frontend) permite al equipo registrar informacion relevante sobre el cliente: preferencias, historial de comunicaciones, acuerdos verbales, etc.
| Metodo | Ruta | Descripcion | Permiso |
|---|
POST | /admin/clients | Crear cliente | AdminPanel |
GET | /admin/clients | Listar clientes (paginado) | 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 | Descripcion |
|---|
ClerkAuthGuard | Clase | Autenticacion JWT obligatoria |
AdminPanelGuard | Clase | Requiere Super Admin o Platform Admin |
SuperAdminGuard | Delete | Solo Super Admin puede eliminar clientes |
| Parametro | Tipo | Descripcion |
|---|
page | number | Pagina (default: 1) |
limit | number | Registros por pagina (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 organizacion (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 | Descripcion |
|---|
ClientsTable | components/admin/clients-table.tsx | Tabla principal con filtros, paginacion 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 organizacion (Super Admin) |
| Hook | Tipo | Descripcion |
|---|
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 | Validacion | Revalidacion |
|---|
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 | Descripcion |
|---|
modules/clients/clients.controller.ts | Controller con endpoints CRUD |
modules/clients/clients.service.ts | Service con logica de negocio |
modules/clients/dto/create-client.dto.ts | DTO de creacion con class-validator |
modules/clients/dto/update-client.dto.ts | DTO de actualizacion (PartialType) |
modules/clients/clients.module.ts | Modulo NestJS |
prisma/schema.prisma | Modelo Client, enums, FK en Event |
| Archivo | Descripcion |
|---|
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 | Pagina 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 |