feat(instances): POST /provision — trajeto inteligente e idempotente
Cria+configura a conta no motor em um passo: instância (engine 'baileys7' = API estável por padrão), setor default (idempotente por [tenantId,name]), webhook opcional e connect opcional (emite QR). Devolve o 'trajeto' com cada passo visível (instancia/engine/setor/webhook/conexao/business/secretaria/conhecimento), marcando como pendentes os passos de plugin/satélite. Re-rodar não duplica. InstanceRepository: provision() (upsert por unique [tenantId,name]) e ensureSector(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+35
@@ -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 } })
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
Reference in New Issue
Block a user