From e3ac4f58b33ccf253397e5e7e063924ef3be5cf0 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 21 Apr 2025 22:29:26 -0300 Subject: [PATCH] refactor(GBOService, MainModel): clean up code and enhance model structure for better clarity and maintainability --- packages/core.gbapp/services/GBMinService.ts | 1 + packages/saas.gbapp/dialog/NewUserDialog.ts | 219 +++--------- packages/saas.gbapp/index.ts | 6 +- packages/saas.gbapp/model/MainModel.ts | 131 +++---- packages/saas.gbapp/service/GBOService.ts | 43 +-- packages/saas.gbapp/service/MainService.ts | 349 +++++++------------ web.config | 37 -- 7 files changed, 248 insertions(+), 538 deletions(-) mode change 100755 => 100644 packages/saas.gbapp/dialog/NewUserDialog.ts mode change 100755 => 100644 packages/saas.gbapp/model/MainModel.ts mode change 100755 => 100644 packages/saas.gbapp/service/MainService.ts delete mode 100644 web.config diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index b30a9aad..6bbb0cbd 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -459,6 +459,7 @@ export class GBMinService { this.createCheckHealthAddress(GBServer.globals.server, min, min.instance); + // Setups official handler for WhatsApp. GBServer.globals.server diff --git a/packages/saas.gbapp/dialog/NewUserDialog.ts b/packages/saas.gbapp/dialog/NewUserDialog.ts old mode 100755 new mode 100644 index 4114cc7a..3bd97ed9 --- a/packages/saas.gbapp/dialog/NewUserDialog.ts +++ b/packages/saas.gbapp/dialog/NewUserDialog.ts @@ -1,36 +1,4 @@ // BotServer/packages/saas.gbapp/dialog/NewUserDialog.ts -/*****************************************************************************\ -| █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® | -| ██ █ ███ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| ██ ███ ████ █ ██ █ ████ █████ ██████ ██ ████ █ █ █ ██ | -| ██ ██ █ █ ██ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| █████ █████ █ ███ █████ ██ ██ ██ ██ █████ ████ █████ █ ███ | -| | -| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. | -| Licensed under the AGPL-3.0. | -| | -| According to our dual licensing model, this program can be used either | -| under the terms of the GNU Affero General Public License, version 3, | -| or under a proprietary license. | -| | -| The texts of the GNU Affero General Public License with an additional | -| permission and of our proprietary license can be found at and | -| in the LICENSE file you have received along with this program. | -| | -| This program is distributed in the hope that it will be useful, | -| but WITHOUT ANY WARRANTY, without even the implied warranty of | -| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | -| GNU Affero General Public License for more details. | -| | -| "General Bots" is a registered trademark of pragmatismo.com.br. | -| The licensing of the program under the AGPLv3 does not imply a | -| trademark license. Therefore any rights, title and interest in | -| our trademarks remain entirely with us. | -| | -\*****************************************************************************/ - -'use strict'; - import { IGBDialog, GBMinInstance } from 'botlib'; import { Messages } from '../strings.js'; import { MainService } from '../service/MainService.js'; @@ -44,22 +12,22 @@ export class NewUserDialog extends IGBDialog { id: '/welcome_saas_plan', waterfall: [ async step => { - const locale = 'en-US'; await step.context.sendActivity('Please choose your plan:'); - await step.context.sendActivity('1. Personal - $9.99/month (basic features)'); - await step.context.sendActivity('2. Professional - $29.99/month (advanced features)'); - return await step.prompt('textPrompt', 'Enter 1 or 2 to select your plan:'); + await step.context.sendActivity('1. Free - $0/month (basic features)'); + await step.context.sendActivity('2. Personal - $50/month (more features)'); + await step.context.sendActivity('3. Professional - $150/month (advanced features)'); + return await step.prompt('textPrompt', 'Enter 1, 2 or 3 to select your plan:'); }, async step => { const planChoice = step.context.activity.text.trim(); if (planChoice === '1') { - step.activeDialog.state.options.planId = 'personal'; - step.activeDialog.state.options.amount = 9.99; + step.activeDialog.state.options.planId = 'free'; } else if (planChoice === '2') { + step.activeDialog.state.options.planId = 'personal'; + } else if (planChoice === '3') { step.activeDialog.state.options.planId = 'professional'; - step.activeDialog.state.options.amount = 29.99; } else { - await step.context.sendActivity('Invalid choice. Please select 1 or 2.'); + await step.context.sendActivity('Invalid choice. Please select 1, 2 or 3.'); return await step.replaceDialog('/welcome_saas_plan'); } return await step.replaceDialog('/welcome_saas_botname', step.activeDialog.state.options); @@ -102,50 +70,16 @@ export class NewUserDialog extends IGBDialog { }; } - static getStripePaymentDialog(min: GBMinInstance) { - return { - id: '/welcome_saas_stripe_payment', - waterfall: [ - async step => { - const locale = 'en-US'; - await step.context.sendActivity(`Please enter your credit card details for the ${step.activeDialog.state.options.planId} plan ($${step.activeDialog.state.options.amount}/month):`); - return await step.prompt('textPrompt', 'Card number (e.g., 4242424242424242):'); - }, - async step => { - step.activeDialog.state.options.ccNumber = step.context.activity.text.trim(); - return await step.prompt('textPrompt', 'Expiration month (MM):'); - }, - async step => { - step.activeDialog.state.options.ccExpiresOnMonth = step.context.activity.text.trim(); - return await step.prompt('textPrompt', 'Expiration year (YYYY):'); - }, - async step => { - step.activeDialog.state.options.ccExpiresOnYear = step.context.activity.text.trim(); - return await step.prompt('textPrompt', 'CVC:'); - }, - async step => { - step.activeDialog.state.options.ccSecuritycode = step.context.activity.text.trim(); - await step.context.sendActivity('Processing payment...'); - await NewUserDialog.createBot(step, min, false); - return await step.replaceDialog('/ask', { isReturning: true }); - } - ] - }; - } - - static getBotTemplateDialog(min: GBMinInstance) { return { id: '/welcome_saas_bottemplate', waterfall: [ async step => { - const locale = 'en-US'; - await step.context.sendActivity('Aqui estão alguns modelos para você escolher:'); + await step.context.sendActivity('Here are some templates to choose from:'); let gboService = new GBOService(); const list = await gboService.listTemplates(min); let templateMessage = undefined; - await CollectionUtil.asyncForEach(list, async item => { if (item.name !== 'Shared.gbai') { templateMessage = templateMessage ? `${templateMessage}\n- ${item.name}` : `- ${item.name}`; @@ -154,108 +88,63 @@ export class NewUserDialog extends IGBDialog { await step.context.sendActivity(templateMessage); step.activeDialog.state.options.templateList = list; - return await step.prompt('textPrompt', `Qual modelo de bot você gostaria de usar?`); + return await step.prompt('textPrompt', `Which bot template would you like to use?`); }, async step => { const list = step.activeDialog.state.options.templateList; let template = null; let gboService = new GBOService(); await CollectionUtil.asyncForEach(list, async item => { - if (gboService.kmpSearch(step.context.activity.originalText, item.name) != -1) { template = item.name; } }); if (template === null) { - await step.context.sendActivity(`Escolha, por favor, um destes modelos listados.`); - + await step.context.sendActivity(`Please choose one of the listed templates.`); return await step.replaceDialog('/welcome_saas_bottemplate', step.activeDialog.state.options); } else { step.activeDialog.state.options.templateName = template; - await NewUserDialog.createBot(step, min, true); + const service = new MainService(); + const result: any = await service.startSubscriptionProcess( + min, + step.activeDialog.state.options.name, + step.activeDialog.state.options.email, + step.activeDialog.state.options.mobile, + step.activeDialog.state.options.botName, + template, + step.activeDialog.state.options.planId + ); - return await step.replaceDialog('/ask', { isReturning: true }); + if (step.activeDialog.state.options.planId === 'free') { + await step.context.sendActivity(`Your free bot has been created! Access it here: ${result.botUrl}`); + return await step.replaceDialog('/ask', { isReturning: true }); + } else { + await step.context.sendActivity(`Please complete your payment here: ${result.paymentUrl}`); + await step.context.sendActivity('I will check for payment completion every few seconds...'); + + try { + const finalResult = await service.waitForPaymentCompletion( + min, + result.subscriptionId, + template + ); + + await step.context.sendActivity(`Payment verified and bot created successfully!`); + await step.context.sendActivity(`Access your bot here: ${finalResult.botUrl}`); + return await step.replaceDialog('/ask', { isReturning: true }); + } catch (error) { + await step.context.sendActivity(`Error: ${error.message}`); + return await step.replaceDialog('/welcome_saas_plan'); + } + } } } ] }; } - static getReturnFromCC(min: GBMinInstance) { - return { - id: '/welcome_saas_return_cc', - waterfall: [ - async step => { - const locale = 'en-US'; - await step.context.sendActivity(Messages[locale].thanks_payment); - await NewUserDialog.createBot(step, min, false); - - return await step.replaceDialog('/ask', { isReturning: true }); - } - ] - }; - } - - static getReturnFromDocument(min: GBMinInstance) { - return { - id: '/welcome_saas_return_document', - waterfall: [ - async step => { - step.activeDialog.state.options.nextDialog = 'welcome_saas_return_payment'; - - return await step.replaceDialog('/bank_payment_type', step.activeDialog.state.options); - } - ] - }; - } - - static getReturnFromPayment(min: GBMinInstance) { - return { - id: '/welcome_saas_return_payment', - waterfall: [ - async step => { - if (step.activeDialog.state.options.paymentType === 'cc') { - step.activeDialog.state.options.nextDialog = 'welcome_saas_return_cc'; - await step.replaceDialog(`/bank_ccnumber`, step.activeDialog.state.options); - } else { - const locale = 'en-US'; - await step.context.sendActivity(Messages[locale].boleto_mail); - - await step.context.sendActivity('textPrompt', Messages[locale].thanks_payment); - await NewUserDialog.createBot(step, min, false); - - return await step.replaceDialog('/ask', { isReturning: true }); - } - } - ] - }; - } - - private static async createBot(step: any, min: GBMinInstance, free: boolean) { - const locale = 'en-US'; - await step.context.sendActivity(Messages[locale].ok_procceding_creation); - const url = `${process.env.BOT_ID}/${step.activeDialog.state.options.botName}`; - await step.context.sendActivity(Messages[locale].bot_created(url)); - const service = new MainService(); - await service.createSubscription( - min, - step.activeDialog.state.options.name, - step.activeDialog.state.options.document, - step.activeDialog.state.options.email, - step.activeDialog.state.options.mobile, - step.activeDialog.state.options.botName, - step.activeDialog.state.options.ccNumber, - step.activeDialog.state.options.ccExpiresOnMonth, - step.activeDialog.state.options.ccExpiresOnYear, - step.activeDialog.state.options.ccSecuritycode, - step.activeDialog.state.options.templateName, - free, step.activeDialog.state.options.planId, - ); - } - - static getDialog(min: GBMinInstance) { return { id: '/welcome_saas', @@ -263,23 +152,21 @@ export class NewUserDialog extends IGBDialog { async step => { const locale = 'en-US'; - step.activeDialog.state.options.document = null; - step.activeDialog.state.options.email = null; - step.activeDialog.state.options.botName = null; - step.activeDialog.state.options.ccNumber = null; - step.activeDialog.state.options.ccExpiresOnMonth = null; - step.activeDialog.state.options.ccExpiresOnYear = null; - step.activeDialog.state.options.ccSecuritycode = null; - step.activeDialog.state.options.templateName = null; - step.activeDialog.state.options.planId = null; - step.activeDialog.state.options.amount = null; + step.activeDialog.state.options = { + document: null, + email: null, + botName: null, + templateName: null, + planId: null, + name: null, + mobile: null, + nextDialog: 'welcome_saas_plan' + }; await step.context.sendActivity(Messages[locale].welcome); const mobile = step.context.activity.from.id; - step.activeDialog.state.options.nextDialog = 'welcome_saas_plan'; - if (isNaN(mobile as any)) { await step.context.sendActivity(Messages[locale].ok_get_information); return await step.replaceDialog('/profile_name', step.activeDialog.state.options); diff --git a/packages/saas.gbapp/index.ts b/packages/saas.gbapp/index.ts index 6f669bea..2f372418 100755 --- a/packages/saas.gbapp/index.ts +++ b/packages/saas.gbapp/index.ts @@ -48,9 +48,9 @@ export class SaaSPackage implements IGBPackage { return [NewUserDialog.getDialog(min), NewUserDialog.getBotNameDialog(min), NewUserDialog.getBotTemplateDialog(min), - NewUserDialog.getReturnFromPayment(min), - NewUserDialog.getReturnFromCC(min), - NewUserDialog.getReturnFromDocument(min), + + NewUserDialog.getPlanSelectionDialog(min), + ]; } diff --git a/packages/saas.gbapp/model/MainModel.ts b/packages/saas.gbapp/model/MainModel.ts old mode 100755 new mode 100644 index 28204380..ef14a253 --- a/packages/saas.gbapp/model/MainModel.ts +++ b/packages/saas.gbapp/model/MainModel.ts @@ -1,88 +1,59 @@ // BotServer/packages/saas.gbapp/model/MainModel.ts -/*****************************************************************************\ -| █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® | -| ██ █ ███ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| ██ ███ ████ █ ██ █ ████ █████ ██████ ██ ████ █ █ █ ██ | -| ██ ██ █ █ ██ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| █████ █████ █ ███ █████ ██ ██ ██ ██ █████ ████ █████ █ ███ | -| | -| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. | -| Licensed under the AGPL-3.0. | -| | -| According to our dual licensing model, this program can be used either | -| under the terms of the GNU Affero General Public License, version 3, | -| or under a proprietary license. | -| | -| The texts of the GNU Affero General Public License with an additional | -| permission and of our proprietary license can be found at and | -| in the LICENSE file you have received along with this program. | -| | -| This program is distributed in the hope that it will be useful, | -| but WITHOUT ANY WARRANTY, without even the implied warranty of | -| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | -| GNU Affero General Public License for more details. | -| | -| "General Bots" is a registered trademark of pragmatismo.com.br. | -| The licensing of the program under the AGPLv3 does not imply a | -| trademark license. Therefore any rights, title and interest in | -| our trademarks remain entirely with us. | -| | -\*****************************************************************************/ - "use strict" - -import { - Table, - Column, - Model, - HasMany, - BelongsTo, - BelongsToMany, - Length, - ForeignKey, - CreatedAt, - UpdatedAt, - DataType, - IsUUID, - PrimaryKey, - AutoIncrement -} from "sequelize-typescript" +import { Table, Column, Model, DataType } from 'sequelize-typescript'; @Table({ tableName: 'GBOnlineSubscription' }) export class GBOnlineSubscription extends Model { - - @PrimaryKey - @AutoIncrement - @Column - Id: number - - @Column - instanceId: number; - - @Column - externalSubscriptionId: string; - - @Column - saasSubscriptionStatus: string; - - @Column - isFreeTrial: boolean; - - @Column - planId: string; - - @Column - quantity: number; - - @Column - lastCCFourDigits: string; - - @Column - status: string; - @Column({ - type: DataType.DECIMAL(10, 2) + primaryKey: true, + autoIncrement: true, + type: DataType.INTEGER }) - amount: number; + declare subscriptionId: number; -} \ No newline at end of file + @Column(DataType.INTEGER) + declare instanceId: number; + + @Column(DataType.STRING(100)) + declare customerName: string; + + @Column(DataType.STRING(100)) + declare customerEmail: string; + + @Column(DataType.STRING(100)) + declare stripeSessionId: string; + + @Column(DataType.STRING(100)) + declare stripePaymentIntentId: string; + + + @Column(DataType.STRING(20)) + declare customerMobile: string; + + @Column(DataType.STRING(50)) + declare botName: string; + + @Column(DataType.STRING(20)) + declare planId: string; + + @Column(DataType.STRING(20)) + declare status: string; // 'pending_payment', 'active', 'cancelled' + + @Column(DataType.FLOAT) + declare paymentAmount: number; + + @Column(DataType.STRING(500)) + declare paymentUrl: string; + + @Column(DataType.STRING(100)) + declare paymentToken: string; + + @Column(DataType.STRING(4)) + declare lastCCFourDigits: string; + + @Column(DataType.DATE) + declare createdAt: Date; + + @Column(DataType.DATE) + declare activatedAt: Date; +} diff --git a/packages/saas.gbapp/service/GBOService.ts b/packages/saas.gbapp/service/GBOService.ts index 91393f6f..fa71e6af 100755 --- a/packages/saas.gbapp/service/GBOService.ts +++ b/packages/saas.gbapp/service/GBOService.ts @@ -1,32 +1,4 @@ -/*****************************************************************************\ -| █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® | -| ██ █ ███ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| ██ ███ ████ █ ██ █ ████ █████ ██████ ██ ████ █ █ █ ██ | -| ██ ██ █ █ ██ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| █████ █████ █ ███ █████ ██ ██ ██ ██ █████ ████ █████ █ ███ | -| | -| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. | -| Licensed under the AGPL-3.0. | -| | -| According to our dual licensing model, this program can be used either | -| under the terms of the GNU Affero General Public License, version 3, | -| or under a proprietary license. | -| | -| The texts of the GNU Affero General Public License with an additional | -| permission and of our proprietary license can be found at and | -| in the LICENSE file you have received along with this program. | -| | -| This program is distributed in the hope that it will be useful, | -| but WITHOUT ANY WARRANTY, without even the implied warranty of | -| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | -| GNU Affero General Public License for more details. | -| | -| "General Bots" is a registered trademark of pragmatismo.com.br. | -| The licensing of the program under the AGPLv3 does not imply a | -| trademark license. Therefore any rights, title and interest in | -| our trademarks remain entirely with us. | -| | -\*****************************************************************************/ +// General Bots Copyright (c) pragmatismo.com.br. All rights reserved. Licensed under the AGPL-3.0. "use strict" @@ -44,17 +16,12 @@ import path from "path"; import { Client } from "minio"; + export class GBOService { - - public isValidCardNumber(ccNumber) { + + + constructor() { } - - public isValidSecurityCode(ccNumber, cvcNumber) { - } - - public isValidExpireDate(month, year) { - } - public async sendEmail(token: string, to: string, from: string, subject: string, text: string, html: string) { return new Promise((resolve, reject) => { diff --git a/packages/saas.gbapp/service/MainService.ts b/packages/saas.gbapp/service/MainService.ts old mode 100755 new mode 100644 index 785492de..05c6cfd5 --- a/packages/saas.gbapp/service/MainService.ts +++ b/packages/saas.gbapp/service/MainService.ts @@ -1,35 +1,4 @@ -/*****************************************************************************\ -| █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® | -| ██ █ ███ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| ██ ███ ████ █ ██ █ ████ █████ ██████ ██ ████ █ █ █ ██ | -| ██ ██ █ █ ██ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | -| █████ █████ █ ███ █████ ██ ██ ██ ██ █████ ████ █████ █ ███ | -| | -| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. | -| Licensed under the AGPL-3.0. | -| | -| According to our dual licensing model, this program can be used either | -| under the terms of the GNU Affero General Public License, version 3, | -| or under a proprietary license. | -| | -| The texts of the GNU Affero General Public License with an additional | -| permission and of our proprietary license can be found at and | -| in the LICENSE file you have received along with this program. | -| | -| This program is distributed in the hope that it will be useful, | -| but WITHOUT ANY WARRANTY, without even the implied warranty of | -| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | -| GNU Affero General Public License for more details. | -| | -| "General Bots" is a registered trademark of pragmatismo.com.br. | -| The licensing of the program under the AGPLv3 does not imply a | -| trademark license. Therefore any rights, title and interest in | -| our trademarks remain entirely with us. | -| | -\*****************************************************************************/ - -'use strict'; - +// BotServer/packages/saas.gbapp/service/MainService.ts import { GBOnlineSubscription } from '../model/MainModel.js'; import { GBMinInstance, GBLog } from 'botlib'; import { CollectionUtil } from 'pragmatismo-io-framework'; @@ -37,124 +6,142 @@ import urlJoin from 'url-join'; import { GBOService } from './GBOService.js'; import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js'; import Stripe from 'stripe'; +import { GBUtil } from '../../../src/util.js'; export class MainService { + private gboService: GBOService; private stripe: Stripe; + private readonly PAYMENT_CHECK_INTERVAL = 5000; // 5 seconds + private readonly PAYMENT_CHECK_TIMEOUT = 300000; // 5 minutes timeout constructor() { + this.gboService = new GBOService(); this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY); } - async createStripeCustomer(name: string, email: string, paymentMethodId: string) { - const customer = await this.stripe.customers.create({ - name, - email, - payment_method: paymentMethodId, - invoice_settings: { - default_payment_method: paymentMethodId - } - }); - return customer; - } - - async createStripeSubscription(customerId: string, priceId: string) { - const subscription = await this.stripe.subscriptions.create({ - customer: customerId, - items: [{ price: priceId }], - expand: ['latest_invoice.payment_intent'] - }); - return subscription; - } - - async createPaymentMethod(cardNumber: string, expMonth: number, expYear: number, cvc: string) { - const paymentMethod = await this.stripe.paymentMethods.create({ - type: 'card', - card: { - number: cardNumber, - exp_month: expMonth, - exp_year: expYear, - cvc: cvc - } - }, {}); - return paymentMethod; - } - - async createSubscription( + public async startSubscriptionProcess( min: GBMinInstance, name: string, - document: string, email: string, mobile: string, botName: string, - ccNumber: string, - ccExpiresOnMonth: number, - ccExpiresOnYear: number, - ccCode: string, templateName: string, - free: boolean, planId: string, + planId: string ) { - let externalSubscriptionId = null; - - if (!free) { - try { - // Create Stripe payment method - const paymentMethod = await this.createPaymentMethod( - ccNumber, - ccExpiresOnMonth, - ccExpiresOnYear, - ccCode - ); - - // Create Stripe customer - const customer = await this.createStripeCustomer( - name, - email, - paymentMethod.id - ); - - // Determine price ID based on plan - const priceId = planId === 'professional' - ? process.env.STRIPE_PROFESSIONAL_PRICE_ID - : process.env.STRIPE_PERSONAL_PRICE_ID; - - // Create subscription - const subscription = await this.createStripeSubscription( - customer.id, - priceId - ); - - externalSubscriptionId = subscription.id; - } catch (error) { - GBLog.error(`Stripe payment failed: ${error.message}`); - throw error; - } - } - - // Syncs internal subscription management - const status = free ? 'FreeTrial' : 'Active'; - GBLog.info(`Creating subscription for ${name} (${email}, ${mobile}) with status: ${status}`); - - const quantity = 1; - const amount = 1; - + // Create initial subscription record const subscription = await GBOnlineSubscription.create({ instanceId: min.instance.instanceId, - isFreeTrial: free, + customerName: name, + customerEmail: email, + customerMobile: mobile, + botName: botName, planId: planId, - quantity: quantity, - status: status, - amount: amount, - lastCCFourDigits: ccNumber ? ccNumber.slice(-4) : null + status: planId === 'free' ? 'active' : 'pending_payment', + createdAt: new Date(), + activatedAt: planId === 'free' ? new Date() : null }); - // Creates a bot - GBLog.info('Deploying a blank bot to storage...'); - const instance = await min.deployService.deployBlankBot(botName, mobile, email); + if (planId === 'free') { + return await this.createBotResources(min, subscription, templateName); + } else { + const priceId = this.getPriceIdForPlan(planId); + const session = await this.stripe.checkout.sessions.create({ + payment_method_types: ['card'], + line_items: [{ + price: priceId, + quantity: 1, + }], + + mode: 'subscription', + metadata: { + subscriptionId: subscription.subscriptionId.toString(), + botName: botName + } + }); - GBLog.info('Creating subscription...'); - subscription.instanceId = instance.instanceId; - subscription.externalSubscriptionId = externalSubscriptionId; - await subscription.save(); + await subscription.update({ + stripeSessionId: session.id + }); + + return { + paymentUrl: session.url, + subscriptionId: subscription.subscriptionId, + nextStep: 'Please complete the payment in the new window. I will check for completion automatically.' + }; + } + } + + public async waitForPaymentCompletion( + min: GBMinInstance, + subscriptionId: number, + templateName: string + ): Promise { + const startTime = Date.now(); + + while ((Date.now() - startTime) < this.PAYMENT_CHECK_TIMEOUT) { + const subscription = await GBOnlineSubscription.findOne({ + where: { subscriptionId } + }); + + if (!subscription) { + throw new Error('Subscription not found'); + } + + if (subscription.stripeSessionId) { + const session = await this.stripe.checkout.sessions.retrieve( + subscription.stripeSessionId, + { expand: ['payment_intent'] } + ); + + if (session.payment_status === 'paid') { + await subscription.update({ + status: 'active', + activatedAt: new Date(), + stripePaymentIntentId: (session.payment_intent as any)?.id + }); + + return await this.createBotResources(min, subscription, templateName); + } + + if (session.payment_status === 'unpaid' || session.status === 'expired') { + throw new Error('Payment failed or session expired. Please try again.'); + } + } + + await GBUtil.sleep(this.PAYMENT_CHECK_INTERVAL); + } + + throw new Error('Payment processing timed out. Please check your payment and try again.'); + } + + private getPriceIdForPlan(planId: string): string { + const priceIds = { + personal: process.env.STRIPE_PERSONAL_PLAN_PRICE_ID, + professional: process.env.STRIPE_PROFESSIONAL_PLAN_PRICE_ID + }; + + if (!priceIds[planId]) { + throw new Error(`No price ID configured for plan: ${planId}`); + } + + return priceIds[planId]; + } + + private async createBotResources( + min: GBMinInstance, + subscription: GBOnlineSubscription, + templateName: string + ) { + GBLog.info('Deploying a blank bot to storage...'); + const instance = await min.deployService.deployBlankBot( + subscription.botName, + subscription.customerMobile, + subscription.customerEmail + ); + + await subscription.update({ + instanceId: instance.instanceId + }); let token = GBConfigService.get('GB_MODE') === 'legacy' ? @@ -163,98 +150,32 @@ export class MainService { let siteId = process.env.STORAGE_SITE_ID; let libraryId = process.env.STORAGE_LIBRARY; - let gboService = new GBOService(); - - let sleep = ms => { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); - }; GBLog.info('Creating .gbai folder ...'); - let item = await gboService.createRootFolder(token, `${botName}.gbai`, siteId, libraryId); + let item = await this.gboService.createRootFolder( + token, + `${subscription.botName}.gbai`, + siteId, + libraryId + ); GBLog.info('Copying Templates...'); - await gboService.copyTemplates(min, item, templateName, 'gbkb', botName); - await gboService.copyTemplates(min, item, templateName, 'gbot', botName); - await gboService.copyTemplates(min, item, templateName, 'gbtheme', botName); - await gboService.copyTemplates(min, item, templateName, 'gbdata', botName); - await gboService.copyTemplates(min, item, templateName, 'gbdialog', botName); - await gboService.copyTemplates(min, item, templateName, 'gbdrive', botName); + await this.gboService.copyTemplates(min, item, templateName, 'gbkb', subscription.botName); + await this.gboService.copyTemplates(min, item, templateName, 'gbot', subscription.botName); + await this.gboService.copyTemplates(min, item, templateName, 'gbtheme', subscription.botName); + await this.gboService.copyTemplates(min, item, templateName, 'gbdata', subscription.botName); + await this.gboService.copyTemplates(min, item, templateName, 'gbdialog', subscription.botName); + await this.gboService.copyTemplates(min, item, templateName, 'gbdrive', subscription.botName); - await sleep(10000); GBLog.info('Configuring .gbot...'); - await min.core['setConfig'](min, instance.botId, "Can Publish", mobile + ";"); - await min.core['setConfig'](min, instance.botId, "Admin Notify E-mail", email); + await min.core['setConfig'](min, instance.botId, "Can Publish", subscription.customerMobile + ";"); + await min.core['setConfig'](min, instance.botId, "Admin Notify E-mail", subscription.customerEmail); await min.core['setConfig'](min, instance.botId, 'WebDav Username', instance.botId); await min.core['setConfig'](min, instance.botId, 'WebDav Secret', instance.adminPass); - GBLog.info('Bot creation done.'); + return { + success: true, + botUrl: urlJoin(process.env.BOT_URL, subscription.botName) + }; } - - public async otherTasks(min, botName, webUrl, instance, language) { - let message = `Seu bot ${botName} está disponível no endereço: -
${urlJoin(process.env.BOT_URL, botName)}. -
-
Os pacotes do General Bots (ex: .gbkb, .gbtheme) para seu Bot devem ser editados no repositório de pacotes: -
-
${webUrl}. -
-
Digite /publish do seu WhatsApp para publicar os pacotes. Seu número está autorizado na pasta ${botName}.gbot/Config.xlsx -
-
-
O arquivo .zip em anexo pode ser importado no Teams conforme instruções em: -
https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload. -
-
Log in to the Teams client with your Microsoft 365 account. -
Select Apps and choose Upload a custom app. -
Select this .zip file attached to this e-mail. An install dialog displays. -
Add your Bot to Teams. -
-
Atenciosamente, -
General Bots Online. -
-
-
E-mail remetido por Pragmatismo. -
`; - - message = await min.conversationalService.translate( - min, - message, - language - ); - - GBLog.info('Generating MS Teams manifest....'); - - const appManifest = await min.deployService.getBotManifest(min.instance); - - // GBLog.info( 'Sending e-mails....'); - // const emailToken = process.env.SAAS_SENDGRID_API_KEY; - // gboService.sendEmail( - // emailToken, - // email, - // `${botName}`, - // message, - // message, - // { - // content: appManifest, - // filename: `${min.instance.botId}-Teams.zip`, - // type: `application/zip`, - // disposition: "attachment" - // } - // ); - - const contacts = process.env.SECURITY_LIST.split(';'); - - // TODO: await CollectionUtil.asyncForEach(contacts, async item => { - // await (min.whatsAppDirectLine as any)['sendToDevice']( - // item, - // `Novo bot criado agora: http://gb.pragmatismo.com.br/${botName} para *${name}* (${email}, ${mobile}). Por favor, entre em contato para que mais um bot seja configurado adequadamente. ` - // ); - // }); - - // GBLog.info( 'Sharing .gbai folder...'); - // await gboService.shareFolder(token, item.parentReference.driveId, item.id, email); - - } -} +} \ No newline at end of file diff --git a/web.config b/web.config deleted file mode 100644 index 4f827f1b..00000000 --- a/web.config +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -