# Handoff — STA Onda 2.D (Full) complete, awaiting push/PR auth

**Data:** 2026-04-27
**Worktree:** `/Users/luizpenha/monetarie/.worktrees/sta-paridade`
**Branch:** `sta/onda2-password-mgmt` (cut from `origin/main` HEAD `78b01df7` post Onda 2.C deploy plumbing merge; will rebase onto current `a2b7753e` at push time if needed)
**Status:** All 4 implementation tasks complete + plan + handoff. 22 new tests / 0 failures. Ready for push/PR after explicit user authorization.

---

## TL;DR

Implemented Onda 2.D per option **(A) Full per parent rollout plan §Sub-PR 2.D**: `Client.password_change/2` + `Client.password_expiry/1` (Manual §9.1 + §9.2) + `PasswordManager` GenServer with notification escalation + **Oban full setup** (deps + migration + supervision tree + 3-queue config) + `PasswordExpiryWorker` daily 06:00 BRT cron. 5 commits, ~660 LOC delta. **22 new tests** across 3 new files + extended client_test.exs; combined 284/278 PASS (vs. 262/256 post-Onda 2.C baseline; the 6 `Soap.parse_response` baseline failures are unchanged). Compile preserves 5 baseline warnings exactly. Zero out-of-scope file changes.

---

## What's in this branch (chronological)

| # | SHA | Subject |
|---|---|---|
| 1 | `eea11464` | Onda 2.D (Full) plan + Task 0 Manual Verification §9 |
| 2 | `55fbb595` | Client.password_change/2 + password_expiry/0 per Manual §9 (Task 1) |
| 3 | `d4e7f6c2` | PasswordManager GenServer + decision logic (Task 2) |
| 4 | `7fc27720` | Oban full setup — deps + migration + supervision (Task 3) |
| 5 | `6177e384` | PasswordExpiryWorker + daily 06:00 BRT cron (Task 4) |
| 6 | (this) | Onda 2.D (Full) end-of-session handoff |

---

## Acceptance criteria — all satisfied

From rollout plan §Sub-PR 2.D (verbatim):

- ✅ **"Senha can be changed via API call"** — Task 1 implements `Client.password_change/2` per Manual §9.1.1 (PUT /senha + Content-Type application/xml + canonical XML body + 204 success).
- ✅ **"Cron emits a notification when expiry ≤ 7 days"** — Task 2 + Task 4: `PasswordExpiryWorker` daily cron triggers `PasswordManager.check_expiry/0` → `notify_decision/3` routes ≤7 days to `Notifications.notify(:warning, ...)` and ≤2 days to `Notifications.notify(:critical, ...)`.
- ✅ **"No surprise downtime from senha expiring silently"** — daily cron alert + 2-tier escalation gives operator at minimum 7 days warning + escalating urgency at 2 days.

Plus (A) full extras:
- Oban full setup (deps + migration + supervision + 3-queue config) lands as part of this PR.
- Manual Verification gate satisfied (9 [VERIFIED §X.Y] + 5 [STUDY-CONJECTURE] + 2 [NEEDS-OPERATOR-CONFIRM]).
- No out-of-scope changes (`git diff --diff-filter=A origin/main..HEAD --name-only` is exclusively under `sta/*`).
- `application_test.exs` 3/3 — supervisor boots cleanly with new children (PasswordManager + Oban).

---

## Test surface delta

### Touchstone files (post-Onda 2.C baseline)

| File | Pre-2.D | Post-2.D | Δ | Notes |
|---|---|---|---|---|
| `client_test.exs` | 94 / 6 | **101 / 6** | +7 | 4 password_change + 3 password_expiry. Baseline 6 `Soap.parse_response` failures preserved. |
| `operations_test.exs` | 58 / 0 | **58 / 0** | 0 | unchanged |
| `outbound_file_test.exs` | 7 / 0 | **7 / 0** | 0 | unchanged |
| `application_test.exs` | 3 / 0 | **3 / 0** | 0 | Supervisor still boots cleanly with new children |
| `outbound_test.exs` | 38 / 0 | **38 / 0** | 0 | unchanged |
| `outbound_files_controller_test.exs` | 7 / 0 | **7 / 0** | 0 | unchanged |
| **Touchstone subtotal** | **207 / 6** | **214 / 6** | **+7** | |

### Onda 2 prior new test files

