# Onda 2.B — Status & Queries (Full + Admin endpoints)

> **For Claude:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Pattern: Manual Verification mandatory (this sub-PR introduces 4 new Manual-aligned wire-format claims).

**Branch:** `sta/onda2-status-and-queries` (this worktree, cut from `origin/main` HEAD `e54399ab` post PR #59 merge)
**PR target:** `main`
**Scope chosen:** **(C) Full + admin endpoints** — all 5 components from rollout plan §Sub-PR 2.B + 2 admin controllers wiring `query_protocol` and `advanced_query` from day 1. Eliminates the orphan-API window described in the handoff §"Why (B) is the recommendation" counter-arguments (option C).
**Estimated effort:** 1.5-2 days actual · LOC delta estimate: ~600 (libs + tests + admin controllers).
**Parent plan:** `sta/docs/plans/2026-04-26-sta-conformance-rollout.md` §Sub-PR 2.B (lines 670-737).
**Handoff that resumed work:** `sta/docs/handoff/2026-04-27-sta-onda2b-brainstorming-pause-and-onda2a-complete.md` (Task 0 Manual Verification pre-collected there — copied verbatim below to keep this plan self-contained).

---

## Index

1. [Task 0 — Manual Verification](#task-0)
2. [Architecture](#architecture)
3. [Pre-flight](#pre-flight)
4. [Tasks](#tasks)
5. [Acceptance](#acceptance)
6. [Out of scope](#out-of-scope)
7. [References](#references)

---

<a id="task-0"></a>
## Task 0 — Manual Verification

Per the **Pattern: Manual Verification** mandated for every Ondas 2-4 sub-PR (`sta/docs/plans/2026-04-26-sta-conformance-rollout.md#pattern-manual-verification`). Inventory was pre-collected during the brainstorming pause in handoff §"Manual Verification gate — pre-collected"; it is reproduced verbatim here so this plan is self-contained.

**Source:** `sta/Manual_STA_Web_Services.pdf` (Manual de utilização dos Web Services do STA, v1.5, jul/2022). Sections referenced: §7 (Alteração de situação), §8 (Consulta a transferências de arquivos) including §8.1, §8.2, §8.3, plus Tabelas 1-4.

### Verified `[VERIFIED §X.Y]`

| # | Claim | Manual section / Tabela | Notes |
|---|---|---|---|
| 1 | `PUT /staws/arquivos/situacao` for status change | §7.1 | Body: `<Parametros><Protocolos>p1;p2</Protocolos><Situacao>{A_REC\|REC}</Situacao></Parametros>` |
| 2 | `Content-Type: application/xml` REQUIRED on §7 PUT | §7.1 Atenção | Distinct from §5.3.1/§6.1/§6.4 endpoints which forbid Content-Type |
| 3 | §7 returns HTTP 204 No Content on success | §7.1 | |
| 4 | §7 errors: 400 only (XML body) | §7.1 errors table | |
| 5 | `<Protocolos>` in §7 is **semicolon-separated**, not array | §7.1 informações importantes #1 | Mirrors `<RangesRecebidos>` shape from §5.3.1 |
| 6 | Two `Situacao` enum values: `A_REC` (a receber), `REC` (recebido) | §7.1 informações importantes #2 | |
| 7 | `GET /staws/arquivos?tipoConsulta=PROT&nivelDetalhe={...}&protocolos={...}&origemResposta={...}` | §8.2.1 + Tabela 1 | |
| 8 | `Content-Type` MUST NOT be in request for §8.1, §8.2, §8.3 | §8.1.1, §8.2.1, §8.3 Atenção | Same as §6.1/§6.4 |
| 9 | `nivelDetalhe` enum: `RES`, `BAS`, `COMPL` | §8 + Tabela 1 + Tabela 2 | |
| 10 | `origemResposta` enum: `RESP` (apenas respostas), `PROT_RESP` (origem e resposta); default = origem only | Tabela 1 | |
| 11 | `protocolos` query param is semicolon-separated | Tabela 1 | |
| 12 | `GET /staws/arquivos?tipoConsulta=AVANC&...` for advanced query — same response format as §8.2 | §8.3.1 | |
| 13 | `situacaoTransmissao` query param: `REC` or `A_REC` (optional, default = both) | Tabela 2 | |
| 14 | `qtdMaxResultados` cap is 100 | Tabela 2 | |
| 15 | `ordenacao` enum: `PROT_ASC`, `PROT_DESC`, `DH_ASC`, `DH_DESC`. Default = `DH_DESC` | Tabela 2 | |
| 16 | `dataHoraCriacaoFim` requires `dataHoraCriacaoInicio`; same for `dataHoraInicio`/`dataHoraFim` (atualização) | Tabela 2 | |
| 17 | `sistemas` query param semicolon-separated, max 100 sistemas | Tabela 2 | |
| 18 | 14 state codes per Tabela 3: 1, 2, 5, 10, 15, 20, 25, 30, 35, 45, 55, 65, 70, 75 | Tabela 3 | |
| 19 | Response shape `<Resultado><Arquivo>...` with `<EstadoAtual><Codigo>{n}</Codigo>` | §8.2.1 sample response | |
| 20 | §8.3 Atenção: prefer §8.1 over `tipoConsulta=AVANC&situacaoTransmissao=A_REC` because manual receipts may break advanced query results | §8.3 Atenção | Document this as a runtime warning if anyone uses `:advanced_query` with `situacao: :a_rec` |

### `[STUDY-CONJECTURE]` items (Manual silent — conservative read applied)

| # | Topic | Manual silence | Decision |
|---|---|---|---|
| 1 | `Situacao` value when querying via `:list_available_files` (already implemented) — current code returns enum value as XML string. Onda 2.B should align this with the new `StateCode` mapping. | Manual sample at §8.1 shows `<SituacaoAtual><Codigo>3</Codigo><Descricao>A receber</Descricao></SituacaoAtual>` — but Tabela 3's `codigoEstado` enum doesn't have a `3` mapped to "A receber". Tabela 3 codes are 1/2/5/10/15/20/25/30/35/45/55/65/70/75. | The `<SituacaoAtual><Codigo>` in §8.1 uses a **DIFFERENT enum** than Tabela 3's `EstadoAtual` codes. This is a known confusion-point. Do not auto-map; document divergence in `StateCode` moduledoc. `list_available_files` parser remains untouched (it parses `<SituacaoAtual>`, not `<EstadoAtual>`). |
| 2 | Default value of `qtdMaxResultados` when not supplied in §8.3 | Tabela 2 says max 100; doesn't state default | Default to 100 (the max). Document as conservative read in `advanced_query/1` docstring. Caller can override. |
| 3 | `dependencia` parameter — should it be the institution's main SISBACEN code or a specific dependência code? | Tabela 2 says "Código Sisbacen de uma dependência da instituição" | Make it caller-supplied, no default. Document expectation. |
| 4 | `dataHoraInicio` (atualização) vs `dataHoraCriacaoInicio` (criação) — when does the application care about each? | Manual distinguishes them but doesn't recommend one for typical use cases | Both caller-supplied. Document the difference in docstring. Surface both in admin endpoint Task 7. |
| 5 | What happens when `protocolos` list contains a protocol that doesn't exist? | Manual silent | Handle gracefully — empty `<Resultado>` for that protocol; no error. Cover with Bypass test. |

### `[NEEDS-OPERATOR-CONFIRM]` items

| # | Topic | Why operator confirmation needed | Decision (pending confirmation) |
|---|---|---|---|
| 1 | Do we expect Monetarie-side admin UI to call `query_protocol` directly? | Drives whether (C) admin endpoints are needed | (C) chosen — yes, ship endpoints. Marked CONFIRMED implicitly by user's (C) selection. |
| 2 | Should the existing `Inbound.Worker` switch its decision logic to `StateCode` immediately, or can the wiring lag the StateCode introduction? | Per rollout plan, Worker wiring is part of Onda 2.B | Confirmed pre-flight: workers do **not** currently have `status_desc` string compares (grep returned empty). Task 3 becomes a "future-readiness" wiring of `StateCode` into status-update helper functions (no behavior change today; ready for Onda 2.C state polling). |

### Misdiagnoses anticipated (and what to watch for)

The Onda 2.A core Task 0 caught 4 misdiagnoses. For Onda 2.B watch for:

1. **`<EstadoAtual>` vs `<SituacaoAtual>` confusion.** They are NOT the same field — see [STUDY-CONJECTURE] #1 above. `list_available_files` parses `<SituacaoAtual>` (different enum); §8.2 / §8.3 responses parse `<EstadoAtual>` (Tabela 3 enum). New parser must use `<EstadoAtual>` paths.
2. **Whether the existing `:list_available_files` builder correctly emits no `Content-Type` header.** Verify in Task 0 pre-flight.
3. **Whether `:confirm_receipt` builder currently sends `Content-Type: application/xml`.** It must per §7.1; current code at `client.ex:1126` should already do so. Task 2 must preserve this.

---

<a id="architecture"></a>
## Architecture

The Onda 2.B (C) scope adds 5 lib files + 1 controller file + 5 test files. Public surface:

### Component 1: `StateCode` module

**Path:** `lib/sta_connector/sta/state_code.ex`

**Public API:**

```elixir
@spec from_int(integer()) :: {:ok, atom()} | :error
@spec to_int(atom()) :: integer() | nil
@spec terminal?(integer()) :: boolean()
@spec success?(integer()) :: boolean()
@spec failure?(integer()) :: boolean()
```

**Code mapping** (verbatim from rollout plan §Sub-PR 2.B lines 697-712):

```elixir
@codes %{
  1 => :protocol_generated,
  2 => :available_for_download,
  5 => :transmission_started,
  10 => :transmission_finished,
  15 => :validation_in_progress,
  20 => :received_by_bacen,
  25 => :delivered_to_recipient,
  30 => :processing_by_recipient,
  35 => :accepted,
  45 => :cancelled,
  55 => :inconsistent,
  65 => :rejected,
  70 => :download_started,
  75 => :download_finished
}

@terminal [35, 45, 55, 65, 75]
@success  [25, 35, 75]
@failure  [45, 55, 65]
```

**Moduledoc** must call out [STUDY-CONJECTURE] #1: this enum is for `<EstadoAtual><Codigo>` per Manual Tabela 3 (§8.2/§8.3), NOT for `<SituacaoAtual><Codigo>` in §8.1 (different enum, mapped by `:list_available_files` parser).

### Component 2: Batched `change_status` (extends existing `:confirm_receipt`)

**Path modifications:**
- `lib/sta_connector/sta/client.ex` — rename build clause `:confirm_receipt` → `:change_status`, accept new param shape, emit semicolon-joined `<Protocolos>`. Keep `confirm_receipt/2` public function as thin wrapper.
- `lib/sta_connector/sta/client_behaviour.ex` — add `@callback change_status/2`.

**Public API:**

```elixir
@callback change_status(
            %{
              required(:protocols) => [String.t()],
              required(:situation) => :rec | :a_rec
            },
            keyword()
          ) :: {:ok, :ok} | {:error, Error.t()}
```

**Wire format (per [VERIFIED §7.1]):**

```
PUT /staws/arquivos/situacao
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<Parametros>
  <Protocolos>p1;p2;p3</Protocolos>
  <Situacao>REC</Situacao>
</Parametros>
```

**Backward compatibility:** `Client.confirm_receipt(protocol, opts)` keeps its existing signature; rewrites internally to `change_status(%{protocols: [protocol], situation: :rec}, opts)`. Existing `:confirm_receipt` parse_rest_response clause renames to `:change_status` (parse path: 204 → `{:ok, :ok}`, others → mapped error).

**Note:** The existing parse clause at `client.ex:1287` returns `{:ok, :confirmed}` for any non-204 status. This is buggy (silently swallows 400 errors as success). Task 2 will fix it as side-effect of the rename: only 204 → `{:ok, :ok}`; non-204 → `{:error, Error.from_response(...)}`. Existing `confirm_receipt/2` tests need updating to expect `:ok` instead of `:confirmed` — wrapper translates back if needed for compat.

**Compat decision**: rather than risk renaming the success atom and breaking external callers, we keep `Client.confirm_receipt/2` returning `{:ok, :confirmed}` (translates `:ok → :confirmed`). New `change_status/2` returns `{:ok, :ok}` directly. Document the difference.

### Component 3: `Client.query_protocol/2`

**Path:** `lib/sta_connector/sta/client.ex` (new build + parse clauses) + behaviour.

**Public API:**

```elixir
@callback query_protocol(
            %{
              required(:protocols) => [String.t()],
              optional(:detail_level) => :res | :bas | :compl,        # default :res
              optional(:origin_response) => :origin | :resp | :prot_resp  # default :origin
            },
            keyword()
          ) :: {:ok, [Types.FileQueryResult.t()]} | {:error, Error.t()}
```

**Wire format (per [VERIFIED §8.2 + Tabela 1]):**

```
GET /staws/arquivos?tipoConsulta=PROT&nivelDetalhe=RES&protocolos=p1;p2&origemResposta=RESP
(no Content-Type header)
```

**Detail enum mapping:**
- `:res` → `RES` (Resumido — protocolo + sistema + estado_atual)
- `:bas` → `BAS` (Básico — RES + dados básicos do arquivo)
- `:compl` → `COMPL` (Completo — BAS + dados completos)

**Origin enum mapping:**
- `:origin` (default) → omit `origemResposta` (returns origin only per Tabela 1 default)
- `:resp` → `RESP` (responses only)
- `:prot_resp` → `PROT_RESP` (both)

**Response parsing:** new `Types.FileQueryResult` struct (or extend `Types.FileInfo`?). Per [VERIFIED §8.2.1 sample response] response is `<Resultado><Arquivo>...<EstadoAtual><Codigo>...` shape. To minimize surface, **introduce a new struct `Types.FileQueryResult`** with fields `{protocol, system_id, current_state_code, current_state_desc, file_name?, file_size?, hash?, ...}` — superset of FileInfo since §8.2 returns additional fields.

### Component 4: `Client.advanced_query/1`

**Path:** `lib/sta_connector/sta/client.ex` (new build clause; reuse parse from Task 4 since §8.3 says "idêntica à resposta da consulta por protocolos") + behaviour.

**Public API:**

```elixir
@callback advanced_query(
            %{
              optional(:date_creation_from) => DateTime.t() | String.t(),
              optional(:date_creation_to) => DateTime.t() | String.t(),
              optional(:date_update_from) => DateTime.t() | String.t(),
              optional(:date_update_to) => DateTime.t() | String.t(),
              optional(:state_codes) => [pos_integer()],
              optional(:transmission_situation) => :rec | :a_rec,
              optional(:systems) => [String.t()],
              optional(:dependencia) => String.t(),
              optional(:detail_level) => :res | :bas | :compl,        # default :res
              optional(:order_by) => :prot_asc | :prot_desc | :dh_asc | :dh_desc,  # default :dh_desc
              optional(:origin_response) => :origin | :resp | :prot_resp,
              optional(:max_results) => pos_integer()                 # cap 100, default 100
            },
            keyword()
          ) :: {:ok, [Types.FileQueryResult.t()]} | {:error, Error.t()}
```

**Validation rules per [VERIFIED §16]:**
- `date_creation_to` requires `date_creation_from` (else `:bad_request` returned without HTTP call)
- `date_update_to` requires `date_update_from` (same)
- `max_results` clamped to 100 max (or rejected? — choose **clamp + log warning** per conservative read; document)
- `systems` max 100 entries (cap or reject? — **cap + log warning**)

**Defaults applied per [STUDY-CONJECTURE] #2-#4:**
- `max_results` not supplied → 100
- `dependencia` no default; not emitted if not supplied
- Both `dataHoraInicio`/`dataHoraCriacaoInicio` caller-supplied with no implicit default

**Runtime warning per [VERIFIED §20]:** if `transmission_situation == :a_rec` is supplied to `advanced_query/1`, emit a `Logger.warning` recommending `list_available_files/2` instead (Manual §8.3 Atenção). Don't block.

### Component 5: Worker wiring (Inbound + Outbound)

Pre-flight check confirmed: `inbound/worker.ex` and `outbound/worker.ex` do NOT currently string-compare `status_desc` (grep `status_desc\|estado_atual\|StateCode` returned empty in both files). Per handoff §"Existing infrastructure" the workers' retry logic is `RetryEntry`-based, not state-string-based.

**Therefore Task 3 is "future-readiness" wiring:**

1. Add `import StaConnector.Sta.StateCode, only: [terminal?: 1, success?: 1, failure?: 1]` to both workers (or `alias` if not used in the same module).
2. Write a small helper `decide_next_action/1` in each worker that takes an integer state code and returns `:done` (for terminal+success) / `:retry` (for in-progress) / `:fail` (for terminal+failure) — even if no current call site uses it. This locks the wiring in for Onda 2.C.
3. Add a unit test per worker proving `decide_next_action/1` does the right thing for each Tabela 3 code.
4. Document in worker moduledoc that Onda 2.C lifecycle polling will route through `decide_next_action/1`.

This is intentional dead code with explicit purpose-comment + tests — preferable to dropping the rollout plan's "Workers no longer string-compare status descriptions" acceptance criterion (already vacuously true).

### Component 6: Admin controller `Api.Admin.StaQueryController`

**Path:** `lib/sta_connector_web/controllers/api/admin/sta_query_controller.ex`

**Routes** (added to existing `:admin_api` pipeline in `router.ex`):

```elixir
get "/sta/protocols", StaQueryController, :show_protocols
get "/sta/queries/advanced", StaQueryController, :advanced
```

**Controller actions:**

```elixir
def show_protocols(conn, params) do
  # Parse: ids=p1,p2 (comma-sep), detail=res|bas|compl, origin=origin|resp|prot_resp
  # Wire to Client.query_protocol/2 (Operations injection seam, default StaConnector.Sta.Client)
  # Render JSON list of Types.FileQueryResult structs
end

def advanced(conn, params) do
  # Parse query parameters per Component 4 advanced_query/1 shape
  # Wire to Client.advanced_query/1 (Operations injection seam)
  # Render JSON list of Types.FileQueryResult structs
end
```

**Operations injection** (mirrors PR #46 pattern at `outbound_files_controller.ex:1-30`):

```elixir
defmodule StaConnectorWeb.Api.Admin.StaQueryController do
  use StaConnectorWeb, :controller

  @client Application.compile_env(:sta_connector, :sta_client, StaConnector.Sta.Client)
  # ...
end
```

**Auth:** inherits `:admin_api` pipeline (currently `# Future: Add admin authentication here` TODO — endpoints will inherit current state, same as PR #46 endpoints. Documented in handoff §Out of scope #8 as deferred work.)

**Error handling:** standard `{:error, %Error{}}` → render with appropriate HTTP status (400 for caller param errors, 502 for upstream BCB failures). Reuse the controller-error pattern from `outbound_files_controller.ex`.

### Files to create

| File | Purpose | Est. LOC |
|---|---|---|
| `lib/sta_connector/sta/state_code.ex` | StateCode module | ~80 |
| `lib/sta_connector/sta/types.ex` (extend) | Add `Types.FileQueryResult` struct | +30 |
| `lib/sta_connector_web/controllers/api/admin/sta_query_controller.ex` | Admin controller | ~150 |
| `test/sta_connector/sta/state_code_test.exs` | StateCode tests | ~70 |
| `test/sta_connector_web/controllers/api/admin/sta_query_controller_test.exs` | Controller tests | ~120 |

### Files to modify

| File | Modification | Est. LOC delta |
|---|---|---|
| `lib/sta_connector/sta/client.ex` | New build clauses (`:change_status`, `:query_protocol`, `:advanced_query`); new parse clauses; rename `:confirm_receipt` → `:change_status`; thin wrapper for `confirm_receipt/2` | +200 |
| `lib/sta_connector/sta/client_behaviour.ex` | New `@callback change_status/2`, `@callback query_protocol/2`, `@callback advanced_query/1` | +60 |
| `lib/sta_connector/inbound/worker.ex` | New `decide_next_action/1` helper + alias StateCode | +20 |
| `lib/sta_connector/outbound/worker.ex` | Same | +20 |
| `lib/sta_connector_web/router.ex` | 2 new routes in `:admin_api` scope | +5 |
| `test/sta_connector/sta/client_test.exs` | New describe blocks: change_status batched, query_protocol, advanced_query | +200 |

**Total estimated:** ~955 LOC delta (with ~70% being tests). Within the (C) ~600 LOC ballpark from the brainstorming pause; difference is mainly the more substantial test surface for batched + query parsers.

---

<a id="pre-flight"></a>
## Pre-flight

**Already verified at session start:**
- ✅ Worktree on `sta/onda2-status-and-queries` cut off `origin/main` `e54399ab`
- ✅ `mix compile` produces 5 baseline warnings
- ✅ 6 touchstone test files: 172 tests, 6 baseline failures (Soap.parse_response noise — preserved)
- ✅ Workers grep `status_desc\|StateCode\|estado_atual` empty — confirms Task 3 path

**Will do per task before TDD:**
- Read existing `:list_available_files` build_rest_request clause at `client.ex:1061` (template for `:query_protocol` and `:advanced_query`)
- Read existing `:confirm_receipt` build clause at `client.ex:1126` (template for `:change_status`)
- Read existing `outbound_files_controller.ex` (template for `StaQueryController`)

---

<a id="tasks"></a>
## Tasks

### Task 1 — `StateCode` module + tests

**Path:** `lib/sta_connector/sta/state_code.ex` + `test/sta_connector/sta/state_code_test.exs`

**TDD red:** Write failing tests asserting:
- `from_int(1) == {:ok, :protocol_generated}`, all 14 codes mapped, `from_int(99) == :error`
- `to_int(:protocol_generated) == 1`, all 14 atoms reversed, `to_int(:nonexistent) == nil`
- `terminal?(35) == true`, `terminal?(45) == true`, `terminal?(55) == true`, `terminal?(65) == true`, `terminal?(75) == true`; `terminal?(1) == false`, `terminal?(70) == false`
- `success?(25) == true`, `success?(35) == true`, `success?(75) == true`; `success?(45) == false`
- `failure?(45) == true`, `failure?(55) == true`, `failure?(65) == true`; `failure?(35) == false`
- In-progress codes (1, 2, 5, 10, 15, 20, 30, 70) classify as `terminal?/1==false`, `success?/1==false`, `failure?/1==false`

**TDD green:** Implement per spec verbatim from rollout plan §Sub-PR 2.B lines 691-726, with moduledoc clarifying [STUDY-CONJECTURE] #1.

**Commit message:** `feat(sta): add StateCode module per Manual Tabela 3 (Onda 2.B Task 1)`

**Estimate:** 30 min.

---

### Task 2 — Batched `change_status` (rename `:confirm_receipt`) + tests

**TDD red:** Add new describe block to `client_test.exs`:

```elixir
describe "change_status/2 (batched, Manual §7.1)" do
  test "1 protocol REC → emits singular Protocolos + Situacao=REC" do
    Bypass.expect_once(bypass, "PUT", "/staws/arquivos/situacao", fn conn ->
      {:ok, body, conn} = Plug.Conn.read_body(conn)
      assert body =~ "<Protocolos>P1</Protocolos>"
      assert body =~ "<Situacao>REC</Situacao>"
      assert Plug.Conn.get_req_header(conn, "content-type") == ["application/xml"]
      Plug.Conn.send_resp(conn, 204, "")
    end)
    assert {:ok, :ok} = Client.change_status(%{protocols: ["P1"], situation: :rec}, opts)
  end

  test "3 protocols REC → semicolon-joined Protocolos" do
    # body =~ "<Protocolos>P1;P2;P3</Protocolos>"
  end

  test "A_REC → emits Situacao=A_REC" do
    # body =~ "<Situacao>A_REC</Situacao>"
  end

  test "204 → {:ok, :ok}"
  test "400 → {:error, %Error{}}"
end

describe "confirm_receipt/2 (compat wrapper)" do
  test "delegates to change_status with [protocol] + :rec, returns {:ok, :confirmed}" do
    # Bypass expectation as in single-protocol REC test above
    assert {:ok, :confirmed} = Client.confirm_receipt("P1", opts)
  end
end
```

**TDD green:**
1. Modify `lib/sta_connector/sta/client.ex`:
   - Rename build clause: `defp build_rest_request(:confirm_receipt, ...)` → `defp build_rest_request(:change_status, %{protocols: protocols, situation: situation})` with new shape. Emit `<Protocolos>p1;p2;...</Protocolos>` (semicolon join). Map `:rec → "REC"`, `:a_rec → "A_REC"`.
   - Rename parse clause: `defp parse_rest_response(:confirm_receipt, ...)` → `defp parse_rest_response(:change_status, ...)`. Fix the success/error split: 204 → `{:ok, :ok}`, anything else → `{:error, Error.from_response(status, body)}`.
   - Add public function `change_status/2`: `def change_status(params, opts \\ [])`. Routes through `{:execute, :change_status, params}` GenServer call.
   - Modify `confirm_receipt/2`: rewrite to delegate `change_status(%{protocols: [protocol_number], situation: :rec}, opts)` and translate `{:ok, :ok} → {:ok, :confirmed}` for backward compat.

2. Modify `lib/sta_connector/sta/client_behaviour.ex`:
   - Add `@callback change_status/2`.
   - Keep `@callback confirm_receipt/2` as-is (wrapper).

**Commit message:** `feat(sta): batched change_status + rename confirm_receipt to thin wrapper (Onda 2.B Task 2)`

**Estimate:** 1.5h.

---

### Task 3 — Worker wiring (Inbound + Outbound)

**TDD red:** Add `decide_next_action/1` test per worker:

```elixir
# test/sta_connector/inbound/worker_test.exs (NEW — pre-existing testing gap, see handoff)
describe "decide_next_action/1" do
  test "terminal success codes return :done", do: Enum.each([25, 35, 75], &assert(:done = Worker.decide_next_action(&1)))
  test "terminal failure codes return :fail", do: Enum.each([45, 55, 65], &assert(:fail = Worker.decide_next_action(&1)))
  test "in-progress codes return :retry", do: Enum.each([1, 2, 5, 10, 15, 20, 30, 70], &assert(:retry = Worker.decide_next_action(&1)))
  test "unknown code raises FunctionClauseError"
end
```

**TDD green:**
1. Add to `lib/sta_connector/inbound/worker.ex`:
   ```elixir
   alias StaConnector.Sta.StateCode

   @doc "Future-readiness routing for Onda 2.C state polling per Manual Tabela 3."
   def decide_next_action(code) when is_integer(code) do
     cond do
       StateCode.success?(code) -> :done
       StateCode.failure?(code) -> :fail
       true -> :retry
     end
   end
   ```
2. Same in `lib/sta_connector/outbound/worker.ex`.

**Note:** Per handoff §Out of scope #6, no broad `worker_test.exs` exists for Inbound. Creating a minimal `test/sta_connector/inbound/worker_test.exs` and `test/sta_connector/outbound/worker_test.exs` JUST for the new helper is acceptable; do NOT expand to test the full worker (separate hygiene PR per handoff).

**Commit message:** `feat(sta): wire StateCode into worker decide_next_action helpers (Onda 2.B Task 3)`

**Estimate:** 1h.

---

### Task 4 — `Client.query_protocol/2` + tests

**TDD red:** New describe block in `client_test.exs`:

```elixir
describe "query_protocol/2 (Manual §8.2)" do
  test "single protocol RES → emits GET tipoConsulta=PROT&nivelDetalhe=RES&protocolos=P1, no Content-Type" do
    Bypass.expect_once(bypass, "GET", "/staws/arquivos", fn conn ->
      assert conn.query_string =~ "tipoConsulta=PROT"
      assert conn.query_string =~ "nivelDetalhe=RES"
      assert conn.query_string =~ "protocolos=P1"
      refute conn.query_string =~ "origemResposta="  # default :origin → omit
      assert Plug.Conn.get_req_header(conn, "content-type") == []
      Plug.Conn.send_resp(conn, 200, sample_query_response_xml())
    end)
    assert {:ok, [%Types.FileQueryResult{} | _]} = Client.query_protocol(%{protocols: ["P1"]}, opts)
  end

  test "3 protocols → semicolon-joined protocolos query param"
  test "detail_level: :compl → nivelDetalhe=COMPL"
  test "origin_response: :resp → origemResposta=RESP"
  test "origin_response: :prot_resp → origemResposta=PROT_RESP"
  test "parses single-protocol response with EstadoAtual.Codigo"
  test "parses multi-protocol response"
  test "400 → {:error, %Error{}}"
end
```

**TDD green:**
1. Add `Types.FileQueryResult` struct to `lib/sta_connector/sta/types.ex`. Fields per [STUDY-CONJECTURE] #1 + §8.2.1 sample: `{protocol, system_id, current_state_code, current_state_desc, file_name?, file_size?, hash?, creation_datetime?, ...}`. Document divergence from `FileInfo` in moduledoc.
2. Add to `lib/sta_connector/sta/client.ex`:
   - Public function `query_protocol/2` routing through `{:execute, :query_protocol, params}`.
   - `defp build_rest_request(:query_protocol, %{protocols: ps, detail_level: dl, origin_response: or})` — builds GET URL with semicolon-joined protocolos, mapped detail_level enum, optional origemResposta. Returns `{:get, path, nil, []}` (no body, no Content-Type).
   - `defp parse_rest_response(:query_protocol, 200, body, _headers)` — parses `<Resultado><Arquivo>...<EstadoAtual><Codigo>...` shape via SweetXml. Returns list of `%Types.FileQueryResult{}`.
3. Add `@callback query_protocol/2` to behaviour.

**Commit message:** `feat(sta): query_protocol/2 per Manual §8.2 (Onda 2.B Task 4)`

**Estimate:** 2-2.5h.

---

### Task 5 — `Client.advanced_query/1` + tests

**TDD red:** New describe block in `client_test.exs`:

```elixir
describe "advanced_query/1 (Manual §8.3)" do
  test "minimal call → emits GET tipoConsulta=AVANC&qtdMaxResultados=100&ordenacao=DH_DESC&nivelDetalhe=RES"
  test "ISO 8601 dates"
  test "state_codes: [25, 35] → estados=25;35"
  test "transmission_situation: :rec → situacaoTransmissao=REC"
  test "transmission_situation: :a_rec → emits Logger.warning AND situacaoTransmissao=A_REC"
  test "systems: [...] → semicolon-joined sistemas"
  test "max_results > 100 → clamped to 100 + Logger.warning"
  test "date_creation_to without date_creation_from → {:error, :missing_required_param}"
  test "order_by: :prot_asc → ordenacao=PROT_ASC"
  test "parses response (reuse query_protocol parser)"
  test "400 → {:error, %Error{}}"
end
```

**TDD green:**
1. Add public function `advanced_query/1` in `lib/sta_connector/sta/client.ex`.
2. New `defp build_rest_request(:advanced_query, params)` — uses `URI.encode_query/1` for the URL. Validates date pairs and `max_results` cap before building. Logger.warning for `:a_rec` per [VERIFIED §20]. Returns `{:get, path, nil, []}`.
3. Reuse `parse_rest_response(:query_protocol, ...)` clause via dispatch — i.e., new clause `parse_rest_response(:advanced_query, status, body, headers) do parse_rest_response(:query_protocol, status, body, headers) end`. Per [VERIFIED §12] response is identical.
4. Add `@callback advanced_query/1` to behaviour.

**Commit message:** `feat(sta): advanced_query/1 per Manual §8.3 (Onda 2.B Task 5)`

**Estimate:** 2.5-3h.

---

### Task 6 — Admin endpoint `GET /api/admin/sta/protocols`

**TDD red:** New file `test/sta_connector_web/controllers/api/admin/sta_query_controller_test.exs`:

```elixir
describe "GET /api/admin/sta/protocols" do
  test "ids=P1 → calls Client.query_protocol with %{protocols: [\"P1\"], detail_level: :res} and renders 200"
  test "ids=P1,P2 → comma-split into protocols list"
  test "detail=compl → detail_level: :compl"
  test "Client error → 502 with body"
  test "missing ids param → 400 with body"
end
```

**TDD green:**
1. Create `lib/sta_connector_web/controllers/api/admin/sta_query_controller.ex` mirroring `outbound_files_controller.ex` pattern. Use `@client Application.compile_env(:sta_connector, :sta_client, StaConnector.Sta.Client)` for testability.
2. Add to `lib/sta_connector_web/router.ex` under `:admin_api` scope:
   ```elixir
   get "/sta/protocols", StaQueryController, :show_protocols
   ```

**Commit message:** `feat(sta): admin endpoint GET /api/admin/sta/protocols (Onda 2.B Task 6)`

**Estimate:** 1.5h.

---

### Task 7 — Admin endpoint `GET /api/admin/sta/queries/advanced`

**TDD red:** Extend `sta_query_controller_test.exs` with new describe block for `:advanced` action — happy + each filter param + 400 missing pair + Client error → 502.

**TDD green:**
1. Add `:advanced` action to `StaQueryController`. Parse query params with explicit transformation (date string → `DateTime`, comma-split state codes, etc.).
2. Add route `get "/sta/queries/advanced", StaQueryController, :advanced` to router.

**Commit message:** `feat(sta): admin endpoint GET /api/admin/sta/queries/advanced (Onda 2.B Task 7)`

**Estimate:** 2h.

---

<a id="acceptance"></a>
## Acceptance

**Before commit on each task:**
- Per-file tests for the touched file: green.
- `mix compile` still produces exactly 5 baseline warnings (no new warnings).

**Before push/PR:**
- All 6 touchstone test files green per baseline counts (172/166 with 6 expected `Soap.parse_response` failures preserved).
- `git diff $(git merge-base origin/main HEAD)..HEAD --name-only | grep -v "^sta/"` is empty (zero out-of-scope changes).
- New tests added: `state_code_test.exs` (~10), `client_test.exs` (~25 new), `inbound/worker_test.exs` (~4 new), `outbound/worker_test.exs` (~4 new), `sta_query_controller_test.exs` (~10).
- Touchstone counts updated in PR body to reflect new totals.
- Manual Verification gate satisfied (this Task 0 inventory referenced in PR body).

**Acceptance criteria from rollout plan §Sub-PR 2.B (verbatim):**
- ✅ "Workers no longer string-compare status descriptions" — vacuously true today (none currently). Future-readiness wiring satisfies the spirit.
- ✅ "A_REC works against Bypass mock" — covered in Task 2 + Task 5.
- ✅ "query_protocol parses both single-protocol and batch responses" — covered in Task 4.

---

<a id="out-of-scope"></a>
## Out of scope

Carried verbatim from handoff §"Out of scope":

1. Onda 2.C scope — HTTP 410 protocol-expired auto-renew, HTTP 429 with `Retry-After`, 48h protocol expiry, 3 separate rate limiter buckets.
2. Onda 2.D scope — `/senhaws/senha` password management endpoints.
3. Onda 3 scope — leiautes corpus.
4. Onda 4 scope — identifier mapping in `core/`.
5. Worker.init auto-recovery for upload-side resume.
6. Full `worker_test.exs` for Inbound.Worker — only the minimal `decide_next_action/1` tests are added in Task 3.
7. Cross-process / cross-restart download resume persistence.
8. Admin auth enforcement — `:admin_api` pipeline still has the `# Future: Add admin authentication here` TODO.
9. Real BCB smoke test — deferred to operator post-merge.
10. Telemetry events for the new endpoints — Wave 11 P2 metrics framework will be the home.

**Plus this sub-PR specifically does NOT:**

11. Refactor `:list_available_files` parser to align with `<EstadoAtual>` semantics. Per [STUDY-CONJECTURE] #1, `<SituacaoAtual>` is a different enum and the existing parser is correct as-is. Documenting the divergence in `StateCode` moduledoc is the deliverable.
12. Add new `Types.FileInfo` fields. New `Types.FileQueryResult` is added separately in Task 4 to avoid touching the §8.1 path.
13. Validate `dependencia` content / format. Caller-supplied opaque string per [STUDY-CONJECTURE] #3.
14. Add an `update` action to `StaQueryController`. Read-only endpoints only.

---

<a id="references"></a>
## References

### Plan docs
- **Parent rollout plan:** `sta/docs/plans/2026-04-26-sta-conformance-rollout.md` §Sub-PR 2.B (lines 670-737) and §Pattern: Manual Verification.
- **Onda 2.A core plan:** `sta/docs/plans/2026-04-26-sta-onda2a-resilient-transfer.md`
- **Onda 2.A follow-up #2 plan (template):** `sta/docs/plans/2026-04-26-sta-onda2a-followup-2-ranged-resume.md`
- **Onda 2.A caller-wiring plan:** `sta/docs/plans/2026-04-26-sta-onda2a-followup-caller-wiring.md`

### Handoff docs
- **Resumption handoff:** `sta/docs/handoff/2026-04-27-sta-onda2b-brainstorming-pause-and-onda2a-complete.md` — contains the Task 0 Manual Verification inventory copied verbatim into this plan's §Task 0.

### Manual STA references
- `sta/Manual_STA_Web_Services.pdf` — v1.5 jul/2022.
- §7 Alteração de situação
- §8 Consulta a transferências de arquivos (§8.1, §8.2, §8.3)
- Tabela 1 — Parâmetros §8.2
- Tabela 2 — Parâmetros §8.3
- Tabela 3 — 14 state codes
- Tabela 4 — Parâmetros §8.1

### Onda 2.A merged PRs (chronological — for cross-reference)
| PR | Merge SHA | Title |
|---|---|---|
| #24 | `301503ec` | Onda 1 wire-format + ICP-Brasil chain |
| #30 | `3af17964` | Onda 2.A core (chunked upload + ranged download) |
| #34 | `ed03ce39` | Worker chunked-upload wiring |
| #36 | `100e5154` | Operations.resume_upload/3 |
| #43 | `99e6e57c` | Streaming download + ranged-resume mid-download |
| #46 | `1c10b7fa` | Caller-wiring bundle + OutboundFile schema fix |
| #52 | `fbf4dac2` | upload_attempts retry-cap fix |
| #59 | `e54399ab` | Onda 2.B brainstorming pause handoff |

---

**End of plan.** Implementation begins at Task 1.
