Guarda información
Guardar información
El framework proporciona una arquitectura robusta para persistir datos durante las conversaciones con tu bot. Esta funcionalidad es esencial para mantener el estado de las interacciones, almacenar información de usuarios, gestionar entidades de negocio y construir aplicaciones con memoria a largo plazo.
La persistencia de datos se estructura en tres capas fundamentales: interfaces (que definen la forma de los datos), modelos (que encapsulan la lógica de negocio), y repositorios (que gestionan las operaciones de base de datos). Esta separación de responsabilidades facilita el mantenimiento, testing y escalabilidad de tu aplicación.
Conceptos fundamentales
Antes de implementar la persistencia de datos, es importante comprender tres elementos clave del framework:
IStorableData
IStorableData es una interfaz base que deben extender todas las interfaces de datos que se almacenarán en la base de datos. Esta interfaz garantiza que tus estructuras de datos sean compatibles con el sistema de persistencia del framework y proporciona propiedades estándar para el manejo de registros.
import { IStorableData } from '@wabot-dev/framework'
export interface IMisDatos extends IStorableData { // Tus propiedades personalizadas nombre: string email: string}Entity
Entity es una clase base genérica que encapsula datos y proporciona lógica de negocio asociada. Todas las entidades del dominio (como Offer, Prospect, Sale, etc.) deben extender de Entity, pasando como tipo genérico la interfaz que define su estructura de datos.
Las entidades no solo almacenan datos, sino que también contienen métodos que implementan reglas de negocio, validaciones y transformaciones relacionadas con esos datos.
import { Entity, IStorableData } from '@wabot-dev/framework'
export interface IMisDatos extends IStorableData { nombre: string}
export class MiEntidad extends Entity<IMisDatos> { // Getters para acceder a los datos get nombre() { return this.data.nombre }
// Métodos de lógica de negocio cambiarNombre(nuevoNombre: string) { this.data.nombre = nuevoNombre }}PgCrudRepository
PgCrudRepository es una clase base que proporciona operaciones CRUD (Create, Read, Update, Delete) preconfiguradas para PostgreSQL. Al extender de esta clase, heredas automáticamente métodos como create(), update(), delete(), findById(), y findAll().
El framework viene con PostgreSQL configurado por defecto, por lo que puedes comenzar a persistir datos inmediatamente sin configuraciones adicionales de base de datos.
import { PgCrudRepository } from '@wabot-dev/framework'import { Pool } from 'pg'
export class MiRepositorio extends PgCrudRepository<MiEntidad> { constructor(pool: Pool) { super(pool, { schema: 'mi_schema', table: 'mi_tabla', constructor: MiEntidad, }) }}Paso 1: Definir las interfaces
El primer paso es crear las interfaces que definen la estructura de los datos que necesitas almacenar. Estas interfaces deben extender de IStorableData y pueden incluir tipos y constantes que representen valores válidos para ciertos campos.
src/models/interfaces/IProspect.ts
import { IStorableData } from '@wabot-dev/framework'
// Constantes para valores predefinidosexport const PROSPECT_INTEREST_LEVEL_OPTIONS = ['high', 'medium', 'low', 'not_qualified'] as const
export const PROSPECT_STATUS_OPTIONS = [ 'new', 'contacted', 'qualified', 'negotiating', 'won', 'lost',] as const
export const PROSPECT_BUDGET_RANGE_OPTIONS = [ 'under_5k', '5k_to_15k', '15k_to_50k', 'over_50k', 'not_disclosed',] as const
export const PROSPECT_CONTACT_SOURCE_OPTIONS = [ 'whatsapp', 'telegram', 'website', 'referral', 'social_media',] as const
export const PROSPECT_INDUSTRY_OPTIONS = [ 'technology', 'retail', 'healthcare', 'finance', 'manufacturing', 'education', 'other',] as const
// Tipos derivados de las constantesexport type IProspectInterestLevel = (typeof PROSPECT_INTEREST_LEVEL_OPTIONS)[number]export type IProspectStatus = (typeof PROSPECT_STATUS_OPTIONS)[number]export type IProspectBudgetRange = (typeof PROSPECT_BUDGET_RANGE_OPTIONS)[number]export type IProspectContactSource = (typeof PROSPECT_CONTACT_SOURCE_OPTIONS)[number]export type IProspectIndustry = (typeof PROSPECT_INDUSTRY_OPTIONS)[number]
// Interface para información de contactoexport interface IProspectContactInfo extends IStorableData { type: 'email' | 'phone' | 'linkedin' | 'website' value: string verified: boolean}
// Interface para objeciones registradasexport interface IProspectObjection extends IStorableData { type: string description: string resolved: boolean resolution?: string timestamp: string}
// Interface para notas del prospectoexport interface IProspectNote extends IStorableData { category: string content: string priority: 'low' | 'normal' | 'high' timestamp: string}
// Interface principal del prospectoexport interface IProspectData extends IStorableData { chatId: string firstName?: string lastName?: string companyName?: string industry?: IProspectIndustry position?: string interestLevel: IProspectInterestLevel status: IProspectStatus budgetRange?: IProspectBudgetRange contactSource: IProspectContactSource contactInfo: IProspectContactInfo[] objections: IProspectObjection[] notes: IProspectNote[] lastInteractionDate: string createdAt: string}src/models/interfaces/IOffer.ts
import { IStorableData } from '@wabot-dev/framework'
export const OFFER_STATUS_OPTIONS = [ 'draft', 'sent', 'negotiating', 'accepted', 'rejected', 'expired',] as const
export type IOfferStatus = (typeof OFFER_STATUS_OPTIONS)[number]
export interface IOfferNote extends IStorableData { type: string content: string priority: 'low' | 'normal' | 'high' timestamp: string}
export interface IOfferData extends IStorableData { chatId: string prospectId: string productName: string quantity: number unitPrice: number discount: number totalPrice: number status: IOfferStatus validUntil: string specialConditions?: string notes: IOfferNote[] createdAt: string updatedAt: string acceptedAt?: string rejectedAt?: string statusHistory: Array<{ status: IOfferStatus reason?: string timestamp: string }>}Paso 2: Crear los modelos (Entidades)
Los modelos extienden de Entity y encapsulan tanto los datos como la lógica de negocio relacionada. Estos modelos proporcionan métodos para manipular datos de forma segura y aplicar reglas de negocio.
src/models/Prospect.ts
import { Entity, IStorableData } from '@wabot-dev/framework'import { IProspectData, IProspectContactInfo, IProspectObjection, IProspectNote, IProspectInterestLevel, IProspectStatus, IProspectBudgetRange, IProspectIndustry,} from './interfaces/IProspect'
export class Prospect extends Entity<IProspectData> { // Getters para acceso a propiedades principales get chatId() { return this.data.chatId }
get fullName() { const { firstName, lastName } = this.data if (firstName && lastName) return `${firstName} ${lastName}` if (firstName) return firstName return 'Prospecto sin nombre' }
get interestLevel() { return this.data.interestLevel }
get status() { return this.data.status }
get budgetRange() { return this.data.budgetRange }
get contactInfo() { return this.data.contactInfo }
get objections() { return this.data.objections }
get notes() { return this.data.notes }
// Métodos de lógica de negocio
/** * Verifica si el prospecto está calificado para recibir una oferta formal */ isQualified(): boolean { const hasHighInterest = this.data.interestLevel === 'high' || this.data.interestLevel === 'medium' const hasReasonableBudget = this.data.budgetRange === '5k_to_15k' || this.data.budgetRange === '15k_to_50k' || this.data.budgetRange === 'over_50k' const hasContactInfo = this.data.contactInfo.length > 0 const isInValidStatus = this.data.status === 'contacted' || this.data.status === 'qualified'
return hasHighInterest && hasReasonableBudget && hasContactInfo && isInValidStatus }
/** * Verifica si tiene objeciones sin resolver */ hasUnresolvedObjections(): boolean { return this.data.objections.some((obj) => !obj.resolved) }
/** * Calcula el puntaje de prioridad del prospecto (0-100) */ calculatePriorityScore(): number { let score = 0
// Puntos por nivel de interés const interestScores = { high: 40, medium: 25, low: 10, not_qualified: 0 } score += interestScores[this.data.interestLevel]
// Puntos por rango de presupuesto const budgetScores = { over_50k: 30, '15k_to_50k': 20, '5k_to_15k': 10, under_5k: 5, not_disclosed: 0, } score += this.data.budgetRange ? budgetScores[this.data.budgetRange] : 0
// Puntos por estado const statusScores = { negotiating: 20, qualified: 15, contacted: 10, new: 5, won: 0, lost: 0, } score += statusScores[this.data.status]
// Penalización por objeciones sin resolver if (this.hasUnresolvedObjections()) { score -= 10 }
return Math.max(0, Math.min(100, score)) }
// Métodos para actualizar información personal
setPersonalInfo(firstName: string, lastName?: string) { this.data.firstName = firstName if (lastName) this.data.lastName = lastName }
setCompanyInfo(companyName: string, industry?: IProspectIndustry, position?: string) { this.data.companyName = companyName if (industry) this.data.industry = industry if (position) this.data.position = position }
setInterestLevel(level: IProspectInterestLevel) { this.data.interestLevel = level }
setStatus(status: IProspectStatus) { this.data.status = status }
setBudgetRange(range: IProspectBudgetRange) { this.data.budgetRange = range }
// Métodos para gestionar información de contacto
addContactInfo(contactInfo: IProspectContactInfo) { // Evitar duplicados const exists = this.data.contactInfo.some( (info) => info.type === contactInfo.type && info.value === contactInfo.value, ) if (!exists) { this.data.contactInfo.push(contactInfo) } }
verifyContactInfo(type: string, value: string) { const contact = this.data.contactInfo.find((info) => info.type === type && info.value === value) if (contact) { contact.verified = true } }
// Métodos para gestionar objeciones
addObjection(objection: IProspectObjection) { this.data.objections.push(objection) }
resolveObjection(objectionType: string, resolution: string) { const objection = this.data.objections.find( (obj) => obj.type === objectionType && !obj.resolved, ) if (objection) { objection.resolved = true objection.resolution = resolution } }
// Métodos para gestionar notas
addNote(note: IProspectNote) { this.data.notes.push(note) }
getHighPriorityNotes(): IProspectNote[] { return this.data.notes.filter((note) => note.priority === 'high') }
// Método para actualizar fecha de última interacción
updateLastInteraction() { this.data.lastInteractionDate = new Date().toISOString() }}src/models/Offer.ts
import { Entity } from '@wabot-dev/framework'import { IOfferData, IOfferStatus, IOfferNote } from './interfaces/IOffer'
export class Offer extends Entity<IOfferData> { // Getters get chatId() { return this.data.chatId }
get prospectId() { return this.data.prospectId }
get status() { return this.data.status }
get totalPrice() { return this.data.totalPrice }
get notes() { return this.data.notes }
get isExpired() { return new Date(this.data.validUntil) < new Date() }
get isActive() { return ['draft', 'sent', 'negotiating'].includes(this.data.status) && !this.isExpired }
// Métodos de lógica de negocio
/** * Calcula el precio total con descuento aplicado */ calculateTotal(): number { const subtotal = this.data.quantity * this.data.unitPrice const discountAmount = subtotal * (this.data.discount / 100) const total = subtotal - discountAmount this.data.totalPrice = total return total }
/** * Calcula el ahorro total por el descuento */ calculateSavings(): number { const subtotal = this.data.quantity * this.data.unitPrice return subtotal * (this.data.discount / 100) }
/** * Actualiza el estado de la oferta y registra en el historial */ updateStatus(newStatus: IOfferStatus, reason?: string) { this.data.status = newStatus this.data.updatedAt = new Date().toISOString()
// Registrar en historial this.data.statusHistory.push({ status: newStatus, reason, timestamp: new Date().toISOString(), })
// Actualizar fechas específicas según el estado if (newStatus === 'accepted') { this.data.acceptedAt = new Date().toISOString() } else if (newStatus === 'rejected') { this.data.rejectedAt = new Date().toISOString() } }
/** * Agrega una nota a la oferta */ addNote(note: IOfferNote) { this.data.notes.push(note) }
/** * Actualiza los términos de la oferta (precio, cantidad, descuento) */ updateTerms(params: { quantity?: number unitPrice?: number discount?: number validUntil?: string specialConditions?: string }) { if (params.quantity) this.data.quantity = params.quantity if (params.unitPrice) this.data.unitPrice = params.unitPrice if (params.discount !== undefined) this.data.discount = params.discount if (params.validUntil) this.data.validUntil = params.validUntil if (params.specialConditions) this.data.specialConditions = params.specialConditions
this.calculateTotal() this.data.updatedAt = new Date().toISOString() }
/** * Extiende la fecha de vencimiento de la oferta */ extendValidity(newValidUntil: string) { this.data.validUntil = newValidUntil this.data.updatedAt = new Date().toISOString() }
/** * Obtiene el historial de cambios de estado */ getStatusHistory() { return this.data.statusHistory.sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), ) }
/** * Verifica si se puede modificar la oferta */ canBeModified(): boolean { return ['draft', 'sent', 'negotiating'].includes(this.data.status) && !this.isExpired }}Paso 3: Crear los repositorios
Los repositorios gestionan las operaciones de persistencia con la base de datos. Extienden de PgCrudRepository para heredar operaciones CRUD básicas y pueden agregar métodos de consulta personalizados.
src/repositories/ProspectRepository.ts
import { Prospect } from '@/models/Prospect'import { PgCrudRepository, singleton } from '@wabot-dev/framework'import { Pool } from 'pg'
@singleton()export class ProspectRepository extends PgCrudRepository<Prospect> { constructor(pool: Pool) { super(pool, { schema: 'sales_bot', table: 'prospect', constructor: Prospect, }) }
/** * Busca un prospecto por su chatId */ async findByChatId(chatId: string): Promise<Prospect | null> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb LIMIT 1 ` const items = await this.query(query, [JSON.stringify({ chatId })]) return items[0] ?? null }
/** * Busca prospectos por estado */ async findByStatus(status: string): Promise<Prospect[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb ORDER BY data->>'lastInteractionDate' DESC ` return await this.query(query, [JSON.stringify({ status })]) }
/** * Busca prospectos calificados (interés alto/medio y presupuesto adecuado) */ async findQualifiedProspects(): Promise<Prospect[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE (data->>'interestLevel' = 'high' OR data->>'interestLevel' = 'medium') AND data->>'budgetRange' IN ('5k_to_15k', '15k_to_50k', 'over_50k') AND (data->>'status' = 'contacted' OR data->>'status' = 'qualified') ORDER BY data->>'lastInteractionDate' DESC ` return await this.query(query, []) }
/** * Busca prospectos por rango de presupuesto */ async findByBudgetRange(budgetRange: string): Promise<Prospect[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb ORDER BY data->>'createdAt' DESC ` return await this.query(query, [JSON.stringify({ budgetRange })]) }
/** * Busca prospectos con objeciones sin resolver */ async findWithUnresolvedObjections(): Promise<Prospect[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE EXISTS ( SELECT 1 FROM jsonb_array_elements(data->'objections') AS objection WHERE objection->>'resolved' = 'false' ) ORDER BY data->>'lastInteractionDate' DESC ` return await this.query(query, []) }
/** * Busca prospectos por industria */ async findByIndustry(industry: string): Promise<Prospect[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb ORDER BY data->>'createdAt' DESC ` return await this.query(query, [JSON.stringify({ industry })]) }
/** * Busca prospectos que no han sido contactados recientemente (más de X días) */ async findInactiveProspects(daysInactive: number): Promise<Prospect[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE (data->>'lastInteractionDate')::timestamp < NOW() - INTERVAL '${daysInactive} days' AND data->>'status' NOT IN ('won', 'lost') ORDER BY data->>'lastInteractionDate' ASC ` return await this.query(query, []) }}src/repositories/OfferRepository.ts
import { Offer } from '@/models/Offer'import { PgCrudRepository, singleton } from '@wabot-dev/framework'import { Pool } from 'pg'
@singleton()export class OfferRepository extends PgCrudRepository<Offer> { constructor(pool: Pool) { super(pool, { schema: 'sales_bot', table: 'offer', constructor: Offer, }) }
/** * Busca una oferta por chatId (última oferta del chat) */ async findByChatId(chatId: string): Promise<Offer | null> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb ORDER BY data->>'createdAt' DESC LIMIT 1 ` const items = await this.query(query, [JSON.stringify({ chatId })]) return items[0] ?? null }
/** * Busca todas las ofertas de un prospecto por prospectId */ async findByProspectId(prospectId: string): Promise<Offer[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb ORDER BY data->>'createdAt' DESC ` return await this.query(query, [JSON.stringify({ prospectId })]) }
/** * Busca ofertas por estado */ async findByStatus(status: string): Promise<Offer[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb ORDER BY data->>'updatedAt' DESC ` return await this.query(query, [JSON.stringify({ status })]) }
/** * Busca ofertas activas (no expiradas y en estado draft, sent, o negotiating) */ async findActiveOffers(): Promise<Offer[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data->>'status' IN ('draft', 'sent', 'negotiating') AND (data->>'validUntil')::timestamp > NOW() ORDER BY data->>'validUntil' ASC ` return await this.query(query, []) }
/** * Busca ofertas próximas a expirar (dentro de los próximos X días) */ async findExpiringOffers(daysUntilExpiration: number): Promise<Offer[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data->>'status' IN ('sent', 'negotiating') AND (data->>'validUntil')::timestamp BETWEEN NOW() AND NOW() + INTERVAL '${daysUntilExpiration} days' ORDER BY data->>'validUntil' ASC ` return await this.query(query, []) }
/** * Busca ofertas por rango de precio total */ async findByPriceRange(minPrice: number, maxPrice: number): Promise<Offer[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE (data->>'totalPrice')::numeric BETWEEN $1 AND $2 ORDER BY (data->>'totalPrice')::numeric DESC ` return await this.query(query, [minPrice, maxPrice]) }
/** * Busca ofertas aceptadas en un rango de fechas */ async findAcceptedInDateRange(startDate: string, endDate: string): Promise<Offer[]> { const query = ` SELECT ${this.columns} FROM ${this.table} WHERE data->>'status' = 'accepted' AND (data->>'acceptedAt')::timestamp BETWEEN $1::timestamp AND $2::timestamp ORDER BY data->>'acceptedAt' DESC ` return await this.query(query, [startDate, endDate]) }
/** * Calcula el valor total de ofertas aceptadas */ async getTotalAcceptedValue(): Promise<number> { const query = ` SELECT SUM((data->>'totalPrice')::numeric) as total FROM ${this.table} WHERE data->>'status' = 'accepted' ` const result = await this.query(query, []) return result[0]?.total || 0 }}Paso 4: Usar los repositorios en los módulos
Ahora que tenemos las interfaces, modelos y repositorios, podemos utilizarlos en nuestros módulos para persistir y recuperar información durante las conversaciones.
src/modules/ProspectManagementModule.ts
import { Chat, mindsetFunction, mindsetModule } from '@wabot-dev/framework'import { ProspectRepository } from '@/repositories/ProspectRepository'import { Prospect } from '@/models/Prospect'import { CreateProspectReq, UpdateProspectInfoReq, AddProspectObjectionReq, ResolveObjectionReq} from './requests'
@mindsetModule({ name: 'prospectManagement', description: ` Módulo de gestión de prospectos que permite crear perfiles, actualizar información, registrar objeciones y dar seguimiento al proceso de calificación de leads `})export class ProspectManagementModule { constructor( private prospectRepository: ProspectRepository, private chat: Chat, ) {}
@mindsetFunction({ description: ` Crea un nuevo registro de prospecto cuando un usuario inicia una conversación por primera vez. Debe ser llamada al inicio de la interacción para establecer el perfil básico del prospecto antes de comenzar la calificación. ` }) async createProspect(req: CreateProspectReq) { const prospect = new Prospect({ chatId: this.chat.id, firstName: req.firstName, lastName: req.lastName, companyName: req.companyName, industry: req.industry, position: req.position, interestLevel: 'medium', // Por defecto status: 'new', contactSource: req.contactSource, contactInfo: [], objections: [], notes: [], lastInteractionDate: new Date().toISOString(), createdAt: new Date().toISOString(), })
await this.prospectRepository.create(prospect)
return { prospectId: prospect.id, message: 'Prospecto creado exitosamente' } }
@mindsetFunction({ description: ` Actualiza la información del prospecto cuando se descubre nueva información durante la conversación, como nivel de interés, rango de presupuesto, estado de calificación, o datos de contacto adicionales. ` }) async updateProspectInfo(req: UpdateProspectInfoReq) { const prospect = await this.resolveProspect()
if (req.interestLevel) prospect.setInterestLevel(req.interestLevel) if (req.status) prospect.setStatus(req.status) if (req.budgetRange) prospect.setBudgetRange(req.budgetRange)
if (req.contactInfo) { prospect.addContactInfo(req.contactInfo) }
prospect.updateLastInteraction() await this.prospectRepository.update(prospect)
return { prospectId: prospect.id, isQualified: prospect.isQualified(), priorityScore: prospect.calculatePriorityScore(), message: 'Información del prospecto actualizada' } }
@mindsetFunction({ description: ` Registra una objeción expresada por el prospecto durante la conversación. Debe ser llamada cada vez que el prospecto manifieste una preocupación, duda o razón por la que no puede avanzar con la compra. ` }) async addProspectObjection(req: AddProspectObjectionReq) { const prospect = await this.resolveProspect()
prospect.addObjection({ type: req.objectionType, description: req.objectionDescription, resolved: false, timestamp: new Date().toISOString(), })
prospect.updateLastInteraction() await this.prospectRepository.update(prospect)
return { prospectId: prospect.id, hasUnresolvedObjections: prospect.hasUnresolvedObjections(), message: 'Objeción registrada' } }
@mindsetFunction({ description: ` Marca una objeción como resuelta cuando se ha manejado exitosamente durante la conversación y el prospecto ha aceptado la respuesta o solución proporcionada.`})async resolveObjection(req: ResolveObjectionReq) { const prospect = await this.resolveProspect() prospect.resolveObjection(req.objectionType, req.resolution) prospect.updateLastInteraction() await this.prospectRepository.update(prospect)
return { prospectId: prospect.id, hasUnresolvedObjections: prospect.hasUnresolvedObjections(), message: 'Objeción resuelta exitosamente' }}
/* Método auxiliar para resolver o crear un prospecto*/private async resolveProspect(): Promise<Prospect> {let prospect = await this.prospectRepository.findByChatId(this.chat.id)if (!prospect) { // Crear prospecto automáticamente si no existe prospect = new Prospect({ chatId: this.chat.id, interestLevel: 'medium', status: 'new', contactSource: 'whatsapp', contactInfo: [], objections: [], notes: [], lastInteractionDate: new Date().toISOString(), createdAt: new Date().toISOString(), }) await this.prospectRepository.create(prospect)}
return prospect}}src/modules/requests/CreateProspectReq.ts
import { param } from '@wabot-dev/framework'
export class CreateProspectReq { @param({ description: ` Primer nombre del prospecto extraído de la conversación. ` }) firstName!: string
@param({ description: ` Apellido del prospecto si fue mencionado en la conversación. ` }) lastName?: string
@param({ description: ` Nombre de la empresa u organización del prospecto si fue mencionado. ` }) companyName?: string
@param({ description: ` Industria o sector al que pertenece el prospecto. Usar valores estandarizados en inglés: 'technology', 'retail', 'healthcare', 'finance', 'manufacturing', 'education', 'other'. ` }) industry?: string
@param({ description: ` Posición o cargo del prospecto en su empresa, como 'CEO', 'Director de Ventas', 'Gerente de Marketing', etc. ` }) position?: string
@param({ description: ` Fuente de contacto del prospecto. Usar valores estandarizados: 'whatsapp', 'telegram', 'website', 'referral', 'social_media'. ` }) contactSource!: string}src/modules/requests/UpdateProspectInfoReq.ts
import { param } from '@wabot-dev/framework'
export class UpdateProspectInfoReq { @param({ description: ` Nivel de interés del prospecto basado en sus respuestas. Valores válidos: 'high' - Muy interesado, quiere avanzar rápidamente 'medium' - Interesado pero evaluando opciones 'low' - Poco interés o dudas significativas 'not_qualified' - No cumple con el perfil deseado `, }) interestLevel?: 'high' | 'medium' | 'low' | 'not_qualified'
@param({ description: ` Estado actual del prospecto en el proceso de venta. Valores válidos: 'new' - Primer contacto 'contacted' - Ya se estableció comunicación 'qualified' - Cumple con los criterios de calificación 'negotiating' - En proceso de negociación 'won' - Cerró la venta exitosamente 'lost' - No se concretó la venta `, }) status?: 'new' | 'contacted' | 'qualified' | 'negotiating' | 'won' | 'lost'
@param({ description: ` Rango de presupuesto disponible del prospecto. Valores válidos: 'under_5k' - Menos de $5,000 USD '5k_to_15k' - Entre $5,000 y $15,000 USD '15k_to_50k' - Entre $15,000 y $50,000 USD 'over_50k' - Más de $50,000 USD 'not_disclosed' - No ha revelado su presupuesto `, }) budgetRange?: 'under_5k' | '5k_to_15k' | '15k_to_50k' | 'over_50k' | 'not_disclosed'
@param({ description: ` Nueva información de contacto del prospecto. Debe incluir el tipo (email, phone, linkedin, website) y el valor correspondiente. `, }) contactInfo?: { type: 'email' | 'phone' | 'linkedin' | 'website' value: string verified: boolean }}src/modules/requests/AddProspectObjectionReq.ts
import { param } from '@wabot-dev/framework'
export class AddProspectObjectionReq { @param({ description: ` Tipo de objeción expresada por el prospecto. Usar categorías estandarizadas en minúsculas y en inglés, como: 'price_too_high', 'need_more_time', 'budget_not_available', 'competitor_comparison', 'technical_concerns', 'implementation_complexity', 'contract_terms', 'roi_uncertainty'. `, }) objectionType!: string
@param({ description: ` Descripción detallada de la objeción tal como fue expresada por el prospecto. Capturar el contexto completo para poder darle seguimiento apropiado. `, }) objectionDescription!: string}src/modules/requests/ResolveObjectionReq.ts
import { param } from '@wabot-dev/framework'
export class ResolveObjectionReq { @param({ description: ` Tipo de objeción que se está resolviendo. Debe coincidir con el tipo usado al registrar la objeción originalmente. `, }) objectionType!: string
@param({ description: ` Explicación de cómo se resolvió la objeción, qué solución se ofreció, o qué información adicional convenció al prospecto. Esta información es valiosa para casos futuros similares. `, }) resolution!: string}Actualización del módulo de gestión de ofertas
Ahora podemos actualizar el módulo de ofertas para que se integre con el repositorio y persista la información:
src/modules/OfferManagementModule.ts (versión actualizada)
import { Chat, mindsetFunction, mindsetModule } from '@wabot-dev/framework'import { CreateOfferReq, UpdateOfferStatusReq, AddOfferNoteReq } from './requests'import { OfferRepository } from '@/repositories/OfferRepository'import { ProspectRepository } from '@/repositories/ProspectRepository'import { Offer } from '@/models/Offer'
@mindsetModule({ name: 'offerManagement', description: ` Módulo de gestión de ofertas comerciales que permite crear, actualizar y dar seguimiento a propuestas comerciales durante el proceso de venta `,})export class OfferManagementModule { constructor( private offerRepository: OfferRepository, private prospectRepository: ProspectRepository, private chat: Chat, ) {}
@mindsetFunction({ description: ` Crea una nueva oferta comercial cuando el prospecto muestra interés en un producto específico o cuando se ha identificado claramente su necesidad y presupuesto. Esta función debe ser llamada después de calificar al prospecto y antes de enviar una propuesta formal. `, }) async createOffer(req: CreateOfferReq) { // Obtener el prospecto asociado const prospect = await this.prospectRepository.findByChatId(this.chat.id)
if (!prospect) { throw new Error('Debe existir un prospecto antes de crear una oferta') }
const offer = new Offer({ chatId: this.chat.id, prospectId: prospect.id, productName: req.productName, quantity: req.quantity, unitPrice: req.unitPrice, discount: req.discount || 0, totalPrice: 0, // Se calculará automáticamente status: 'draft', validUntil: req.validUntil, specialConditions: req.specialConditions, notes: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), statusHistory: [ { status: 'draft', timestamp: new Date().toISOString(), }, ], })
const totalPrice = offer.calculateTotal() await this.offerRepository.create(offer)
return { offerId: offer.id, totalPrice: totalPrice, savings: offer.calculateSavings(), message: 'Oferta creada exitosamente', } }
@mindsetFunction({ description: ` Actualiza el estado de una oferta existente según el progreso de la negociación. Usar cuando el prospecto acepta, rechaza, solicita modificaciones o cuando la oferta expira. Los estados válidos son: 'draft', 'sent', 'negotiating', 'accepted', 'rejected', 'expired'. `, }) async updateOfferStatus(req: UpdateOfferStatusReq) { const offer = await this.offerRepository.findByChatId(this.chat.id)
if (!offer) { throw new Error('No se encontró una oferta activa para este chat') }
offer.updateStatus(req.newStatus, req.reason) await this.offerRepository.update(offer)
return { offerId: offer.id, currentStatus: offer.status, updatedAt: offer.updatedAt, isActive: offer.isActive, message: `Oferta actualizada a estado: ${req.newStatus}`, } }
@mindsetFunction({ description: ` Registra notas importantes sobre la oferta durante la conversación, como objeciones del cliente, solicitudes especiales, puntos de negociación o cualquier información relevante que ayude a dar seguimiento efectivo a la oportunidad de venta. `, }) async addOfferNote(req: AddOfferNoteReq) { const offer = await this.offerRepository.findByChatId(this.chat.id)
if (!offer) { throw new Error('No se encontró una oferta activa para este chat') }
offer.addNote({ type: req.noteType, content: req.noteContent, timestamp: new Date().toISOString(), priority: req.priority || 'normal', })
await this.offerRepository.update(offer)
return { offerId: offer.id, notesCount: offer.notes.length, message: 'Nota agregada exitosamente', } }
@mindsetFunction({ description: ` Modifica los términos de una oferta existente cuando el prospecto solicita cambios en cantidad, precio, descuento o condiciones especiales. Solo se pueden modificar ofertas en estado 'draft', 'sent' o 'negotiating' que no hayan expirado. `, }) async updateOfferTerms(req: UpdateOfferTermsReq) { const offer = await this.offerRepository.findByChatId(this.chat.id)
if (!offer) { throw new Error('No se encontró una oferta activa para este chat') }
if (!offer.canBeModified()) { throw new Error('Esta oferta no puede ser modificada en su estado actual') }
offer.updateTerms({ quantity: req.quantity, unitPrice: req.unitPrice, discount: req.discount, validUntil: req.validUntil, specialConditions: req.specialConditions, })
await this.offerRepository.update(offer)
return { offerId: offer.id, newTotalPrice: offer.totalPrice, newSavings: offer.calculateSavings(), message: 'Términos de la oferta actualizados exitosamente', } }}src/modules/requests/UpdateOfferTermsReq.ts
import { param } from '@wabot-dev/framework'
export class UpdateOfferTermsReq { @param({ description: ` Nueva cantidad de unidades si el prospecto solicita modificar la cantidad. Debe ser un número entero positivo. `, }) quantity?: number
@param({ description: ` Nuevo precio unitario si se negocia un precio diferente al original. Debe ser en dólares (USD). `, }) unitPrice?: number
@param({ description: ` Nuevo porcentaje de descuento si se autoriza un descuento adicional. Debe ser un número entre 0 y 100. `, }) discount?: number
@param({ description: ` Nueva fecha de vencimiento si se extiende la validez de la oferta. Formato ISO (YYYY-MM-DD). `, }) validUntil?: string
@param({ description: ` Condiciones especiales modificadas o adicionales según la negociación. `, }) specialConditions?: string}Ejemplo de uso completo en un mindset
Aquí tienes un ejemplo de cómo usar ambos módulos en un mindset de ventas:
src/mindsets/SalesBotMindset.ts
import { BaseMindset, IMindset, IMindsetIdentity, IMindsetLlm } from '@wabot-dev/framework'import { ProspectManagementModule } from '@/modules/ProspectManagementModule'import { OfferManagementModule } from '@/modules/OfferManagementModule'
export class SalesBotMindset extends BaseMindset implements IMindset { constructor( private prospectManagement: ProspectManagementModule, private offerManagement: OfferManagementModule, ) { super() }
async identity(): Promise<IMindsetIdentity> { return { name: 'Martín Sales', language: 'Español', age: 32, personality: 'Profesional, persuasivo y orientado a resultados', emotions: 'Entusiasta al presentar soluciones, empático con las preocupaciones del cliente, y confiado al manejar objeciones', } }
async skills(): Promise<string> { return ` Eres un experto en identificar las necesidades específicas del cliente mediante preguntas estratégicas que revelan sus puntos de dolor.
Eres un experto en calificar prospectos evaluando su presupuesto, autoridad, necesidad y timeline (metodología BANT).
Eres un experto en presentar productos de forma persuasiva, destacando beneficios concretos que resuelven los problemas identificados del cliente.
Eres un experto en manejar objeciones de venta con empatía, convirtiendo dudas en oportunidades para reforzar el valor de la oferta.
Eres un experto en detectar señales de compra y guiar al cliente hacia el cierre de forma natural sin presionar.
Eres un experto en usar los módulos de gestión de prospectos y ofertas para registrar información valiosa durante la conversación. ` }
async workflow(): Promise<string> { return ` - Saluda al prospecto de forma profesional y cálida. Si es la primera vez que interactúas con este prospecto, usa la función createProspect del módulo de gestión de prospectos para crear su perfil.
- Tu misión principal es calificar al prospecto y, si cumple con los criterios, crear una oferta comercial personalizada que lo motive a comprar.
- Criterios de calificación (BANT): * Budget (Presupuesto): Mínimo $5,000 USD disponible * Authority (Autoridad): Es tomador de decisiones o influye directamente * Need (Necesidad): Tiene un problema claro que nuestro producto resuelve * Timeline (Tiempo): Busca implementar una solución en los próximos 6 meses
- Flujo de calificación:
1. Descubre su necesidad con preguntas como: "¿Qué desafío específico estás buscando resolver?" "¿Cómo está afectando esto a tu operación actualmente?"
2. Evalúa su presupuesto con tacto: "¿Han considerado una inversión para resolver esta situación?" "¿Qué rango de presupuesto están manejando para esta solución?"
3. Confirma su autoridad: "¿Quiénes más están involucrados en esta decisión?" "¿Cuál es tu rol en el proceso de selección de proveedores?"
4. Identifica su timeline: "¿Cuándo necesitarían tener una solución implementada?" "¿Hay alguna fecha límite o evento que impulse esta decisión?"
- Cada vez que descubras información relevante (nivel de interés, presupuesto, industria, objeciones, datos de contacto), usa inmediatamente las funciones del módulo de gestión de prospectos para registrarla: * updateProspectInfo: Para nivel de interés, presupuesto, estado * addProspectObjection: Cuando exprese una preocupación o duda * resolveObjection: Cuando hayas manejado exitosamente una objeción
- Si el prospecto expresa una objeción, regístrala con addProspectObjection y luego manéjala apropiadamente: * "Precio muy alto" → Presenta ROI y ahorro proyectado * "Necesito tiempo" → Crea urgencia con promoción vigente * "Debo consultarlo" → Ofrece hablar con referencias de clientes actuales Una vez resuelta, marca como resuelta con resolveObjection.
- Cuando el prospecto esté calificado (cumple criterios BANT), usa la función createOffer del módulo de gestión de ofertas para crear una propuesta formal.
- Al crear la oferta, asegúrate de: * Seleccionar el paquete apropiado según su necesidad y presupuesto * Aplicar descuento si está autorizado (máximo 15% sin aprobación) * Establecer validez de 7-14 días para crear urgencia * Incluir condiciones especiales que se hayan negociado
- Durante la negociación de la oferta: * Usa addOfferNote para registrar puntos importantes de la conversación * Si solicita cambios, usa updateOfferTerms (validando que esté autorizado) * Actualiza el estado con updateOfferStatus según avance la negociación
- Información de productos y precios:
Paquete Starter: $5,000 USD - Hasta 20 usuarios - Soporte estándar por email - Implementación básica incluida - Ideal para: Pequeñas empresas y startups
Paquete Professional: $12,000 USD - Hasta 100 usuarios - Soporte prioritario 24/7 - Implementación avanzada y capacitación - Integraciones con sistemas existentes - Ideal para: Empresas medianas en crecimiento
Paquete Enterprise: Cotización personalizada - Usuarios ilimitados - Soporte premium con gerente dedicado - Implementación completa y customización - SLA garantizado - Ideal para: Grandes corporaciones
- Beneficios clave a destacar: * Reducción de costos operativos promedio del 35% en 6 meses * Implementación rápida: 2-4 semanas * ROI positivo típicamente en 8-10 meses * Más de 200 empresas en Latinoamérica ya lo usan * Garantía de satisfacción de 30 días
- Descuentos autorizados: * Hasta 10% por pago anual anticipado * Hasta 15% para órdenes múltiples (3+ licencias) * Descuentos mayores requieren aprobación de gerencia
- Si el prospecto acepta la oferta, actualiza el estado a 'accepted' y felicítalo por su decisión. Explica los próximos pasos: envío del contrato, proceso de onboarding, timeline de implementación.
- Si rechaza la oferta, actualiza el estado a 'rejected', pregunta cortésmente la razón y regístrala en las notas. Mantén la puerta abierta para futuro contacto.
- Mantén siempre un tono profesional pero amigable. No presiones, pero sé propositivo y guía la conversación hacia el cierre. ` }
async limits(): Promise<string> { return ` No puedes ofrecer descuentos superiores al 15% sin autorización previa.
No puedes compartir información sobre márgenes de ganancia, costos de producción o estrategias de pricing internas.
No puedes garantizar tiempos de entrega o funcionalidades sin confirmar primero con el equipo técnico.
No puedes hablar negativamente sobre competidores ni hacer comparaciones desleales.
No puedes prometer características que el producto no posee actualmente.
No puedes revelar información personal de otros clientes sin su autorización. ` }
async llms(): Promise<IMindsetLlm[]> { return [ { model: 'gpt-4o', provider: 'openai', }, { model: 'claude-3-5-sonnet-20241022', provider: 'anthropic', }, ] }}Con esta estructura completa, tu bot vendedor puede:
- Crear y gestionar perfiles de prospectos
- Calificarlos según criterios BANT
- Registrar objeciones y su resolución
- Crear ofertas comerciales personalizadas
- Dar seguimiento al estado de las negociaciones
- Persistir toda la información para análisis posterior
Toda la información se guarda automáticamente en PostgreSQL y está disponible para consultas, reportes y análisis del proceso de ventas.
Siguiente paso
Aprende a conectar tu bot con los usuarios a través de diferentes plataformas: Controladores y Canales.