codigoencasa / bot-whatsapp

🤖 Crear Chatbot WhatsApp en minutos. Únete a este proyecto OpenSource (Typescript Version Pronto)

Home Page:https://bot-whatsapp.netlify.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[🐛] Cuando uso gotoFlow, el siguiente action o answer, no entra en el callback o no muestra el flowDynamic

estebancores opened this issue · comments

¿Que versión estas usando?

v2

¿Sobre que afecta?

Flujo de palabras (Flow)

Describe tu problema

require('dotenv').config()

const { createBot, createProvider, createFlow, addKeyword, EVENTS } = require('@bot-whatsapp/bot')

const QRPortalWeb = require('@bot-whatsapp/portal')
const BaileysProvider = require('@bot-whatsapp/provider/baileys')
const MockAdapter = require('@bot-whatsapp/database/mock')

const axios = require('axios')

const API_BASE_URL = process.env.API_BASE_URL || 'localhost:3005'

//General options - functions
const dayTop = [...Array(31)]
const monthTop = [...Array(12)]
const keywords = []
for (let i = 0; i < monthTop.length; i++) {
    for (let j = 0; j < dayTop.length; j++) {
        keywords.push(`${j < 10 ? '0' : ''}${j + 1}-${i < 10 ? '0' : ''}${i + 1}`)
    }
}
const dateOptions = {
    weekday: 'short',
    month: 'short',
    day: 'numeric',
    timeZone: 'America/Bogota'
};

const documentTypes = {
    1: 'Cédula',
    2: 'Cédula Extragera',
    3: 'Pasaporte'
}
const documentTypeOptions = Object.values(documentTypes).map((type, index) => {
    return `${index + 1}. ${type}`
})

// General functions
const _getUserData = async (phone) => {
    const user = await axios
        .get(`${API_BASE_URL}/v1/bot/client/${phone}`)
        .catch(
            (error) => console.log('error listando el usuario :> ', error)
        )
    return user.data.data || {
        name: 'test',
        documentType: 'test',
        documentNumber: 'test'
    }
}
const _generateDatesOptions = () => {
    const numDates = [...Array(7)]
    const options = {
        weekday: 'short',
        month: 'short',
        day: 'numeric',
        timeZone: 'America/Bogota'
    };

    const techDateOptions = {
        year: 'numeric',
        month: 'numeric',
        day: 'numeric',
        timeZone: 'America/Bogota'
    }
    const ftmDateObj = (dateString, techDate) => ({ dateString, date: techDate.split('/').reverse().join('-') })

    const dates = numDates.map((_, index) => {
        const option = index + 1
        const dateTemp = new Date(Date.now())
        const dateAdded = new Date(dateTemp.setDate(dateTemp.getDate() + index))

        const dateFmt = dateAdded.toLocaleString('es-CO', options)
        const techDate = dateAdded.toLocaleString('es-CO', techDateOptions)

        const dateToUpperCase = dateFmt[0].toUpperCase() + dateFmt.slice(1)

        if (index === 0) return ftmDateObj(`${option}. Hoy`, techDate)

        if (index === 1) return ftmDateObj(`${option}. Mañana`, techDate)

        return ftmDateObj(`${option}. ${dateToUpperCase}`, techDate)
    })
    return dates
}
const _getAvailableHoursByDate = (date, optionSelected) => {
    const hours = [
        {
            "code": 1,
            "isActive": true,
            "price": 20000,
            "text": "00:00 a 01:00"
        },
        {
            "code": 2,
            "isActive": true,
            "price": 20000,
            "text": "01:00 a 02:00"
        },
        {
            "code": 3,
            "isActive": true,
            "price": 20000,
            "text": "02:00 a 03:00"
        },
        {
            "code": 4,
            "isActive": true,
            "price": 20000,
            "text": "03:00 a 04:00"
        },
        {
            "code": 5,
            "isActive": true,
            "price": 20000,
            "text": "04:00 a 05:00"
        },
        {
            "code": 6,
            "isActive": true,
            "price": 40000,
            "text": "05:00 a 06:00"
        },
        {
            "code": 9,
            "isActive": true,
            "price": 180000,
            "text": "09:00 a 10:30"
        },
        {
            "code": 10,
            "isActive": true,
            "price": 180000,
            "text": "10:30 a 12:00"
        },
        {
            "code": 11,
            "isActive": true,
            "price": 80000,
            "text": "12:00 a 13:00"
        },
        {
            "code": 12,
            "isActive": true,
            "price": 80000,
            "text": "13:00 a 14:00"
        },
        {
            "code": 13,
            "isActive": true,
            "price": 80000,
            "text": "14:00 a 15:00"
        },
        {
            "code": 14,
            "isActive": true,
            "price": 60000,
            "text": "15:00 a 16:00"
        },
        {
            "code": 15,
            "isActive": true,
            "price": 60000,
            "text": "16:00 a 17:00"
        },
        {
            "code": 18,
            "isActive": true,
            "price": 80000,
            "text": "19:00 a 20:00"
        },
        {
            "code": 19,
            "isActive": true,
            "price": 80000,
            "text": "20:00 a 21:00"
        },
        {
            "code": 20,
            "isActive": true,
            "price": 80000,
            "text": "21:00 a 22:00"
        },
        {
            "code": 21,
            "isActive": true,
            "price": 80000,
            "text": "22:00 a 23:00"
        },
        {
            "code": 22,
            "isActive": true,
            "price": 40000,
            "text": "23:00 a 00:00"
        }
    ]
    const holyDates = [
        '2023-12-25',
        '2023-12-26',
        '2023-12-27',
        '2023-12-28',
    ]

    // Si seleccionaron PM o AM debo se filtra el array segun el rango y dependiendo si es festivo o no
    // Si es festivo la llave de partida es 10 sino 11 ya que lunes a viernes tiene más rangos de horas
    // 1 => AM, 2 => PM
    const isMorning = optionSelected === '1'
    const isHoliDay = holyDates.includes(date)
    const scheduledKey = isHoliDay ? 10 : 11
    const avaiableHours = hours.filter((hour) => {
        return isMorning
            ? hour.code < scheduledKey
            : hour.code >= scheduledKey
    })

    return avaiableHours
}
const _getMessageFormatedHours = (date, optionSelected) => {
    const hours = _getAvailableHoursByDate(date, optionSelected)
    const fmtMoneyNumber = (num) => {
        const fmmtNumber = new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' })
            .format(num)
        return fmmtNumber.toString().split(',')[0]
    }
    const optionsFormatHours = hours.map(
        (hour, index) => `${index + 1}. 🕖 ${hour.text} / 💰 ${fmtMoneyNumber(hour.price)}`
    )
    return {
        optionsFormatHours,
        indexedHours: hours.map((hour) => hour.code)
    }
}



