# W1-F3: Docs do cliente, FRENTE CORE (CCS + relatórios Vulci + SISBAJUD + AMES)

> **For Claude:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development (ou superpowers:executing-plans em sessão dedicada). Gates G1/G2/G3 do plano mestre (`docs/plans/2026-07-18-aceleracao-wave1-plano-mestre.md`) são OBRIGATÓRIOS em toda task: contrato provado antes de codar, TDD sobre a forma real, validação viva como definition of done. Ambientes: HML/PRD são READ-ONLY para subagentes; escrita e deploy só pelo orquestrador.

**Goal:** Fechar a FRENTE CORE dos 4 documentos do cliente: relatório de movimentações CCS com export PDF/Excel + notificação CCS por e-mail; relatórios Vulci #1 (extrato em 4 formatos com OFX), #2 (avisos de créditos), #3 (movimentações analítico/sintético), #14 (saldos por gerente) e a parte core do #8; SISBAJUD B.3 (titular, natureza, vara/juízo) e B.4 (atendimento editável); AMES ponta a ponta (solicitação, resposta, envio STA, tracker, tela).

**Architecture:** Tudo estende módulos existentes do core: relatórios novos seguem o padrão Reports Hub (use case JSON em `use_cases/reports/` + view Vue com `ReportLayout`/`ReportExportMenu` + export client-side via `dispatchReportDownload`); exports server-side (CSV/OFX) seguem o padrão `send_csv`/`export_statement`; e-mail liga o `Monetarie.Mailer` (Swoosh) que hoje é código morto, com adapter fail-safe; SISBAJUD ganha colunas aditivas em `judicial_orders` + decode + JOIN com `judicial_courts`; AMES nasce sobre `regulatory_files` + `StaDelivery` + `StaHandler` (correlação por tracker já existente), com tabela nova só para a solicitação.

**Tech Stack:** Elixir/Phoenix (core/backend), Ecto/Aurora PG, Oban, NATS (`monetarie.sta.*`), Swoosh + gen_smtp, Vue 3 + PrimeVue (core/apps/admin), SheetJS (`xlsx`) + jsPDF/autotable client-side.

---

## Contexto provado (G1 da fase de planejamento, 2026-07-18)

Sondas executadas em HML (READ-ONLY, ECS exec rpc no core-api, cluster `monetarie-greenfield-homolog`) e leitura de PRD só para investigação. Fatos que moldam o plano:

1. **`ccs_records` está VAZIA em HML (0) e em PRD (0).** A tabela existe com o shape perfeito para o relatório (document, holder_name, agency, account_number, status, datas, bcb_confirmed) mas NENHUM caminho a popula. A verdade do pipeline CCS vive em **`ccs_files`** (HML: 26 linhas; PRD: 13). O relatório será derivado de `ccs_files` em tempo de leitura (JOIN com users/accounts), sem duplicar estado.
2. **Shape REAL de `ccs_files.metadata` (HML, remessa `202607180001`, 3 operações):** `{"empty": false, "persons": [{"dt_fim": null, "dt_ini": "2026-07-17", "cpf_cnpj": "67384087813", "tp_op_ccs": "I", "tp_pessoa": "F", "qualifdr_op_ccs": "N"}, ...], "message_id": "...", "consolidated": true, "generated_at": "..."}`. Arquivo avulso usa os campos top-level (`tp_op_ccs`, `cpf_cnpj`, `dt_ini`, `dt_fim`).
3. **E-mail é STUB, provado:** `Monetarie.Mailer` (Swoosh) existe em `core/backend/lib/monetarie/mailer.ex` mas tem ZERO call sites; `config/runtime.exs` NÃO configura adapter nenhum (grep vazio para Mailer/Swoosh/SMTP/SES); o único fluxo vivo (`Auth.Notifier.deliver/3`) cai sempre em `Logger.info` e o branch `:smtp` é stub `{:ok, :queued}` (`notifier.ex:188-197`). Deps prontas: `swoosh ~> 1.16`, `gen_smtp ~> 1.2`, `ex_aws_ses ~> 2.4` (`mix.exs:74-78`). O plano LIGA a infra mínima com fail-safe (Task 4).
4. **OFX já existe: OFX 1.0.2 SGML** em `StatementEngine.to_ofx/5` (`use_cases/accounts/statement_engine.ex:101-167`, header `OFXHEADER:100/VERSION:102`), orquestrado por `Statements.export_statement/4` (`statements.ex:95`). Decisão: MANTER 1.0.2 SGML (compatibilidade ampla com conciliadores BR); só migrar para 2.x se o cliente exigir.
5. **Export multi-formato canônico é client-side:** `core/apps/admin/src/composables/useReportExport.ts` (`dispatchReportDownload`: csv/xlsx/pdf via SheetJS + jsPDF/autotable). Não há writer XLSX/PDF server-side (backend só tem `xlsxir`, que é leitor). Export server-side existente: CSV (`send_csv` em `coreproviders_parity_controller.ex:3126`, BOM UTF-8 + content-disposition) e OFX.
6. **"Gerente" NÃO existe como dimensão:** busca em `information_schema.columns` no HML por manager/gerente/officer/carteira só acha risco de crédito (SCR/portfolio) e `branches.manager_name` (gerente da AGÊNCIA; HML tem 1 branch, manager_name NULL). Relatório #14 exige campo novo (Task 11).
7. **Fontes de avisos de crédito no core (HML):** `account_entries` com `amount > 0` = 195.270 linhas (fonte primária, razão consolidado); `transactions` com `direction='inbound'` = 195.268; `spb_inbound_credits` = 5. Shape real de um crédito: `["2026-07-18", 24000, "Transferência recebida - Camille Zanetti Conceição", "transfer", "TEF-4d06d657750363f9_RCV", {"source": "tef_receiver", "payer_name": "...", "payer_document": "23137355753", "counterparty_name": "..."}]` (amount em SUBCENTAVOS: 24000 = R$ 2,40).
8. **SISBAJUD:** `judicial_orders` = 0 em HML e PRD (validação viva será por remessa de teste). Nome do titular NÃO está no leiaute 5301/registro 03 (só resolvido no preview via `lookup_cooperado`, `file_processor.ex:423-450`, e descartado). `tipo_natureza_acao` = código cru 2 dígitos (parser `parser.ex:102`), SEM tabela de decode em lugar nenhum do repo. `judicial_courts` tem todas as colunas ricas (tribunal/endereco/email/tipo_justica) NUNCA preenchidas (parser 5305 lê só código+nome, `varas/parser.ex:72-77`). **Descasamento de chave provado:** `judicial_orders.court_code` perde zeros à esquerda (`parse_n` usa Integer.parse) e `judicial_courts.codigo_vara_juizo` preserva (`parse_a`), então JOIN direto falha (normalizar com pad de 5).
9. **AMES é lacuna total no core, provado por grep:** zero ocorrências em `core/backend` e `core/apps`; a cabine STA só reconhece o prefixo no regex `~r/(ACCS\d{3}|AJUD\d{3}|AMES\d{3}|...)/i` (`sta/backend/lib/sta_connector_web/controllers/api/admin/files_controller.ex:306`). O pipeline genérico para reusar existe: `regulatory_files` + `StaDelivery.deliver/1` (NATS `monetarie.sta.regulatory.upload`) + aceite correlacionado por `tracker_type/tracker_id` no `StaHandler` (`sta_handler.ex:290`).
10. **Unidades:** `account_entries.amount` = SUBCENTAVOS (base units, BRL x 10.000, sinal +crédito/-débito). Use cases de relatório existentes convertem para CENTAVOS na fronteira (`@scale 100`, padrão `statements.ex`). Front do Hub formata centavos com `formatCurrency` (`src/lib/format.ts:35`, divide por 100). NUNCA dividir na mão em código novo: usar `Monetarie.Util.MoneyUnit.to_cents/1` no backend e `formatCurrency`/`fromCents` no front.
11. **Requisitos verbatim do cliente** extraídos dos .docx do Desktop (convertidos em `scratchpad`): as 8 colunas do relatório CCS são Data do evento, Tipo da movimentação (Abertura/Encerramento), Nome do titular, CPF/CNPJ, Número da conta, Situação da conta (Ativa/Encerrada), Data de envio ao BACEN, Status do envio (Enviado/Aceito/Rejeitado); mais pesquisa por CPF/CNPJ/nome/conta, ordenação, export PDF e Excel, modo consolidado ou analítico. AMES: geração no layout, envio STA, status (pendente/enviado/aceito/rejeitado), histórico, timestamps de geração/envio/processamento, mensagens de retorno, reenvio.

## Decisões que dependem do dono (levantar ANTES ou DURANTE a execução, nada bloqueia o início)

- **D1 (e-mail):** transporte real de produção: SMTP relay (quem provê? credenciais no Secrets Manager) ou Amazon SES (exige validar domínio `monetarie.com.br` com DKIM/SPF). O plano entrega a fiação completa com adapter fail-safe (Logger quando não configurado) e flag OFF por default; ligar = decisão do dono. Destinatários via env `CCS_NOTIFICATION_EMAILS`.
- **D2 (OFX):** manter OFX 1.0.2 SGML existente. Só reabrir se o cliente exigir 2.x XML.
- **D3 (gerente):** modelo mínimo proposto = campo texto `manager_name` por CONTA (editável no admin). Validar com o dono se prefere por cliente ou cadastro estruturado de gerentes; o campo texto migra fácil.
- **D4 (natureza da ação):** a tabela oficial de códigos do leiaute 5301 não está no repo nem nos legados varridos. O decode nasce data-driven (mapa em módulo) com os códigos que aparecerem nos arquivos reais + fallback honesto ("Código NN"); completar o mapa = insumo do dono/cliente (manual SISBAJUD).
- **D5 (vara 5305):** parser posicional é best-effort declarado; os campos ricos só serão parseados se o leiaute oficial for confirmado (pedir ao cliente). Sem ele, a tela mostra código+nome (que já existem) sem inventar.
- **D6 (AMES):** o `system_id`/tipo de arquivo STA de cada resposta AMES varia por demanda do BACEN; a tela pede o código ao operador (default `AMES001` a validar quando chegar a primeira demanda real).
- **D7 (grade horária nominal do CCS):** o doc do cliente lista e-mails por horário do AutBank (09:00...20:42). Nosso pipeline é event-driven + job 01:00 BRT. A notificação por e-mail cobrirá os EVENTOS reais (geração, envio, aceite/rejeição, retorno ACCS003/009, dia sem envio). Espelhar a grade nominal = decisão futura do dono (fora deste plano).