| File | Tests | Failures | Source |
|---|---|---|---|
| `state_code_test.exs` | 14 / 0 | 0 | 2.B |
| `inbound/worker_test.exs` | 3 / 0 | 0 | 2.B |
| `outbound/worker_test.exs` | 8 / 0 | 0 | 2.B + 2.C |
| `sta_query_controller_test.exs` | 16 / 0 | 0 | 2.B |
| `rate_limiter_three_bucket_test.exs` | 14 / 0 | 0 | 2.C |

### NEW test files (this PR — Onda 2.D)

| File | Tests | Failures | Source |
|---|---|---|---|
| `password_manager_test.exs` | **10 / 0** | 0 | Task 2 (7 notify_decision + 3 validate_thresholds!) |
| `password_expiry_worker_test.exs` | **5 / 0** | 0 | Task 4 (perform/1 dispatch on success/retry/cancel/defensive) |

### Combined batch (13 files)

```bash
mix test \
  test/sta_connector/sta/client_test.exs \
  test/sta_connector/sta/operations_test.exs \
  test/sta_connector/files/outbound_file_test.exs \
  test/sta_connector/application_test.exs \
  test/sta_connector/outbound_test.exs \
  test/sta_connector_web/controllers/api/admin/outbound_files_controller_test.exs \
  test/sta_connector/sta/state_code_test.exs \
  test/sta_connector/inbound/worker_test.exs \
  test/sta_connector/outbound/worker_test.exs \
  test/sta_connector_web/controllers/api/admin/sta_query_controller_test.exs \
  test/sta_connector/sta/rate_limiter_three_bucket_test.exs \
  test/sta_connector/sta/password_manager_test.exs \
  test/sta_connector/workers/password_expiry_worker_test.exs

# Result: 284 tests, 6 failures (Soap baseline preserved)
# Time:   ~2.6s (2.3s async + 0.3s sync)
```

### Compile

5 baseline warnings preserved across all 4 task commits. No new warnings introduced.

---

## Manual Verification gate — execution log

Inventory inventoried in plan §Task 0 + executed during implementation:

- **9 `[VERIFIED §X.Y]`** all confirmed via `/tmp/manual_sta.txt` extract (line citations preserved in commit messages).
- **5 `[STUDY-CONJECTURE]`** items decisions:
  - #1 GET /senha/vencimento sends no Content-Type (convention from §6/§8) — implemented as such; tests assert refute.
  - #2 Notify thresholds 7/2 days (warn/critical) — implemented as defaults; configurable via `STA_PWD_EXPIRY_WARN_DAYS` + `STA_PWD_EXPIRY_ERROR_DAYS` env vars.
  - #3 HTTP Basic auth uses `STA_BCB_USERNAME`/`STA_BCB_PASSWORD` (same as STA endpoints); body's `<Senha>` field is also the current pass — Manual is explicit.
  - #4 Auto-rotation of `monetarie/{env}/sta/bcb_password` Secrets Manager value is OUT of scope. PasswordManager moduledoc + handoff §Operator runbook documents the manual `aws secretsmanager update-secret` + `aws ecs update-service --force-new-deployment` step.
  - #5 `:password_expiry` routed through `:queries` rate-limit bucket implicitly (passes through Client GenServer, not specifically gated).
- **2 `[NEEDS-OPERATOR-CONFIRM]`** items addressed:
  - #1 Cron `0 9 * * *` UTC = 06:00 BRT — Brasília UTC-3 with no DST since 2019. Documented inline in config comment.
  - #2 Notify thresholds 7/2 — env-tunable. Default chosen conservatively.

### Misdiagnoses caught

None. Manual §9 is brief (24 pages of original doc) and unambiguous. Pre-flight surfaced 3 deltas vs the canonical plan that I documented in plan §Pre-flight:

1. Finch pools for `www9.bcb.gov.br` + `www3.bcb.gov.br` ALREADY wired in Onda 1 with mTLS via ICP-Brasil chain (plan step 6 already done).
2. `Notifications.notify/4` ALREADY exists (no need for new helper).
3. Severity atom is `:warning` (not `:warn`) and `:critical` (not `:error`) per existing `Notifications` module — adjusted at decision-table-design time.

---

## Public API additions (cheat sheet)

### `StaConnector.Sta.Client`

```elixir
@spec password_change(
        %{required(:current_password) => String.t(), required(:new_password) => String.t()},
        keyword()
      ) :: {:ok, :ok} | {:error, Error.t()}
def password_change(params, opts \\ [])

@spec password_expiry(keyword()) ::
        {:ok, %{days_remaining: non_neg_integer()}} | {:error, Error.t()}
def password_expiry(opts \\ [])
```

