Implantação da geração de mensagem de forma livre e mudança na engine de mensagem
This commit is contained in:
41
db/migrate/20260824000001_create_notificacao_contatos.rb
Normal file
41
db/migrate/20260824000001_create_notificacao_contatos.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# Lista de destinatários das notificações, cadastrada à mão pelo ADM.
|
||||
#
|
||||
# Antes, "quem recebe" era implícito: o NotificacaoService procurava o User
|
||||
# motorista pelo NOME da consolidação. Quem não é usuário do sistema (diretoria,
|
||||
# cliente, terceiro) não tinha como ser avisado, e não havia tela nenhuma para
|
||||
# controlar isso.
|
||||
#
|
||||
# O contato pertence a UM grupo, e é o GRUPO que assina os eventos (ver
|
||||
# 20260824000002) — assim cadastrar alguém novo é escolher o grupo, não repetir
|
||||
# a configuração pessoa por pessoa.
|
||||
class CreateNotificacaoContatos < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :grupos_contato do |t|
|
||||
t.string :nome, null: false
|
||||
t.string :descricao
|
||||
t.boolean :ativo, null: false, default: true
|
||||
t.timestamps
|
||||
end
|
||||
add_index :grupos_contato, 'LOWER(nome)', unique: true, name: 'index_grupos_contato_nome_unico'
|
||||
|
||||
create_table :contatos do |t|
|
||||
t.references :grupo_contato, foreign_key: true, index: true
|
||||
|
||||
t.string :nome, null: false
|
||||
# E.164 normalizado (+5511920051157). Vazio = contato só de e-mail.
|
||||
t.string :telefone
|
||||
t.string :email
|
||||
t.boolean :ativo, null: false, default: true
|
||||
t.string :observacao
|
||||
|
||||
# Quando o contato É um usuário do sistema (motorista), guardar o vínculo
|
||||
# permite o disparo "avisar o envolvido" achar a pessoa certa sem depender
|
||||
# de casar nome por string, que é o que se faz hoje.
|
||||
t.references :user, foreign_key: true, index: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :contatos, :telefone
|
||||
add_index :contatos, :email
|
||||
end
|
||||
end
|
||||
70
db/migrate/20260824000002_create_notificacao_eventos.rb
Normal file
70
db/migrate/20260824000002_create_notificacao_eventos.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
# Eventos de notificação + assinatura por grupo.
|
||||
#
|
||||
# O ADM cadastra quantos eventos quiser, mas o GATILHO sai de uma lista fixa:
|
||||
# gatilho é código. `manual` cobre o aviso pontual (botão "disparar agora") e
|
||||
# `agendado` cobre o resumo periódico; os demais são pontos que o sistema já
|
||||
# conhece (consolidação finalizada, pagamento efetuado, valor alterado, dados da
|
||||
# operação mudaram).
|
||||
#
|
||||
# `sistema: true` marca os eventos que nascem com o app: podem ser editados e
|
||||
# desligados, mas não apagados — apagar deixaria o código disparando no vazio.
|
||||
class CreateNotificacaoEventos < ActiveRecord::Migration[7.1]
|
||||
GATILHOS = %w[manual consolidacao_finalizada pagamento_efetuado
|
||||
valor_alterado operacao_alterada agendado].freeze
|
||||
|
||||
def up
|
||||
create_table :eventos_notificacao do |t|
|
||||
t.string :nome, null: false
|
||||
t.string :chave, null: false # slug estável usado pelo código
|
||||
t.string :gatilho, null: false
|
||||
t.string :descricao
|
||||
t.boolean :ativo, null: false, default: true
|
||||
t.boolean :sistema, null: false, default: false
|
||||
|
||||
# Avisar também a pessoa diretamente envolvida no fato (o motorista do
|
||||
# pagamento), além dos grupos assinantes. É o comportamento de hoje.
|
||||
t.boolean :notificar_envolvido, null: false, default: true
|
||||
|
||||
# Só para gatilho `agendado`.
|
||||
t.string :frequencia # diaria | semanal
|
||||
t.integer :hora, default: 8 # 0..23
|
||||
t.integer :dia_semana # 0..6 (domingo=0), só na semanal
|
||||
t.datetime :ultimo_disparo_em
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :eventos_notificacao, :chave, unique: true
|
||||
|
||||
create_table :grupo_evento_assinaturas do |t|
|
||||
t.references :grupo_contato, null: false, foreign_key: true
|
||||
t.references :evento_notificacao, null: false, foreign_key: true
|
||||
t.string :canal, null: false, default: 'ambos' # ambos | whatsapp | email
|
||||
t.boolean :ativo, null: false, default: true
|
||||
t.timestamps
|
||||
end
|
||||
add_index :grupo_evento_assinaturas, %i[grupo_contato_id evento_notificacao_id],
|
||||
unique: true, name: 'index_assinatura_grupo_evento_unico'
|
||||
|
||||
# Os dois eventos que o código JÁ dispara hoje, para nada parar de funcionar
|
||||
# no deploy. SQL cru de propósito: migration não deve depender de model.
|
||||
agora = 'NOW()'
|
||||
[
|
||||
['Pagamento fechado', 'consolidacao_finalizada', 'consolidacao_finalizada',
|
||||
'Disparado quando uma consolidação é finalizada.'],
|
||||
['Pagamento efetuado', 'pagamento_efetuado', 'pagamento_efetuado',
|
||||
'Disparado quando o pagamento de um motorista é marcado como pago.']
|
||||
].each do |nome, chave, gatilho, descricao|
|
||||
execute(<<~SQL.squish)
|
||||
INSERT INTO eventos_notificacao
|
||||
(nome, chave, gatilho, descricao, ativo, sistema, notificar_envolvido, hora, created_at, updated_at)
|
||||
VALUES (#{quote(nome)}, #{quote(chave)}, #{quote(gatilho)}, #{quote(descricao)},
|
||||
true, true, true, 8, #{agora}, #{agora})
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :grupo_evento_assinaturas
|
||||
drop_table :eventos_notificacao
|
||||
end
|
||||
end
|
||||
31
db/migrate/20260824000003_create_notificacao_envios.rb
Normal file
31
db/migrate/20260824000003_create_notificacao_envios.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# Log de cada mensagem disparada — quem recebeu, por qual canal, com que texto e
|
||||
# se deu certo.
|
||||
#
|
||||
# Hoje uma falha de envio só vira linha no log do Rails (NotificacaoService
|
||||
# engole a exceção de propósito, para um SMTP fora do ar não travar o
|
||||
# fechamento). Isso é certo, mas deixa o operador sem resposta para "o motorista
|
||||
# recebeu?". Com o WhatsApp por sessão QR — que cai sozinho e precisa
|
||||
# repareamento — essa pergunta passa a ser rotina.
|
||||
class CreateNotificacaoEnvios < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :notificacao_envios do |t|
|
||||
t.references :evento_notificacao, foreign_key: true, index: true
|
||||
t.references :contato, foreign_key: true, index: true
|
||||
t.references :user, foreign_key: true, index: true
|
||||
|
||||
t.string :canal, null: false # whatsapp | email
|
||||
t.string :destino, null: false # telefone E.164 ou e-mail
|
||||
t.string :assunto
|
||||
t.text :corpo
|
||||
t.string :status, null: false, default: 'pendente' # pendente|enviado|falhou
|
||||
t.text :erro
|
||||
t.integer :tentativas, null: false, default: 0
|
||||
t.datetime :enviado_em
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :notificacao_envios, %i[status created_at]
|
||||
add_index :notificacao_envios, :created_at
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
# WhatsApp por sessão pareada com QR code (Baileys), no lugar do Twilio.
|
||||
#
|
||||
# `whatsapp_provedor` decide quem envia. Nasce com o valor que a instalação já
|
||||
# usa ('twilio' se havia credencial configurada), então o deploy não muda o
|
||||
# comportamento até o ADM parear o QR e trocar o provedor na tela.
|
||||
#
|
||||
# ⚠️ O envio por QR é a porta do WhatsApp Web (engenharia reversa), fora dos
|
||||
# Termos do WhatsApp: a Meta PODE banir o número. Por isso o intervalo entre
|
||||
# mensagens é configurável e nasce em 5s — disparo em rajada para dezenas de
|
||||
# contatos é a forma mais rápida de perder o número.
|
||||
class AddBaileysToConfiguracaoNotificacoes < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
change_table :configuracao_notificacoes, bulk: true do |t|
|
||||
t.string :whatsapp_provedor, null: false, default: 'twilio' # twilio | baileys
|
||||
t.string :baileys_url
|
||||
t.text :baileys_token_cifrado
|
||||
t.string :whatsapp_numero_conectado
|
||||
t.datetime :whatsapp_conectado_em
|
||||
t.integer :whatsapp_intervalo_segundos, null: false, default: 5
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
change_table :configuracao_notificacoes, bulk: true do |t|
|
||||
t.remove :whatsapp_provedor, :baileys_url, :baileys_token_cifrado,
|
||||
:whatsapp_numero_conectado, :whatsapp_conectado_em,
|
||||
:whatsapp_intervalo_segundos
|
||||
end
|
||||
end
|
||||
end
|
||||
29
db/migrate/20260824000005_create_mensagem_templates.rb
Normal file
29
db/migrate/20260824000005_create_mensagem_templates.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# Corpo das mensagens montado pelo ADM, um template por EVENTO e por CANAL.
|
||||
#
|
||||
# Antes, o texto era string interpolada dentro do NotificacaoService: mudar uma
|
||||
# palavra exigia deploy. Agora o ADM empilha blocos (cabeçalho, texto, tabela de
|
||||
# valores, aviso, botão, rodapé) na tela.
|
||||
#
|
||||
# `blocos` é jsonb e não tabela filha de propósito: a ordem faz parte do dado
|
||||
# (é uma lista, não um conjunto), o conteúdo de cada tipo de bloco é diferente, e
|
||||
# salvar o template inteiro numa transação evita estado meio-salvo. A validação
|
||||
# da forma acontece no model (MensagemTemplate#normalizar_blocos), não no banco.
|
||||
class CreateMensagemTemplates < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :mensagem_templates do |t|
|
||||
t.references :evento_notificacao, null: false, foreign_key: true
|
||||
|
||||
t.string :canal, null: false # whatsapp | email
|
||||
t.string :assunto # só e-mail
|
||||
t.jsonb :blocos, null: false, default: []
|
||||
t.boolean :ativo, null: false, default: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
# Um template por evento+canal: dois templates ativos para o mesmo par
|
||||
# deixariam o envio escolhendo em silêncio.
|
||||
add_index :mensagem_templates, %i[evento_notificacao_id canal],
|
||||
unique: true, name: 'index_template_evento_canal_unico'
|
||||
end
|
||||
end
|
||||
27
db/migrate/20260824000006_create_operacao_snapshots.rb
Normal file
27
db/migrate/20260824000006_create_operacao_snapshots.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
# Retrato dos números de cada operação, para detectar o que MUDOU entre uma
|
||||
# varredura e a seguinte.
|
||||
#
|
||||
# A maior parte das mudanças da operação não passa pelo nosso código: elas
|
||||
# acontecem no SimpliRoute e chegam pelo sync do espelho (que é read-only aqui).
|
||||
# Sem guardar o retrato anterior não há como dizer "3 NFs entraram, 1 saiu" —
|
||||
# só dá para mostrar o número de agora, que é o que os dashboards já fazem.
|
||||
#
|
||||
# Uma linha por tabela de operação. É snapshot, não histórico: a linha é
|
||||
# ATUALIZADA a cada varredura. Guardar a série inteira seria outra feature (e a
|
||||
# tabela cresceria por operação × varredura sem ninguém consultar).
|
||||
class CreateOperacaoSnapshots < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :operacao_snapshots do |t|
|
||||
t.string :tabela, null: false
|
||||
t.integer :total_notas, null: false, default: 0
|
||||
t.integer :entregues, null: false, default: 0
|
||||
t.integer :nao_entregues, null: false, default: 0
|
||||
t.integer :pendentes, null: false, default: 0
|
||||
t.datetime :capturado_em, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :operacao_snapshots, :tabela, unique: true
|
||||
end
|
||||
end
|
||||
35
db/migrate/20260824000007_semear_eventos_de_gatilho.rb
Normal file
35
db/migrate/20260824000007_semear_eventos_de_gatilho.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# Cria os eventos de sistema dos gatilhos ligados na etapa 3.
|
||||
#
|
||||
# Sem isso, `Despachante.disparar(chave: 'valor_alterado')` não acharia evento
|
||||
# nenhum e o gatilho ficaria mudo — e o ADM teria de adivinhar que precisa criar
|
||||
# um evento cujo nome gere exatamente aquela `chave`.
|
||||
#
|
||||
# Nascem ATIVOS e isso é seguro: sem grupo assinante não há destinatário, então
|
||||
# nada é enviado até o ADM marcar um grupo na tela.
|
||||
class SemearEventosDeGatilho < ActiveRecord::Migration[7.1]
|
||||
EVENTOS = [
|
||||
['Valor alterado no fechamento', 'valor_alterado', 'valor_alterado',
|
||||
'Disparado quando o valor de um motorista muda depois da consolidação finalizada.'],
|
||||
['Dados da operação mudaram', 'operacao_alterada', 'operacao_alterada',
|
||||
'NF que entrou ou saiu, entrega que trocou de status, lançamento corrigido no SimpliRoute.']
|
||||
].freeze
|
||||
|
||||
def up
|
||||
EVENTOS.each do |nome, chave, gatilho, descricao|
|
||||
# Idempotente: rodar de novo (ou num banco que já tenha o evento criado à
|
||||
# mão) não pode estourar índice único.
|
||||
next if select_value("SELECT 1 FROM eventos_notificacao WHERE chave = #{quote(chave)}")
|
||||
|
||||
execute(<<~SQL.squish)
|
||||
INSERT INTO eventos_notificacao
|
||||
(nome, chave, gatilho, descricao, ativo, sistema, notificar_envolvido, hora, created_at, updated_at)
|
||||
VALUES (#{quote(nome)}, #{quote(chave)}, #{quote(gatilho)}, #{quote(descricao)},
|
||||
true, true, true, 8, NOW(), NOW())
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
EVENTOS.each { |_, chave, _, _| execute("DELETE FROM eventos_notificacao WHERE chave = #{quote(chave)}") }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user