diff --git a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/repositories/InstanceRepository.ts b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/repositories/InstanceRepository.ts index d185f4a..2fa9580 100644 --- a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/repositories/InstanceRepository.ts +++ b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/repositories/InstanceRepository.ts @@ -19,6 +19,41 @@ export class InstanceRepository { }) } + /** + * Provisiona uma instância de forma idempotente (re-rodar não duplica). + * Cria com engine "API estável" (baileys7) por padrão; se já existir, só + * atualiza o webhookUrl quando informado e diferente. + */ + async provision(tenantId: string, name: string, opts: { engine?: string; webhookUrl?: string } = {}) { + const existing = await prisma.instance.findUnique({ + where: { tenantId_name: { tenantId, name } }, + }) + if (existing) { + if (opts.webhookUrl && opts.webhookUrl !== existing.webhookUrl) { + const instance = await prisma.instance.update({ + where: { id: existing.id }, + data: { webhookUrl: opts.webhookUrl }, + }) + return { instance, created: false, webhookUpdated: true } + } + return { instance: existing, created: false, webhookUpdated: false } + } + const instance = await prisma.instance.create({ + data: { tenantId, name, engine: opts.engine ?? 'baileys7', webhookUrl: opts.webhookUrl ?? null }, + }) + return { instance, created: true, webhookUpdated: Boolean(opts.webhookUrl) } + } + + /** Garante um setor (idempotente por [tenantId, name]). */ + async ensureSector(tenantId: string, name: string) { + const existing = await prisma.sector.findUnique({ + where: { tenantId_name: { tenantId, name } }, + }) + if (existing) return { sector: existing, created: false } + const sector = await prisma.sector.create({ data: { tenantId, name } }) + return { sector, created: true } + } + async updateStatus(id: string, status: InstanceStatus) { return prisma.instance.update({ where: { id }, data: { status } }) } diff --git a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/whatsapp.routes.ts b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/whatsapp.routes.ts index fc9f31a..117995e 100644 --- a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/whatsapp.routes.ts +++ b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/whatsapp.routes.ts @@ -33,6 +33,61 @@ export function buildWhatsAppRoutes(manager: WhatsAppConnectionManager): Router res.status(201).json(instance) }) + // Provisionamento inteligente: cria+configura a conta no motor de forma + // idempotente e devolve o "trajeto" (cada passo visível, não caixa-preta). + // Body: { name, engine?, webhookUrl?, sector?, connect? } + router.post('/provision', async (req: Request, res: Response) => { + const tenantId = req.tenantId! + const { name, engine, webhookUrl, sector, connect } = req.body as { + name?: string; engine?: string; webhookUrl?: string; sector?: string; connect?: boolean + } + if (!name) { + res.status(400).json({ error: 'name é obrigatório' }) + return + } + if (engine && engine !== 'infinite' && engine !== 'official' && engine !== 'baileys7') { + res.status(400).json({ error: 'engine deve ser "infinite", "official" ou "baileys7"' }) + return + } + + const trajeto: Array<{ passo: string; status: string; detalhe?: string }> = [] + + // 1. Instância (idempotente; engine "API estável" por padrão ao criar) + const prov = await repo.provision(tenantId, name, { engine, webhookUrl }) + trajeto.push({ passo: 'instancia', status: prov.created ? 'criada' : 'existente', detalhe: prov.instance.id }) + trajeto.push({ passo: 'engine', status: prov.created ? 'definida' : 'mantida', detalhe: prov.instance.engine }) + + // 2. Setor padrão (idempotente) + const sectorName = sector?.trim() || 'Atendimento' + const sec = await repo.ensureSector(tenantId, sectorName) + trajeto.push({ passo: 'setor', status: sec.created ? 'criado' : 'existente', detalhe: sectorName }) + + // 3. Webhook + trajeto.push({ + passo: 'webhook', + status: webhookUrl ? (prov.webhookUpdated ? 'configurado' : 'inalterado') : 'omitido', + detalhe: prov.instance.webhookUrl ?? undefined, + }) + + // 4. Conexão (opcional) — emite QR via Socket.IO; Business é detectada no connect + if (connect) { + manager.connect(prov.instance.id, tenantId).catch((err: unknown) => { + logger.error({ err, instanceId: prov.instance.id }, '[Provision] Falha ao iniciar conexão') + }) + trajeto.push({ passo: 'conexao', status: 'iniciada', detalhe: 'aguarde o QR via WebSocket' }) + } else { + trajeto.push({ passo: 'conexao', status: 'pulada', detalhe: 'envie connect:true ou POST /:id/connect' }) + } + trajeto.push({ passo: 'business', status: 'pendente', detalhe: 'detectada ao conectar (getBusinessProfile)' }) + + // 5. Passos de plugin/satélite (fora do core do motor) — visíveis e honestos + trajeto.push({ passo: 'secretaria', status: 'pendente', detalhe: 'criar agente default via plugin secretaria' }) + trajeto.push({ passo: 'conhecimento', status: 'pendente', detalhe: 'rodar sync-knowledge no satélite' }) + + logger.info({ tenantId, instanceId: prov.instance.id, created: prov.created }, '[Provision] trajeto executado') + res.status(prov.created ? 201 : 200).json({ instance: prov.instance, trajeto }) + }) + // Conectar instância (emite QR via Socket.IO) router.post('/:id/connect', async (req: Request, res: Response) => { const tenantId = req.tenantId!