Routed to `www9/www3.bcb.gov.br/senhaws` host based on environment via new `get_endpoint_url(config, :senha)` and `endpoint_kind_for/1` dispatcher. Tests use `:sta_password_base_url_override` env (parallel to `:sta_base_url_override`) for isolated Bypass routing.

### `StaConnector.Sta.PasswordManager`

```elixir
@spec check_expiry(keyword()) ::
        {:ok, %{days_remaining: non_neg_integer(), notified: :none | :warning | :critical}}
        | {:error, term()}
def check_expiry(opts \\ [])

# Pure helpers (exposed for testing):
@spec notify_decision(integer(), non_neg_integer(), non_neg_integer()) ::
        :none | :warning | :critical

@spec validate_thresholds!(non_neg_integer(), non_neg_integer()) :: :ok
```

GenServer in supervision tree (after Client, before Oban). Reads thresholds from env vars at boot; raises if invalid.

### `StaConnector.Workers.PasswordExpiryWorker`

```elixir
use Oban.Worker, queue: :audit, max_attempts: 5

@impl Oban.Worker
def perform(%Oban.Job{args: _}) :: :ok | {:error, String.t()} | {:cancel, String.t()}
```

Cron at `0 9 * * *` UTC (= 06:00 BRT daily). Routes PasswordManager errors to Oban retry/cancel atoms.

### Config additions

```elixir
config :sta_connector, Oban,
  repo: StaConnector.Repo,
  queues: [default: 10, audit: 5, regulatory: 5],
  plugins: [
    {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
    {Oban.Plugins.Cron,
     crontab: [{"0 9 * * *", StaConnector.Workers.PasswordExpiryWorker}]},
    Oban.Plugins.Lifeline
  ]
```

### Env vars (NEW — production opt-in via task def)

| Var | Default | Override impact |
|---|---|---|
| `STA_PWD_EXPIRY_WARN_DAYS` | `7` | Lower → less warning runway; higher → earlier alerts |
| `STA_PWD_EXPIRY_ERROR_DAYS` | `2` | Lower → narrower critical window; must be ≤ warn_days |

---

## Files touched (8 changed + 6 created — all in `sta/*`)

```
sta/backend/config/config.exs                                              (modified, +13)
sta/backend/lib/sta_connector/application.ex                               (modified, +11)
sta/backend/lib/sta_connector/sta/client.ex                                (modified, +90)
sta/backend/lib/sta_connector/sta/client_behaviour.ex                      (modified, +40)
sta/backend/lib/sta_connector/sta/password_manager.ex                      (NEW, ~217 LOC)
sta/backend/lib/sta_connector/workers/password_expiry_worker.ex            (NEW, ~50 LOC)
sta/backend/mix.exs                                                        (modified, +5)
sta/backend/mix.lock                                                       (modified, +1)
sta/backend/priv/repo/migrations/20260427153455_create_oban_jobs.exs       (NEW, ~10 LOC)
sta/backend/test/sta_connector/sta/client_test.exs                         (modified, +180)
sta/backend/test/sta_connector/sta/password_manager_test.exs               (NEW, ~80 LOC)
sta/backend/test/sta_connector/workers/password_expiry_worker_test.exs     (NEW, ~80 LOC)
sta/docs/plans/2026-04-27-sta-onda2d-password-mgmt-full.md                 (NEW, plan)
sta/docs/handoff/2026-04-27-sta-onda2d-full-complete.md                    (NEW, this handoff)
```

---

## Out of scope (deferred)

1. **Auto-rotation** of `monetarie/{env}/sta/bcb_password` Secrets Manager value after `password_change` success. Operator must update manually + force-new-deployment. Documented in `PasswordManager` moduledoc + this handoff §Operator runbook.
2. **Admin endpoints** `POST /api/admin/sta/password/change` + `GET /api/admin/sta/password/expiry`. Cron handles scheduled case; ad-hoc operator runs `bin/sta_connector eval "Oban.insert(StaConnector.Workers.PasswordExpiryWorker.new(%{}))"` or `eval "StaConnector.Sta.PasswordManager.check_expiry()"` directly.
3. **2.C-2 follow-up `protocol_expires_at`** — still deferred per Onda 2.C handoff.
4. **G5 `:admin_api` RBAC** — separate hygiene PR (still blocks prod deployment).
5. **Onda 3 leiautes corpus** — independent, next priority.
6. **Onda 4 identifier mapping in `core/`** — independent + cross-cabin.
7. **Real BCB smoke test** — operator post-merge (gated on G6 BCB credentials populated).
8. **Telemetry for password mgmt** — Wave 11 P2 framework will compose Prometheus collectors from existing emission events.

