chore(migrations): renumera a credencial de mapas para 0504

Alocação da rodada de 30/09, por ordem de merge (triagem, passe 3-sexies):
#2048=0501, #2009=0502, #2013=0503, #2042=0504. O carimbo 20260930210000
fica. Muda o arquivo, o rótulo do apêndice no baseline, a linha do MANIFEST
e as menções em comentário e teste.

Válvulas: os dois arquivos em tests/invariants/ NASCEM neste PR (não existem
na main); a edição troca só o número 0503 -> 0504 dentro de uma string de
razão e de uma mensagem de asserção. Nenhuma asserção muda.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiTRHEcyddq8ZtQXyHDQsc
This commit is contained in:
melgarafael
2026-09-30 23:11:39 -03:00
co-authored by Claude Opus 5.5
parent b3496fb2cb
commit a7c3ec4632
12 changed files with 13 additions and 13 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* O cartão do pino mostra à equipe o endereço aproximado que o Google deu
* (chave de Mapas, 0503) — e, sem ele, o mesmo "Localização compartilhada" de antes.
* (chave de Mapas, 0504) — e, sem ele, o mesmo "Localização compartilhada" de antes.
*/
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
+1 -1
View File
@@ -3188,7 +3188,7 @@ motor, não mexido aqui).
Contexto: medido numa loja, 10 de 10 pinos do mês chegaram só com coordenadas —
o agente lia um link e perguntava a cidade de novo. Com a chave da Geocoding API
(Agente de IA › Provedores, cartão «Mapas (Google)»; tabela
`map_provider_credentials`, migration 0503; regra em `lib/mapas/`), o pino ganha
`map_provider_credentials`, migration 0504; regra em `lib/mapas/`), o pino ganha
o endereço aproximado no corpo (o que o agente lê) e no cartão do pino da
conversa. Os nomes vêm no idioma da organização (`organizations.locale`, pelo
registro de idiomas).
+1 -1
View File
@@ -999,7 +999,7 @@ export const AUDIT_ACTIONS = [
"company_people.updated",
"contacts.person_linked",
"imports.companies_people",
// A chave de Mapas da organização (0503, Agente de IA › Provedores): gravada ou
// A chave de Mapas da organização (0504, Agente de IA › Provedores): gravada ou
// trocada, e removida. O metadata diz O QUE mudou — nunca a chave.
"ai.maps_credential_saved",
"ai.maps_credential_removed",
+1 -1
View File
@@ -2468,7 +2468,7 @@ export const DICIONARIO: Traducoes = {
"ex: vip, carrinho-abandonado": { es: "Ej.: vip, carrito-abandonado" },
"Cancelar se o lead responder": { es: "Cancelar si el lead responde" },
"Salvar gatilho": { es: "Guardar disparador" },
// Cartão "Mapas" em Provedores (0503) e a API dele.
// Cartão "Mapas" em Provedores (0504) e a API dele.
"Mapas (Google)": { es: "Mapas (Google)" },
"Chave gravada": { es: "Clave guardada" },
"Sem chave": { es: "Sin clave" },
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* A chave de MAPAS da organização (migration 0503, `map_provider_credentials`).
* A chave de MAPAS da organização (migration 0504, `map_provider_credentials`).
*
* Opcional: sem chave, o pino do WhatsApp segue exatamente como antes (só o
* link do mapa). Mesma cifra das outras integrações (`fn_encrypt_oauth`), e a
+1 -1
View File
@@ -45419,7 +45419,7 @@ alter table public.ai_agent_versions
add constraint ai_agent_versions_inbound_debounce_ms_check
check (inbound_debounce_ms is null or (inbound_debounce_ms >= 0 and inbound_debounce_ms <= 60000));
-- ---- a chave de mapas da organização (migration 0503) ----
-- ---- a chave de mapas da organização (migration 0504) ----
-- Pino do WhatsApp → rua/bairro/cidade aproximados (lib/mapas/). Server-side only.
create table if not exists public.map_provider_credentials (
id uuid primary key default gen_random_uuid(),
@@ -1,4 +1,4 @@
-- 0503 — A chave de MAPAS da organização (geocodificação reversa do pino).
-- 0504 — A chave de MAPAS da organização (geocodificação reversa do pino).
--
-- Medido numa loja que vende pelo WhatsApp (28/09/2026): 10 de 47 conversas do
-- mês tiveram pino de localização, e os 10 chegaram só com coordenadas. O agente
+1 -1
View File
@@ -488,4 +488,4 @@ To re-apply on a fresh Supabase project, replay the migrations in version order
| `20260930150000` | `0498_debounce_rajada_configuravel` | **A janela de rajada do WhatsApp vira campo da versão do agente (#1856).** A janela que junta mensagens do MESMO contato numa resposta só era só a env do worker `INBOUND_DEBOUNCE_MS` (global, invisível na tela, exige VPS). Coluna `ai_agent_versions.inbound_debounce_ms` (integer, sem DEFAULT): NULL = usa a env da instalação (regressão zero para quem só atualiza), 0 desliga a coalescência, 1..60000 define a janela em ms com TETO de 60s — o teto é reforçado no leitor do worker (`debounceEfetivo` em `lib/agent-engine/edge/crm/debounce.ts`) e com CHECK `0..60000` no banco (par drop/add pra reaplicar). O drain lê a config do agente da conversa (`loadConversationAgentConfig`) e usa o valor do agente no lugar do literal. Gate: `lib/agent-engine/edge/crm/debounce.test.ts` (default = env, campo = valor, teto clamp no limite). |
| `20260930160000` | `0499_atraso_humano_por_conexao` | **O atraso humano vira configuração por conexão (issue #653).** Os quatro números que governam o atraso ANTES da primeira bolha (`atraso_notar_ms`/`ms_por_caractere`/`atraso_minimo_ms`/`atraso_maximo_ms`) saem do literal em `atraso-humano.ts` e passam a ser 4 colunas NULL em `channel_knobs` (a MESMA tabela dos knobs de pacing por conexão — 0010 e 0495; nada de tabela nova). NULL = default conservador em `defaults.ts` (900/22/1200/7500 — os valores de antes: regressão zero). O jitter entre bolhas (`1200 + rand*800` em `inbound-turn.ts`) NÃO ganha coluna: ele já é `throttle_ms` + `jitter_max_ms` (defaults 1200/800) e passa apenas a LER os knobs da conexão em vez do literal. CHECK de sanidade (`atraso_maximo_ms >= atraso_minimo_ms`, teto intervalMaxMs); exposição na ficha Anti-ban via `POST/GET /api/v1/ai/pacing`. Gate: `tests/unit/atraso-humano-knobs.test.ts`. |
| `20260930170000` | `0500_avisos_do_jev_na_central` | **Os avisos do Jev na Central (onda 3 do Jev, bloco 3.2 — "Avisar a equipe").** Quando a empresa põe "Perceber pedido para falar com uma pessoa" ou "…para parar de receber mensagens" em **Avisar a equipe**, o Jev abre UM aviso na Central por conversa e pedido onde percebeu o pedido que a regra de hoje não pegou — e só isso: nunca passa a conversa, nunca cala o agente, nunca bloqueia, nunca responde. Três partes. (1) `jev_pedido_de_humano` e `jev_parar_de_receber` no CHECK de `agent_inbox_items.kind`, com a LISTA INTEIRA derivada do `baseline.sql` (bloco único, o da 0105). Kind próprio e não `other`: a Central dá rótulo e destino por kind, e o `other` não leva a uma conversa. (2) **Um aviso por conversa e pedido é do banco:** índice único parcial `agent_inbox_jev_pedido_unico` em (organization_id, kind, ref_id) para os dois kinds, SEM status (precedente: `agent_inbox_routing_unique`); o gravador faz o insert e, no 23505, REABRE o aviso que existe — a busca antes da escrita deixava dois drenos abrirem dois. Antes do índice, os repetidos saem (fica o aberto, e o mais novo). (3) **O aviso fecha quando o pedido foi atendido, por qualquer caminho:** `trg_fechar_avisos_do_jev_da_conversa` (em `conversations`, `update of assigned_to_user_id,status,bot_silenced_until,last_handoff_at`) fecha os dois quando a conversa sai dos estados abertos (encerrada), e SÓ o de falar com uma pessoa quando ela fica com uma pessoa — alguém assume, ou ela é PASSADA: `last_handoff_at` novo ou o robô calado (`bot_silenced_until` no futuro), o que `performHumanHandoff`, o orquestrador do clima e a pausa manual gravam (a pausa manual nem toca no status, e o gatilho de atribuição da 0228 não a veria). O de parar de receber NÃO fecha ao assumir nem na passagem: o texto dele pede que a equipe assuma E peça ao cliente o PARAR, e fechá-lo no primeiro passo sumiria com o lembrete de um pedido de descadastro antes do passo que o atende; `trg_fechar_aviso_do_jev_ao_bloquear` (em `contacts`, quando `is_blocked` passa a true — o único escritor é o STOP do próprio cliente, `lib/channels/pos-entrada.ts`; não há bloqueio à mão no produto) fecha o de parar de receber de todas as conversas do contato. Funções `security definer`, sem HTTP, filtrando a organização da própria linha, revogadas de public/anon/authenticated. O corpo do aviso NÃO leva a frase do cliente: a Central é lida por toda a organização, e a conversa só por quem a enxerga. Aditiva e idempotente. Apêndice no `baseline.sql` antes da varredura de anon (texto idêntico às partes 2 e 3); o CHECK no bloco único. Guardado por `tests/invariants/jev-aviso-na-central.test.ts` (CHECK, assumir e encerrar), `tests/invariants/jev-aviso-fecha-sozinho-e-e-unico.test.ts` (a passagem, o bloqueio, o índice único e a deduplicação) e `tests/invariants/jev-parar-de-receber-sobrevive-a-assumir.test.ts` (assumir e passar não fecham o de parar de receber; encerrar fecha). |
| `20260930210000` | `0503_credencial_de_mapas` | **A chave de mapas da organização (`map_provider_credentials`).** Com uma chave da Geocoding API do Google, o pino de localização do WhatsApp — que chegava só com coordenadas (medido: 10 de 10 pinos do mês) — ganha rua, cidade e região aproximados (bairro e número não saem: medidos pouco confiáveis), e o agente deixa de perguntar a cidade. Opcional: sem chave, nada muda. Server-side only como a 0214: RLS ligada sem policies, grants revogados de anon/authenticated, chave cifrada por `fn_encrypt_oauth` e só os 4 últimos caracteres na tela. |
| `20260930210000` | `0504_credencial_de_mapas` | **A chave de mapas da organização (`map_provider_credentials`).** Com uma chave da Geocoding API do Google, o pino de localização do WhatsApp — que chegava só com coordenadas (medido: 10 de 10 pinos do mês) — ganha rua, cidade e região aproximados (bairro e número não saem: medidos pouco confiáveis), e o agente deixa de perguntar a cidade. Opcional: sem chave, nada muda. Server-side only como a 0214: RLS ligada sem policies, grants revogados de anon/authenticated, chave cifrada por `fn_encrypt_oauth` e só os 4 últimos caracteres na tela. |
@@ -1,5 +1,5 @@
/**
* A CHAVE DE MAPAS É SERVER-SIDE ONLY — E ISSO SE MEDE (migration 0503).
* A CHAVE DE MAPAS É SERVER-SIDE ONLY — E ISSO SE MEDE (migration 0504).
*
* `map_provider_credentials` guarda a chave do Google da organização. A anon
* key vai para o browser; se o PostgREST servisse esta tabela, qualquer um com
@@ -40,7 +40,7 @@ describe("o PostgREST não serve a chave de mapas", () => {
const existe = sql(`
select count(*) from information_schema.tables where table_schema = 'public' and table_name = '${TABELA}';
`).trim();
expect(existe, "a 0503 não chegou ao baseline — o kit self-host não cria a tabela").toBe("1");
expect(existe, "a 0504 não chegou ao baseline — o kit self-host não cria a tabela").toBe("1");
});
it("`anon` e `authenticated` não têm privilégio NENHUM", () => {
@@ -250,7 +250,7 @@ const PROVA_PROPRIA: readonly Excecao[] = [
"tests/invariants/credencial-de-mapas-e-server-side.test.ts — privilégio " +
"NENHUM para anon e authenticated, `permission denied` sob `set role`, RLS " +
"ligada, zero policies, `organization_id` NOT NULL com FK em cascata. Guarda " +
"a chave do Google da organização (0503), que só o servidor lê.",
"a chave do Google da organização (0504), que só o servidor lê.",
},
{
tabela: "ad_conversion_dispatches",
+1 -1
View File
@@ -820,7 +820,7 @@ const HOSTS_DECLARADOS: Record<string, EntradaDeHost> = {
motivo:
"endpoint público da BrasilAPI (`lib/brasil-api/client.ts`) que devolve os dados cadastrais de um CNPJ. É o destino do request, só chamado com o módulo de empresas ligado e só para o CNPJ que alguém da organização cadastrou ou importou; trocar pelo domínio do revendedor faria a consulta não chegar a lugar nenhum.",
},
// ── geocodificação reversa do pino (0503): destino de chamada ──
// ── geocodificação reversa do pino (0504): destino de chamada ──
"maps.googleapis.com": {
categoria: "FORNECEDOR",
motivo:
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* O PINO GANHA RUA, BAIRRO E CIDADE APROXIMADOS — quando a organização tem a
* chave de Mapas (migration 0503).
* chave de Mapas (migration 0504).
*
* Medido numa loja (28/09/2026): 10 de 10 pinos do mês chegaram só com
* coordenadas, e o agente lia um link sem saber a cidade. As respostas do