const flowConfirmReservation = addKeyword([])
    .addAnswer(
        'Perfecto, vamos a confirmar tu reserva con los siguientes datos:',
        null,
        async (_, { state, flowDynamic }) => {
            console.log('Entra al callback')
            const currentState = state.getMyState()
            console.log(currentState)
            const { user, hoursText, reservation } = currentState

            let dateFmtMessage = new Date(reservation.selectedDate)
            dateFmtMessage = dateFmtMessage.toLocaleString('es-CO', dateOptions)

            const hourPrice = hoursText[reservation.selectedHour].split(' / ')

            let messageString = `\n`
            messageString += `*${user.name}*\n`
            messageString += `*${user.documentType}: ${user.documentNumber}*\n`
            messageString += `\n`
            messageString += `🥅 *Cancha #8*\n`
            messageString += `🗓️ *${dateFmtMessage[0].toUpperCase() + dateFmtMessage.slice(1)}*\n`
            messageString += `*${hourPrice[0].slice(3)}*\n`
            messageString += `*${hourPrice[1]}*\n`
            return flowDynamic(messageString)
        }
    )
    .addAnswer([
        'Escribe:',
        '👉 1 para *Confirmar*',
        '👉 2 para *Iniciar de nuevo*',
    ],
        { capture: true },
        async (ctx, { endFlow, flowDynamic }) => {
            if (ctx.body == '1') {
                await flowDynamic('Perfecto, vamos a generar el link de pago')
            } else {
                return endFlow({ body: 'Su solicitud de reserva cancelada, escribe *"hola"* para iniciar de nuevo' })
            }
        }
    )