---

## Operator runbook (post-deploy operational lifecycle)

Once STA cabin is deployed (per `sta/docs/handoff/2026-04-27-sta-paridade-EOS-COMPREHENSIVE.md` §6):

### Initial setup (1×)

1. `terraform apply` — provisions Aurora + ECS + 3 secret shells + Oban migration row.
2. Populate 3 secret values (BCB user/pass + admin pwd) via `aws secretsmanager put-secret-value`.
3. Bump `desired_count=1` via `aws ecs update-service`.
4. Run Aurora migrations:
   ```bash
   aws ecs run-task --cluster monetarie-greenfield-homolog \
     --task-definition monetarie-sta-api-homolog \
     --launch-type FARGATE --network-configuration ... \
     --overrides '{"containerOverrides":[{"name":"sta-api","command":["bin/sta_connector","eval","StaConnector.Release.migrate()"]}]}' \
     --profile monetarie --region sa-east-1
   ```
   Will create the 7 STA tables + 1 Oban schema (`oban_jobs` + indexes).

### Daily ongoing (zero operator intervention)

- Cron fires at 06:00 BRT daily.
- `PasswordExpiryWorker` calls `PasswordManager.check_expiry/0`.
- If days_remaining > 7: silent.
- If 2 < days_remaining ≤ 7: `Notifications.notify(:warning, ...)` — landed in PubSub channel `notifications:events` + persisted via Notifications context. Frontend admin sees warning indicator.
- If days_remaining ≤ 2: `Notifications.notify(:critical, ...)` — escalated severity.

### Operator response when alert fires

1. Run `Client.password_change/2` from console:
   ```bash
   aws ecs run-task --cluster monetarie-greenfield-homolog \
     --task-definition monetarie-sta-api-homolog \
     --launch-type FARGATE --network-configuration ... \
     --overrides '{"containerOverrides":[{"name":"sta-api","command":["bin/sta_connector","eval","StaConnector.Sta.Client.password_change(%{current_password: System.get_env(\"STA_BCB_PASSWORD\"), new_password: \"<NEW-PASS>\"})"]}]}' \
     --profile monetarie --region sa-east-1
   ```
2. Update Secrets Manager:
   ```bash
   aws secretsmanager update-secret \
     --secret-id monetarie/homolog/sta/bcb_password \
     --secret-string '<NEW-PASS>' \
     --profile monetarie --region sa-east-1
   ```
3. Force new deployment so new secret is read at boot:
   ```bash
   aws ecs update-service \
     --cluster monetarie-greenfield-homolog \
     --service sta-api \
     --force-new-deployment \
     --profile monetarie --region sa-east-1
   ```
4. (Optional) Trigger immediate `check_expiry` to verify new password took effect:
   ```bash
   aws ecs run-task --cluster monetarie-greenfield-homolog \
     --task-definition monetarie-sta-api-homolog \
     --launch-type FARGATE --network-configuration ... \
     --overrides '{"containerOverrides":[{"name":"sta-api","command":["bin/sta_connector","eval","StaConnector.Sta.PasswordManager.check_expiry()"]}]}' \
     --profile monetarie --region sa-east-1
   ```
   Should return `{:ok, %{days_remaining: ~90, notified: :none}}` (BCB resets to fresh expiry on successful change).

---

## Resumption checklist

```bash
cd /Users/luizpenha/monetarie/.worktrees/sta-paridade
pwd                                                  # → /Users/luizpenha/monetarie/.worktrees/sta-paridade
git fetch origin --prune
git status                                           # working tree clean

# After this PR merges:
git checkout --detach origin/main
git branch -d sta/onda2-password-mgmt                # cleanup local
git log --oneline -1                                 # → merge commit

cd sta/backend
mix deps.get                                         # picks up :oban dep on first checkout
mix compile 2>&1 | grep -c "^[[:space:]]*warning:"   # → 5

# Touchstone batch (13 files):
mix test \
  test/sta_connector/sta/client_test.exs \
  test/sta_connector/sta/operations_test.exs \
  test/sta_connector/files/outbound_file_test.exs \
  test/sta_connector/application_test.exs \
  test/sta_connector/outbound_test.exs \
  test/sta_connector_web/controllers/api/admin/outbound_files_controller_test.exs \
  test/sta_connector/sta/state_code_test.exs \
  test/sta_connector/inbound/worker_test.exs \
  test/sta_connector/outbound/worker_test.exs \
  test/sta_connector_web/controllers/api/admin/sta_query_controller_test.exs \
  test/sta_connector/sta/rate_limiter_three_bucket_test.exs \
  test/sta_connector/sta/password_manager_test.exs \
  test/sta_connector/workers/password_expiry_worker_test.exs
# Expected: 284 tests, 6 failures (Soap baseline preserved)
```

