mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
docs(suspensao): cabeçalho do apêndice, MANIFEST, comentário do dreno e fragmento dizem o que existe
- baseline.sql: o cabeçalho do apêndice 0496 lista as seções copiadas de fato (A, B, C0, C, E, F e G), não só A, B, C e E. - MANIFEST.md: a linha da 0496 descreve C0 (descarte da fila que assenta rascunho, Meet e turn_discarded), F (claim do follow-up sem org parada, retomada na reativação) e G (turn_discarded só pelo servidor), e cita o invariante followup-org-suspensa. - dispatcher-org-parada.test.ts: o comentário do caso da org operante diz o que a sabotagem mediu — `!ehOperante` já avermelhava; só "toda org parada" escapava sem o caso novo. - .changes/suspensao-que-suspende.md: os follow-ups ficam parados na suspensão e retomam na reativação; o passo com envio descartado ganha envio novo, no ritmo normal da fila. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a3a6cae7e6
commit
60ccab5bb0
@@ -8,4 +8,4 @@ Até aqui, suspender uma empresa em Admin › Empresas só tirava as pessoas da
|
||||
|
||||
As mensagens que chegam continuam gravadas, e as páginas de anúncio e o link de rastreio seguem no ar. Quem entra numa empresa suspensa cai numa tela que diz o que fazer. Quem administra vê o contato do suporte e os pedidos de LGPD dos clientes, que não param durante a suspensão. As demais pessoas leem que devem avisar o administrador, e quem participa de outra empresa ativa volta para ela com um clique.
|
||||
|
||||
Ao reativar, nada sai em rajada: a Central mostra um aviso com quantas conversas receberam mensagem durante a suspensão e leva ao Inbox, porque a IA não vai respondê-las sozinha. Quem tem acesso só de leitura ao painel da instalação deixa de conseguir suspender, reativar ou alterar o estado de uma empresa, pela tela ou pela API, e também deixa de mudar configurações ou apagar dados de uma empresa em que é só participante. Nenhuma configuração ou ação é necessária.
|
||||
Ao reativar, nada sai em rajada: a Central mostra um aviso com quantas conversas receberam mensagem durante a suspensão e leva ao Inbox, porque a IA não vai respondê-las sozinha. Os follow-ups em andamento ficam parados enquanto a suspensão durar e, ao reativar, retomam de onde pararam; o passo cujo envio foi descartado na suspensão ganha um envio novo, no ritmo normal da fila. Quem tem acesso só de leitura ao painel da instalação deixa de conseguir suspender, reativar ou alterar o estado de uma empresa, pela tela ou pela API, e também deixa de mudar configurações ou apagar dados de uma empresa em que é só participante. Nenhuma configuração ou ação é necessária.
|
||||
|
||||
@@ -42993,7 +42993,7 @@ create policy followup_flow_versions_delete on public.followup_flow_versions
|
||||
|
||||
-- ---- org operante e suspensão tipada (migration 0496) ----
|
||||
-- A suspensão que suspende (spec cobrança do revendedor §2.1, §3.1). Corpo e
|
||||
-- porquê: a migration 0496. Cópia byte a byte das seções A, B, C e E dela; a
|
||||
-- porquê: a migration 0496. Cópia byte a byte das seções A, B, C0, C, E, F e G dela; a
|
||||
-- seção D (kind 'org_reativada') entra NO LUGAR, no bloco único de
|
||||
-- agent_inbox_items_kind_check. Entra ANTES da VARREDURA anon porque cria função.
|
||||
|
||||
|
||||
@@ -484,4 +484,4 @@ To re-apply on a fresh Supabase project, replay the migrations in version order
|
||||
| `20260929115848` | `0491_dedupe_de_event_dead_atomico` | **O aviso `event_dead` não abre em dobro com dois drenos concorrentes (#880).** O dedupe era pergunta e escrita separadas: `lib/event-log/drain.ts` consulta se já existe aviso aberto e só depois insere, e o `insert … where not exists` de `insertInboxItem` (`lib/agent-engine/db/repository.ts`) não tinha índice nenhum que sustentasse a condição — o cron `event-log-drain` e o drain-loop do worker rodam `drainEventLog` no mesmo instante e os dois leem "não existe" antes de qualquer escrita. Índice único parcial `agent_inbox_event_dead_aberto_unico (organization_id, kind, title) where status = 'open' and kind = 'event_dead'`: a chave leva o TÍTULO porque as duas famílias do `event_dead` (a da IA que deixou de responder e a de mídia/automação, `lib/event-log/aviso-de-evento-morto.ts`) precisam conviver abertas na mesma organização, e o predicado é PARCIAL porque `status` na chave guardaria uma linha resolvida para sempre e mataria a reabertura. Escopo só `event_dead` — os outros dedupes desta tabela são por ref e querem várias linhas abertas com o mesmo título, uma por conversa ou por lead. Prévia resolve as cópias abertas repetidas (só a mais antiga fica aberta; nunca delete, régua da 0064). `insertInboxItem` e o dreno passam a tratar `23505` como "já havia um aviso aberto" — devolve `null` e não loga erro —, e a reabertura para um slot que já tem um aberto igual volta `409` em vez de `500` (`app/api/v1/ai/inbox/[id]/route.ts`). Idempotente nas duas pontas. Gate: `tests/unit/aviso-event-dead-concorrente-abre-uma-vez.test.ts`. |
|
||||
| `20260930010300` | `0494_lgpd_cascata_banco_alcanca_notas_itens` | **A cascata do BANCO alcança `lead_notes`, `ai_agent_runs.tool_calls`, `lead_state` e `contacts.social_identity` (issue #1964, follow-up do #1958).** O gatilho da virada de `is_anonymized` (desenho da 0391, a porta que os DOIS caminhos de anonimização cruzam) passou a redigir também: `lead_notes.headline`/`body` (e `embedding`) → `(anonimizado)`, `ai_agent_runs.tool_calls` → redação que PRESERVA o nome da ferramenta e apaga argumentos/resultados (espelha `redigirToolCalls`/`serialize.ts`, com `redacted = true` em todo passo), `lead_state.next_action` → `null` e `qualification` → `{}`, e `contacts.social_identity` → `null` (o índice único parcial `where social_identity is not null` torna anular seguro). Filtra organização **e** contato em todo passo; idempotente — guard no WHERE repete o marcador de "já redigido" da app para a varredura diária não reescrever o que já está anonimizado. `fn_lgpd_redigir_tool_calls(jsonb)` é função nova `immutable` revogada de `anon`/`authenticated` (único chamador é o gatilho, `security definer` de dono postgres). Sem cura retroativa no corpo (a varredura diária da app já cura o que veio antes); apêndice idempotente no `baseline.sql` antes da VARREDURA anon. Gate: `tests/invariants/lgpd-cascata-do-banco-alcanca-notas-itens.test.ts`. |
|
||||
| `20260930120000` | `0495_janela_de_resposta_separada` | **A janela de RESPOSTA sai da janela de DISPARO (#1983, de @suporteubere99-coder).** As duas coisas eram regidas por UM par de horas em `channel_knobs` (`window_start_hour`/`window_end_hour`, da 0010), então abrir o atendimento para 24h abria junto o disparo em massa, a prospecção e o `followup_turn` (retomada de conversa parada). Duas colunas nullable, `resposta_start_hour`/`resposta_end_hour`, sem DEFAULT: **NULL herda a janela de disparo, coluna a coluna** (`loadChannelKnobs` em `lib/agent-engine/pacing/store.ts` e `effectiveKnobs` em `lib/ai/pacing-knobs.ts`, a mesma regra que a ficha Anti-ban mostra no placeholder) — quem só atualiza não muda de operação. CHECK 0..23 / 1..24 com `drop … if exists` antes do `add`. Um bloco `do` guardado renomeia `reengajar_start_hour`/`reengajar_end_hour` para o nome novo onde a primeira versão do PR (nome antigo) já tinha sido aplicada. A separação vive em `PacingInput.resposta` (omitido = disparo: todo chamador fora do turno de resposta fica na janela de disparo); só `inbound_turn`/`case_reply_turn` passam `true` (`eTurnoDeResposta` em `inbound-turn.ts`), inclusive na pergunta do roteiro que sai no mesmo turno. `nextDayOpen` segue na janela de disparo: cap diário é proteção anti-ban. Cap, warm-up e throttle continuam para os dois lados. Sem função nova. Gates: `lib/agent-engine/pacing/janela-de-resposta.test.ts` e `tests/unit/protecao-de-envio-aceita-data-em-branco.test.ts` (0/24 aceito, 22/7 recusado com 422). |
|
||||
| `20260930130000` | `0496_org_operante_e_suspensao_tipada` | **A suspensão que suspende (spec cobrança do revendedor, PR 1).** Suspender só tirava a pessoa da tela: a rota fazia leitura, UPDATE e `event_log` sem await em três passos soltos, nada parava jobs `pending` nem mensagens `queued`, e `status` era gravável pelo PostgREST por qualquer platform admin (`orgs_write_platform_admin` aceita `fn_is_platform_admin()`, que ignora o scope — um `support_readonly` reativava uma suspensa). (A) `organizations.suspended_kind` (`administrativa`/`cobranca`), backfill antes do CHECK, SEM CHECK de coerência com `status` (o `lgpd-redact-worker` troca para `redacted` sem limpar o tipo; regra de leitura em `lib/organizacao/operante.ts`) e `fn_org_operante(uuid)`, a régua SQL do predicado. (B) Gatilho `trg_organizacao_estado_so_pelo_servidor` (invoker, molde `fn_meet_stamp`): `authenticated`/`anon` não inserem organização nem mudam `status`, `suspended_*` ou `created_by` (42501). (C) `fn_suspender_organizacao` (definer, uma transação, `for update`): jobs `pending` → `failed`/`org_nao_operante`, mensagens `queued` → `failed`/`org_suspensa`, `tenant.suspended` no `event_log` na mesma transação; a administrativa prevalece. (D) kind `org_reativada` no bloco único de `agent_inbox_items_kind_check` (lista completa do baseline, `kind-check-migration-x-baseline`). (E) `fn_reativar_organizacao` (definer): exige o tipo (suspensão nula vale como administrativa), zera a suspensão, falha `pending` remanescente e abre UM item `org_reativada` com a contagem de conversas com mensagem durante a suspensão — nada é reprocessado. Nenhuma das duas cita `cobranca_assinaturas` (PR 2). EXECUTE revogado de `public`, `anon` e `authenticated`; só `service_role`. Apêndice antes da VARREDURA anon (seção D editada no lugar). Gate: `tests/invariants/org-suspensa.test.ts` e o par `organizations.suspended_kind` × `TIPOS_DE_SUSPENSAO` em `tests/invariants/vocabulario-banco-x-typescript.test.ts`. |
|
||||
| `20260930130000` | `0496_org_operante_e_suspensao_tipada` | **A suspensão que suspende (spec cobrança do revendedor, PR 1).** Suspender só tirava a pessoa da tela: a rota fazia leitura, UPDATE e `event_log` sem await em três passos soltos, nada parava jobs `pending` nem mensagens `queued`, e `status` era gravável pelo PostgREST por qualquer platform admin (`orgs_write_platform_admin` aceita `fn_is_platform_admin()`, que ignora o scope — um `support_readonly` reativava uma suspensa). (A) `organizations.suspended_kind` (`administrativa`/`cobranca`), backfill antes do CHECK, SEM CHECK de coerência com `status` (o `lgpd-redact-worker` troca para `redacted` sem limpar o tipo; regra de leitura em `lib/organizacao/operante.ts`) e `fn_org_operante(uuid)`, a régua SQL do predicado. (B) Gatilho `trg_organizacao_estado_so_pelo_servidor` (invoker, molde `fn_meet_stamp`): `authenticated`/`anon` não inserem organização nem mudam `status`, `suspended_*` ou `created_by` (42501). (C0) `fn_org_parada_descarta_fila` (invoker, EXECUTE revogado até de `service_role`; só as duas funções de estado a chamam): jobs `pending` → `failed`/`org_nao_operante` e, na mesma CTE, o que dependia deles — rascunho `approved` → `failed`/`org_suspensa`, link do Meet → `state failed` + aviso na Central (`fn_meet_notice`) — e o evento `turn_discarded` para a inscrição de follow-up parada num nó `action`. (C) `fn_suspender_organizacao` (definer, uma transação, `for update`): chama a C0, mensagens `queued` → `failed`/`org_suspensa`, `tenant.suspended` no `event_log` na mesma transação; a administrativa prevalece. (D) kind `org_reativada` no bloco único de `agent_inbox_items_kind_check` (lista completa do baseline, `kind-check-migration-x-baseline`). (E) `fn_reativar_organizacao` (definer): exige o tipo (suspensão nula vale como administrativa), zera a suspensão, falha `pending` remanescente e abre UM item `org_reativada` com a contagem de conversas (fora grupos) com mensagem durante a suspensão — nada é reprocessado. (F) `fn_claim_due_followup_enrollments` (definição vigente da 0308 + `exists` de org `active` na CTE `orgs`): o motor de follow-up não reclama inscrição de org parada nem toca o lease; na reativação ela RETOMA, e a de nó `action` com `turn_discarded` ganha turno novo em vez de esgotar o dead-man. (G) `fn_followup_generation_write` (definição vigente da 0488): recusa `turn_discarded` vindo da sessão (`auth.uid()`, 42501 `followup_step_internal`) — só o servidor grava o evento que faz o motor enfileirar. Nenhuma das duas cita `cobranca_assinaturas` (PR 2). EXECUTE revogado de `public`, `anon` e `authenticated`; só `service_role`. Apêndice antes da VARREDURA anon (seção D editada no lugar). Gate: `tests/invariants/org-suspensa.test.ts`, `tests/invariants/followup-org-suspensa.test.ts` e o par `organizations.suspended_kind` × `TIPOS_DE_SUSPENSAO` em `tests/invariants/vocabulario-banco-x-typescript.test.ts`. |
|
||||
|
||||
@@ -201,9 +201,10 @@ describe("drainEventLog lê o status das orgs do lote", () => {
|
||||
expect(resumo.pulados).toContain(`${EVENTO_DE_TESTE}/${PREFIXO_DE_TESTE}-pula: org_nao_operante`);
|
||||
});
|
||||
|
||||
// Controle do lado que mais pesa: sem este caso, inverter a régua do dreno
|
||||
// (`!ehOperante`) calaria a IA e as automações de TODAS as empresas ativas
|
||||
// com a suíte verde — os demais casos só olham a org parada.
|
||||
// Controle do lado que mais pesa. Medido por sabotagem: inverter a régua
|
||||
// (`!ehOperante`) já deixa o caso acima vermelho; o que passava com a suíte
|
||||
// verde era tratar TODA org como parada (`ehOperante(o.status) && false`), que
|
||||
// calaria a IA e as automações de todas as empresas ativas. Só este caso pega.
|
||||
it("org operante: o handler 'pula' roda e o evento fecha done com a chave em consumed_by, sem org_nao_operante", async () => {
|
||||
handlePula.mockClear();
|
||||
const { admin, updates } = dublarAdmin({ linhas: [linha()], orgs: [{ id: "org-1", status: "active" }] });
|
||||
|
||||
Reference in New Issue
Block a user