// optiona step, create user
const flowCreateNewUser = addKeyword(
    ['Registro', 'registro', 'registrarse', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
)
    .addAnswer(
        ['Escribe tu nombre completo'],
        null,
        async (ctx, { state }) => {
            const { body } = ctx
            const currentState = state.getMyState()
            await state.update({
                ...currentState,
                reservation: {
                    ...currentState.reservation,
                    selectedHour: currentState?.indexedHours[parseInt(body) - 1] // Codigo interno de set en field object Mongo
                }

            })
        }
    )
    .addAnswer('Recuerda incluir todos apellidos',
        { capture: true },
        async (ctx, { state }) => {
            const currentState = state.getMyState()
            state.update({
                ...currentState,
                user: {
                    ...currentState.user,
                    name: ctx.body
                }
            })
        },
        addKeyword('')
            .addAnswer(
                'Escribe tu correo electrónico',
                { capture: true },
                async (ctx, { state }) => {
                    const currentState = state.getMyState()
                    state.update({
                        ...currentState,
                        user: {
                            ...currentState.user,
                            email: ctx.body
                        }
                    })
                },
                addKeyword('')
                    .addAnswer(
                        ['Elige un tipo de documento: ', ...documentTypeOptions],
                        { capture: true },
                        async (ctx, { state }) => {
                            const currentState = state.getMyState()
                            state.update({
                                ...currentState,
                                user: {
                                    ...currentState.user,
                                    documentType: documentTypes[parseInt(ctx.body)]
                                }
                            })
                        },
                        addKeyword('')
                            .addAnswer(
                                'Escribe tu número de documento *(solo números sin puntos ni comas)*',
                                { capture: true },
                                async (ctx, { state }) => {
                                    const currentState = state.getMyState()
                                    state.update({
                                        ...currentState,
                                        user: {
                                            ...currentState.user,
                                            documentNumber: ctx.body
                                        }
                                    })
                                },
                                flowConfirmReservation
                            )
                    )
            )
    )

const flowPreConfirmRegisteredUser = addKeyword(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'])
    .addAction(
        null,
        async (ctx, { state }) => {
            const { body } = ctx
            const currentState = state.getMyState()
            await state.update({
                ...currentState,
                reservation: {
                    ...currentState.reservation,
                    selectedHour: currentState?.indexedHours[parseInt(body) - 1] // Codigo interno de set en field object Mongo
                }
            })
        }
    )
    .addAnswer('Confirmando...', null, null, flowConfirmReservation)

// Select hours options, Step 5
const flowShowHoursAvailable = addKeyword(['1', '2'])
    .addAnswer([
        'Los horarios disponibles son:',
        '*Recuerda que es horario militar (24h)*'
    ],
        null,
        async (ctx, { state, flowDynamic, gotoFlow }) => {
            const currentState = state.getMyState()
            const hours = _getMessageFormatedHours(currentState?.reservation.selectedDate, ctx.body)
            await state.update({ indexedHours: hours['indexedHours'] })
            await state.update({ hoursText: hours['optionsFormatHours'] })
            await flowDynamic(hours['optionsFormatHours'].join('\n'))
        }
    )
    .addAnswer(
        'Cual horario te parece mejor?',
        { capture: true },
        async (ctx, { state, gotoFlow }) => {
            const { body } = ctx
            const currentState = state.getMyState()
            await state.update({
                ...currentState,
                reservation: {
                    ...currentState.reservation,
                    selectedHour: currentState?.indexedHours[parseInt(body) - 1] // Codigo interno de set en field object Mongo
                }
            })

            await gotoFlow(flowConfirmReservation)
        }
    )


const flowShowTimeRangeOptions = addKeyword(['1', '2', '3', '4', '5', '6', '7'])
    .addAnswer('Elige la franja horario en la cual quieres jugar:')
    .addAnswer([
        'Escribe:',
        '👉 1 para *AM* 🌄',
        '👉 2 para *PM* 🌃'
    ],
        null,
        async (ctx, { state }) => {
            const { body } = ctx
            const datesIndexed = _generateDatesOptions().map((date) => date.date)
            const selectedDate = datesIndexed[(parseInt(body) - 1)]
            const currentState = state.getMyState()
            await state.update({
                ...currentState,
                reservation: {
                    selectedDate
                }
            })
        },
        flowShowHoursAvailable
    )



// No context flow for Step 4.2
const flowCustomDateSelectedNoContext = addKeyword(keywords)
    .addAnswer('Elige la franja horario en la cual quieres jugar:')
    .addAnswer([
        'Escribe:',
        '👉 1 para *AM* 🌄',
        '👉 2 para *PM* 🌃'
    ],
        null,
        null,
        flowShowHoursAvailable
    )

// Custom date selected option, Step 4.2
const flowCustomDate = addKeyword('8')
    .addAnswer('Escribe la fecha en formato DD-MM, Ejemplo: 19-10',
        { capture: true },
        async (ctx, { state, flowDynamic, fallBack }) => {

            if (!ctx.body.includes('-')) {
                flowDynamic('Recuerda usar "-" (guión) para separar el día y el mes DD-MM')
                return fallBack()
            }

            const validMonths = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
            const validDays = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
            const [day, month] = ctx.body.split('-')

            if (!validDays.includes(parseInt(day)) || !validMonths.includes(parseInt(month))) {
                flowDynamic('Fecha inválida')
                return fallBack()
            }

            const currentState = state.getMyState()
            const date = new Date()
            const year = date.getFullYear()
            await state.update({
                ...currentState,
                reservation: {
                    selectedDate: `${year}-${month}-${day}`
                }
            })
        },
        flowCustomDateSelectedNoContext
    )


// Date selection, step 3
const flowShowDateOptions = addKeyword(['1', 'uno', 'primera', 'primero'])
    .addAnswer('Elige la fecha en la cuál deseas jugar')
    .addAnswer(
        [
            'Para elegirla, escribe el número que corresponda:',
            ..._generateDatesOptions().map((date) => date.dateString),
            '8. Otra Fecha'
        ],
        null,
        null,
        [flowShowTimeRangeOptions, flowCustomDate]
    )


// Reservation, update, cancel, step 2
const flowFirstAction = addKeyword(['1', 'reservacion', 'reserva', 'reservación'])
    .addAnswer('Escribe el número de la opción que quieres elegir:')
    .addAnswer([
        '👉 1 para *Reservar* 🎾 una cancha.',
        '👉 2 para *Modificar* ✏️ una reserva existente.',
        '👉 3 para *Cancelar* ❌ una reserva.',
    ],
        null,
        async (ctx, { state }) => {
            const user = await _getUserData(ctx.from)
            const currentState = state.getMyState()
            if (user) {
                await state.update({
                    ...currentState,
                    user: {
                        name: user.name,
                        documentType: user.documentType,
                        documentNumber: user.documentNumber,
                    }
                })
            }
        },
        [
            flowShowDateOptions,
            // updateReservationFlow(),
            // cancelReservationFlow()
        ]
    )





// No aceptar mensajes de voz
const flowVoiceNote = addKeyword(EVENTS.VOICE_NOTE)
    .addAnswer('Recuerda que soy un Bot y no puedo escuchar tus notas de voz.')



const flowNoAcceptingTerms = addKeyword(['2', 'No Acepto', 'no acepto'])
    .addAnswer('Para poder hacer uso del sistema de reservas debes aceptar la política de Habeas Data, te invitamos a conocerla en el siguiente link: https://bit.ly/jvkdkc')
    .addAnswer('¿Estás de acuerdo?')
    .addAnswer([
        'Escribe:',
        '👉 1 para *Aceptar*',
        '👉 2 para *No Aceptar*',
    ],
        { capture: true },
        async (ctx, { gotoFlow }) => {
            if (ctx.body == '1') {
                gotoFlow(flowFirstAction)
            }

        },
        // Flow
        addKeyword(['2', 'No Acepto', 'no acepto']).addAnswer('No puedes hacer uso del sistema de reservas al no aceptar las políticas de privacidad.')
    )


// Respuesta a hola Step 1
const flowWelcome = addKeyword(EVENTS.WELCOME)
    .addAnswer('Hola 🙌 Bienvenido al sistema de reservas de *Locos X Pádel* 🎾 para continuar, debes aceptar las políticas de uso de información personal')
    .addAnswer('Conoce las políticas en el siguiente link: https://bit.ly/jvkdkc')
    .addAnswer('¿Estás de acuerdo?')
    .addAnswer([
        'Escribe:',
        '👉 1 para *Aceptar*',
        '👉 2 para *No Aceptar*',
    ],
        null,
        null,
        [flowFirstAction, flowNoAcceptingTerms]
    )



const main = async () => {
    const adapterDB = new MockAdapter()
    const adapterFlow = createFlow([flowWelcome, flowVoiceNote])
    const adapterProvider = createProvider(BaileysProvider)

    createBot({
        flow: adapterFlow,
        provider: adapterProvider,
        database: adapterDB,
    })

    QRPortalWeb({ port: 3001 })
}

main()


Como puede ver el flujo flowConfirmReservation tiene en el callback, un flowDynamic que se supone debe mostrar el mensaje con los datos, sin embargo, nisiquiera entra al callback, lo cual no entiendo, cosa que si pasa si lo llamara desde un contexto anidado en el flujo flowShowHoursAvailable este es todo el código, deberia funcionar con copiar y pegar.

Reproducir error

No response

Información Adicional

No response

Hola @estebancores
Me sucede lo mismo. Si sien se ejecuta addAnswer, no se llama a addAction.

Ejemplo:

const endFlow = import BotWhatsapp from '@bot-whatsapp/bot';

export default BotWhatsapp
.addKeyword(BotWhatsapp.EVENTS.ACTION)
.addAction((_, { endFlow, state }) => {

const currentState = state.getMyState();
const baned = currentState?.baned ?? false

console.log('baned2', baned)

if (baned) return endFlow();

})
.addAnswer(["Lo siento, no podre seguir asistiendote"]);

const welcomeFllow = BotWhatsapp.addKeyword(BotWhatsapp.EVENTS.WELCOME)
.addAnswer("⏱️")
.addAction(async (ctx, ctxFn) => {

const currentState = ctxFn.state.getMyState();

const baned = currentState?.baned ?? false

console.log('baned1', baned)

ctxFn.gotoFlow(endFlow);

});

¿Alguna novedad sobre esta ISSUE?

¿Alguna novedad sobre esta ISSUE?

Nada, al parecer no estan soportando mas la libreria, te toca buscar la manera de no usar gotoflow

@estebancores A mi me paso algo similar. Pude resolverlo agregando al addAnswer, como ultimo parametro, el flow al que lo queria redirigir entre corchetes. trato de dejar un ejemplo con tu codigo con el flow que dijiste que no te funcionaba:

// Select hours options, Step 5
const flowShowHoursAvailable = addKeyword(['1', '2'])
    .addAnswer([
        'Los horarios disponibles son:',
        '*Recuerda que es horario militar (24h)*'
    ],
        null,
        async (ctx, { state, flowDynamic, gotoFlow }) => {
            const currentState = state.getMyState()
            const hours = _getMessageFormatedHours(currentState?.reservation.selectedDate, ctx.body)
            await state.update({ indexedHours: hours['indexedHours'] })
            await state.update({ hoursText: hours['optionsFormatHours'] })
            await flowDynamic(hours['optionsFormatHours'].join('\n'))
        }
    )
    .addAnswer(
        'Cual horario te parece mejor?',
        { capture: true },
        async (ctx, { state, gotoFlow }) => {
            const { body } = ctx
            const currentState = state.getMyState()
            await state.update({
                ...currentState,
                reservation: {
                    ...currentState.reservation,
                    selectedHour: currentState?.indexedHours[parseInt(body) - 1] // Codigo interno de set en field object Mongo
                }
            })
            await gotoFlow(flowConfirmReservation)
        },
        [flowConfirmReservation] // <-------- AGREGA ESTA LINEA ----------
    )

Contame si te sirvio.

@franmastromarino en tu caso seria:

const endFlow = import BotWhatsapp from '@bot-whatsapp/bot';

export default BotWhatsapp
.addKeyword(BotWhatsapp.EVENTS.ACTION)
.addAction((_, { endFlow, state }) => {
const currentState = state.getMyState();
const baned = currentState?.baned ?? false

console.log('baned2', baned)

if (baned) return endFlow();
})
.addAnswer(["Lo siento, no podre seguir asistiendote"]);

const welcomeFllow = BotWhatsapp.addKeyword(BotWhatsapp.EVENTS.WELCOME)
.addAnswer("⏱️")
.addAction(async (ctx, ctxFn) => {
const currentState = ctxFn.state.getMyState();

const baned = currentState?.baned ?? false

console.log('baned1', baned)

ctxFn.gotoFlow(endFlow);
},
[endFlow] // <-------- AGREGA ESTA LINEA ----------
);


@franmastromarino @estebancores tambien es importante agregar todos los flows al adapterFlow !!

¿Alguna novedad sobre esta ISSUE?