# Onda 2.A Caller-Wiring Bundle (download `:expected_size` + admin `resume_upload` endpoint)

> **For Claude:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task.

**Branch:** `sta/onda2a-followup-callers-wiring` (this worktree, cut from `origin/main` HEAD `99e6e57c` post follow-up #2 merge)
**PR target:** `main`
**Estimated effort:** 1-2h actual · LOC delta estimate: +400 (mostly new controller + tests)
**Parent rollout plan:** `sta/docs/plans/2026-04-26-sta-conformance-rollout.md`
**Prior follow-ups:** PR #34 (worker chunked-upload wiring, MERGED), PR #36 (`Operations.resume_upload/3`, MERGED), PR #43 (ranged-resume mid-download, MERGED)

---

## Pattern: Manual Verification — gate skipped

This sub-PR introduces **no new wire-format / endpoint / response-code / field-name / numeric-threshold claims**. Both changes are **pure internal wiring** of existing wire-formatted APIs:
- Inbound.Worker switches its existing download path from `Client.download_file/2` to `Operations.download_with_retry/2` and threads `:expected_size` opt — zero new BCB-facing claims; the Range-resume wire format was pinned in PR #43.
- New admin endpoint `POST /api/admin/outbound_files/:id/resume_upload` is internal control-plane (admin-auth pipeline, JSON request/response) — no BCB-facing claims; the underlying `Operations.resume_upload/3` had its wire format pinned in PR #36.

The Pattern: Manual Verification gate explicitly exempts this case (mirrors PR #34 + PR #36 which were also pure-wiring follow-ups). Documented here in lieu of a Task 0 inventory.

---

## What changes

### 1. `Inbound.Worker` switches download path to `Operations.download_with_retry/2`

**Files:** `sta/backend/lib/sta_connector/inbound/worker.ex`

Three call sites currently call `Client.download_file/2` directly:
- `download_file/1` (private, line 178)
- `retry_download/1` (private, line 387)
- `retry_parse/1` (private, line 406)

Each gets replaced with `Operations.download_with_retry(protocol.protocol_number, opts)` where:
- `opts[:expected_size] = protocol.file_size` IF `is_integer(protocol.file_size) and protocol.file_size > 0`. Otherwise `expected_size` is omitted (Operations falls through to existing single-shot path — zero behaviour change for files where size is unknown).
- `opts[:verify_hash] = false` to avoid double-hash-verification (Worker has its own `verify_hash/2` step that runs separately).
- `opts[:max_retries] = 0` so Operations does NOT auto-retry; failures propagate to Worker's existing DB-backed `RetryEntry` retry mechanism. **This preserves the current retry semantics** — Worker stays the source of retry truth; Operations is just the streaming/resume primitive.
  - Wait: `max_retries: 0` would short-circuit the resume state machine on the very first attempt-step. That defeats the purpose. Revised: `max_retries: 3` (Operations default) for in-memory resume on the same call, but the BCB-side will absorb 1-3 in-memory attempts before Worker decides to DB-queue a retry. This is two-layer retry (in-memory fast + DB-backed slow) and is **the intended layering** for resilient transfer.

**Test strategy:** Inbound.Worker has no `worker_test.exs` in this codebase. Existing `poller_test.exs` and `supervisor_test.exs` exercise the worker indirectly. We rely on:
1. Compile clean
2. Existing supervisor/poller tests still pass (regression)
3. The Operations.download_with_retry path that the worker now hits has its OWN 58 tests (PR #43)

This is a known gap. Documented; not addressed here. If a regression surfaces post-merge, follow-up PR adds `worker_test.exs`.

### 2. New admin endpoint `POST /api/admin/outbound_files/:id/resume_upload`

**Files:**
- NEW `sta/backend/lib/sta_connector_web/controllers/api/admin/outbound_files_controller.ex`
- NEW `sta/backend/test/sta_connector_web/controllers/api/admin/outbound_files_controller_test.exs`
- MODIFIED `sta/backend/lib/sta_connector_web/router.ex` — add route under existing `scope "/api/admin"` block

**Why a new controller (not extending `Api.Admin.FilesController`):** the existing `FilesController` operates on `Files.ProcessedProtocol` (inbound). `OutboundFile` is a separate schema. Mixing them in one controller would conflate concerns; the route prefix `/outbound_files/...` makes the schema distinction explicit at the URL level.

**Action `:resume_upload` flow:**
1. Look up `OutboundFile` by `id` via `Outbound.get_file/1`. 404 if not found.
2. Validate state guards:
   - `status == "uploading"` (pre-condition for resume)
   - `protocol_number != nil`
   - `bytes_uploaded > 0`
   422 if any guard fails — body explains which guard failed.
3. Read content from `file.file_path` via `File.read/1`. 422 if file is missing on disk.
4. Call `Operations.resume_upload(file.protocol_number, content, opts)`.
   - `opts[:on_chunk_progress]` updates `OutboundFile.bytes_uploaded` per chunk (mirrors PR #34's wiring contract).
5. On `{:ok, _}`: update `OutboundFile.status` to `"completed"`, return 200 with serialized file.
6. On `{:error, reason}`: leave file in `"uploading"` (operator can re-issue), return 502 with reason.

**Authorization:** wired through the existing `scope "/api/admin"` pipeline. The codebase's admin auth is currently a TODO (router.ex:11 says "Future: Add admin authentication here"); this controller does NOT introduce new auth. When the admin auth lands, it'll cover this route automatically.

**Test surface:** 5 tests in `outbound_files_controller_test.exs`:
1. file not found → 404
2. file in wrong state (status="pending") → 422 + state-guard error message
3. file with `bytes_uploaded == 0` → 422 (would be a fresh upload, not a resume)
4. happy path → mock Operations.resume_upload (via Mox or by stubbing the public function) returns ok; expect 200 and OutboundFile.status updated to "completed"
5. error path → mock Operations.resume_upload returns error; expect 502 with error details

For test 4+5, since `Operations` doesn't have a Mox-friendly behavior, we use a thin pluggable interface:
- Define `@operations_module Application.compile_env(:sta_connector, :operations_module, StaConnector.Sta.Operations)` in the new controller
- In test, `Application.put_env(:sta_connector, :operations_module, MyTestStub)`
- This is a tiny test-time injection seam; production behavior is unchanged.

---

## Out of scope (explicit cuts)

1. **Worker.init auto-recovery for upload-side resume.** The handoff considered this for `resume_upload/3`; deferred per Onda 2.A core handoff F1 risk note ("explicit operator says resume is safer than worker.init auto-recovery"). Could become a future follow-up if production demands automatic post-crash recovery.
2. **`worker_test.exs` for Inbound.Worker.** Pre-existing gap; not introduced by this PR. Could be a follow-up if regressions surface.
3. **Persistence of download resume state across worker crashes.** Onda 2.A follow-up #2 left this out-of-scope; remains out-of-scope here.
4. **Admin auth.** The `/api/admin/*` pipeline still has the "Future: Add admin authentication" TODO. Out of scope here.

---

## Tasks

### Task 1 — `Inbound.Worker` threads `:expected_size`

**Files:**
- Modify: `sta/backend/lib/sta_connector/inbound/worker.ex` (3 call-site swaps + 1 new private `build_download_opts/1`)

**Steps:**
1. Add `alias StaConnector.Sta.Operations`.
2. Replace 3 occurrences of `Client.download_file(protocol.protocol_number)` with `Operations.download_with_retry(protocol.protocol_number, build_download_opts(protocol))`.
3. Add private `build_download_opts/1` that returns `[verify_hash: false] ++ maybe_expected_size(protocol)` where `maybe_expected_size` is `[expected_size: file_size]` if non-nil, `[]` otherwise.
4. Run touchstones (compile clean, existing tests pass).
5. Commit `feat(sta): Inbound.Worker threads :expected_size to enable mid-download resume`.

**Estimate:** 15-25 min.

### Task 2 — Admin endpoint `POST /api/admin/outbound_files/:id/resume_upload`

**Files:**
- Create: `sta/backend/lib/sta_connector_web/controllers/api/admin/outbound_files_controller.ex`
- Create: `sta/backend/test/sta_connector_web/controllers/api/admin/outbound_files_controller_test.exs`
- Modify: `sta/backend/lib/sta_connector_web/router.ex` (add 1 route)

**Steps (TDD):**
1. RED: write 5 controller tests covering not-found / state-guards (status, protocol_number, bytes_uploaded) / happy / error paths.
2. GREEN: implement controller action; add Operations injection seam; add router entry.
3. Touchstones.
4. Commit `feat(sta): admin endpoint POST /api/admin/outbound_files/:id/resume_upload`.

**Estimate:** 45-60 min.

### Task 3 — Touchstones + handoff doc + final commit + push/PR auth

**Steps:**
1. Run all 5 touchstones. Confirm:
   - 5 baseline warnings on `mix compile` (preserved)
   - `client_test.exs` / `operations_test.exs` / `outbound_file_test.exs` / `application_test.exs` / `outbound_test set_bytes_uploaded`: unchanged from main baseline
   - New `outbound_files_controller_test.exs`: 5 new tests passing
2. Guard rail: `git diff $(git merge-base origin/main HEAD)..HEAD --name-only | grep -v "^sta/"` returns empty.
3. Write handoff doc summarizing wiring + listing remaining out-of-scope items.
4. Commit `docs(sta): handoff for Onda 2.A caller-wiring bundle PR`.
5. Stop. Ask user explicit auth before push + PR.

---

## Acceptance

- 4 commits land on branch (1 docs+plan, 1 feat inbound, 1 feat admin endpoint, 1 docs handoff)
- Touchstones at PR creation:
  - Compile: 5 baseline warnings (unchanged)
  - All existing test files: unchanged counts/failures
  - New `outbound_files_controller_test.exs`: 5/5 passing
- `mix format --check-formatted` clean for touched files only
- Guard rail: zero touches outside `sta/*`
- After this merges, **Onda 2.A's two unwired public APIs are now wired**: `Operations.download_with_retry(:expected_size: ...)` is reachable via Inbound.Worker; `Operations.resume_upload/3` is reachable via the admin endpoint.

---

## References

- **Onda 2.A core (PR #30, MERGED):** https://github.com/FluxiqBR/monetarie/pull/30
- **Follow-up #1 — Worker chunked-upload wiring (PR #34, MERGED):** https://github.com/FluxiqBR/monetarie/pull/34
- **Follow-up #3 — `Operations.resume_upload/3` (PR #36, MERGED):** https://github.com/FluxiqBR/monetarie/pull/36
- **Follow-up #2 — Ranged-resume mid-download (PR #43, MERGED):** https://github.com/FluxiqBR/monetarie/pull/43
- **Follow-up #2 plan + handoff:** `sta/docs/plans/2026-04-26-sta-onda2a-followup-2-ranged-resume.md` + `sta/docs/handoff/2026-04-26-sta-onda2a-followup-2-ranged-resume.md`
- **Operations module:** `sta/backend/lib/sta_connector/sta/operations.ex`
- **Outbound module:** `sta/backend/lib/sta_connector/outbound.ex` + `sta/backend/lib/sta_connector/files/outbound_file.ex`
- **Existing admin controller pattern:** `sta/backend/lib/sta_connector_web/controllers/api/admin/files_controller.ex`

---

**Plan version:** 1.0 · **Author:** Claude (under direction of Luiz Marcelo) · **Date:** 2026-04-26