---

## Decision tree for next session

### Suggested next sub-PR

**Onda 3.A leiautes corpus + XSD validator** — `sta/onda3a-leiautes-corpus-validator`. ~1-2 dias / ~400 LOC + 2-3 MB corpus. Independent of all prior work. Destrava `ACCS001` / `ACCS010` builders for Compliance traffic via STA. Per parent rollout plan §Sub-PR 3.A (lines 809-836).

Alternatives:
- **G5 `:admin_api` RBAC hygiene** — ~1 dia / ~200 LOC. Closes the only remaining production blocker for STA. Required before prod deploy.
- **2.C-2 follow-up `protocol_expires_at`** — only worth doing once an admin-UI consumer exists.
- **Onda 4 identifier mapping** — touches `core/` (cross-cabin); coordinate with Core team.

---

## References

### This PR
- **This handoff:** `sta/docs/handoff/2026-04-27-sta-onda2d-full-complete.md`
- **This plan:** `sta/docs/plans/2026-04-27-sta-onda2d-password-mgmt-full.md`
- **Branch:** `sta/onda2-password-mgmt`
- **Base SHA:** `78b01df7` (origin/main HEAD at session start, post Onda 2.C #74 merge)

### Predecessor handoff
- `sta/docs/handoff/2026-04-27-sta-paridade-EOS-COMPREHENSIVE.md` — Comprehensive 15-section EOS handoff covering Ondas 1+2.A+2.B+2.C deploy readiness.

### Plans
- **Parent rollout plan:** `sta/docs/plans/2026-04-26-sta-conformance-rollout.md` §Sub-PR 2.D (lines 774-799).
- **Onda 2.B/2.C plans (templates):** same dir.
- **PIX PW1.3.2 Oban template** (cross-cabin reference): `pix/backend/apps/shared/priv/repo/migrations/20260426204500_create_oban_jobs.exs` + `pix/backend/config/config.exs` Oban block.

### Manual STA references
- `sta/Manual_STA_Web_Services.pdf` — v1.5 jul/2022.
- §9 intro (line 1092 of `/tmp/manual_sta.txt` extract).
- §9.1 Alteração de senha (line 1105) + §9.1.1 Especificação (line 1108).
- §9.2 Consulta vencimento de senha (line 1148) + §9.2.1 Especificação (line 1152).

### Cabin precedent for Oban
- PIX PW1.3.2 (PR #47) — first cabin to wire Oban (audit + default + med queues; status_inquiry every 1min, timer_enforcer every 5min, cautelar_worker manual-only).

### Onda 1 + 2.A + 2.B + 2.C + 2.D PRs (cross-reference)
| PR | SHA | Title |
|---|---|---|
| #24 | `301503ec` | Onda 1 — wire-format + ICP-Brasil chain (also wired Finch pools for senhaws hosts) |
| #62 | `67924840` | Onda 2.B status & queries (C full+admin) |
| #66 | `a55ad325` | Onda 2.C rate & lifecycle (B trim) |
| #74 | `78b01df7` | Onda 2.C deploy plumbing — CI workflow |

---

## Awaiting auth

Per Hard rule #6, no `git push` or `gh pr create` without explicit user authorization. When ready:

```bash
cd /Users/luizpenha/monetarie/.worktrees/sta-paridade
git push -u origin sta/onda2-password-mgmt
gh pr create --title "feat(sta): Onda 2.D password mgmt (Full)" --body "..."
```

If origin/main has advanced (PRs #75/#76/#77 already merged from parallel cabins), GitHub merge UI may show 8 incidental files from those PRs in the diff — they are NOT changes from this branch (`git diff --diff-filter=A origin/main..HEAD --name-only` shows all our adds are exclusively under `sta/*`).

---

**End of handoff.** All Onda 2.D (Full) deliverables complete; awaiting push + PR authorization.
