Atualização do READ.ME
This commit is contained in:
127
README.md
127
README.md
@@ -2264,11 +2264,13 @@ docker compose exec app bin/rails runner '
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>📣 Notificações: WhatsApp por QR (Baileys) no lugar do Twilio + contatos, grupos e eventos — ETAPA 1 (24/08/2026)</strong></summary>
|
<summary><strong>📣 Notificações: WhatsApp por QR (Baileys) no lugar do Twilio + contatos, grupos e eventos — ETAPA 1 (24/08/2026)</strong></summary>
|
||||||
|
|
||||||
> ⚠️ **STATUS: implementado, NADA executado.** Não há Ruby/Bundler, Postgres nem Docker rodando na
|
> ✅ **STATUS: NO AR no ambiente de teste (24/08/2026).** Migrations aplicadas, container da ponte
|
||||||
> máquina de desenvolvimento. Foi conferida a sintaxe de todos os `.rb`/`.erb` (com um checker que
|
> buildado e **WhatsApp pareado por QR** — número `5511920051157`, conectado às 18:07, intervalo em
|
||||||
> emula o handler ERB do Rails, porque `<%= form_with … do %>` não passa no ERB da stdlib) e do
|
> 20s. Telas de contatos, grupos, eventos, envios e conexão validadas no navegador.
|
||||||
> `server.js` (`node --check`). **Migration, `npm install`, build do container, pareamento do QR e a
|
>
|
||||||
> suíte continuam pendentes** — roteiro no fim.
|
> ⚠️ **A suíte continua sem rodar** — não há Ruby/Bundler na máquina de desenvolvimento. A sintaxe
|
||||||
|
> de todos os `.rb`/`.erb` foi conferida (com um checker que emula o handler ERB do Rails, porque
|
||||||
|
> `<%= form_with … do %>` não passa no ERB da stdlib) e a do `server.js` com `node --check`.
|
||||||
|
|
||||||
> Esta é a **etapa 1 de 3**. Ver "O que NÃO está aqui" no fim.
|
> Esta é a **etapa 1 de 3**. Ver "O que NÃO está aqui" no fim.
|
||||||
|
|
||||||
@@ -2397,10 +2399,12 @@ docker compose exec app bundle exec rspec \
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>🧱 Editor de blocos das mensagens — ETAPA 2 (24/08/2026)</strong></summary>
|
<summary><strong>🧱 Editor de blocos das mensagens — ETAPA 2 (24/08/2026)</strong></summary>
|
||||||
|
|
||||||
> ⚠️ **STATUS: implementado, NADA executado.** Sem Ruby/Bundler/Postgres/Docker na máquina de
|
> ✅ **STATUS: NO AR no ambiente de teste (24/08/2026).** Migration aplicada; a tela de eventos já
|
||||||
> desenvolvimento. Conferida a sintaxe de todos os `.rb`, de todos os `.erb` (checker que emula o
|
> mostra o ✓ dos canais com mensagem montada.
|
||||||
> handler do Rails) e **do JavaScript do editor** (`node --check` sobre o `<script>` extraído).
|
>
|
||||||
> **Migration, `npm install` e a suíte continuam pendentes.**
|
> ⚠️ **Não validado ainda**: o arrastar/soltar e o preview com dado real, na mão, no navegador — e a
|
||||||
|
> suíte. Sintaxe conferida em todos os `.rb`, `.erb` e **no JavaScript do editor** (`node --check`
|
||||||
|
> sobre o `<script>` extraído).
|
||||||
|
|
||||||
### 🎯 O que mudou
|
### 🎯 O que mudou
|
||||||
Na etapa 1, o corpo das mensagens ainda era string interpolada em Ruby — mudar uma palavra exigia
|
Na etapa 1, o corpo das mensagens ainda era string interpolada em Ruby — mudar uma palavra exigia
|
||||||
@@ -2498,9 +2502,10 @@ gatilhos hoje só dispara pelo botão manual.
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>🔔 Gatilhos: valor alterado, operação mudou e resumo agendado — ETAPA 3 (24/08/2026)</strong></summary>
|
<summary><strong>🔔 Gatilhos: valor alterado, operação mudou e resumo agendado — ETAPA 3 (24/08/2026)</strong></summary>
|
||||||
|
|
||||||
> ⚠️ **STATUS: implementado, NADA executado.** Sem Ruby/Bundler/Postgres/Docker na máquina de
|
> ✅ **STATUS: migrations no ar (24/08/2026).**
|
||||||
> desenvolvimento. Sintaxe conferida em todos os `.rb`, `.rake` e `.erb`. **2 migrations, o
|
>
|
||||||
> `whenever --update-crontab` e a suíte continuam pendentes.**
|
> ⚠️ **Não validado ainda**: um disparo real de cada gatilho, o `whenever --update-crontab` e a
|
||||||
|
> suíte. Sintaxe conferida em todos os `.rb`, `.rake` e `.erb`.
|
||||||
|
|
||||||
### 🎯 O que faltava
|
### 🎯 O que faltava
|
||||||
Nas etapas 1 e 2, `valor_alterado`, `operacao_alterada` e `agendado` existiam como opção de gatilho
|
Nas etapas 1 e 2, `valor_alterado`, `operacao_alterada` e `agendado` existiam como opção de gatilho
|
||||||
@@ -2593,3 +2598,101 @@ docker compose exec app bundle exec rake notificacao:mudancas_operacao
|
|||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>🛠️ Deploy das notificações: os 3 tropeços e as correções (24/08/2026)</strong></summary>
|
||||||
|
|
||||||
|
Registro do que quebrou entre "implementado" e "no ar", porque os três são armadilhas que voltam.
|
||||||
|
|
||||||
|
### 1. `PG::UndefinedTable: relation "grupo_contatos" does not exist`
|
||||||
|
`t.references :grupo_contato, foreign_key: true` faz o Rails **pluralizar** o nome para achar a
|
||||||
|
tabela alvo. O Inflector não fala português: `grupo_contato` → `grupo_contatos` e
|
||||||
|
`evento_notificacao` → `evento_notificacaos`. As tabelas reais são `grupos_contato` e
|
||||||
|
`eventos_notificacao` (plural no **primeiro** termo).
|
||||||
|
|
||||||
|
**Correção:** `foreign_key: { to_table: :grupos_contato }` nas 5 referências afetadas. `user` e
|
||||||
|
`contato` pluralizam certo e não precisaram.
|
||||||
|
|
||||||
|
> É a MESMA armadilha que já tinha aparecido nas rotas (`grupos`/`eventos`/`envios` em vez dos nomes
|
||||||
|
> dos controllers). Sempre que uma tabela em português tiver o plural no primeiro termo, `to_table:`
|
||||||
|
> e nome de rota explícito são obrigatórios.
|
||||||
|
|
||||||
|
### 2. `npm error syscall spawn git / ENOENT` na build da ponte
|
||||||
|
`errno -2` = o **binário** `git` não existe na imagem. O `node:20-alpine` não traz git, e o Baileys
|
||||||
|
resolve `libsignal` de um **repositório git**, não do registry do npm.
|
||||||
|
|
||||||
|
**Correção:** `apk add --no-cache git` no `whatsapp/Dockerfile`. Não é erro de rede, de firewall nem
|
||||||
|
de versão do pacote — e a linha do `apk` tem comentário explicando, para não ser "limpa" depois.
|
||||||
|
|
||||||
|
### 3. Não havia como escolher o provedor
|
||||||
|
As colunas `whatsapp_provedor`, `baileys_url`, `baileys_token` e `whatsapp_intervalo_segundos`
|
||||||
|
nasceram na migration, mas a tela **Configurações → Notificações** continuou 100% Twilio. Como o
|
||||||
|
padrão da coluna é `twilio`, o caminho do QR ficava **inalcançável pela interface** — dava para
|
||||||
|
parear e nada usaria a ponte. Pior: a tela do QR linkava para lá dizendo que o intervalo se
|
||||||
|
configurava ali, o que era falso.
|
||||||
|
|
||||||
|
**Correção:** seletor de Provedor, campo de intervalo e bloco "Modo QR code" (endereço + token, em
|
||||||
|
branco caem no `.env`) na tela de configuração; e `Notificacao::TesteWhatsapp` passou a ramificar
|
||||||
|
pelo provedor em vez de ir direto ao Twilio.
|
||||||
|
|
||||||
|
### 4. Evento criado pelo ADM não disparava
|
||||||
|
`Despachante.disparar(chave:)` buscava **um** evento pela chave. Como o código de negócio chama com
|
||||||
|
a chave fixa (`'consolidacao_finalizada'`…), só o evento de **sistema** disparava: um evento criado
|
||||||
|
na tela com o mesmo gatilho ficava mudo para sempre, **sem erro nenhum**. A tela oferecia o gatilho
|
||||||
|
e não acontecia nada.
|
||||||
|
|
||||||
|
**Correção:** `Despachante.disparar_gatilho(gatilho:)` dispara **todos** os eventos ativos daquele
|
||||||
|
gatilho, cada um com seus grupos e sua mensagem — que é justamente o ponto de poder cadastrar
|
||||||
|
eventos. O `disparar(chave:)` continua existindo para o botão "disparar agora", que mira um evento
|
||||||
|
específico.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>💡 Ideias para o editor de mensagens — backlog priorizado</strong></summary>
|
||||||
|
|
||||||
|
Levantado com a tela já em uso. Ordenado por **valor ÷ esforço**; nada aqui está implementado.
|
||||||
|
|
||||||
|
### A. Chegar até o editor
|
||||||
|
|
||||||
|
O problema, olhando a tela de eventos hoje: os acessos são **dois links de texto cinza**
|
||||||
|
("WhatsApp" e "E-mail") do lado de "Editar", com o mesmo peso visual. Nada diz que ali se
|
||||||
|
**escreve a mensagem** — parece mais um filtro de canal. E o ✓ de "já tem mensagem montada" é
|
||||||
|
pequeno demais para ser lido de relance.
|
||||||
|
|
||||||
|
| # | Ideia | Esforço |
|
||||||
|
|---|---|---|
|
||||||
|
| A1 | **Botão "Montar mensagem"** com ícone, no lugar dos dois links soltos. Abre o editor com abas WhatsApp/E-mail **dentro** dele, em vez de duas portas separadas. | baixo |
|
||||||
|
| A2 | **Estado como badge colorido** — `WhatsApp ✓ · E-mail —` em verde/cinza, não link. Mostra num relance o que falta. | baixo |
|
||||||
|
| A3 | **Miniatura do preview no card do evento**: as 2 primeiras linhas da mensagem renderizada. Responde "o que esse evento manda?" sem entrar. | médio |
|
||||||
|
| A4 | **Passo seguinte explícito**: ao salvar um evento novo, redirecionar para o editor com "Agora monte a mensagem". Hoje você cria o evento e fica sem pista do que fazer. | baixo |
|
||||||
|
| A5 | **Item "Mensagens" no menu** — tabela evento × canal com o estado de cada um. Hoje só se chega pelo evento. | médio |
|
||||||
|
| A6 | **Alerta de evento mudo**: ativo, com grupo assinante e sem mensagem própria → avisar que vai usar o texto padrão do sistema. | baixo |
|
||||||
|
|
||||||
|
### B. Design das mensagens
|
||||||
|
|
||||||
|
Hoje: 7 blocos, cores fixas no renderizador, só texto.
|
||||||
|
|
||||||
|
| # | Ideia | Esforço | Observação |
|
||||||
|
|---|---|---|---|
|
||||||
|
| B1 | **Ocultar bloco quando a variável estiver vazia** — checkbox por bloco. Resolve o "⚠️ Alterações: " sem nada depois, que hoje aparece. | baixo | O ganho de qualidade mais barato da lista. |
|
||||||
|
| B2 | **Enviar teste direto do editor** — "mandar para o meu WhatsApp" sem sair da tela. Hoje: salvar → eventos → disparo manual. | baixo | |
|
||||||
|
| B3 | **Modelos prontos** ("Aviso de fechamento", "Resumo diário", "Alteração de valor") — começar de um layout em vez da folha em branco. | médio | |
|
||||||
|
| B4 | **Duplicar template** entre canais e entre eventos ("copiar do WhatsApp para o e-mail"). | baixo | |
|
||||||
|
| B5 | **Cor de destaque configurável** — hoje o laranja está fixo no `Renderizador`. | baixo | |
|
||||||
|
| B6 | **Logo no cabeçalho do e-mail** | médio | Precisa de URL absoluta pública ou embed em base64; cliente de e-mail bloqueia imagem remota por padrão. |
|
||||||
|
| B7 | **Bloco de lista** (bullets) — hoje só existe tabela rótulo/valor. | baixo | |
|
||||||
|
| B8 | **Preview em moldura de celular** para o WhatsApp e alternância desktop/mobile no e-mail. | médio | Hoje o preview do WhatsApp é um `<pre>` escuro. |
|
||||||
|
| B9 | **Contador de caracteres** no WhatsApp — mensagem longa vira "ler mais" e o começo é o que decide se abrem. | baixo | |
|
||||||
|
| B10 | **Bloco de imagem** | **alto** | No e-mail é `<img>`. No WhatsApp exige **enviar mídia**, e a ponte hoje só faz `sendMessage` de texto — mexe no `server.js`, no cliente Ruby e no log de envios. |
|
||||||
|
| B11 | **Versões do template** — voltar à anterior depois de estragar. | médio | |
|
||||||
|
|
||||||
|
### Se fosse escolher três
|
||||||
|
**A1 + A2** (o editor deixa de ser escondido) e **B1** (mensagem sem buraco quando a variável vem
|
||||||
|
vazia). Juntos são baixo esforço e resolvem o que mais incomoda no uso diário.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|||||||
@@ -24,16 +24,37 @@ module Notificacao
|
|||||||
class Despachante
|
class Despachante
|
||||||
# Dispara em background. Nunca levanta: uma notificação não pode derrubar o
|
# Dispara em background. Nunca levanta: uma notificação não pode derrubar o
|
||||||
# fechamento de um pagamento (mesma regra do NotificacaoService).
|
# fechamento de um pagamento (mesma regra do NotificacaoService).
|
||||||
|
# Dispara UM evento específico (disparo manual pelo botão da tela).
|
||||||
def self.disparar(chave:, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
|
def self.disparar(chave:, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
|
||||||
evento = EventoNotificacao.ativos.find_by(chave: chave.to_s)
|
evento = EventoNotificacao.ativos.find_by(chave: chave.to_s)
|
||||||
return if evento.nil?
|
return if evento.nil?
|
||||||
|
|
||||||
NotificacaoJob.perform_later(evento.id, (dados || {}).stringify_keys,
|
agendar(evento, dados: dados, assunto: assunto, corpo: corpo, envolvido: envolvido)
|
||||||
assunto.to_s, corpo.to_s, envolvido&.id)
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error("[Despachante] falha ao agendar #{chave}: #{e.class}: #{e.message}")
|
Rails.logger.error("[Despachante] falha ao agendar #{chave}: #{e.class}: #{e.message}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Dispara TODOS os eventos ativos de um gatilho.
|
||||||
|
#
|
||||||
|
# É o que o código de negócio usa. Buscar pela `chave` (como era antes) fazia
|
||||||
|
# só o evento de SISTEMA disparar: um evento criado pelo ADM com o mesmo
|
||||||
|
# gatilho ficava mudo para sempre, sem erro nenhum — a tela oferecia o
|
||||||
|
# gatilho e nada acontecia. Com N eventos no mesmo gatilho, cada um tem seus
|
||||||
|
# próprios grupos e sua própria mensagem, que é o ponto de poder cadastrar.
|
||||||
|
def self.disparar_gatilho(gatilho:, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
|
||||||
|
EventoNotificacao.ativos.where(gatilho: gatilho.to_s).find_each do |evento|
|
||||||
|
agendar(evento, dados: dados, assunto: assunto, corpo: corpo, envolvido: envolvido)
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("[Despachante] falha ao agendar gatilho #{gatilho}: #{e.class}: #{e.message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.agendar(evento, dados:, assunto:, corpo:, envolvido:)
|
||||||
|
NotificacaoJob.perform_later(evento.id, (dados || {}).stringify_keys,
|
||||||
|
assunto.to_s, corpo.to_s, envolvido&.id)
|
||||||
|
end
|
||||||
|
private_class_method :agendar
|
||||||
|
|
||||||
def initialize(evento, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
|
def initialize(evento, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
|
||||||
@evento = evento
|
@evento = evento
|
||||||
@dados = (dados || {}).stringify_keys
|
@dados = (dados || {}).stringify_keys
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ module Notificacao
|
|||||||
atual = consolidacao_motorista.valor_total.to_f
|
atual = consolidacao_motorista.valor_total.to_f
|
||||||
return if anterior.to_f == atual
|
return if anterior.to_f == atual
|
||||||
|
|
||||||
Despachante.disparar(
|
Despachante.disparar_gatilho(
|
||||||
chave: 'valor_alterado',
|
gatilho: 'valor_alterado',
|
||||||
dados: Variaveis.comuns_reais.merge(
|
dados: Variaveis.comuns_reais.merge(
|
||||||
'motorista' => consolidacao_motorista.motorista_nome,
|
'motorista' => consolidacao_motorista.motorista_nome,
|
||||||
'consolidacao' => consolidacao.nome,
|
'consolidacao' => consolidacao.nome,
|
||||||
@@ -54,8 +54,8 @@ module Notificacao
|
|||||||
# (DetectarMudancasOperacaoJob) — é por onde chega a maioria, já que o
|
# (DetectarMudancasOperacaoJob) — é por onde chega a maioria, já que o
|
||||||
# espelho sincroniza mudanças feitas fora daqui.
|
# espelho sincroniza mudanças feitas fora daqui.
|
||||||
def operacao_alterada(operacao:, o_que_mudou:, nf: nil)
|
def operacao_alterada(operacao:, o_que_mudou:, nf: nil)
|
||||||
Despachante.disparar(
|
Despachante.disparar_gatilho(
|
||||||
chave: 'operacao_alterada',
|
gatilho: 'operacao_alterada',
|
||||||
dados: Variaveis.comuns_reais.merge(
|
dados: Variaveis.comuns_reais.merge(
|
||||||
'operacao' => operacao.to_s,
|
'operacao' => operacao.to_s,
|
||||||
'o_que_mudou' => o_que_mudou.to_s,
|
'o_que_mudou' => o_que_mudou.to_s,
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ class NotificacaoService
|
|||||||
# passa a vir do template configurado pelo ADM.
|
# passa a vir do template configurado pelo ADM.
|
||||||
def avisar_grupos(cm, evento, user)
|
def avisar_grupos(cm, evento, user)
|
||||||
evt = EVENTOS.fetch(evento)
|
evt = EVENTOS.fetch(evento)
|
||||||
Notificacao::Despachante.disparar(
|
Notificacao::Despachante.disparar_gatilho(
|
||||||
chave: evt[:chave],
|
gatilho: evt[:chave],
|
||||||
dados: variaveis(cm, user),
|
dados: variaveis(cm, user),
|
||||||
assunto: "#{evt[:titulo]} — #{@consolidacao.nome}",
|
assunto: "#{evt[:titulo]} — #{@consolidacao.nome}",
|
||||||
corpo: mensagem_para_grupos(cm, evento, user)
|
corpo: mensagem_para_grupos(cm, evento, user)
|
||||||
|
|||||||
@@ -132,3 +132,43 @@ RSpec.describe Notificacao::Despachante do
|
|||||||
expect { despachar }.not_to change(NotificacaoEnvio, :count)
|
expect { despachar }.not_to change(NotificacaoEnvio, :count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Buscar o evento pela `chave` fazia só o de SISTEMA disparar: um evento criado
|
||||||
|
# pelo ADM com o mesmo gatilho ficava mudo para sempre, sem erro nenhum — a tela
|
||||||
|
# oferecia o gatilho e nada acontecia.
|
||||||
|
RSpec.describe Notificacao::Despachante, '.disparar_gatilho' do
|
||||||
|
it 'agenda TODOS os eventos ativos do gatilho, não só o de sistema' do
|
||||||
|
sistema = create(:evento_notificacao, gatilho: 'valor_alterado', chave: 'valor_alterado', sistema: true)
|
||||||
|
do_adm = create(:evento_notificacao, gatilho: 'valor_alterado', chave: 'aviso_extra')
|
||||||
|
create(:evento_notificacao, gatilho: 'pagamento_efetuado', chave: 'outro')
|
||||||
|
|
||||||
|
agendados = []
|
||||||
|
allow(NotificacaoJob).to receive(:perform_later) { |id, *| agendados << id }
|
||||||
|
|
||||||
|
described_class.disparar_gatilho(gatilho: 'valor_alterado', corpo: 'x')
|
||||||
|
|
||||||
|
expect(agendados).to contain_exactly(sistema.id, do_adm.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignora evento desativado' do
|
||||||
|
create(:evento_notificacao, gatilho: 'valor_alterado', chave: 'desligado', ativo: false)
|
||||||
|
allow(NotificacaoJob).to receive(:perform_later)
|
||||||
|
|
||||||
|
described_class.disparar_gatilho(gatilho: 'valor_alterado', corpo: 'x')
|
||||||
|
|
||||||
|
expect(NotificacaoJob).not_to have_received(:perform_later)
|
||||||
|
end
|
||||||
|
|
||||||
|
# O botão "disparar agora" mira UM evento — ali a chave é o identificador certo.
|
||||||
|
it '.disparar continua agendando só o evento da chave' do
|
||||||
|
alvo = create(:evento_notificacao, gatilho: 'manual', chave: 'so_este')
|
||||||
|
create(:evento_notificacao, gatilho: 'manual', chave: 'outro_manual')
|
||||||
|
|
||||||
|
agendados = []
|
||||||
|
allow(NotificacaoJob).to receive(:perform_later) { |id, *| agendados << id }
|
||||||
|
|
||||||
|
described_class.disparar(chave: 'so_este', corpo: 'x')
|
||||||
|
|
||||||
|
expect(agendados).to eq([alvo.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Notificacao::Gatilhos do
|
RSpec.describe Notificacao::Gatilhos do
|
||||||
before { allow(Notificacao::Despachante).to receive(:disparar) }
|
before { allow(Notificacao::Despachante).to receive(:disparar_gatilho) }
|
||||||
|
|
||||||
describe '.valor_alterado' do
|
describe '.valor_alterado' do
|
||||||
let(:consolidacao) { create(:consolidacao, status: :finalizada) }
|
let(:consolidacao) { create(:consolidacao, status: :finalizada) }
|
||||||
@@ -14,26 +14,26 @@ RSpec.describe Notificacao::Gatilhos do
|
|||||||
|
|
||||||
described_class.valor_alterado(rascunho, cm_rasc, anterior: 50.0)
|
described_class.valor_alterado(rascunho, cm_rasc, anterior: 50.0)
|
||||||
|
|
||||||
expect(Notificacao::Despachante).not_to have_received(:disparar)
|
expect(Notificacao::Despachante).not_to have_received(:disparar_gatilho)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'não dispara quando o valor não mudou' do
|
it 'não dispara quando o valor não mudou' do
|
||||||
described_class.valor_alterado(consolidacao, cm, anterior: 5060.0)
|
described_class.valor_alterado(consolidacao, cm, anterior: 5060.0)
|
||||||
expect(Notificacao::Despachante).not_to have_received(:disparar)
|
expect(Notificacao::Despachante).not_to have_received(:disparar_gatilho)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'descreve a mudança em {{o_que_mudou}}' do
|
it 'descreve a mudança em {{o_que_mudou}}' do
|
||||||
described_class.valor_alterado(consolidacao, cm, anterior: 4900.0)
|
described_class.valor_alterado(consolidacao, cm, anterior: 4900.0)
|
||||||
|
|
||||||
expect(Notificacao::Despachante).to have_received(:disparar) do |args|
|
expect(Notificacao::Despachante).to have_received(:disparar_gatilho) do |args|
|
||||||
expect(args[:chave]).to eq('valor_alterado')
|
expect(args[:gatilho]).to eq('valor_alterado')
|
||||||
expect(args[:dados]['o_que_mudou']).to eq('Valor passou de R$ 4.900,00 para R$ 5.060,00')
|
expect(args[:dados]['o_que_mudou']).to eq('Valor passou de R$ 4.900,00 para R$ 5.060,00')
|
||||||
expect(args[:dados]['valor']).to eq('R$ 5.060,00')
|
expect(args[:dados]['valor']).to eq('R$ 5.060,00')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'não levanta quando algo dá errado — não pode travar um fechamento' do
|
it 'não levanta quando algo dá errado — não pode travar um fechamento' do
|
||||||
allow(Notificacao::Despachante).to receive(:disparar).and_raise(StandardError, 'boom')
|
allow(Notificacao::Despachante).to receive(:disparar_gatilho).and_raise(StandardError, 'boom')
|
||||||
expect { described_class.valor_alterado(consolidacao, cm, anterior: 1.0) }.not_to raise_error
|
expect { described_class.valor_alterado(consolidacao, cm, anterior: 1.0) }.not_to raise_error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -43,8 +43,8 @@ RSpec.describe Notificacao::Gatilhos do
|
|||||||
described_class.operacao_alterada(operacao: 'UBS NORTE AGO 2026',
|
described_class.operacao_alterada(operacao: 'UBS NORTE AGO 2026',
|
||||||
o_que_mudou: '3 notas na operação a mais', nf: '85382')
|
o_que_mudou: '3 notas na operação a mais', nf: '85382')
|
||||||
|
|
||||||
expect(Notificacao::Despachante).to have_received(:disparar) do |args|
|
expect(Notificacao::Despachante).to have_received(:disparar_gatilho) do |args|
|
||||||
expect(args[:chave]).to eq('operacao_alterada')
|
expect(args[:gatilho]).to eq('operacao_alterada')
|
||||||
expect(args[:dados]['operacao']).to eq('UBS NORTE AGO 2026')
|
expect(args[:dados]['operacao']).to eq('UBS NORTE AGO 2026')
|
||||||
expect(args[:dados]['nf']).to eq('85382')
|
expect(args[:dados]['nf']).to eq('85382')
|
||||||
end
|
end
|
||||||
@@ -60,7 +60,7 @@ RSpec.describe Notificacao::Gatilhos do
|
|||||||
|
|
||||||
described_class.valor_alterado(consolidacao, cm, anterior: 1.0)
|
described_class.valor_alterado(consolidacao, cm, anterior: 1.0)
|
||||||
|
|
||||||
expect(Notificacao::Despachante).to have_received(:disparar) do |args|
|
expect(Notificacao::Despachante).to have_received(:disparar_gatilho) do |args|
|
||||||
esperadas = Notificacao::Variaveis.nomes('valor_alterado') - ['contato'] # vem por destinatário
|
esperadas = Notificacao::Variaveis.nomes('valor_alterado') - ['contato'] # vem por destinatário
|
||||||
expect(args[:dados].keys).to include(*esperadas)
|
expect(args[:dados].keys).to include(*esperadas)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -139,13 +139,25 @@ async function lerCorpo(req) {
|
|||||||
return partes.length ? JSON.parse(Buffer.concat(partes).toString('utf8')) : {};
|
return partes.length ? JSON.parse(Buffer.concat(partes).toString('utf8')) : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destino -> JID. Duas formas:
|
||||||
// "+55 11 92005-1157" / "5511920051157" -> "5511920051157@s.whatsapp.net"
|
// "+55 11 92005-1157" / "5511920051157" -> "5511920051157@s.whatsapp.net"
|
||||||
function jidDe(numero) {
|
// "120363012345678901@g.us" -> passa direto (grupo)
|
||||||
const digitos = String(numero || '').replace(/\D/g, '');
|
// O grupo NAO e telefone: e um identificador proprio do WhatsApp, entao vem
|
||||||
|
// pronto do cadastro e so precisa ser validado no formato.
|
||||||
|
function jidDe(destino) {
|
||||||
|
const valor = String(destino || '').trim();
|
||||||
|
if (/^\d{5,}@g\.us$/.test(valor)) return valor;
|
||||||
|
if (/^\d{10,15}@s\.whatsapp\.net$/.test(valor)) return valor;
|
||||||
|
|
||||||
|
const digitos = valor.replace(/\D/g, '');
|
||||||
if (digitos.length < 12 || digitos.length > 13) return null;
|
if (digitos.length < 12 || digitos.length > 13) return null;
|
||||||
return `${digitos}@s.whatsapp.net`;
|
return `${digitos}@s.whatsapp.net`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ehGrupo(jid) {
|
||||||
|
return String(jid).endsWith('@g.us');
|
||||||
|
}
|
||||||
|
|
||||||
const servidor = createServer(async (req, res) => {
|
const servidor = createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url, 'http://local');
|
const url = new URL(req.url, 'http://local');
|
||||||
@@ -174,14 +186,44 @@ const servidor = createServer(async (req, res) => {
|
|||||||
const resultado = await enfileirar(async () => {
|
const resultado = await enfileirar(async () => {
|
||||||
// Mandar para numero que nao tem WhatsApp e um dos sinais que a Meta usa
|
// Mandar para numero que nao tem WhatsApp e um dos sinais que a Meta usa
|
||||||
// para marcar conta de spam — conferir antes vale o round-trip.
|
// para marcar conta de spam — conferir antes vale o round-trip.
|
||||||
|
//
|
||||||
|
// A checagem NAO vale para grupo: `onWhatsApp` responde sobre numeros e
|
||||||
|
// devolveria vazio para um JID de grupo, derrubando um envio valido.
|
||||||
|
if (!ehGrupo(jid)) {
|
||||||
const [existe] = await sock.onWhatsApp(jid.split('@')[0]);
|
const [existe] = await sock.onWhatsApp(jid.split('@')[0]);
|
||||||
if (!existe?.exists) throw new Error('numero sem WhatsApp');
|
if (!existe?.exists) throw new Error('numero sem WhatsApp');
|
||||||
|
}
|
||||||
return sock.sendMessage(jid, { text: String(texto) });
|
return sock.sendMessage(jid, { text: String(texto) });
|
||||||
});
|
});
|
||||||
|
|
||||||
return json(res, 200, { ok: true, id: resultado?.key?.id || null });
|
return json(res, 200, { ok: true, id: resultado?.key?.id || null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grupos de que o numero conectado PARTICIPA. Existe porque o id de um grupo
|
||||||
|
// e opaco (`120363...@g.us`) — digitar na mao e pedir erro. O admin escolhe
|
||||||
|
// pelo nome na tela e o id vai junto.
|
||||||
|
if (url.pathname === '/grupos' && req.method === 'GET') {
|
||||||
|
if (!estado.conectado || !sock) return json(res, 503, { erro: 'whatsapp desconectado' });
|
||||||
|
|
||||||
|
const meu = (sock.user?.id || '').split(':')[0];
|
||||||
|
const todos = await sock.groupFetchAllParticipating();
|
||||||
|
|
||||||
|
const grupos = Object.values(todos || {}).map((g) => {
|
||||||
|
const eu = (g.participants || []).find((p) => String(p.id).startsWith(meu));
|
||||||
|
return {
|
||||||
|
id: g.id,
|
||||||
|
nome: g.subject || g.id,
|
||||||
|
participantes: (g.participants || []).length,
|
||||||
|
// `announce` = so admin escreve. Sem ser admin, o envio seria recusado
|
||||||
|
// e o erro so apareceria no log — melhor avisar antes, na tela.
|
||||||
|
somente_admin: Boolean(g.announce),
|
||||||
|
sou_admin: Boolean(eu && eu.admin),
|
||||||
|
};
|
||||||
|
}).sort((a, b) => a.nome.localeCompare(b.nome));
|
||||||
|
|
||||||
|
return json(res, 200, { grupos });
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname === '/logout' && req.method === 'POST') {
|
if (url.pathname === '/logout' && req.method === 'POST') {
|
||||||
if (sock) await sock.logout().catch(() => {});
|
if (sock) await sock.logout().catch(() => {});
|
||||||
await rm(AUTH_DIR, { recursive: true, force: true }).catch(() => {});
|
await rm(AUTH_DIR, { recursive: true, force: true }).catch(() => {});
|
||||||
|
|||||||
Reference in New Issue
Block a user