## Convenções obrigatórias (todas as tasks)

- TDD: RED primeiro, sempre. Fixture nasce do shape REAL provado no G1 (itens acima ou sonda própria da task).
- pt-br correto e SEM travessão em toda prosa de UI, docs e commits.
- Dinheiro: backend entrega CENTAVOS na borda dos relatórios (padrão `@scale 100` de `statements.ex`); front formata com `formatCurrency`/`fromCents`; nunca dividir na mão.
- Migrations: aditivas, idempotentes (`add_if_not_exists` onde couber, gotcha #13 do core/CLAUDE.md), timestamp novo único, arquivo no MESMO commit do código que as usa, aplicar em HML/PRD via `bin/monetarie rpc` (só o orquestrador).
- Oban: cron novo entra em `config/config.exs` E `config/runtime.exs` (o `ObanCronParityTest` reprova paridade quebrada). Não criar queue nova (usar `:regulatory`).
- Rotas CCS novas SEMPRE acima de `get "/ccs/:id"` (`router.ex:897`, catch-all documentado).
- Antes de `mix test` local: `lsof -i :15432` (túnel SSM do Aurora sombreia o PG de teste, gotcha conhecido).
- Testes focados durante dev; suíte completa no gate de merge (orquestrador).
- Comandos de teste: backend `cd /Users/luizpenha/monetarie/core/backend && mix test <path>`; admin `cd /Users/luizpenha/monetarie/core && pnpm --filter @monetarie/admin test -- --run <path>`.

---

# FASE 1: CCS (relatório de movimentações + e-mail)

### Task 1: Use case do relatório de movimentações CCS

**CONTRATO PROVADO:** `ccs_files` (schema `Monetarie.Schemas.Regulatory.Ccs.Accs001File`, `schemas/regulatory/ccs/accs001_file.ex:30-84`): consolidado carrega operações em `metadata["persons"]` (shape real no item 2 do contexto), avulso nos campos top-level. Nome do titular vem de `users.name` por `users.tax_id == cpf_cnpj` (mesmo JOIN de `use_cases/regulatory/ccs/ccs.ex:119-134`); conta/agência de `accounts` (schema `Monetarie.Schemas.Relational.Account`, campos `agency`, `account_number`, `status`, `user_id`, `inserted_at`, `closing_date`). Situação: `status == "active"` vira "Ativa", senão "Encerrada" (vocabulário legado: encerrada = `inactive`, regra provada na tela de contas). Status do envio: `ccs_files.status` (`generated|submitted|confirmed|rejected|error`) + `submitted_at`.

**Files:**
- Create: `core/backend/lib/monetarie/use_cases/regulatory/ccs/movements_report.ex`
- Test: `core/backend/test/monetarie/regulatory/ccs/movements_report_test.exs`

**Step 1: Escrever o teste de caracterização RED com o shape REAL**

O fixture reproduz byte a byte o metadata provado em HML (item 2 do contexto). Criar user+account reais no sandbox para o JOIN.

```elixir
defmodule Monetarie.Regulatory.Ccs.MovementsReportTest do
  use Monetarie.DataCase, async: true

  alias Monetarie.UseCases.Regulatory.Ccs.MovementsReport
  alias Monetarie.Schemas.Regulatory.Ccs.Accs001File

  # Shape REAL capturado em HML 2026-07-18 (ccs_files 202607180001, rpc read-only)
  @real_consolidated_metadata %{
    "empty" => false,
    "consolidated" => true,
    "message_id" => "d1cf88f9-ef39-4cc7-a7a3-0e9fafca8843",
    "generated_at" => "2026-07-18T04:00:00.714976Z",
    "persons" => [
      %{"dt_fim" => nil, "dt_ini" => "2026-07-17", "cpf_cnpj" => "67384087813",
        "tp_op_ccs" => "I", "tp_pessoa" => "F", "qualifdr_op_ccs" => "N"}
    ]
  }

  setup do
    user = insert_user(%{tax_id: "67384087813", name: "Qa Lifecycle Titular"})
    account = insert_account(%{user_id: user.id, agency: "0001", account_number: "1017551", status: "active"})

    {:ok, file} =
      %Accs001File{}
      |> Accs001File.changeset(%{
        num_remessa: "202607180001", dt_movto: ~D[2026-07-18],
        file_name: "ACCS001_202607180001.xml", file_content: "<x/>",
        status: "confirmed", submitted_at: ~U[2026-07-18 04:01:00Z],
        confirmed_at: ~U[2026-07-18 09:00:00Z],
        total_operations: 1, metadata: @real_consolidated_metadata
      })
      |> Repo.insert()

    %{user: user, account: account, file: file}
  end

  test "analitico: explode persons do consolidado com as 8 colunas do cliente" do
    {:ok, %{rows: [row], meta: meta}} =
      MovementsReport.run(%{start_date: ~D[2026-07-01], end_date: ~D[2026-07-31], mode: "analitico"})

    assert row.event_date == ~D[2026-07-17]
    assert row.movement_type == "I"
    assert row.movement_type_label == "Abertura"
    assert row.holder_name == "Qa Lifecycle Titular"
    assert row.document == "67384087813"
    assert row.account_number == "1017551"
    assert row.agency == "0001"
    assert row.account_situation == "Ativa"
    assert row.sent_at == ~U[2026-07-18 04:01:00Z]
    assert row.send_status == "confirmed"
    assert row.send_status_label == "Aceito"
    assert row.num_remessa == "202607180001"
    assert meta.total == 1
  end

  test "titular sem cadastro no core sai com nome nulo, nunca inventado" do
    # segunda pessoa do arquivo real sem user correspondente
    {:ok, file} = Repo.get_by!(Accs001File, num_remessa: "202607180001")
      |> Ecto.Changeset.change(metadata: put_in(@real_consolidated_metadata, ["persons"],
        [%{"dt_fim" => nil, "dt_ini" => "2026-07-17", "cpf_cnpj" => "15312810282",
           "tp_op_ccs" => "I", "tp_pessoa" => "F", "qualifdr_op_ccs" => "N"}]))
      |> Repo.update()

    {:ok, %{rows: [row]}} =
      MovementsReport.run(%{start_date: ~D[2026-07-01], end_date: ~D[2026-07-31], mode: "analitico"})

    assert row.holder_name == nil
    assert row.account_number == nil
  end

  test "filtros: busca por documento, nome e conta" do
    base = %{start_date: ~D[2026-07-01], end_date: ~D[2026-07-31], mode: "analitico"}
    assert {:ok, %{rows: [_]}} = MovementsReport.run(Map.put(base, :search, "673.840.878-13"))
    assert {:ok, %{rows: [_]}} = MovementsReport.run(Map.put(base, :search, "Qa Lifecycle"))
    assert {:ok, %{rows: [_]}} = MovementsReport.run(Map.put(base, :search, "1017551"))
    assert {:ok, %{rows: []}} = MovementsReport.run(Map.put(base, :search, "00000000000"))
  end

  test "consolidado: totais por dia e por tipo" do
    {:ok, %{rows: [row]}} =
      MovementsReport.run(%{start_date: ~D[2026-07-01], end_date: ~D[2026-07-31], mode: "consolidado"})

    assert row.event_date == ~D[2026-07-17]
    assert row.aberturas == 1
    assert row.encerramentos == 0
    assert row.atualizacoes == 0
    assert row.total == 1
  end

  test "encerramento usa dt_fim como data do evento e rotula Encerramento" do
    # arquivo avulso (nao consolidado), campos top-level, shape real de avulso
    {:ok, _} =
      %Accs001File{}
      |> Accs001File.changeset(%{
        num_remessa: "202607180002", dt_movto: ~D[2026-07-18],
        file_name: "ACCS001_202607180002.xml", file_content: "<x/>",
        tp_op_ccs: "E", tp_pessoa: "F", cpf_cnpj: "67384087813",
        dt_ini: ~D[2026-07-01], dt_fim: ~D[2026-07-18],
        status: "submitted", submitted_at: ~U[2026-07-18 05:00:00Z],
        total_operations: 1, metadata: %{}
      })
      |> Repo.insert()

    {:ok, %{rows: rows}} =
      MovementsReport.run(%{start_date: ~D[2026-07-01], end_date: ~D[2026-07-31], mode: "analitico"})

    encerramento = Enum.find(rows, &(&1.movement_type == "E"))
    assert encerramento.event_date == ~D[2026-07-18]
    assert encerramento.movement_type_label == "Encerramento"
    assert encerramento.send_status_label == "Enviado"
  end
end
```

Nota para o implementador: `insert_user`/`insert_account` são os helpers de fixture existentes do DataCase do core (procurar em `test/support`; se o helper tiver outro nome, usar o real, NUNCA criar user via SQL cru). A busca normalizada com máscara reusa `Monetarie.Util.SearchTerm.normalize_document/1` (já existe, provado na tela de clientes).

**Step 2: Rodar e ver falhar**

Run: `mix test test/monetarie/regulatory/ccs/movements_report_test.exs`
Expected: FAIL (module MovementsReport is not available)

**Step 3: Implementar o use case mínimo**

```elixir
defmodule Monetarie.UseCases.Regulatory.Ccs.MovementsReport do
  @moduledoc """
  Relatorio de movimentacoes de contas enviadas ao BACEN via CCS (doc do cliente, ACCS).
  Fonte da verdade: ccs_files (pipeline ACCS001 real). Cada arquivo consolidado explode
  metadata["persons"]; arquivo avulso usa os campos top-level. Nome do titular e conta
  vem de users/accounts por tax_id (mesmo JOIN de Regulatory.Ccs.sync_from_accounts).
  ccs_records NAO e fonte (vazia em HML e PRD, provado 2026-07-18).
  """

  import Ecto.Query
  alias Monetarie.Repo
  alias Monetarie.Schemas.Regulatory.Ccs.Accs001File
  alias Monetarie.Util.SearchTerm

  @movement_labels %{"I" => "Abertura", "A" => "Atualização", "E" => "Encerramento"}
  @status_labels %{
    "generated" => "Gerado", "submitted" => "Enviado",
    "confirmed" => "Aceito", "rejected" => "Rejeitado", "error" => "Falha de envio"
  }

  def run(params) do
    with {:ok, start_date} <- fetch_date(params, :start_date),
         {:ok, end_date} <- fetch_date(params, :end_date) do
      rows =
        list_files(start_date, end_date)
        |> Enum.flat_map(&explode_operations/1)
        |> Enum.filter(&in_range?(&1, start_date, end_date))
        |> enrich_holders()
        |> maybe_search(params[:search])
        |> sort_rows(params[:sort_by], params[:sort_dir])

      case params[:mode] || "analitico" do
        "consolidado" -> {:ok, %{rows: consolidate(rows), meta: %{total: length(rows), mode: "consolidado"}}}
        _ -> {:ok, %{rows: rows, meta: %{total: length(rows), mode: "analitico"}}}
      end
    end
  end

  defp list_files(start_date, end_date) do
    # janela por dt_movto com folga de 45 dias para tras: o EVENTO (dt_ini/dt_fim)
    # pode ser anterior ao dia da remessa; o filtro fino e por event_date em memoria
    from(f in Accs001File,
      where: f.dt_movto >= ^Date.add(start_date, -45) and f.dt_movto <= ^end_date,
      order_by: [desc: f.dt_movto]
    )
    |> Repo.all()
  end

  defp explode_operations(%Accs001File{metadata: %{"persons" => persons}} = f) when is_list(persons) do
    Enum.map(persons, fn p ->
      base_row(f, p["tp_op_ccs"], p["cpf_cnpj"], parse_date(p["dt_ini"]), parse_date(p["dt_fim"]))
    end)
  end

  defp explode_operations(%Accs001File{cpf_cnpj: doc} = f) when is_binary(doc) do
    [base_row(f, f.tp_op_ccs, doc, f.dt_ini, f.dt_fim)]
  end

  defp explode_operations(_empty_file), do: []

  defp base_row(f, tp_op, doc, dt_ini, dt_fim) do
    %{
      event_date: (if tp_op == "E", do: dt_fim || dt_ini, else: dt_ini),
      movement_type: tp_op,
      movement_type_label: Map.get(@movement_labels, tp_op, tp_op),
      document: doc,
      holder_name: nil, account_number: nil, agency: nil, account_situation: nil,
      num_remessa: f.num_remessa,
      sent_at: f.submitted_at,
      send_status: f.status,
      send_status_label: Map.get(@status_labels, f.status, f.status),
      rejection_reason: f.rejection_reason
    }
  end

  defp enrich_holders(rows) do
    docs = rows |> Enum.map(& &1.document) |> Enum.uniq()

    holders =
      from(u in Monetarie.Schemas.Relational.User,
        left_join: a in Monetarie.Schemas.Relational.Account, on: a.user_id == u.id,
        where: u.tax_id in ^docs,
        select: {u.tax_id, %{name: u.name, agency: a.agency,
                             account_number: a.account_number, status: a.status,
                             account_inserted_at: a.inserted_at}}
      )
      |> Repo.all()
      |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))

    Enum.map(rows, fn row ->
      case pick_account(holders[row.document], row) do
        nil -> row
        h ->
          %{row | holder_name: h.name, agency: h.agency, account_number: h.account_number,
                  account_situation: situation_label(h.status)}
      end
    end)
  end

  # Multi-conta: para Abertura prefere a conta criada na data do evento; senao a primeira
  defp pick_account(nil, _row), do: nil
  defp pick_account(candidates, row) do
    Enum.find(candidates, &same_day?(&1.account_inserted_at, row.event_date)) || List.first(candidates)
  end

  defp situation_label("active"), do: "Ativa"
  defp situation_label(_), do: "Encerrada"
  # ... maybe_search (SearchTerm.normalize_document + nome case-insensitive + conta),
  # sort_rows, consolidate (group_by event_date, contar por tipo), in_range?, parse_date,
  # fetch_date: retornos {:error, :invalid_period} quando datas ausentes/invalidas
end
```

Escrever as funções privadas completas (o esqueleto acima marca o contrato; `consolidate/1` agrupa por `event_date` e soma `aberturas`/`atualizacoes`/`encerramentos`/`total`; `maybe_search` compara documento normalizado, `String.contains?` case-insensitive no nome e sufixo/igualdade na conta).

**Step 4: Rodar e ver passar**

Run: `mix test test/monetarie/regulatory/ccs/movements_report_test.exs`
Expected: PASS (5 tests)

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie/use_cases/regulatory/ccs/movements_report.ex core/backend/test/monetarie/regulatory/ccs/movements_report_test.exs
git commit -m "feat(ccs): use case do relatorio de movimentacoes (8 colunas do cliente, analitico/consolidado)"
```

### Task 2: Endpoint do relatório CCS

**CONTRATO PROVADO:** controller `MonetarieWeb.Regulatory.CcsController` (`controllers/regulatory/ccs_controller.ex`), plugs de permissão nas linhas 52-55 (`compliance.cadoc` view + staff `regulatory|reports_all|audit_all`). Rota nova PRECISA ficar acima de `get "/ccs/:id"` (`router.ex:897`); padrão de paginação/serialização copiado de `accs001_index/2` (`:219`) e `serialize_accs001_file/1` (`:535`).

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/regulatory/ccs_controller.ex` (nova action `movements_report/2` + serializer privado)
- Modify: `core/backend/lib/monetarie_web/router.ex` (rota `get "/ccs/movements-report"` ANTES da linha 897)
- Test: `core/backend/test/monetarie_web/controllers/regulatory/ccs_controller_movements_test.exs`

**Step 1: Teste RED do controller** (auth de admin com permissão, fixture da Task 1 reaproveitada via helper): asserta 200 com `data` (lista com as 8 chaves em camelCase: `eventDate`, `movementType`, `movementTypeLabel`, `holderName`, `document`, `agency`, `accountNumber`, `accountSituation`, `sentAt`, `sendStatus`, `sendStatusLabel`, `numRemessa`) + `meta.total`; 422 sem período; e que usuário sem a permissão `compliance.cadoc` recebe 403.

**Step 2:** Run `mix test test/monetarie_web/controllers/regulatory/ccs_controller_movements_test.exs` , Expected: FAIL (404 na rota)

**Step 3: Implementar.** Action:

```elixir
def movements_report(conn, params) do
  report_params = %{
    start_date: params["start_date"], end_date: params["end_date"],
    mode: params["mode"], search: params["search"],
    sort_by: params["sort_by"], sort_dir: params["sort_dir"]
  }

  case MovementsReport.run(report_params) do
    {:ok, %{rows: rows, meta: meta}} ->
      json(conn, %{data: Enum.map(rows, &serialize_movement_row/1), meta: meta})
    {:error, :invalid_period} ->
      conn |> put_status(422) |> json(%{error: "Período inválido: informe start_date e end_date"})
  end
end
```

Adicionar `movements_report` ao `@view_actions` do plug de permissão. Rota (acima da 897): `get "/ccs/movements-report", Regulatory.CcsController, :movements_report`.

**Step 4:** Run o teste, Expected: PASS. Rodar também `mix test test/monetarie_web/controllers/regulatory/` para regressão local do controller.

**Step 5: Commit** `feat(ccs): endpoint GET /regulatory/ccs/movements-report`

### Task 3: Tela do relatório CCS com export PDF/Excel/CSV

**CONTRATO PROVADO:** padrão Reports Hub: `ReportLayout`/`ReportFilters`/`ReportTable`/`ReportExportMenu` (`core/apps/admin/src/views/reports/components/shared/`), export via `dispatchReportDownload(format, report, columns, opts)` (`src/composables/useReportExport.ts:33`, formatos `csv|xlsx|pdf`); tipos `ExportFormat`/`ExportColumn` em `useClientsReports.ts:17`. Rotas Vue de relatório em `src/router/index.ts` (padrão linhas 1212-1315). Dinheiro não aparece neste relatório (só contagens), então sem formatação monetária.

**Files:**
- Create: `core/apps/admin/src/views/reports/regulatory/CcsMovementsReportView.vue`
- Create: `core/apps/admin/src/composables/useCcsMovementsReport.ts`
- Modify: `core/apps/admin/src/router/index.ts` (rota `reports/regulatory/ccs-movements`, meta `requiredPermission: 'regulatory'`, `requiredRbac: 'compliance.cadoc.view'`)
- Modify: `core/apps/admin/src/views/reports/ReportsHubView.vue` (card novo "Movimentações CCS")
- Modify: `core/apps/admin/src/views/ccs/CcsOperationsView.vue` (botão/atalho "Relatório de movimentações" apontando para a rota nova)
- Test: `core/apps/admin/src/composables/__tests__/useCcsMovementsReport.spec.ts`

**Step 1: Teste RED do composable** (vitest, mock do axios): monta filtros (período, mode, search), chama `GET /v1/regulatory/ccs/movements-report`, mapeia a resposta para linhas da tabela, e `exportReport('pdf')` delega a `dispatchReportDownload` com as 8 colunas na ordem do cliente.

**Step 2:** `pnpm --filter @monetarie/admin test -- --run src/composables/__tests__/useCcsMovementsReport.spec.ts` , Expected: FAIL

**Step 3: Implementar.** Composable segue o padrão de `useAccountsReports.ts` (fetch + estado + `dispatchReportDownload`). Colunas (ordem do cliente): Data do evento, Tipo da movimentação, Nome do titular, CPF/CNPJ, Agência, Conta, Situação da conta, Data de envio ao BACEN, Status do envio, Nº remessa. View com `ReportLayout` (título "Movimentações CCS enviadas ao BACEN"), filtros: período (obrigatório), modo Analítico/Consolidado (SelectButton), busca livre; tabela com ordenação client-side; `ReportExportMenu` com csv/xlsx/pdf. Toda prosa da tela em pt-br acentuado, sem travessão.

**Step 4:** teste PASS + `pnpm --filter @monetarie/admin build` verde.

**Step 5: Commit** `feat(admin): tela do relatorio de movimentacoes CCS com export CSV/XLSX/PDF`

### Task 4: Notificação CCS por e-mail (infra mínima viva + ganchos + digest)

**CONTRATO PROVADO:** e-mail hoje é stub (item 3 do contexto). Ganchos reais do ciclo CCS: veredito ACCS002 em `Monetarie.UseCases.Ccs.StaInbound.apply_accs002/2` (`sta_inbound.ex:120`, tem o arquivo e o resultado parseado); envio em `StaSubmission.submit/2` (`sta_submission.ex:75`); geração diária no worker `Monetarie.Workers.Cadoc.CcsAccs001DailyJob` (cron `0 4 * * *`, `runtime.exs:649`); pendências agregadas em `Monetarie.UseCases.Cadoc.CCS.dashboard_alerts/0` (`use_cases/cadoc/ccs.ex:1298`). ACCS003/009 persistidos em `StaSubmission.apply_inbound_accs003/2` (`:665`) e `apply_inbound_accs009/2` (`:681`). Queue Oban `:regulatory` já existe (`runtime.exs:742`). Cron novo exige paridade config.exs/runtime.exs.

**Files:**
- Modify: `core/backend/config/runtime.exs` (bloco de config do Mailer + cron do digest)
- Modify: `core/backend/config/config.exs` (cron do digest, paridade)
- Create: `core/backend/lib/monetarie/use_cases/ccs/notifications.ex`
- Create: `core/backend/lib/monetarie/workers/cadoc/ccs_email_notifier.ex` (Oban worker, queue `:regulatory`)
- Create: `core/backend/lib/monetarie/workers/cadoc/ccs_daily_digest_worker.ex` (cron diário)
- Modify: `core/backend/lib/monetarie/use_cases/ccs/sta_inbound.ex` (gancho pós-veredito)
- Modify: `core/backend/lib/monetarie/use_cases/ccs/sta_submission.ex` (gancho pós-submit e pós-ACCS003/009)
- Modify: `core/backend/lib/monetarie/workers/cadoc/ccs_accs001_daily_job.ex` (gancho geração ok/falha)
- Test: `core/backend/test/monetarie/ccs/notifications_test.exs`, `core/backend/test/monetarie/workers/ccs_email_notifier_test.exs`

**Step 1: Teste RED** com `Swoosh.Adapters.Test` (já configurado em test.exs) + `Swoosh.TestAssertions`:

```elixir
test "veredito confirmado dispara e-mail de aceite com remessa e protocolo" do
  file = insert_ccs_file(%{num_remessa: "202607180001", status: "confirmed", sta_protocol: "433477214"})
  assert {:ok, _} = Notifications.notify(:accs002_verdict, file)
  assert_email_sent(fn email ->
    assert email.subject =~ "CCS: remessa 202607180001 ACEITA pelo BACEN"
    assert email.text_body =~ "433477214"
  end)
end

test "sem destinatarios configurados vira no-op logado, nunca erro" do
  Application.put_env(:monetarie, :ccs_notification_emails, [])
  assert {:ok, :skipped} = Notifications.notify(:accs002_verdict, insert_ccs_file(%{}))
end

test "worker enfileirado pelo gancho e fail-soft: erro de entrega nao propaga" do
  # adapter de teste que explode; notify_async/2 retorna :ok mesmo assim
end
```

Cobrir os 6 eventos: `:generated`, `:generation_failed`, `:submitted`, `:accs002_verdict` (aceite E rejeição com `error_code`/`rejection_reason` traduzido pelo `ErrorDictionary`), `:accs003_received`, `:accs009_received`, `:daily_digest` (corpo com `pending_files`/`missing_days` de `dashboard_alerts/0`).

**Step 2:** Run, Expected: FAIL

**Step 3: Implementar.**

(a) `runtime.exs` (fail-safe, nada quebra sem env):

```elixir
# E-mail (Task 4 F3): adapter real so quando SMTP_HOST presente; senao Logger (fail-safe).
# Transporte definitivo (SMTP relay vs SES) = decisao D1 do dono.
if config_env() == :prod do
  case System.get_env("SMTP_HOST") do
    nil ->
      config :monetarie, Monetarie.Mailer, adapter: Swoosh.Adapters.Logger, level: :info
    host ->
      config :monetarie, Monetarie.Mailer,
        adapter: Swoosh.Adapters.SMTP,
        relay: host,
        port: String.to_integer(System.get_env("SMTP_PORT") || "587"),
        username: System.get_env("SMTP_USERNAME"),
        password: System.get_env("SMTP_PASSWORD"),
        tls: :if_available, auth: :if_available, retries: 1
  end

  config :monetarie,
    ccs_notification_emails:
      (System.get_env("CCS_NOTIFICATION_EMAILS") || "")
      |> String.split(",", trim: true) |> Enum.map(&String.trim/1),
    mailer_from: System.get_env("EMAIL_FROM") || "nao-responda@monetarie.com.br"
end
```

(b) `Notifications`: monta `Swoosh.Email` (texto puro pt-br, assunto padronizado `"CCS: <evento> <remessa>"`), `notify/2` síncrono (para testes) e `notify_async/2` que enfileira `CcsEmailNotifier` (Oban, `queue: :regulatory`, `max_attempts: 3`) e NUNCA propaga erro (rescue + `Logger.warning`). O worker chama `Monetarie.Mailer.deliver/1`.

(c) Ganchos: em `StaInbound.apply_accs002/2` após aplicar o veredito, `Notifications.notify_async(:accs002_verdict, file)`; em `StaSubmission.submit/2` no sucesso, `:submitted`; nos `apply_inbound_accs003/009`, os respectivos; no `CcsAccs001DailyJob`, `:generated` no sucesso e `:generation_failed` no rescue existente. Todos fail-soft (o pipeline CCS NUNCA falha por causa de e-mail).

(d) `CcsDailyDigestWorker`: cron `{"30 12 * * *", Monetarie.Workers.Cadoc.CcsDailyDigestWorker}` (09:30 BRT, depois da grade CCS 08:00) em config.exs E runtime.exs; corpo do e-mail com o resultado de `dashboard_alerts/0`; se `status == :ok` e nada pendente, e-mail curto de "tudo em dia" APENAS se `CCS_DIGEST_ALWAYS=true` (default: só alerta).

**Step 4:** Run testes novos + `mix test test/monetarie/ccs/ test/monetarie/workers/ --include integration` focado + `mix test test/monetarie/oban_cron_parity_test.exs` (paridade do cron novo). Expected: PASS.

**Step 5: Commit** `feat(ccs): notificacao por e-mail dos eventos CCS (Mailer vivo fail-safe + digest diario)`

Nota de deploy (orquestrador): envs novas `SMTP_HOST/SMTP_PORT/SMTP_USERNAME/SMTP_PASSWORD/EMAIL_FROM/CCS_NOTIFICATION_EMAILS` só entram na task-def quando o dono decidir D1; sem elas o adapter é Logger e o G3 desta task valida o e-mail no log estruturado de HML.

---

# FASE 2: Relatórios Vulci (itens 1, 2, 3, 14 e parte core do 8)

### Task 5: Extrato: export server-side CSV/OFX na rota admin por conta

**CONTRATO PROVADO:** `Statements.export_statement/4` (`use_cases/accounts/statements.ex:95`) já gera CSV e OFX 1.0.2 do extrato canônico (fonte `account_entries`, conversão subcentavos para centavos com `@scale 100`). A rota v1 `GET /api/v1/accounts/:id/statement/export` existe (`accounts/statement_controller.ex`, `parse_format/1:118` aceita `csv|ofx`), mas o admin (`/admin/bank-accounts/:id/statement`, `admin/bank_accounts_controller.ex:496`, RBAC `contas.lista`) só devolve JSON. Existe um segundo gerador OFX redundante em `coreproviders_parity_controller.ex` (`build_ofx`, linha ~684): NÃO tocar nele nesta task (follow-up de unificação anotado no fecho).

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/admin/bank_accounts_controller.ex` (action `statement_export/2`)
- Modify: `core/backend/lib/monetarie_web/router.ex` (rota `get "/admin/bank-accounts/:id/statement/export"` junto da linha 2479)
- Test: `core/backend/test/monetarie_web/controllers/admin/bank_accounts_statement_export_test.exs`

**Step 1: Teste RED:** com conta + `account_entries` reais no sandbox (amount em SUBCENTAVOS, ex.: 24000 = R$ 2,40, shape do item 7 do contexto): `GET .../statement/export?format=ofx&start_date=...&end_date=...` responde 200, content-type `application/x-ofx`, corpo contém `OFXHEADER:100`, `VERSION:102`, `<TRNAMT>2.40` (prova da unidade: 24000 subcentavos viram 2.40 no OFX, nunca 240.00); `format=csv` responde `text/csv` com `Valor` `2,40`; formato inválido responde 422; sem RBAC responde 403.

**Step 2:** Run, Expected: FAIL (rota inexistente)

**Step 3: Implementar** action fina delegando a `Statements.export_statement/4` e respondendo com o padrão attachment (copiar de `accounts/statement_controller.ex:118-124`: content-type por formato + `content-disposition: attachment; filename="extrato_<numero>_<start>_<end>.<ext>"`).

**Step 4:** Run, Expected: PASS.

**Step 5: Commit** `feat(admin): export CSV/OFX server-side do extrato por conta no coreadmin`

### Task 6: Extrato em 4 formatos na tela admin (PDF/Excel client-side + CSV/OFX server)

**CONTRATO PROVADO:** a tela de extrato do coreadmin é `core/apps/admin/src/views/accounts/tabs/AccountExtratoTab.vue` (consome `GET /admin/bank-accounts/:id/statement`, JSON com `entries` + `opening_balance` em CENTAVOS). Export client-side: `dispatchReportDownload` (`useReportExport.ts`); download de blob server: `downloadAxiosBlob` (`src/lib/download.ts`). Formatação monetária: `formatCurrency` (`src/lib/format.ts:35`, entrada centavos).

**Files:**
- Modify: `core/apps/admin/src/views/accounts/tabs/AccountExtratoTab.vue` (menu Exportar com 4 opções: PDF, Excel, CSV, OFX)
- Test: `core/apps/admin/src/views/accounts/__tests__/AccountExtratoTab.export.spec.ts`

**Step 1: Teste RED (vitest):** mock do axios; escolher "PDF" chama `dispatchReportDownload('pdf', ...)` com colunas Data/Descrição/Categoria/Valor/Saldo e valores formatados via `formatCurrency` (nunca divisão manual); escolher "OFX" chama `GET /admin/bank-accounts/:id/statement/export?format=ofx` com `responseType: 'blob'` e delega a `downloadAxiosBlob`.

**Step 2:** Run, Expected: FAIL

**Step 3: Implementar:** SplitButton/Menu "Exportar" com os 4 itens; PDF/Excel montam as linhas a partir do estado já carregado da tela (mesmo período filtrado); CSV/OFX vão ao server (fonte única, byte-idêntico ao canônico).

**Step 4:** Run + `pnpm --filter @monetarie/admin build`. Expected: PASS.

**Step 5: Commit** `feat(admin): extrato exportavel em PDF/Excel/CSV/OFX (Vulci item 1)`

### Task 7: Avisos de Créditos Geral: use case + endpoint

**CONTRATO PROVADO:** fonte primária `account_entries` com `amount > 0` e `status = "confirmed"` (195.270 linhas em HML; shape real com `metadata.payer_name/payer_document/counterparty_name/source`, item 7 do contexto). Join conta/titular: `accounts` (agency, account_number) + `users.name` via `accounts.user_id`. Padrão de use case: `use_cases/reports/checking_account_reports.ex` (entrega CENTAVOS). Endpoint no scope `/api/v1/reports/accounts/*` (`AccountsReportsController`, `controllers/reports/accounts_reports_controller.ex`).

**Files:**
- Create: `core/backend/lib/monetarie/use_cases/reports/credit_notices.ex`
- Modify: `core/backend/lib/monetarie_web/controllers/reports/accounts_reports_controller.ex` (actions `credit_notices/2` e `credit_notices_export/2`)
- Modify: `core/backend/lib/monetarie_web/router.ex` (rotas `/reports/accounts/credit-notices` + `/export` junto das rotas de reports existentes)
- Test: `core/backend/test/monetarie/reports/credit_notices_test.exs` + teste de controller

**Step 1: Teste RED de caracterização** com o shape REAL (fixture do item 7 do contexto, incluindo o metadata do TEF):

```elixir
test "lista todos os creditos do periodo com conta, titular, pagador e valor em CENTAVOS" do
  # entry real: amount 24000 subcentavos, metadata com payer_name (shape HML 2026-07-18)
  {:ok, %{rows: [row], totals: totals}} =
    CreditNotices.run(%{start_date: ~D[2026-07-18], end_date: ~D[2026-07-18]})

  assert row.entry_date == ~D[2026-07-18]
  assert row.amount_cents == 240          # 24000 subcentavos = 240 centavos, PROVA da unidade
  assert row.description =~ "Transferência recebida"
  assert row.category == "transfer"
  assert row.payer_name == "Camille Zanetti Conceição"
  assert row.payer_document == "23137355753"
  assert row.account_number != nil and row.holder_name != nil
  assert totals.count == 1 and totals.amount_cents == 240
end

test "classificacao por datas: agrupamento diario com subtotais" do
  {:ok, %{days: days}} = CreditNotices.run(%{start_date: d1, end_date: d2, group_by_date: true})
  # cada dia: %{date, count, amount_cents, rows}
end

test "debitos e lancamentos nao confirmados ficam FORA" do ... end
test "filtro opcional por conta" do ... end
```

**Step 2:** Run, Expected: FAIL

**Step 3: Implementar** query única com JOIN accounts/users, `where: e.amount > 0 and e.status == "confirmed"`, `select_merge` do metadata (payer_* com fallback `counterparty_name`), conversão `MoneyUnit.to_cents/1` na borda, paginação `page/page_size` (default 100, max 10_000 para export), ordenação `entry_date desc, id desc`. Controller no padrão dos exports existentes do Hub (JSON; o arquivo é client-side).

**Step 4:** Run, Expected: PASS.

**Step 5: Commit** `feat(reports): avisos de creditos geral (Vulci item 2)`

### Task 8: Avisos de Créditos: tela no Hub + export

**Files:**
- Create: `core/apps/admin/src/views/reports/accounts/CreditNoticesReportView.vue`
- Create: `core/apps/admin/src/composables/useCreditNoticesReport.ts`
- Modify: `core/apps/admin/src/router/index.ts` + `ReportsHubView.vue` (card "Avisos de Créditos")
- Test: `core/apps/admin/src/composables/__tests__/useCreditNoticesReport.spec.ts`

**Steps 1-5:** mesmo protocolo da Task 3 (teste RED do composable, implementar view com `ReportLayout`, filtros período/conta, toggle "Agrupar por data" com subtotais diários, colunas Data/Conta/Titular/Pagador/Documento/Descrição/Valor, valores com `formatCurrency`, `ReportExportMenu` csv/xlsx/pdf, build verde, commit `feat(admin): tela avisos de creditos geral`).

### Task 9: Movimentações diárias Analítico/Sintético (core) + parte core do #8

**CONTRATO PROVADO:** mesma fonte `account_entries` (todas as linhas confirmed do período, crédito E débito). Sintético = agregação por dia x categoria x sentido com contagem e soma (cobre também o "Relatório Diário de todas as movimentações" do item 8 na parte core: os movimentos de cabine SPB/PIX chegam ao razão do core via `account_entries`, que é o consolidado). Categorias válidas do schema: `deposit withdrawal transfer fee interest reversal adjustment ...` (`schemas/accounts/account_entry.ex:27`).

**Files:**
- Create: `core/backend/lib/monetarie/use_cases/reports/daily_movements.ex`
- Modify: `core/backend/lib/monetarie_web/controllers/reports/accounts_reports_controller.ex` + `router.ex` (rotas `/reports/accounts/daily-movements[.../export]`)
- Test: `core/backend/test/monetarie/reports/daily_movements_test.exs`

**Step 1: Teste RED:** modo `analitico` devolve linhas com data/conta/titular/categoria/sentido (Crédito quando amount > 0, Débito quando < 0)/descrição/valor em CENTAVOS (sinal preservado); modo `sintetico` devolve por dia: `%{date, by_category: [%{category, sentido, count, amount_cents}], total_creditos_cents, total_debitos_cents, saldo_liquido_cents}`; débito 100x nunca (prova de unidade com entrada em subcentavos).

**Steps 2-4:** protocolo TDD padrão. A query sintética agrega no SQL (`group_by fragment date + category + sign`), nunca em memória (195k+ linhas em HML).

**Step 5: Commit** `feat(reports): movimentacoes diarias analitico/sintetico (Vulci item 3 + parte core do 8)`

### Task 10: Movimentações: tela no Hub + export

**Files:** `core/apps/admin/src/views/reports/accounts/DailyMovementsReportView.vue` + composable + rota + card no Hub + spec vitest.

**Steps 1-5:** protocolo da Task 3. SelectButton Analítico/Sintético; no sintético, tabela agrupada por dia com subtotais e linha de saldo líquido; export csv/xlsx/pdf. Commit `feat(admin): tela movimentacoes diarias analitico/sintetico`.

### Task 11: Gerente da conta (campo novo + edição no admin)

**CONTRATO PROVADO:** ausência da dimensão provada (item 6 do contexto). Decisão D3: campo texto por conta. A tabela `accounts` é a `Monetarie.Schemas.Relational.Account`; update de conta no admin passa por `admin/bank_accounts_controller.ex` (conferir a action de update existente e o changeset da conta na implementação).

**Files:**
- Create: `core/backend/priv/repo/migrations/20260718150000_add_manager_name_to_accounts.exs`
- Modify: `core/backend/lib/monetarie/schemas/relational/account.ex` (campo `manager_name`)
- Modify: `core/backend/lib/monetarie_web/controllers/admin/bank_accounts_controller.ex` (aceitar `manager_name` no update + expor no show/serializer)
- Modify: `core/apps/admin/src/views/accounts/...` (campo "Gerente da conta" na tela de dados da conta, editável)
- Test: backend controller test + vitest da tela

**Step 1: Migration** (aditiva, idempotente):

```elixir
defmodule Monetarie.Repo.Migrations.AddManagerNameToAccounts do
  use Ecto.Migration
  def change do
    alter table(:accounts) do
      add_if_not_exists :manager_name, :string
    end
    create_if_not_exists index(:accounts, [:manager_name])
  end
end
```

**Steps 2-4:** teste RED do update (PATCH admin seta manager_name, auditado pelo AuditPlug existente; RBAC de edição de conta), implementar, PASS. Campo no front com label "Gerente da conta".

**Step 5: Commit** `feat(core): gerente da conta (campo editavel, base do relatorio de saldos por gerente)`

### Task 12: Relatório Posição de Saldos por Gerente

**CONTRATO PROVADO:** saldo por conta = SUM de `account_entries` confirmed (mesma regra de `Statements.current_balance/1`, `statements.ex:153-162`, resultado em CENTAVOS na borda). Agrupamento por `accounts.manager_name` com bucket "Sem gerente" para NULL.

**Files:**
- Create: `core/backend/lib/monetarie/use_cases/reports/balances_by_manager.ex`
- Modify: `accounts_reports_controller.ex` + `router.ex` (rota `/reports/accounts/balances-by-manager`)
- Create: `core/apps/admin/src/views/reports/accounts/BalancesByManagerReportView.vue` + composable + rota + card
- Test: backend `test/monetarie/reports/balances_by_manager_test.exs` + vitest

**Step 1: Teste RED:** duas contas com gerente "Maria", uma sem gerente; relatório devolve grupos `[%{manager: "Maria", accounts: [...], total_cents: ...}, %{manager: "Sem gerente", ...}]`, cada conta com titular/agência/número/saldo em CENTAVOS; total geral = soma dos grupos; saldo bate ao centavo com a soma dos entries do sandbox.

**Steps 2-4:** TDD padrão; agregação no SQL (JOIN accounts + SUM entries, GROUP BY manager, conta). Tela: tabela agrupada (rowGroup do PrimeVue) + KPIs (nº gerentes, nº contas, saldo total) + export 3 formatos.

**Step 5: Commit** `feat(reports): posicao de saldos por gerente (Vulci item 14)`

---

# FASE 3: SISBAJUD (B.3, B.4, titular, natureza)

### Task 13: Migration de titular + atendimento + índice de vara

**CONTRATO PROVADO:** `judicial_orders` não tem titular nem atendimento (schema `schemas/judicial/judicial_order.ex:64-119`; colunas vivas conferidas em HML por information_schema). `vara_juizo_codigo` existe (varchar 5) e NUNCA é populado. Padrão de migration da área: `add_<coisa>_to_judicial_orders` (última: `20260709220000`). HML e PRD têm 0 ordens: sem backfill.

**Files:**
- Create: `core/backend/priv/repo/migrations/20260718151000_add_titular_atendimento_to_judicial_orders.exs`
- Modify: `core/backend/lib/monetarie/schemas/judicial/judicial_order.ex` (campos + changeset novo `atendimento_changeset/2`)
- Test: `core/backend/test/monetarie/judicial/judicial_order_atendimento_test.exs`

**Step 1: Migration:**

```elixir
defmodule Monetarie.Repo.Migrations.AddTitularAtendimentoToJudicialOrders do
  use Ecto.Migration
  def change do
    alter table(:judicial_orders) do
      add_if_not_exists :titular_name, :string
      add_if_not_exists :atendimento_status, :string   # pendente|cumprida|cumprida_parcialmente|nao_cumprida
      add_if_not_exists :atendimento_em, :utc_datetime
      add_if_not_exists :atendimento_justificativa, :text
      add_if_not_exists :atendimento_observacoes, :text
      add_if_not_exists :atendimento_por_id, :uuid
      add_if_not_exists :atendimento_por_nome, :string
    end
  end
end
```

**Step 2: Teste RED do changeset:** `atendimento_changeset/2` casta somente os campos de atendimento + valida `atendimento_status` no enum `~w(pendente cumprida cumprida_parcialmente nao_cumprida)`; exige `atendimento_justificativa` quando status é `cumprida_parcialmente` ou `nao_cumprida`; carimba `atendimento_em` (aceita data/hora manual do operador) e ator. `titular_name` entra no `@castable_fields` do changeset principal.

**Steps 3-4:** implementar, PASS (`mix test test/monetarie/judicial/`).

**Step 5: Commit** `feat(sisbajud): colunas de titular e atendimento editavel em judicial_orders`

### Task 14: Persistir titular + normalizar vara + decode da natureza no processamento

**CONTRATO PROVADO:** `FileProcessor.process_block/2` monta `order_attrs` (`file_processor.ex:508-535`) sem titular e sem `vara_juizo_codigo`; a resolução do titular já existe SÓ no preview: `lookup_cooperado/1` (`file_processor.ex:423-450`, Member por cpf_cnpj) e User por tax_id (`:299-305`). `court_code` perde zeros à esquerda (item 8 do contexto). Natureza: `tipo_natureza_acao` cru no metadata (`:532`). `WireEnums` (`regulatory/sisbajud/wire_enums.ex`) é o lugar do decode.

**Files:**
- Modify: `core/backend/lib/monetarie/use_cases/regulatory/sisbajud/file_processor.ex` (order_attrs: `titular_name` + `vara_juizo_codigo` zero-padded; também nos caminhos CANCEL/NOTIFICATION/UNBLOCK/TRANSFER que herdam do bloqueio)
- Modify: `core/backend/lib/monetarie/use_cases/regulatory/sisbajud/wire_enums.ex` (decode `natureza_acao_label/1`)
- Modify: `core/backend/lib/monetarie_web/controllers/regulatory/sisbajud_controller.ex` (`serialize_order/1` linha 296-350: emitir `titular_name`, `natureza_acao` = `%{codigo, label}`, `vara_juizo_codigo`)
- Test: `core/backend/test/monetarie/sisbajud/file_processor_titular_test.exs` + teste do WireEnums

**Step 1: Teste RED** usando as fixtures wire-format reais de `test/fixtures/sisbajud/5301_remessa` + `test/support/sisbajud/fixture_builder.ex`: processar uma remessa de bloqueio de um cooperado existente no sandbox grava `titular_name` = nome do Member (mesma fonte do preview), `vara_juizo_codigo` = código com pad de 5 (ex.: linha com vara `123` grava `"00123"`); ordem de réu sem cadastro grava `titular_name: nil` (nunca inventa). `WireEnums.natureza_acao_label/1`: código presente no mapa devolve `{:ok, label}`; ausente devolve `:unknown` e o serializer emite `label: "Código NN (aguardando tabela oficial)"`.

**Step 2:** Run, Expected: FAIL

**Step 3: Implementar.** No `process_block/2`:

```elixir
titular_name = resolve_titular_name(clean_doc)   # extrai o lookup do preview p/ funcao compartilhada
vara_codigo = record[:vara_juizo] && String.pad_leading(to_string(record[:vara_juizo]), 5, "0")
order_attrs = Map.merge(order_attrs, %{titular_name: titular_name, vara_juizo_codigo: vara_codigo})
```

`resolve_titular_name/1` reusa `lookup_cooperado` (extrair para função pública ou módulo compartilhado para não duplicar). `WireEnums.natureza_acao_label/1` nasce com mapa data-driven documentado como PARCIAL (decisão D4): incluir apenas códigos confirmados por arquivo real ou leiaute que o executor conseguir provar; NUNCA inventar rótulo (o teste garante o fallback honesto). No TRANSFER (registro 08), usar `nome_reu_executado` do próprio arquivo como fallback do titular (`parser.ex:163`).

**Step 4:** Run + regressão focada `mix test test/monetarie/sisbajud/ test/monetarie/judicial/`. Expected: PASS.

**Step 5: Commit** `feat(sisbajud): titular persistido, vara normalizada (pad 5) e decode honesto da natureza`

### Task 15: Vara/Juízo ponta a ponta (B.3): JOIN + serialização + parser 5305

**CONTRATO PROVADO:** `judicial_courts` completo mas só código+nome populados (`varas/parser.ex:72-77` lê posições 3-7 e 8-207; sync upsert por `codigo_vara_juizo`, `varas/sync.ex:64-67`). Fixture real em `test/fixtures/sisbajud/5305_varas_juizos`. Chave da ordem agora normalizada (Task 14).

**Files:**
- Modify: `core/backend/lib/monetarie/use_cases/judicial/judicial.ex` (get_order/list com lookup da vara por `vara_juizo_codigo`, com fallback por `court_code` sem zeros para acervo antigo)
- Modify: `core/backend/lib/monetarie_web/controllers/regulatory/sisbajud_controller.ex` (`serialize_order/1` ganha bloco `vara: %{codigo, nome, tribunal, endereco, email, tipo_justica}` com nils honestos)
- Modify: `core/backend/lib/monetarie/use_cases/regulatory/sisbajud/varas/parser.ex` + `sync.ex` (SOMENTE SE o leiaute oficial do 5305 for obtido, decisão D5: parsear tribunal/endereco/email/tipo_justica e incluir no replace do upsert; senão a task entrega o JOIN com código+nome e deixa comentário com o gate D5)
- Test: `core/backend/test/monetarie_web/controllers/regulatory/sisbajud_show_vara_test.exs`

**Step 1: Teste RED:** semear `judicial_courts` com código "00123" + nome real do fixture 5305; ordem com `vara_juizo_codigo: "00123"`; `GET /regulatory/sisbajud/orders/:id` devolve `vara.nome`; ordem antiga com `court_code: "123"` e `vara_juizo_codigo: nil` TAMBÉM resolve (fallback com pad no lookup); vara inexistente devolve `vara: %{codigo: ..., nome: nil, ...}` sem 500.

**Steps 2-4:** TDD padrão. O lookup é 1 query por show (`Repo.get_by(JudicialCourt, codigo_vara_juizo: codigo)`); na LISTA não fazer N+1 (a lista não mostra vara; só o detalhe).

**Step 5: Commit** `feat(sisbajud): vara/juizo no detalhe da ordem (JOIN normalizado, campos honestos)`

### Task 16: Atendimento editável (B.4): PATCH + auditoria de ator

**CONTRATO PROVADO:** não existe PATCH de ordem (rotas `router.ex:927-934`); padrão de ator: `conn.assigns[:current_user] || conn.assigns[:current_admin_user]` como em `log_pii_access/5` (`sisbajud_controller.ex:354-384`) + `Monetarie.UseCases.Audit.log/1`. Permissões de escrita: `compliance.sisbajud:edit` + staff `regulatory` (plugs `:22-42`).

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/regulatory/sisbajud_controller.ex` (action `update_atendimento/2`)
- Modify: `core/backend/lib/monetarie_web/router.ex` (`patch "/sisbajud/orders/:id/atendimento"`)
- Modify: `core/backend/lib/monetarie/use_cases/judicial/judicial.ex` (função `update_atendimento/3` usando `atendimento_changeset`)
- Test: `core/backend/test/monetarie_web/controllers/regulatory/sisbajud_atendimento_test.exs`

**Step 1: Teste RED:** PATCH com `{status: "cumprida_parcialmente", justificativa: "Saldo parcial", observacoes: "...", data_hora: "2026-07-18T14:00:00Z"}` como admin autenticado: 200, ordem gravada com `atendimento_por_id`/`atendimento_por_nome` do ator REAL do token (nunca do body), linha de audit criada (`action: "sisbajud_atendimento_updated"`, resource_id da ordem, `changes` com before/after); status inválido 422; `cumprida_parcialmente` sem justificativa 422; sem permissão 403; o PATCH NUNCA toca `status` da ordem nem valores (campos de máquina intocados, teste assertando que `blocked_amount`/`status` não mudam).

**Steps 2-4:** TDD padrão. Serializer passa a emitir o bloco `atendimento` completo no show.

**Step 5: Commit** `feat(sisbajud): bloco de atendimento editavel com ator auditado (PATCH)`

### Task 17: Frontend SISBAJUD: titular, natureza, vara e atendimento

**CONTRATO PROVADO:** tela viva `core/apps/admin/src/views/regulatory/SisbajudView.vue` (router `:431-434`); detalhe em Dialog (`:916+`) mostra `court_code` cru (`:947`), natureza crua (`:993-995`); composable `useSisbajud.ts` sem PATCH; types em `views/regulatory/types.ts` (`JudicialOrder` `:163-192`; falta `no_balance` no union `OrderStatus` `:141-149`, corrigir de carona).

**Files:**
- Modify: `core/apps/admin/src/views/regulatory/SisbajudView.vue`
- Modify: `core/apps/admin/src/composables/useSisbajud.ts` (função `updateAtendimento`)
- Modify: `core/apps/admin/src/views/regulatory/types.ts` (campos novos + `no_balance` no union + labels de atendimento)
- Test: `core/apps/admin/src/views/regulatory/__tests__/SisbajudView.atendimento.spec.ts`

**Step 1: Teste RED (vitest):** o detalhe renderiza "Nome do titular", "Natureza da ação" com o label decodificado (e o fallback "Código NN..." quando unknown), bloco "Vara/Juízo" com nome/tribunal/e-mail quando presentes; o form de atendimento envia PATCH com os campos e mostra o usuário responsável devolvido; validação client-side exige justificativa nos status parciais.

**Steps 2-4:** implementar: coluna "Titular" na lista (vem do serializer novo), seção "4. Atendimento da Ordem" no Dialog (Select de status com labels pt-br: Pendente, Cumprida, Cumprida Parcialmente, Não Cumprida; Calendar para data/hora; Textareas; botão Salvar com toast). Prosa acentuada, sem travessão.

**Step 5: Commit** `feat(admin): sisbajud com titular, natureza decodificada, vara e atendimento editavel`

---

# FASE 4: AMES (gerador + tracker + tela)

### Task 18: Fundação AMES: tabela de solicitações + report_type + rota de entrega STA

**CONTRATO PROVADO:** `regulatory_files` (schema `schemas/regulatory/regulatory_file.ex`) é o tracker genérico com ciclo STA completo já cabeado: `StaDelivery.deliver/1` publica NATS `monetarie.sta.regulatory.upload` (`use_cases/regulatory/sta_delivery.ex:66`) e o aceite volta pelo `StaHandler` correlacionado por `tracker_type: "regulatory_file"` (`infra/nats/handlers/sta_handler.ex:290`: uploaded vira delivered + sta_protocol; processed vira confirmed + bcb_ack_at; error vira failed). `@valid_report_types` NÃO tem "AMES"; `sta_system_for/1` (`sta_delivery.ex:95-139`) NÃO tem AMES (cairia no catch-all `PGEN001`). A cabine STA deriva `file_type` do NOME do arquivo (`AMES\d{3}`, `files_controller.ex:306`), então o nome do arquivo DEVE começar com o código AMES informado.

**Files:**
- Create: `core/backend/priv/repo/migrations/20260718152000_create_ames_requests.exs`
- Create: `core/backend/lib/monetarie/schemas/regulatory/ames_request.ex`
- Modify: `core/backend/lib/monetarie/schemas/regulatory/regulatory_file.ex` (adicionar "AMES" a `@valid_report_types`)
- Modify: `core/backend/lib/monetarie/use_cases/regulatory/sta_delivery.ex` (`sta_system_for("AMES", metadata)`: usar o `sta_system_id` do metadata do arquivo, com fallback explícito recusado: sem código informado, erro legível, nunca PGEN001 silencioso)
- Test: `core/backend/test/monetarie/regulatory/ames_request_test.exs`

**Step 1: Migration + schema:**

```elixir
defmodule Monetarie.Repo.Migrations.CreateAmesRequests do
  use Ecto.Migration
  def change do
    create_if_not_exists table(:ames_requests, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :demand_number, :string, null: false     # numero/oficio da solicitacao BACEN
      add :sta_file_code, :string, null: false     # ex AMES001 (decisao D6, operador informa)
      add :received_at, :date, null: false
      add :deadline, :date
      add :description, :text, null: false
      add :response_type, :string, null: false, default: "simples"   # simples|simba
      add :status, :string, null: false, default: "aberta"           # aberta|respondida|enviada|aceita|rejeitada
      add :regulatory_file_id, references(:regulatory_files, type: :uuid, on_delete: :nilify_all)
      add :created_by_id, :uuid
      add :created_by_name, :string
      add :metadata, :map, default: %{}
      timestamps(type: :utc_datetime)
    end
    create_if_not_exists unique_index(:ames_requests, [:demand_number])
  end
end
```

**Step 2: Teste RED do schema/changeset:** enums validados, demand_number único, `sta_file_code` obrigatório e casando `~r/^AMES\d{3}$/i` (o formato que a cabine STA reconhece, provado), `deadline` opcional.

**Steps 3-4:** implementar, PASS. Também teste do `sta_system_for` novo: `regulatory_file` de report_type "AMES" com `metadata["sta_system_id"] = "AMES001"` entrega esse system_id; sem ele, `{:error, :missing_sta_system_id}`.

**Step 5: Commit** `feat(ames): tabela de solicitacoes + report_type AMES no pipeline regulatorio`

### Task 19: Use case AMES (ciclo completo: registrar, responder, enviar, acompanhar)

**CONTRATO PROVADO:** modelo de empacote e entrega: SIMBA (`regulatory/simba/generator.ex`: `generate_case/4` gera 5 TSV e zipa em memória com `:zip.create(..., [:memory])`, persiste `regulatory_file` report_type SIMBA); entrega genérica `StaDelivery.deliver(file)`; conteúdo fica em `regulatory_files.content` (string no DB). Requisitos do cliente (verbatim, item 11 do contexto): geração, envio, status pendente/enviado/aceito/rejeitado, histórico, timestamps, mensagens de retorno, reenvio.

**Sonda G1 obrigatória desta task (ponto NÃO verificado no planejamento):** existem DUAS vias de envio outbound ao STA e não foi provado qual está viva em HML: (a) NATS `monetarie.sta.regulatory.upload` (`StaDelivery`/`StaDispatcher`, usada pelo resend do CADOC admin) e (b) HTTP `POST /api/v1/files` na cabine (`Monetarie.Sta.Client.Http`, usada pelo `StaSubmission.submit` do CCS, caminho com aceite BACEN PROVADO em PRD em 16/07). Antes de codar o `deliver/2`: conferir o valor efetivo de `:sta_client` no runtime e procurar em logs de HML um roundtrip recente da via NATS (ex.: envio de APIX001/regulatory_file). Se a via NATS não tiver prova de vida, o `deliver/2` do AMES usa a via HTTP do CCS (mesma assinatura de upload, `system_id` = `sta_file_code`), mantendo o resto da task igual.

**Files:**
- Create: `core/backend/lib/monetarie/use_cases/regulatory/ames.ex`
- Test: `core/backend/test/monetarie/regulatory/ames_test.exs`

**Step 1: Teste RED do ciclo:**

```elixir
test "registrar solicitacao, anexar resposta simples e enviar cria regulatory_file AMES e publica upload" do
  {:ok, req} = Ames.create_request(%{demand_number: "OF-2026-001", sta_file_code: "AMES001",
    received_at: ~D[2026-07-18], description: "Solicitação de informações X"}, actor)

  {:ok, req} = Ames.attach_simple_response(req.id, %{file_name: "AMES001_resposta.txt",
    content: "conteudo da resposta"}, actor)
  assert req.status == "respondida"
  assert req.regulatory_file_id

  {:ok, req} = Ames.deliver(req.id, actor)
  assert req.status == "enviada"
  # prova do payload: job Oban nats_publish enfileirado com subject monetarie.sta.regulatory.upload,
  # system_id "AMES001", file_name comecando com "AMES001" (regex da cabine provada), content base64,
  # metadata.tracker_type == "regulatory_file"
end

test "resposta SIMBA delega ao Simba.Generator e anexa o zip" do ... end
test "aceite STA (evento processed no StaHandler) reflete status aceita na solicitacao" do
  # simular o caminho real: update do regulatory_file para confirmed via StaHandler.handle_file_event
  # e Ames.refresh_status/1 espelhando em ames_requests
end
test "rejeicao STA carrega a mensagem de retorno (error_message) e permite reenvio" do ... end
test "deliver sem resposta anexada e recusado com erro legivel" do ... end
```

**Step 2:** Run, Expected: FAIL

**Step 3: Implementar:** `create_request/2` (grava ator), `attach_simple_response/3` (persiste `regulatory_file` report_type AMES, `file_name` prefixado pelo `sta_file_code`, `metadata: %{"sta_system_id" => req.sta_file_code}`), `attach_simba_response/3` (chama `Simba.Generator.generate_case/4` e anexa o ZIP base64/binário conforme o padrão SIMBA existente), `deliver/2` (`StaDelivery.deliver/1` + status `enviada`), `refresh_status/1` (espelha o status do `regulatory_file`: delivered mantém enviada, confirmed vira aceita, failed vira rejeitada com `error_message`), `list/1` e `get/1` para o controller. Sem cron: AMES é esporádico, o refresh acontece no read (e o `regulatory_file` já é atualizado por evento NATS).

**Step 4:** Run, Expected: PASS.

**Step 5: Commit** `feat(ames): ciclo completo solicitacao-resposta-envio-aceite sobre o pipeline STA existente`

### Task 20: Controller + rotas AMES

**Files:**
- Create: `core/backend/lib/monetarie_web/controllers/regulatory/ames_controller.ex`
- Modify: `core/backend/lib/monetarie_web/router.ex` (scope `/api/v1/regulatory/ames`, mesmo pipeline de auth do CCS/SISBAJUD, RBAC `compliance.cadoc`)
- Test: `core/backend/test/monetarie_web/controllers/regulatory/ames_controller_test.exs`

Rotas: `GET /ames` (lista com refresh de status), `POST /ames` (criar solicitação), `GET /ames/:id`, `POST /ames/:id/response` (upload simples: content base64 + file_name; ou `{type: "simba", case_params}`), `POST /ames/:id/deliver`, `GET /ames/:id/file` (download do arquivo de resposta, padrão attachment do `accs001_download`).

**Steps 1-5:** TDD padrão (RED nos 6 endpoints com auth/permissão, incluindo 403 sem RBAC e 422 de payload inválido; implementar; PASS; commit `feat(ames): endpoints admin do ciclo AMES`).

### Task 21: Tela AMES

**CONTRATO PROVADO:** padrão de tela: `CadocReportsView.vue` (cards de status, DataTable com ações Gerar/Enviar/Reenviar, timeline STA lida de `metadata.sta_timeline`, polling enquanto processa; `views/regulatory/CadocReportsView.vue:1173-1226` cards, `:1398` botão Enviar via STA, `:645` resend).

**Files:**
- Create: `core/apps/admin/src/views/regulatory/AmesView.vue`
- Create: `core/apps/admin/src/composables/useAmes.ts`
- Modify: `core/apps/admin/src/router/index.ts` (rota `regulatory/ames`, RBAC `compliance.cadoc.view`) + menu/nav onde o CADOC está listado
- Test: `core/apps/admin/src/composables/__tests__/useAmes.spec.ts`

**Steps 1-5:** TDD padrão. Tela: cards (Abertas, Respondidas, Enviadas, Aceitas, Rejeitadas), tabela de solicitações (nº demanda, código STA, recebida em, prazo, tipo de resposta, status, ações), diálogo Nova Solicitação (nº demanda, código AMES, data, prazo, descrição, tipo), diálogo Responder (upload de arquivo OU parâmetros SIMBA: caso, documento, período), botão Enviar via STA, exibição da mensagem de retorno/motivo de rejeição, botão Reenviar quando rejeitada. Commit `feat(admin): tela AMES (solicitacoes do BACEN com ciclo STA completo)`.

---

# FASE 5: Fecho da frente

### Task 22: Regressão completa + preparo de deploy

**Step 1:** `cd /Users/luizpenha/monetarie/core/backend && mix test` (suíte completa; comparar falhas com a baseline da main pura, zero regressão nova).
**Step 2:** `cd /Users/luizpenha/monetarie/core && pnpm --filter @monetarie/shared build && pnpm --filter @monetarie/admin test -- --run && pnpm --filter @monetarie/admin build`.
**Step 3:** Revisão adversarial da frente inteira (revisor fresh, protocolo do plano mestre).
**Step 4:** Montar a nota de deploy para o orquestrador:
- Migrations novas (aplicar via rpc, HML antes de PRD): `20260718150000_add_manager_name_to_accounts`, `20260718151000_add_titular_atendimento_to_judicial_orders`, `20260718152000_create_ames_requests`. Todas aditivas, sem backfill (judicial_orders vazio em HML/PRD, provado).
- Envs novas (entram OFF/ausentes por default, decisão D1 do dono para ligar): `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD` (Secrets Manager), `EMAIL_FROM`, `CCS_NOTIFICATION_EMAILS`, `CCS_DIGEST_ALWAYS`.
- Sem mudança nas cabines (a STA já reconhece o prefixo AMES). Ordem core-api única, sem acoplamento pix/spb.
**Step 5:** Commit final de docs (atualizar o handoff da frente) e entrega ao orquestrador para merge.

### Task 23: VALIDAÇÃO VIVA em HML, tela a tela (definition of done da frente)

Regra do dono: tela só validada com screenshot + zero erro no validador; NUNCA validar com fixture de caminho feliz, só dado real. Executar como admin em HML (túnel SSM quando sem VPN), captura via Playwright, screenshots em `docs/reports/screenshots/2026-07-18-w1-f3-validacao/`. As escritas de validação (remessa de teste SISBAJUD, solicitação AMES, atribuição de gerente) são parte do exercício e ficam registradas no relatório.

**Roteiro:**

1. **Relatório CCS:** abrir `Relatórios > Movimentações CCS`, período 01/07 a 18/07 (HML tem 26 ccs_files reais, incluindo a remessa `202607180001` com 3 pessoas). Conferir as 8 colunas contra o banco (rpc read-only: a mesma query da sonda G1), modo consolidado, busca por CPF real, export PDF e Excel abertos e conferidos (mesmas linhas da tela). Screenshots: tela analítica, consolidada, PDF gerado.
2. **E-mail CCS:** com adapter Logger (default), disparar um sync-STA de arquivo CCS e evidenciar a linha de log do e-mail estruturado em CloudWatch (`/ecs/monetarie/core-api` HML); se o dono já tiver decidido D1, validar entrega real numa caixa de teste. Screenshot do log ou da caixa.
3. **Extrato 4 formatos:** conta real com movimento (ex.: a conta do acervo com TEFs de 18/07 da sonda), baixar PDF, Excel, CSV e OFX; abrir o OFX num validador/parse manual (header `OFXHEADER:100`) e conferir o valor de UMA linha ao centavo contra a tela (prova de unidade). Screenshots: tela + 4 arquivos abertos.
4. **Avisos de créditos:** rodar o dia corrente (HML tem créditos reais de 18/07); conferir total do dia ao centavo contra rpc read-only (`SELECT count(*), SUM(amount) FROM account_entries WHERE amount > 0 AND entry_date = current_date`; lembrar: subcentavos vs centavos na conferência). Screenshot + export.
5. **Movimentações analítico/sintético:** mesmo dia; no sintético, conferir que créditos - débitos = saldo líquido exibido, ao centavo, contra a mesma query. Screenshot dos dois modos.
6. **Gerente:** atribuir gerente a 2 contas reais pela tela, abrir o relatório de saldos por gerente e conferir o saldo dessas contas ao centavo contra o card de saldo das próprias contas no admin. Screenshots: edição + relatório.
7. **SISBAJUD:** subir remessa de teste 5301 pela tela (fixture de `test/fixtures/sisbajud/5301_remessa` adaptada a um cooperado de HML, como na validação viva de 09/07), processar, e conferir no detalhe: titular preenchido, natureza com label (ou fallback honesto), vara (semear a vara correspondente via arquivo 5305 de teste antes), bloco de atendimento: editar para Cumprida Parcialmente com justificativa e conferir ator + linha de auditoria na tela de Logs de Auditoria. Screenshots: detalhe completo antes/depois do atendimento.
8. **AMES:** criar solicitação real de teste (AMES001), anexar resposta simples, enviar via STA HML, acompanhar o retorno da cabine (uploaded/processed) e conferir status Aceita/Rejeitada com a mensagem. Screenshot da linha com timeline completa. Se a cabine STA de HML rejeitar o tipo AMES001 (nunca exercitado), registrar o comportamento real no relatório e ajustar com o dono (D6): a recusa é achado, não falha da validação.
9. **Fecho:** relatório de validação em `docs/reports/2026-07-18-w1-f3-validacao-viva.md` com a tabela tela x evidência x conferência ao centavo, e screenshots referenciados. Zero erro de console/validador em cada captura.

---

## Riscos e mitigações

- **E-mail depende de decisão D1:** mitigado com adapter Logger fail-safe; a fiação inteira é validável sem transporte real.
- **ccs_files com janela de 45 dias para trás no filtro:** eventos muito antigos reenviados podem escapar; documentado no moduledoc, ajustável por parâmetro.
- **Natureza da ação sem tabela oficial (D4):** decode nasce parcial com fallback honesto; nunca inventa rótulo.
- **Parser 5305 best-effort (D5):** campos ricos da vara só entram com leiaute confirmado; a tela mostra nils honestos.
- **AMES nunca exercitado contra a cabine STA (D6):** validação viva item 8 prova o comportamento real; a task não presume aceitação.
- **Volume de account_entries (195k+ em HML):** agregações dos relatórios novos SEMPRE no SQL com índices existentes (`account_id`, `entry_date`); export analítico com teto de linhas (10k) e aviso na tela quando truncado.
- **Sessão paralela na mesma árvore:** F3 trabalha em worktree própria (protocolo do plano mestre); router/config compartilhados mergeados pelo orquestrador.
