Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
estebancores opened this issue Dec 27, 2023 · 7 comments
Labels
bug Something isn't working no-issue-activity

Comments

@estebancores
Copy link

¿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

@estebancores estebancores added the bug Something isn't working label Dec 27, 2023
@franmastromarino
Copy link

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);

});

Copy link
Contributor

¿Alguna novedad sobre esta ISSUE?

@estebancores
Copy link
Author

¿Alguna novedad sobre esta ISSUE?

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

@rodrimarchese
Copy link
Contributor

rodrimarchese commented Mar 30, 2024

@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.

@rodrimarchese
Copy link
Contributor

@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 ----------
);


@rodrimarchese
Copy link
Contributor

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

Copy link
Contributor

¿Alguna novedad sobre esta ISSUE?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working no-issue-activity
Projects
None yet
Development

No branches or pull requests

3 participants