# W1-F2: Telas IB/merchant P0+P1 (QR real, cobrança paga, copia-e-cola, TED agendada v2, limites, TEF, trilho único PIX Automático, v1 /pix) - Plano de implementação

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (pipeline implementador -> revisor de spec -> revisor de qualidade por task). Toda task obedece aos gates G1/G2/G3 do plano mestre `docs/plans/2026-07-18-aceleracao-wave1-plano-mestre.md`. Trabalhar em WORKTREE isolada da frente F2. Ambientes HML/PRD: SOMENTE leitura para subagentes; escrita e deploy são do orquestrador.

**Goal:** Fechar os P0 e P1 das telas do Internet Banking (`core/apps/banking`) e do Merchant Portal (`core/apps/merchant`): QR de recebimento pelo motor REAL da cabine (matar o stub EMV sem CRC), copia-e-cola funcional, cobrança paga visível ponta a ponta, TED agendada no v2/partner, limites reais visíveis, TEF real, trilho único do PIX Automático e fim das rotas v1 `/pix/in|out` que publicavam para a DLQ.

**Architecture:** Os motores reais JÁ existem (validados vivos em 17/07): o QR nasce na cabine via NATS req/reply (`monetarie.pix.qrcode.static|dynamic`, mesmo caminho da Partner API), o "pago" chega pelo consumer `ChargePaid` já deployado, a TED agendada usa `UseCases.Spb.ScheduledTed` (item A), os limites vêm de `TransactionLimits.resolve/2`, o TEF usa `OutboundOrchestrator` com `type: "tef"`. O trabalho é LIGAR as telas nesses motores com contrato provado, sem inventar trilho novo.

**Tech Stack:** Elixir/Phoenix (core/backend), Vue 3 + PrimeVue + Pinia (core/apps/banking e core/apps/merchant), vitest, NATS req/reply Core -> cabine PIX.

---

## Contratos provados (G1 da frente, evidência arquivo:linha)

Toda task abaixo referencia estes fatos. Linhas conferidas no código REAL em 2026-07-18 (as linhas do relatório de 17/07 driftaram em alguns pontos; as daqui são as atuais).

### C1. O stub EMV do v2 e o motor real da cabine

- Stub: `core/backend/lib/monetarie_web/controllers/v2/pix_controller.ex:758-795` (`generate_qrcode`). Linha 774: `# TODO: Monetarie.Pix.BRCode module not yet implemented`; linha 776 monta EMV por interpolação TERMINANDO em `"6304"` sem os 4 hex do CRC; linha 778 `qrcode_base64 = nil`. EMV inválido, nenhum app paga.
- Motor real (adapter Core): `core/backend/lib/monetarie/use_cases/pix/gateway/qr_code.ex` - `generate_static/1` (subject `monetarie.pix.qrcode.static`, amount OPCIONAL em REAIS string), `generate_dynamic/1` (subject `monetarie.pix.qrcode.dynamic`, amount OBRIGATÓRIO em reais string, `expires_in` segundos), `generate_cobv/1`.
- Cabine: `pix/backend/apps/spi_service/lib/spi_service/nats/core_gateway_handler.ex:277-311` (dispatch static/dynamic). Resposta serializada só com CHAVES STRING: `{:ok, %{"id", "txid", "payload", "access_token", "location_url", "revision", "expires_at", "due_date"}}` ou `{:error, "static_failed: ..."}`. Embrulho do Gateway achatado por `normalize_cobv/1` (partner `pix_controller.ex:1647-1651`): sucesso `{:ok, {:ok, m}}` ou `{:ok, m}`; erro da cabine `{:ok, {:error, reason}}`; transporte `{:error, reason}`.
- Consumidor de referência (a Partner API, VALIDADA VIVA em HML 17/07 com CRC MATCH, PoIM 11/12): `core/backend/lib/monetarie_web/controllers/partner_v1/pix_controller.ex:1422-1473` (`create_charge` dinâmico), `:1519-1561` (`create_static_qrcode`), espelhos `persist_charge_mirror/7` (`:1697`) e `persist_static_mirror/7` (`:1728`), conversão centavos -> reais string `reais_string/1` (`:1762`), `sanitize_brcode_field/2` (`:2136`, upcase + só `[A-Z0-9 ]`, name<=25 city<=15), imagem `encode_qrcode/1` (`:2128`) via `Monetarie.UseCases.Pix.BRCode.Encoder.to_base64/2` (`core/backend/lib/monetarie/use_cases/pix/brcode/encoder.ex:55`, devolve `"data:image/png;base64,..."`).
- Forma do stub de teste que espelha a cabine: `core/backend/test/monetarie_web/controllers/partner_v1/pix_controller_test.exs:182-225` (`StubQrGateway`), seam `Application.get_env(:monetarie, :partner_qr_gateway, QrCode)` (`pix_controller.ex:1756-1758`).

### C2. Espelho `qrcodes` e o "pago" ponta a ponta

- Schema: `core/backend/lib/monetarie/schemas/pix/qrcode.ex` - tabela `qrcodes`, PK binary_id, `amount` em BASE UNITS (subcentavos, `:38-40` range 100..10_000_000_000, opcional para estático), obrigatórios `entity_id account_id type pix_key pix_key_type tx_id payload merchant_name merchant_city`, `status` em `~w(active expired used cancelled paid)` (`:77`), campos `paid_at`, `payment_end_to_end_id`, `used_at`, `location_url`, `expires_at`, `external_id`. `tx_id` alfanumérico max 35, unique.
- Evento vivo da cabine (publisher): `pix/backend/apps/settlement_service/lib/settlement_service/qr_codes.ex:439-451` - `link_payment/2` publica `monetarie.settlement.qrcode.paid` com payload `%{txid, event: "pix.charge.paid", payment_end_to_end_id, paid_at (ISO8601)}`.
- Consumer no Core (JÁ DEPLOYADO, P0-3 de 17/07): `core/backend/lib/monetarie/infra/nats/consumers/pix_consumer.ex:147-149` -> `Monetarie.UseCases.Pix.ChargePaid.handle/1` (`core/backend/lib/monetarie/use_cases/pix/charge_paid.ex`). Resolve o espelho POR `tx_id` (`lookup_mirror/1`, `:65-67`), exige crédito liquidado, marca `status: "paid"` só de `active/used` (`mark_paid/3`, `:92-102`) e dispara webhook `pix.charge.paid`. **Sem espelho no Core = ack e nada acontece (`:47-53`)**: é por isso que o QR da tela PRECISA persistir espelho com o `txid` da cabine.
- Defeito do serializer: `core/backend/lib/monetarie_web/controllers/v2/merchant_portal_controller.ex:1072` `defp qrcode_status("paid"), do: "used"` (o lojista nunca vê "pago"); `serialize_qrcode/2` (`:773-787`) não expõe `paidAt` nem `payment_end_to_end_id`. O partner JÁ expõe cru: `render_charge/1` em `partner_v1/pix_controller.ex:2066-2080` (`status` cru + `paidAt`).
- 501 da criação: `merchant_portal_controller.ex:171-176` (`create_pix_charge`), rotas `router.ex:3331` (`POST /pix/charges`) e `:3337` (`POST /qrcodes`). Teste existente que TRAVA o 501 (vai mudar): `test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs` teste "does not simulate unsupported PIX charge creation".

### C3. Frontends (QR e copia-e-cola)

- IB gerar QR: `core/apps/banking/src/views/pix/PixReceiveView.vue:47-48` chama `pix.generateQrCode(Math.round(amount*100), description)` (CENTAVOS) e lê SÓ `result.qr_image` (render `<img>` base64, `:191-196`). Composable: `core/apps/banking/src/composables/usePix.ts:303-322`, `POST /pix/qrcode` com `{description, account_id, amount?}`.
- Merchant gerar QR: `core/apps/merchant/src/views/pix/PixReceiveView.vue:64-74` passa amount em REAIS (BUG de unidade, `parseCurrencyToReais`) e lê aliases `qrcodeBase64|qrCodeBase64|qrcode_base64|image`, `brcode|emv|emvCode|payload`, `pixKey|pix_key|key`; fallback client-side `QrcodeVue` com o EMV (`:179-187`). Composable idêntico ao IB (`core/apps/merchant/src/composables/usePix.ts:222-241`).
- Copia-e-cola IB: `core/apps/banking/src/views/pix/PixCopyPasteView.vue:59` chama `pix.payQrCode(value)` que NÃO existe (export de `usePix.ts:426-440` não o contém) - a tela sempre cai no catch (`isInvalid`). O fluxo certo: preview -> `handleContinue` (`:102-126`) grava `storePixSendData({pixKey, keyType, recipientName, recipientDocument, recipientDocumentType, recipientBankName, recipientKeyType, amount (CENTAVOS), amountFormatted, description, isFromQR, qrType?, txid?})` e navega para `/pix/send/confirm`, onde `PixSendConfirmView.vue:346` fecha com `pix.sendPix` (funil real com Idempotency-Key). `pixSendSession.ts` injeta `clientRequestId`+fingerprint.
- Parser EMV do Core (para o endpoint de preview): `core/backend/lib/monetarie/use_cases/pix/brcode/parser.ex` - `parse/1` valida CRC-16/CCITT-FALSE (`:35-41`, `{:error, :invalid_crc}`), devolve `%{pix_key (26/01), pix_url (26/25), amount (CENTAVOS, :106-114), name (tag 59), city, txid (62/05), point_of_initiation, ...}`.
- Backend pay já existe: `v2/pix_controller.ex:825-880` (`pay_qrcode`, rotas `router.ex:3166` e `:3285`) - parseia e PAGA em um passo; não serve de preview (pagaria no paste).

### C4. Roteamento NATS Core -> cabine (classe DLQ)

- A cabine roteia pelo campo `"event"` no topo (ou em `data.event`): `pix/backend/apps/settlement_service/lib/settlement_service/workers/core_event_processor.ex:66-150`. Eventos válidos: `payment_request`, `return_request`, `internal_settlement`, `key_create`, `key_delete`, `dict_lookup`, `balance_inquiry`, claims, recoveries, infractions. **NÃO existe evento de recorrência/mandato/camt/pibr/reda.** Sem `event` = `unrouted` -> DLQ durável + telemetria (item D, `:161-195`). Confirmado também em `pix/CLAUDE.md` gotcha 8.
- v1 `/pix/in|/pix/out`: `router.ex:290-293`; `core/backend/lib/monetarie_web/controllers/pix_controller.ex:108-111` publica `%{"type" => "pix_in", "data" => ...}` e `:192-207` publica DOIS envelopes `%{"type" => "pix_out"|"pix_payment_request", ...}` - nenhum tem `event` -> DLQ, com resposta 202 mentirosa e linha `PaymentTransactions` presa em processing para sempre.
- Adapter in_house (Core): `core/backend/lib/monetarie/services/pix_providers/in_house/adapter.ex:86-138` - `setup_recurrence/initiate_payment/authorize_payment/setup_mandate/cancel_mandate/send_camt060/send_camt055/send_camt029/send_pibr001/send_reda` publicam `%{"data" => params}` (sem `event`) em `monetarie.core.pix.*` -> DLQ. `send_pix/respond_pix/return_funds` (`:25-46`) publicam `%{"type" => ..., "data" => ...}` também SEM `event` (mesma classe; conferir chamadores na task 14). Os req/reply DICT (`:144+`) são REAIS e ficam intocados.
- PIX Automático VIVO: telas do IB usam o Grupo B `V2.PixAutomaticoController` (rotas `router.ex:3153-3161`, `/merchants/:id/pix/recurrence...`) -> `Monetarie.UseCases.PixAutomatico` -> publica `monetarie.spi.recurrence.execute` (`use_cases/pix_automatico.ex:298`) consumido por `SpiService.Recurrences.ExecutionWorker` (`pix/backend/apps/spi_service/lib/spi_service/recurrences/execution_worker.ex:30`). Grupo A `V2.PixDomainController` (rotas `router.ex:3147-3151`, setup/initiate/authorize/mandates) -> Provider -> adapter -> DLQ, e NENHUMA tela chama (grep vazio nos dois apps). Merchant não tem PIX Automático (só `/pix/scheduled`).

### C5. TED agendada (item A) e o v2 que a ignora

- Use case pronto: `core/backend/lib/monetarie/use_cases/spb/scheduled_ted.ex` - `classify_settlement/1` (nil -> `:immediate`; futura dia útil -> `{:schedule, date}`; passada/feriado/inválida -> `{:error, ...}`), `create/1` (attrs `account_id`, `amount` CENTAVOS, `settlement_date`, `request_params`, `merchant_id`; NUNCA faz hold), `execute_due/3` (executor cron `Monetarie.Workers.Spb.ScheduledTedExecutor`), `cancel/1`, `get/1`. Schema `core/backend/lib/monetarie/schemas/spb/scheduled_ted.ex` (tabela `scheduled_teds`, migration `20260717120000` JÁ aplicada HML+PRD).
- Referência de ramificação (v1, VIVO): `core/backend/lib/monetarie_web/controllers/ted_controller.ex:46-105` (classify -> `create_immediate` | `create_scheduled` | `reject_settlement`; resposta 202 `status: "scheduled"` com aviso de saldo na data).
- v2 quebrado: `v2/transfer_controller.ex:45-144` (`create`, rota `router.ex:3083`, é a que o IB usa) e `:446-538` (`create_ted`, rota `:3278`) - ambos seguram hold e publicam NA HORA; `scheduled_date` só vai de carona no payload (`:672`) e a cabine SPB ignora (zero refs).
- Partner TED sem agendamento: `partner_v1/transfers_controller.ex:122-202` (`ted/2`), sem nenhuma ref a scheduled.
- Front IB: NÃO há campo de data (`TransferSendView.vue`, sem DatePicker; `TransferPayload.scheduledDate` de `useTransfer.ts:5-15` nunca preenchido); envio real: `useTransfer.ts:38` `POST /merchants/:id/transfers` com `{destinationAccountId, amount, description, recipientName, recipientDocument, recipientBankCode, recipientBranch, recipientAccount}` (`TransferSendConfirmView.vue:94-103`). `TransferScheduledView.vue:66-69` lista `GET /merchants/:id/accounts/:id/transactions?status=scheduled&type=ted` (fonte que nunca verá `scheduled_teds`).

### C6. Limites

- Motor: `core/backend/lib/monetarie/use_cases/limits/limit_check.ex` (só `:pix_out` e `:ted`; noturno Res. 142 janela 20h->6h BRT `:42-43`; unidade SUBCENTAVOS; uso em `account_daily_summaries` `:118-141` privado). Resolução 4 níveis: `core/backend/lib/monetarie/use_cases/limits/transaction_limits.ex` `resolve/2` (`:80`) - override da conta (`accounts.pix_out_*`, `accounts.nighttime_limit`) -> conta primária -> `GlobalLimit` por entidade -> defaults (noturno default 10_000_000 subcent = R$ 1.000).
- Serialização de referência (CENTAVOS na borda): `partner_v1/accounts_controller.ex:207-224` (`limits/2`) + `flow_limits_to_centavos/1` e `subcent_to_centavos/1` (`:341-360`, `div(v, 100)`).
- Rota `/limits` NÃO existe no escopo das telas (grep `"/limits"` no router: só RiskController admin `:1816-1817` e partner `:152-153`).
- Merchant front JÁ chama: `core/apps/merchant/src/views/pix/PixLimitsView.vue:67` `api.get('/limits')`, parse aceita `pixOut|pix_out` com `{transactionLimit, dailyLimit, monthlyLimit, nighttimeLimit, dailyUsed, monthlyUsed}` (camel ou snake). IB: `LimitsView.vue` e `PixLimitsView.vue` 100% estáticas via i18n (`banking/src/i18n/pt-BR.ts:1901-1915`).

### C7. TEF

- 501: `merchant_portal_controller.ex:181-182` (`create_between_accounts_transfer`), rota `router.ex:3366` (`POST /transfers/between-accounts`) - usada pela tela do IB e do merchant.
- Handler real de referência: `partner_v1/transfers_controller.ex:237-252` (`internal/2`) - valida centavos -> `MoneyUnit.from_cents` -> autoriza AS DUAS contas -> `OutboundOrchestrator.execute/1` com `%OutboundPaymentParams{common: %Common{type: "tef", amount: base_units, ...}, tef: %Tef{destination_account_id}}` (`internal_params/4` `:513-526`). No orchestrator, TEF pula o gate de limites (`flow_type_for("tef") -> nil`).

### C8. Funil PIX outbound (para o v1 /pix/out)

- `core/backend/lib/monetarie/use_cases/payments/outbound_orchestrator.ex:57` `execute/1` recebe `%OutboundPaymentParams{}` (amount em SUBCENTAVOS), roda kill switches + limites + MED drain + lock + insere transação + despacha. Montagem de referência COMPLETA: `v2/transfer_controller.ex:257-307` (`create_pix_account`: valida CENTAVOS, `MoneyUnit.from_cents`, monta `%Common{type: "pix", ...}` + `%Pix{...}`, `render_outbound_result/2`).

### C9. Tabela de unidades da frente (checklist anti-infantil)

| Fronteira | Unidade |
|---|---|
| API v2 / partner / telas (`amount`) | CENTAVOS |
| `qrcodes.amount`, `transactions.amount`, orchestrator, motor de limites | BASE UNITS (subcentavos, x100 dos centavos) |
| attrs do motor de QR da cabine (`"amount"`) | REAIS em STRING (`reais_string/1`) |
| `ScheduledTed.create` attrs `amount` | CENTAVOS |
| Resposta `GET /limits` (nova) | CENTAVOS |
| Parser EMV `amount` | CENTAVOS |

### Regras duras da frente

1. **NÃO tocar** em `core/backend/lib/monetarie_web/controllers/partner_v1/pix_controller.ex` nem em `dict_api_responder`/MED (território F1). A única exceção partner é `transfers_controller.ex` (task 9, TED agendada), com risco de conflito declarado.
2. `router.ex` é compartilhado com F1: as edições F2 ficam concentradas nas regiões v2 (linhas ~3100-3370) e são mergeadas pelo orquestrador.
3. Toda migration: nenhuma nova é necessária nesta frente (o `scheduled_teds` já existe). Se alguma task descobrir necessidade, PARAR e escalar ao orquestrador.
4. Paridade admin==v2 do comprovante: NENHUMA task toca serializers de comprovante/receipt.
5. Textos de UI sem travessão; i18n nos locales existentes (banking: `pt-BR.ts en.ts es-ES.ts fr.ts zh-CN.ts`; merchant: `plugins/i18n.ts` + `i18n-fallbacks.ts`); PrimeVue Select para opções fixas.
6. Testes focados por arquivo durante dev; suite completa só no gate de merge (task 16).

---

## Task 1: Motor único de QR das telas (`QrGeneration`)

**CONTRATO PROVADO:** C1 + C2. A cabine responde `{:ok, %{"txid", "payload", "location_url", "expires_at", ...}}` (chaves string) aos subjects `monetarie.pix.qrcode.static|dynamic`; o espelho `qrcodes` precisa de `entity_id/account_id/type/pix_key/pix_key_type/tx_id/payload/merchant_name/merchant_city` com amount em base units; `ChargePaid` acha o espelho por `tx_id`. Fallback de entidade: `account.entity_id` -> `Monetarie.UseCases.Partners.institution_entity/0` (`use_cases/partners.ex:72`).

**Files:**
- Create: `core/backend/lib/monetarie/use_cases/pix/qr_generation.ex`
- Test: `core/backend/test/monetarie/use_cases/pix/qr_generation_test.exs`

**Step 1: Escrever o teste RED**

```elixir
defmodule Monetarie.UseCases.Pix.QrGenerationTest do
  use Monetarie.DataCase, async: false

  alias Monetarie.Repo
  alias Monetarie.Schemas.Pix.QRCode
  alias Monetarie.Schemas.Relational.{Account, Bank, User}
  alias Monetarie.UseCases.Pix.QrGeneration
  alias MonetarieTest.Factory

  # Espelha a forma serializada REAL do CoreGatewayHandler (chaves STRING),
  # mesma disciplina do StubQrGateway do partner_v1/pix_controller_test.exs.
  defmodule StubQrGateway do
    def generate_dynamic(attrs) do
      send(self(), {:qr_gateway, :generate_dynamic, attrs})

      {:ok,
       %{
         "id" => Ecto.UUID.generate(),
         "txid" => "TELADYNAMICTX000000000000001",
         "payload" =>
           "00020101021226580014br.gov.bcb.pix2536qrcode-h.monetarie.com/qr/v2/stubtok5204000053039865802BR5904STUB6003SAO62070503***6304ABCD",
         "access_token" => "stubtok",
         "location_url" => "https://qrcode-h.monetarie.com/qr/v2/stubtok",
         "revision" => 0,
         "expires_at" => DateTime.utc_now() |> DateTime.add(3600) |> DateTime.to_iso8601(),
         "due_date" => nil
       }}
    end

    def generate_static(attrs) do
      send(self(), {:qr_gateway, :generate_static, attrs})

      {:ok,
       %{
         "id" => Ecto.UUID.generate(),
         "txid" => "TELASTATICTX01",
         "payload" =>
           "00020101021126330014br.gov.bcb.pix0111123456789015204000053039865802BR5904STUB6003SAO62070503***6304ABCD",
         "access_token" => nil,
         "location_url" => nil,
         "revision" => 0,
         "expires_at" => nil,
         "due_date" => nil
       }}
    end
  end

  defmodule ErrorQrGateway do
    def generate_dynamic(_attrs), do: {:ok, {:error, "dynamic_failed: :whatever"}}
    def generate_static(_attrs), do: {:error, :timeout}
  end

  setup do
    prev = Application.get_env(:monetarie, :v2_qr_gateway)
    Application.put_env(:monetarie, :v2_qr_gateway, StubQrGateway)
    on_exit(fn ->
      case prev do
        nil -> Application.delete_env(:monetarie, :v2_qr_gateway)
        v -> Application.put_env(:monetarie, :v2_qr_gateway, v)
      end
    end)

    entity = Factory.insert(:entity)
    user = insert_user!()
    account = insert_account!(user.id, entity.id)
    {:ok, user: user, account: account, entity: entity}
  end

  test "amount em centavos gera cobranca DINAMICA real e espelha em qrcodes", %{account: account} do
    assert {:ok, qr} = QrGeneration.generate(account, 2_500, description: "Cobranca tela")

    # A cabine recebeu reais em STRING (transporte NATS so leva primitivos)
    assert_received {:qr_gateway, :generate_dynamic, attrs}
    assert attrs["amount"] == "25"
    assert attrs["pix_key"] != nil
    assert String.length(attrs["merchant_name"]) <= 25

    assert qr.brcode =~ "br.gov.bcb.pix"
    assert String.starts_with?(qr.qr_image, "data:image/png;base64,")
    assert qr.tx_id == "TELADYNAMICTX000000000000001"
    assert qr.type == "dynamic"
    assert qr.location_url =~ "/qr/v2/"

    # Espelho persistido: e o elo que o ChargePaid resolve por tx_id
    mirror = Repo.get_by(QRCode, tx_id: qr.tx_id)
    assert mirror.account_id == account.id
    assert mirror.status == "active"
    # centavos -> base units no espelho
    assert mirror.amount == 250_000
  end

  test "amount nil gera QR ESTATICO de valor em aberto", %{account: account} do
    assert {:ok, qr} = QrGeneration.generate(account, nil, [])

    assert_received {:qr_gateway, :generate_static, attrs}
    assert attrs["amount"] == nil
    assert qr.type == "static"

    mirror = Repo.get_by(QRCode, tx_id: qr.tx_id)
    assert mirror.amount == nil
  end

  test "erro da cabine NUNCA vira sucesso", %{account: account} do
    Application.put_env(:monetarie, :v2_qr_gateway, ErrorQrGateway)

    assert {:error, _} = QrGeneration.generate(account, 2_500, [])
    assert {:error, _} = QrGeneration.generate(account, nil, [])
    assert Repo.aggregate(QRCode, :count, :id) == 0
  end

  test "conta sem entity_id usa a entidade da instituicao como fallback", %{user: user} do
    account = insert_account!(user.id, nil)
    ensure_institution_entity!()

    assert {:ok, qr} = QrGeneration.generate(account, 1_000, [])
    mirror = Repo.get_by(QRCode, tx_id: qr.tx_id)
    assert mirror != nil
    assert mirror.entity_id != nil
  end

  # helpers: copiar o padrao de insert_merchant!/insert_account! de
  # test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs:174-218
  # (User.changeset com id explicito 5_000_000+, Account.changeset kind 4),
  # acrescentando entity_id no Account quando dado. ensure_institution_entity!/0
  # insere um Entity com ispb == Monetarie.Util.Config.institution_ispb().
end
```

**Step 2: Rodar e ver falhar**

Run: `cd core/backend && mix test test/monetarie/use_cases/pix/qr_generation_test.exs`
Expected: FAIL (`QrGeneration` indefinido).

**Step 3: Implementação mínima**

```elixir
defmodule Monetarie.UseCases.Pix.QrGeneration do
  @moduledoc """
  Motor UNICO de QR de recebimento para as TELAS (IB + merchant portal).

  Delega a montagem do EMV ao motor REAL da cabine (subjects
  monetarie.pix.qrcode.static|dynamic), o mesmo caminho da Partner API
  validado vivo em HML (17/07). NUNCA monta EMV local (o stub sem CRC do
  v2 era EMV invalido).

  Unidades: amount_cents CENTAVOS na entrada; a cabine recebe REAIS em
  string; o espelho `qrcodes` grava BASE UNITS.

  O espelho em `qrcodes` e o elo do "pago": o ChargePaid (consumer de
  monetarie.settlement.qrcode.paid) resolve por tx_id e marca paid +
  webhook pix.charge.paid. Sem espelho, a cobranca da tela nunca aparece
  paga. Persistencia best-effort com log alto (a cabine e a fonte da
  verdade do EMV).
  """

  require Logger

  alias Monetarie.Repo
  alias Monetarie.Schemas.Pix.QRCode
  alias Monetarie.Schemas.Relational.User
  alias Monetarie.UseCases.Partners
  alias Monetarie.UseCases.Pix.BRCode.Encoder
  alias Monetarie.UseCases.Pix.Gateway.QrCode, as: GatewayQrCode
  alias Monetarie.Util.MoneyUnit

  @default_expires_in 3600

  @spec generate(struct(), pos_integer() | nil, keyword()) :: {:ok, map()} | {:error, term()}
  def generate(account, amount_cents, opts \\ [])

  def generate(account, amount_cents, opts) when is_integer(amount_cents) and amount_cents > 0 do
    attrs =
      base_attrs(account, opts)
      |> Map.put("amount", reais_string(amount_cents))
      |> Map.put("expires_in", opts[:expires_in] || @default_expires_in)

    case normalize(qr_gateway().generate_dynamic(attrs)) do
      {:ok, %{"payload" => brcode} = dyn} when is_binary(brcode) ->
        persist_mirror(account, dyn, "dynamic", amount_cents, attrs, opts)
        {:ok, result(dyn, "dynamic", attrs)}

      {:error, reason} ->
        {:error, reason}
    end
  end

  def generate(account, nil, opts) do
    attrs = base_attrs(account, opts)

    case normalize(qr_gateway().generate_static(attrs)) do
      {:ok, %{"payload" => brcode} = static} when is_binary(brcode) ->
        persist_mirror(account, static, "static", nil, attrs, opts)
        {:ok, result(static, "static", attrs)}

      {:error, reason} ->
        {:error, reason}
    end
  end

  def generate(_account, _amount, _opts), do: {:error, :invalid_amount}

  defp result(cabin, type, attrs) do
    brcode = cabin["payload"]

    %{
      brcode: brcode,
      qr_image: encode_image(brcode),
      tx_id: cabin["txid"],
      type: type,
      location_url: cabin["location_url"],
      expires_at: cabin["expires_at"],
      pix_key: attrs["pix_key"]
    }
  end

  defp base_attrs(account, opts) do
    owner = owner(account)

    %{
      "pix_key" => opts[:pix_key] || owner_document(owner),
      "merchant_name" => sanitize(owner_name(owner), 25),
      "merchant_city" => sanitize(opts[:city] || Monetarie.Util.Config.institution_city(), 15),
      "description" => opts[:description]
    }
  end

  defp persist_mirror(account, cabin, type, amount_cents, attrs, opts) do
    entity_id = account.entity_id || opts[:entity_id] || institution_entity_id()

    mirror_attrs = %{
      entity_id: entity_id,
      account_id: account.id,
      type: type,
      status: "active",
      amount: amount_cents && MoneyUnit.from_cents(amount_cents),
      description: attrs["description"],
      pix_key: attrs["pix_key"],
      pix_key_type: detect_key_type(attrs["pix_key"]),
      tx_id: cabin["txid"],
      payload: cabin["payload"],
      merchant_name: attrs["merchant_name"],
      merchant_city: attrs["merchant_city"],
      location_url: cabin["location_url"],
      expires_at: parse_iso(cabin["expires_at"])
    }

    case Repo.insert(QRCode.changeset(%QRCode{}, mirror_attrs)) do
      {:ok, _qr} ->
        :ok

      {:error, changeset} ->
        Logger.error(
          "[QrGeneration] espelho qrcodes falhou (cobranca da tela ficara sem 'pago'): " <>
            inspect(changeset.errors)
        )

        :ok
    end
  end

  # embrulho do Gateway req/reply: mesmo achatamento do normalize_cobv do partner
  defp normalize({:ok, {:ok, %{} = m}}), do: {:ok, m}
  defp normalize({:ok, {:error, reason}}), do: {:error, reason}
  defp normalize({:ok, %{} = m}), do: {:ok, m}
  defp normalize({:error, reason}), do: {:error, reason}
  defp normalize(other), do: {:error, other}

  defp qr_gateway, do: Application.get_env(:monetarie, :v2_qr_gateway, GatewayQrCode)

  defp encode_image(brcode) do
    Encoder.to_base64(brcode)
  rescue
    _ -> nil
  end

  defp reais_string(cents), do: Decimal.to_string(Decimal.div(Decimal.new(cents), 100))

  defp sanitize(nil, max), do: sanitize("", max)

  defp sanitize(value, max) do
    value
    |> to_string()
    |> String.upcase()
    |> String.replace(~r/[^A-Z0-9 ]/, "")
    |> String.slice(0, max)
  end

  defp owner(account), do: Repo.get(User, account.user_id)
  defp owner_document(%User{tax_id: doc}) when is_binary(doc), do: doc
  defp owner_document(_), do: ""
  defp owner_name(%User{name: name, login: login}), do: name || login
  defp owner_name(_), do: ""

  defp institution_entity_id do
    case Partners.institution_entity() do
      %{id: id} -> id
      _ -> nil
    end
  end

  defp detect_key_type(nil), do: "cpf"

  defp detect_key_type(key) when is_binary(key) do
    digits = String.replace(key, ~r/\D/, "")

    cond do
      String.contains?(key, "@") -> "email"
      String.starts_with?(key, "+") -> "phone"
      Regex.match?(~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, key) -> "evp"
      String.length(digits) == 11 -> "cpf"
      String.length(digits) == 14 -> "cnpj"
      true -> "evp"
    end
  end

  defp parse_iso(nil), do: nil

  defp parse_iso(str) when is_binary(str) do
    case DateTime.from_iso8601(str) do
      {:ok, dt, _} -> DateTime.truncate(dt, :second)
      _ -> nil
    end
  end

  defp parse_iso(_), do: nil
end
```

Atenção do implementador: o teste de fallback exige que `detect_key_type` de um documento sem chave registrada devolva um tipo aceito pelo changeset. Se o espelho recusar (ex.: CPF de 11 dígitos com máscara), o teste do espelho falha: normalizar o documento com `String.replace(~r/\D/, "")` quando `pix_key` for documento cai no MESMO comportamento do partner (`owner_document`); conferir contra o teste antes de "consertar" o changeset.

**Step 4: Rodar e ver passar**

Run: `cd core/backend && mix test test/monetarie/use_cases/pix/qr_generation_test.exs`
Expected: PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie/use_cases/pix/qr_generation.ex core/backend/test/monetarie/use_cases/pix/qr_generation_test.exs
git commit -m "feat(core): motor unico de QR das telas delegando a cabine (espelho qrcodes p/ ChargePaid)"
```

---

## Task 2: `v2 generate_qrcode` liga no motor real (mata o stub EMV)

**CONTRATO PROVADO:** C1 + C3. O IB lê `qr_image`; o merchant lê `qrcodeBase64|brcode|pixKey`. O front manda `{description, account_id, amount?}` com amount em CENTAVOS (IB). Rotas: `router.ex:3103` (merchant-scoped) e `:3283` (self). Erro da cabine nunca vira 2xx (mesma regra do `pix_controller_cabin_error_test.exs`).

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/v2/pix_controller.ex:758-813` (generate_qrcode + helpers mortos `get_user_pix_key/generate_tx_id`)
- Test: `core/backend/test/monetarie_web/controllers/v2/pix_controller_qrcode_test.exs` (novo)

**Step 1: Teste RED**

```elixir
defmodule MonetarieWeb.V2.PixControllerQrcodeTest do
  use MonetarieWeb.ConnCase, async: false

  alias Monetarie.Repo
  alias Monetarie.Schemas.Pix.QRCode

  # Reusar: StubQrGateway/ErrorQrGateway identicos aos da task 1 (copiar),
  # e os helpers insert_merchant!/insert_bank!/insert_account!/merchant_conn
  # de test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs:174-288.

  setup do
    # seam da task 1
    prev = Application.get_env(:monetarie, :v2_qr_gateway)
    Application.put_env(:monetarie, :v2_qr_gateway, StubQrGateway)
    on_exit(fn -> restore(:v2_qr_gateway, prev) end)

    merchant = insert_merchant!()
    bank = insert_bank!()
    account = insert_account!(merchant.id, bank.id, 4)
    {:ok, merchant: merchant, account: account}
  end

  test "POST /pix/qrcode com amount gera cobranca dinamica REAL", %{conn: conn, merchant: m, account: a} do
    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/pix/qrcode", %{"amount" => 2_500, "account_id" => a.id, "description" => "Tela"})
      |> json_response(200)

    assert body["brcode"] =~ "br.gov.bcb.pix"
    refute body["brcode"] =~ ~r/6304$/    # nunca mais EMV terminando sem CRC
    assert body["qr_image"] =~ "data:image/png;base64,"
    assert body["qrcode_base64"] == body["qr_image"]
    assert body["txId"] == "TELADYNAMICTX000000000000001"
    assert body["amount"] == 2_500

    assert %QRCode{account_id: account_id} = Repo.get_by(QRCode, tx_id: body["txId"])
    assert account_id == a.id
  end

  test "POST /pix/qrcode sem amount gera estatico de valor em aberto", %{conn: conn, merchant: m, account: a} do
    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/pix/qrcode", %{"account_id" => a.id})
      |> json_response(200)

    assert body["type"] == "static"
    assert body["amount"] == nil
  end

  test "erro da cabine responde 502 com motivo, nunca 200", %{conn: conn, merchant: m, account: a} do
    Application.put_env(:monetarie, :v2_qr_gateway, ErrorQrGateway)

    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/pix/qrcode", %{"amount" => 2_500, "account_id" => a.id})
      |> json_response(502)

    assert body["error"]["message"] =~ "QR"
  end

  test "account_id de OUTRO usuario e recusado (nunca gera espelho na conta alheia)", %{conn: conn, merchant: m} do
    other = insert_merchant!()
    bank = insert_bank!()
    other_account = insert_account!(other.id, bank.id, 4)

    conn
    |> merchant_conn(m.id)
    |> post("/api/pix/qrcode", %{"amount" => 1_000, "account_id" => other_account.id})
    |> json_response(404)

    assert Repo.aggregate(QRCode, :count, :id) == 0
  end
end
```

**Step 2: Rodar e ver falhar**

Run: `cd core/backend && mix test test/monetarie_web/controllers/v2/pix_controller_qrcode_test.exs`
Expected: FAIL (stub atual devolve brcode fake com `6304` sem CRC, sem espelho, aceita conta alheia).

**Step 3: Implementação**

Substituir o corpo de `generate_qrcode/2` (mantendo `generate_qrcode_self` intacto):

```elixir
def generate_qrcode(conn, %{"merchant_id" => merchant_id} = params) do
  current_user = Guardian.Plug.current_resource(conn)

  unless authorized?(current_user, merchant_id) do
    conn
    |> put_status(:forbidden)
    |> json(%{error: %{status: 403, message: "Acesso nao autorizado"}})
  else
    with {:ok, amount_cents} <- validate_optional_qr_amount(params),
         {:ok, account} <- resolve_qr_account(merchant_id, params["account_id"]) do
      opts = [
        description: params["description"],
        pix_key: presence(params["pix_key"] || params["pixKey"]),
        city: params["city"],
        entity_id: entity_id_from(conn)
      ]

      case Monetarie.UseCases.Pix.QrGeneration.generate(account, amount_cents, opts) do
        {:ok, qr} ->
          json(conn, %{
            brcode: qr.brcode,
            qr_image: qr.qr_image,
            qrcode_base64: qr.qr_image,
            pixKey: qr.pix_key,
            amount: amount_cents,
            description: params["description"],
            txId: qr.tx_id,
            type: qr.type,
            locationUrl: qr.location_url,
            expiresAt: qr.expires_at
          })

        {:error, reason} ->
          Logger.error("[V2 PIX] cabine recusou gerar QR: #{inspect(reason)}")

          conn
          |> put_status(:bad_gateway)
          |> json(%{error: %{status: 502, message: "Falha ao gerar QR na cabine PIX"}})
      end
    else
      {:error, :invalid_amount, msg} ->
        conn |> put_status(:bad_request) |> json(%{error: %{status: 400, message: msg}})

      {:error, :account_not_found} ->
        conn
        |> put_status(:not_found)
        |> json(%{error: %{status: 404, message: "Conta nao encontrada"}})
    end
  end
end

# amount OPCIONAL: ausente/nil/0 = estatico de valor em aberto.
defp validate_optional_qr_amount(params) do
  case params["amount"] do
    nil -> {:ok, nil}
    0 -> {:ok, nil}
    "" -> {:ok, nil}
    _ ->
      case validate_qr_amount(params) do
        {:ok, cents} -> {:ok, cents}
        {:error, msg} -> {:error, :invalid_amount, msg}
      end
  end
end

# A conta do QR PRECISA pertencer ao usuario (o espelho define quem ve o "pago").
defp resolve_qr_account(merchant_id, account_id) do
  user_id = ensure_integer(merchant_id)

  query =
    case account_id do
      nil -> from(a in Account, where: a.user_id == ^user_id and a.kind > 0, order_by: a.id, limit: 1)
      id -> from(a in Account, where: a.user_id == ^user_id and a.id == ^ensure_integer(id), limit: 1)
    end

  case Repo.one(query) do
    %Account{} = account -> {:ok, account}
    nil -> {:error, :account_not_found}
  end
end

defp entity_id_from(conn) do
  case conn.assigns[:current_entity] do
    %{id: id} -> id
    _ -> nil
  end
end

defp presence(nil), do: nil
defp presence(""), do: nil
defp presence(v), do: v
```

Remover `get_user_pix_key/1` e `generate_tx_id/0` (mortos com o stub). Conferir aliases existentes no topo do arquivo (`Account`, `Repo`, `from/2`) e acrescentar os que faltarem. `validate_qr_amount` existente (`:1303-1322`) permanece para o caso com valor.

**Step 4: Rodar e ver passar**

Run: `cd core/backend && mix test test/monetarie_web/controllers/v2/pix_controller_qrcode_test.exs test/monetarie_web/controllers/v2/pix_controller_cabin_error_test.exs test/monetarie_web/controllers/v2/pix_controller_outbox_test.exs`
Expected: PASS (os 2 testes AST existentes continuam verdes; eles não cobrem generate_qrcode).

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/v2/pix_controller.ex core/backend/test/monetarie_web/controllers/v2/pix_controller_qrcode_test.exs
git commit -m "fix(core): QR das telas IB/merchant via motor REAL da cabine (fim do stub EMV sem CRC)"
```

---

## Task 3: Cobrança paga visível (serializer paid + `create_pix_charge` real)

**CONTRATO PROVADO:** C2. `qrcode_status("paid") -> "used"` esconde o pago (`merchant_portal_controller.ex:1072`); o partner expõe status cru + `paidAt` (`render_charge`, `:2066-2080`); `link_payment` da cabine + `ChargePaid` já entregam `status="paid"`, `paid_at`, `payment_end_to_end_id` no espelho. O teste existente trava o 501 de `create_pix_charge` e SERÁ atualizado.

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/v2/merchant_portal_controller.ex` (serialize_qrcode, qrcode_status, create_pix_charge)
- Modify: `core/backend/test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs`

**Step 1: Testes RED** (acrescentar no describe existente)

```elixir
test "cobranca paga aparece como paid com paidAt e endToEndId", %{conn: conn} do
  merchant = insert_merchant!()
  bank = insert_bank!()
  account = insert_account!(merchant.id, bank.id, 4)
  qr = insert_qrcode!(account.id, 25_000)

  paid_at = DateTime.utc_now() |> DateTime.truncate(:second)

  qr
  |> Ecto.Changeset.change(%{status: "paid", paid_at: paid_at, payment_end_to_end_id: "E4602656220260718TESTE0000001"})
  |> Repo.update!()

  body =
    conn
    |> merchant_conn(merchant.id)
    |> get("/api/qrcodes/#{qr.id}")
    |> json_response(200)

  assert body["data"]["status"] == "paid"
  assert body["data"]["paidAt"] =~ "T"
  assert body["data"]["endToEndId"] == "E4602656220260718TESTE0000001"
end

test "POST /pix/charges cria cobranca REAL pelo motor da cabine", %{conn: conn} do
  merchant = insert_merchant!()
  bank = insert_bank!()
  account = insert_account!(merchant.id, bank.id, 4)

  # seam da task 1 (StubQrGateway copiado neste arquivo, ou movido p/ um
  # test/support helper compartilhado se o revisor preferir DRY)
  prev = Application.get_env(:monetarie, :v2_qr_gateway)
  Application.put_env(:monetarie, :v2_qr_gateway, StubQrGateway)
  on_exit(fn -> restore_env(:v2_qr_gateway, prev) end)

  body =
    conn
    |> merchant_conn(merchant.id)
    |> post("/api/pix/charges", %{"amount" => 1_000, "description" => "Teste", "accountId" => account.id})
    |> json_response(201)

  assert body["data"]["brcode"] =~ "br.gov.bcb.pix"
  assert body["data"]["status"] == "active"
  assert Repo.get_by(Monetarie.Schemas.Pix.QRCode, tx_id: body["data"]["txId"])
end
```

E ATUALIZAR o teste "does not simulate unsupported PIX charge creation": sem o seam configurado o gateway real não responde em teste; trocar a asserção de 501 por um caso de erro da cabine -> 502 (usar `ErrorQrGateway`), preservando a regra "nunca simular sucesso".

**Step 2: Rodar e ver falhar**

Run: `cd core/backend && mix test test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs`
Expected: FAIL (status vem "used", 501 no create).

**Step 3: Implementação**

Em `merchant_portal_controller.ex`:

```elixir
# qrcode_status: paid E OS DEMAIS passam crus; so cancelled vira expired
# (compatibilidade com o front atual, que trata expired).
defp qrcode_status("cancelled"), do: "expired"
defp qrcode_status(status) when status in ["active", "expired", "used", "paid"], do: status
defp qrcode_status(_), do: "active"

# serialize_qrcode ganha paidAt/endToEndId:
defp serialize_qrcode(%QRCode{} = qrcode, merchant_id) do
  %{
    id: qrcode.id,
    merchantId: to_string(merchant_id),
    accountId: to_string(qrcode.account_id),
    type: qrcode.type,
    amount: MoneySerializer.from_base(qrcode.amount),
    description: qrcode.description,
    expiresAt: iso8601(qrcode.expires_at),
    payload: qrcode.payload,
    imageUrl: qrcode.location_url,
    status: qrcode_status(qrcode.status),
    paidAt: iso8601(qrcode.paid_at),
    endToEndId: qrcode.payment_end_to_end_id,
    createdAt: iso8601(qrcode.inserted_at)
  }
end

# create_pix_charge REAL (rotas POST /pix/charges e POST /qrcodes):
def create_pix_charge(conn, params) do
  with {:ok, merchant_id} <- authorize_merchant(conn, merchant_id_from(conn, params)),
       {:ok, amount_cents} <- charge_amount(params),
       {:ok, account} <- charge_account(merchant_id, params["accountId"] || params["account_id"]) do
    opts = [description: params["description"], entity_id: current_entity_uuid(conn)]

    case Monetarie.UseCases.Pix.QrGeneration.generate(account, amount_cents, opts) do
      {:ok, qr} ->
        mirror = Repo.get_by(QRCode, tx_id: qr.tx_id)

        conn
        |> put_status(:created)
        |> json(%{
          data: %{
            id: mirror && mirror.id,
            accountId: to_string(account.id),
            brcode: qr.brcode,
            qrcodeBase64: qr.qr_image,
            amount: amount_cents,
            description: params["description"],
            txId: qr.tx_id,
            status: "active",
            locationUrl: qr.location_url,
            expiresAt: qr.expires_at
          }
        })

      {:error, reason} ->
        Logger.error("[MerchantPortal] cabine recusou criar cobranca: #{inspect(reason)}")
        error(conn, 502, "Falha ao criar cobranca na cabine PIX")
    end
  else
    {:error, :invalid_amount} -> error(conn, 400, "Valor invalido (centavos, inteiro > 0)")
    {:error, :account_not_found} -> error(conn, 404, "Conta nao encontrada")
    {:error, status, message} -> error(conn, status, message)
  end
end

defp charge_amount(%{"amount" => amount}) when is_integer(amount) and amount > 0, do: {:ok, amount}
defp charge_amount(%{"amount" => amount}) when is_binary(amount) do
  case Integer.parse(amount) do
    {v, ""} when v > 0 -> {:ok, v}
    _ -> {:error, :invalid_amount}
  end
end
defp charge_amount(_), do: {:error, :invalid_amount}

defp charge_account(merchant_id, nil) do
  case merchant_accounts(merchant_id) do
    [account | _] -> {:ok, account}
    [] -> {:error, :account_not_found}
  end
end

defp charge_account(merchant_id, account_id) do
  id = ensure_integer(account_id)
  case Enum.find(merchant_accounts(merchant_id), &(&1.id == id)) do
    nil -> {:error, :account_not_found}
    account -> {:ok, account}
  end
end

defp current_entity_uuid(conn) do
  case conn.assigns[:current_entity] do
    %{id: id} -> id
    _ -> nil
  end
end
```

Adicionar `require Logger` se ausente.

**Step 4: Rodar e ver passar**

Run: `cd core/backend && mix test test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs`
Expected: PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/v2/merchant_portal_controller.ex core/backend/test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs
git commit -m "fix(core): lojista ve cobranca PAGA (serializer paid + paidAt/e2e) e cria cobranca real pela cabine"
```

---

## Task 4: Front IB, tela de receber QR

**CONTRATO PROVADO:** C3. `PixReceiveView.vue:47-48` só lê `qr_image` (que agora vem); o backend novo aceita amount nil (valor em aberto) e devolve `brcode/txId/pixKey`. `getPixKeys` já existe no composable (`usePix.ts:324-349`). Vitest configurado no banking (14 specs, padrão de mock em `src/composables/__tests__/usePix.test.ts`).

**Files:**
- Modify: `core/apps/banking/src/views/pix/PixReceiveView.vue`
- Modify: `core/apps/banking/src/composables/usePix.ts` (generateQrCode: aceitar pix_key)
- Modify: `core/apps/banking/src/i18n/pt-BR.ts` (+ en/es-ES/fr/zh-CN) bloco `pixReceiveExtra`
- Test: `core/apps/banking/src/composables/__tests__/usePix.test.ts` (estender)

**Step 1: Teste RED (vitest)** - estender `usePix.test.ts` no padrão do mock existente (`vi.hoisted` sobre `@/lib/api`):

```ts
it('generateQrCode envia centavos, account_id e pix_key opcional para POST /pix/qrcode', async () => {
  apiMock.post.mockResolvedValueOnce({ data: { brcode: '000201...', qr_image: 'data:image/png;base64,x', txId: 'T1' } })
  const pix = usePix()
  await pix.generateQrCode(2500, 'desc', 'chave@monetarie.com.br')
  expect(apiMock.post).toHaveBeenCalledWith('/pix/qrcode', expect.objectContaining({
    amount: 2500, description: 'desc', pix_key: 'chave@monetarie.com.br',
  }))
})

it('generateQrCode com amount null omite amount (valor em aberto)', async () => {
  apiMock.post.mockResolvedValueOnce({ data: { brcode: 'x', qr_image: 'y', txId: 'T2' } })
  const pix = usePix()
  await pix.generateQrCode(null)
  const body = apiMock.post.mock.calls.at(-1)![1]
  expect(body).not.toHaveProperty('amount')
})
```

**Step 2: Rodar e ver falhar**

Run: `cd core && pnpm --filter @monetarie/banking test:run src/composables/__tests__/usePix.test.ts`
Expected: FAIL (assinatura atual não aceita pix_key).

**Step 3: Implementação**

- `usePix.ts` `generateQrCode(amount: number | null, description?: string, pixKey?: string)`: incluir `pix_key: pixKey` no payload quando presente (resto igual).
- `PixReceiveView.vue`:
  - amount 0/vazio passa `null` (valor em aberto) em vez de mandar 0;
  - seletor de chave: PrimeVue `Select` alimentado por `pix.getPixKeys()` (label = chave mascarada + tipo; primeira chave pré-selecionada; se o usuário não tem chave, aviso i18n `pixReceiveExtra.noKeyWarning` e o backend cai no documento do titular);
  - além do `<img>` do `qr_image`, mostrar o `brcode` num campo copiável (botão "Copiar código"), lendo `result.brcode || result.data?.brcode`;
  - manter compatibilidade com a resposta atual (`qr_image` primeiro).
- i18n: chaves novas em `pixReceiveExtra` nos 5 locales (`keyLabel`, `openValueHint`, `copyCode`, `copied`, `noKeyWarning`). Sem travessão nos textos.

**Step 4: Rodar e ver passar**

Run: `cd core && pnpm --filter @monetarie/banking test:run` (suite do app)
Expected: PASS, zero regressão nos 14 specs.

**Step 5: Commit**

```bash
git add core/apps/banking/src/views/pix/PixReceiveView.vue core/apps/banking/src/composables/usePix.ts core/apps/banking/src/composables/__tests__/usePix.test.ts core/apps/banking/src/i18n/*.ts
git commit -m "feat(ib): tela receber QR usa motor real (chave selecionavel, valor em aberto, copia do brcode)"
```

---

## Task 5: Front merchant, unidade do QR + status pago nas telas de cobrança

**CONTRATO PROVADO:** C3 (merchant manda REAIS, bug de unidade) + C2 (serializer novo expõe `paid`/`paidAt`/`endToEndId`; type `QrCode` do front só conhece `active|expired|used` em `core/apps/merchant/src/types/index.ts:193`; severidades em `QrCodesView.vue:35-40` e `QrCodeDetailView.vue:36-42`).

**Files:**
- Modify: `core/apps/merchant/src/views/pix/PixReceiveView.vue` (centavos)
- Modify: `core/apps/merchant/src/types/index.ts` (status union + paidAt/endToEndId)
- Modify: `core/apps/merchant/src/views/initiation/QrCodesView.vue` e `QrCodeDetailView.vue` (label/severity `paid`, exibir paidAt/e2e no detalhe)
- Modify: `core/apps/merchant/src/plugins/i18n.ts` + `i18n-fallbacks.ts` (chave `pix.statusPaid` etc. nos blocos pt/en/zh)

**Step 1-2: Teste RED**

O merchant só tem 1 spec (ReceiptView). Criar `core/apps/merchant/src/views/initiation/__tests__/QrCodesView.test.ts` no mesmo padrão de mount do ReceiptView.test.ts: mock da api devolvendo um QR `status: 'paid'` e assert de que a Tag renderizada mostra o label pago (e NÃO "used"). Rodar: `cd core && pnpm --filter @monetarie/merchant-portal test:run` e ver falhar.

**Step 3: Implementação**

- `PixReceiveView.vue:64-70`: converter para CENTAVOS (`Math.round(reais * 100)`) antes de chamar `generateQrCode` (checklist: unidade conferida na fronteira; hoje um QR de "R$ 25,00" sai como 25 centavos).
- `types/index.ts`: `status: 'active' | 'expired' | 'used' | 'paid'`, campos `paidAt?: string`, `endToEndId?: string`.
- Views: mapa de severidade ganha `paid: 'success'` (e `active` muda para `'info'` para não colidir visualmente); detalhe mostra `paidAt` formatado (regra do repo: nunca `new Date('YYYY-MM-DD')` cru; usar o formatter existente do app) e o `endToEndId`.
- i18n inline: adicionar `statusPaid` nos 3 blocos (pt: "Paga", en: "Paid", zh existente no padrão do arquivo) em `i18n.ts` E `i18n-fallbacks.ts`.

**Step 4: Rodar e ver passar**

Run: `cd core && pnpm --filter @monetarie/merchant-portal test:run`
Expected: PASS.

**Step 5: Commit**

```bash
git add core/apps/merchant/src
git commit -m "fix(merchant): QR em centavos e status PAGO visivel nas telas de cobranca"
```

---

## Task 6: Backend do copia-e-cola, endpoint de parse/preview

**CONTRATO PROVADO:** C3 + C4. O paste precisa de PREVIEW (não pagamento): o backend `pay_qrcode` paga na hora, errado para o fluxo da tela (a confirmação acontece depois, em `/pix/send/confirm` via funil real). `Parser.parse/1` valida CRC e devolve amount em CENTAVOS, nome tag 59, chave 26/01, URL 26/25.

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/v2/pix_controller.ex` (nova action `parse_qrcode` + `parse_qrcode_self`)
- Modify: `core/backend/lib/monetarie_web/router.ex` (self scope, junto da linha 3284: `post "/pix/qrcode/parse", PixController, :parse_qrcode_self`; e merchant-scoped junto da 3164: `post "/pix/qrcode/parse", PixController, :parse_qrcode`)
- Test: `core/backend/test/monetarie_web/controllers/v2/pix_controller_parse_qrcode_test.exs`

**Step 1: Teste RED**

```elixir
defmodule MonetarieWeb.V2.PixControllerParseQrcodeTest do
  use MonetarieWeb.ConnCase, async: false

  # helpers merchant_conn/insert_merchant! copiados do padrao merchant_portal_controller_test

  # CRC-16/CCITT-FALSE independente (mesmo polinomio 0x1021, init 0xFFFF) para
  # montar fixtures VALIDAS sem tautologia com o Parser:
  defp crc16(data) do
    import Bitwise
    data
    |> :binary.bin_to_list()
    |> Enum.reduce(0xFFFF, fn byte, crc ->
      crc = bxor(crc, bsl(byte, 8))
      Enum.reduce(0..7, crc, fn _, c ->
        if band(c, 0x8000) != 0, do: band(bxor(bsl(c, 1), 0x1021), 0xFFFF), else: band(bsl(c, 1), 0xFFFF)
      end)
    end)
    |> Integer.to_string(16)
    |> String.pad_leading(4, "0")
    |> String.upcase()
  end

  defp valid_static_emv do
    payload =
      "00020101021126360014br.gov.bcb.pix0114+55119999999995204000053039865406123.455802BR5913FULANO DE TAL6009SAO PAULO62070503***6304"

    payload <> crc16(payload)
  end

  test "parse de EMV valido devolve preview com amount em centavos", %{conn: conn} do
    m = insert_merchant!()

    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/pix/qrcode/parse", %{"emv" => valid_static_emv()})
      |> json_response(200)

    assert body["data"]["pix_key"] == "+5511999999999"
    assert body["data"]["key_type"] == "phone"
    assert body["data"]["amount"] == 12_345
    assert body["data"]["receiver_name"] == "FULANO DE TAL"
    assert body["data"]["qr_type"] in ["static", "dynamic"]
  end

  test "CRC invalido responde 422 invalid_crc", %{conn: conn} do
    m = insert_merchant!()

    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/pix/qrcode/parse", %{"emv" => valid_static_emv() <> "X"})
      |> json_response(422)

    assert body["error"]["code"] == "invalid_crc"
  end

  test "QR dinamico so-URL (sem chave) responde 422 legivel", %{conn: conn} do
    m = insert_merchant!()

    payload =
      "00020101021226500014br.gov.bcb.pix2528qrcode.monetarie.com/qr/v2/x5204000053039865802BR5913FULANO DE TAL6009SAO PAULO62070503***6304"

    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/pix/qrcode/parse", %{"emv" => payload <> crc16(payload)})
      |> json_response(422)

    assert body["error"]["code"] == "url_only_qr_unsupported"
  end
end
```

Nota G2: os offsets EMV das fixtures precisam bater com o Parser real (tamanhos de campo declarados). No RED, se o Parser recusar a fixture por formato, corrigir A FIXTURE (recalcular os length bytes), nunca o Parser.

**Step 2: Rodar e ver falhar**

Run: `cd core/backend && mix test test/monetarie_web/controllers/v2/pix_controller_parse_qrcode_test.exs`
Expected: FAIL (rota/ação inexistentes).

**Step 3: Implementação**

```elixir
def parse_qrcode_self(conn, params), do: parse_qrcode(conn, put_self_merchant_id(conn, params))

@doc """
Preview do copia-e-cola: parseia o EMV (CRC obrigatorio) e devolve os dados
para a tela montar a confirmacao. NAO paga nada; o pagamento segue pelo funil
real (POST /accounts/:id/pix/send) depois da confirmacao do usuario.
"""
def parse_qrcode(conn, %{"merchant_id" => merchant_id, "emv" => emv}) do
  current_user = Guardian.Plug.current_resource(conn)

  unless authorized?(current_user, merchant_id) do
    conn |> put_status(:forbidden) |> json(%{error: %{status: 403, message: "Acesso nao autorizado"}})
  else
    alias Monetarie.UseCases.Pix.BRCode.Parser

    case Parser.parse(emv) do
      {:ok, %{pix_key: nil, pix_url: url}} when is_binary(url) ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{error: %{status: 422, code: "url_only_qr_unsupported",
          message: "QR dinamico com URL ainda nao e suportado no copia e cola. Use a leitura de QR ou digite a chave."}})

      {:ok, qr_data} when not is_nil(qr_data.pix_key) ->
        json(conn, %{data: %{
          pix_key: qr_data.pix_key,
          key_type: detect_parsed_key_type(qr_data.pix_key),
          amount: qr_data.amount,
          receiver_name: qr_data.name,
          receiver_city: qr_data.city,
          tx_id: qr_data.txid,
          qr_type: if(qr_data.point_of_initiation == "12", do: "dynamic", else: "static")
        }})

      {:ok, _} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{error: %{status: 422, code: "no_pix_key", message: "QR Code nao contem chave PIX"}})

      {:error, :invalid_crc} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{error: %{status: 422, code: "invalid_crc", message: "QR Code invalido (CRC incorreto)"}})

      {:error, _} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{error: %{status: 422, code: "invalid_format", message: "Formato de QR Code invalido"}})
    end
  end
end

def parse_qrcode(conn, %{"merchant_id" => _}) do
  conn |> put_status(:bad_request) |> json(%{error: %{status: 400, message: "Parametro 'emv' obrigatorio"}})
end

defp detect_parsed_key_type(key) do
  digits = String.replace(key, ~r/\D/, "")
  cond do
    String.contains?(key, "@") -> "email"
    String.starts_with?(key, "+") -> "phone"
    Regex.match?(~r/^[0-9a-f-]{36}$/i, key) -> "random"
    String.length(digits) == 11 -> "cpf"
    String.length(digits) == 14 -> "cnpj"
    true -> "random"
  end
end
```

(`key_type` usa o vocabulário do FRONT: `random` e não `evp`, porque `storePixSendData`/fluxo de envio usam `random`; conferir em `PixCopyPasteView.vue:105`.)

**Step 4: Rodar e ver passar**

Run: `cd core/backend && mix test test/monetarie_web/controllers/v2/pix_controller_parse_qrcode_test.exs`
Expected: PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/v2/pix_controller.ex core/backend/lib/monetarie_web/router.ex core/backend/test/monetarie_web/controllers/v2/pix_controller_parse_qrcode_test.exs
git commit -m "feat(core): endpoint de parse/preview do copia-e-cola (CRC obrigatorio, nunca paga no paste)"
```

---

## Task 7: Front IB, copia-e-cola funcional

**CONTRATO PROVADO:** C3. A view espera `payQrCode(value)` devolvendo `{data: {receiver_name, pix_key, key_type, amount, ...}}`; o continue navega ao funil real. Endpoint novo da task 6.

**Files:**
- Modify: `core/apps/banking/src/composables/usePix.ts` (nova função `payQrCode` que chama o parse; nome mantido porque a view já o usa)
- Modify: `core/apps/banking/src/views/pix/PixCopyPasteView.vue` (mapear campos novos; erro legível para `url_only_qr_unsupported` e `invalid_crc`)
- Modify: `core/apps/banking/src/i18n/*.ts` (bloco `pixCopyPasteExtra`: `urlOnlyUnsupported`, `invalidCrc`)
- Test: `core/apps/banking/src/composables/__tests__/usePix.test.ts`

**Step 1: Teste RED**

```ts
it('payQrCode chama POST /pix/qrcode/parse e devolve o preview', async () => {
  apiMock.post.mockResolvedValueOnce({ data: { data: { pix_key: 'x@y.z', key_type: 'email', amount: 500, receiver_name: 'FULANO' } } })
  const pix = usePix()
  const out = await pix.payQrCode('000201...')
  expect(apiMock.post).toHaveBeenCalledWith('/pix/qrcode/parse', { emv: '000201...' })
  expect(out.data.pix_key).toBe('x@y.z')
})
```

**Step 2:** `pnpm --filter @monetarie/banking test:run src/composables/__tests__/usePix.test.ts` - FAIL (payQrCode não existe).

**Step 3: Implementação**

```ts
async function payQrCode(emv: string) {
  loading.value = true
  error.value = null
  try {
    const response = await api.post('/pix/qrcode/parse', { emv })
    return response.data
  } catch (e: any) {
    error.value = e.response?.data?.error?.message || 'Erro ao ler QR Code'
    throw e
  } finally {
    loading.value = false
  }
}
```

Exportar `payQrCode` no return do composable. Na view: `data.pix_key/key_type/amount/receiver_name` já são cobertos pelos aliases existentes (`data.pix_key`, `data.key_type`, `data.amount`); ajustar o catch para diferenciar `invalid_crc`/`url_only_qr_unsupported` (mensagens i18n) de erro genérico, populando um `errorMessage` exibido no `Message` da tela em vez do genérico `isInvalid`.

**Step 4:** `pnpm --filter @monetarie/banking test:run` - PASS.

**Step 5: Commit**

```bash
git add core/apps/banking/src
git commit -m "fix(ib): copia-e-cola funcional (payQrCode via parse; confirmacao segue no funil real)"
```

---

## Task 8: TED agendada no v2 (`create` e `create_ted`)

**CONTRATO PROVADO:** C5. `ScheduledTed.classify_settlement/1` + `create/1` (attrs em CENTAVOS, sem hold) prontos; ramificação de referência no v1 `ted_controller.ex:46-105`. O v2 `create` (IB) e `create_ted` recebem amount em CENTAVOS e convertem com `MoneyUnit.from_cents` SÓ no caminho imediato; `request_params` do agendamento precisa carregar as chaves snake_case que `ScheduledTed.default_dispatch/1` lê (`recipient_name/document/ispb/agency/account`, `purpose_code`, `description`) - o v2 recebe camelCase, então normalizar ANTES de gravar.

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/v2/transfer_controller.ex`
- Test: `core/backend/test/monetarie_web/controllers/v2/transfer_controller_scheduled_ted_test.exs` (novo)

**Step 1: Teste RED**

```elixir
defmodule MonetarieWeb.V2.TransferControllerScheduledTedTest do
  use MonetarieWeb.ConnCase, async: false

  alias Monetarie.Repo
  alias Monetarie.Schemas.Spb.ScheduledTed, as: ScheduledTedRow
  alias Monetarie.Schemas.Relational.Transaction

  # helpers merchant_conn/insert_merchant!/insert_bank!/insert_account! copiados
  # do padrao merchant_portal_controller_test (kind 1 = conta bancaria)

  defp next_business_day do
    # dia util futuro real (usa o mesmo calendario do use case)
    Enum.find(1..10, fn n -> Monetarie.UseCases.Calendar.business_day?(Date.add(Date.utc_today(), n)) end)
    |> then(&Date.add(Date.utc_today(), &1))
  end

  test "scheduledDate futura agenda SEM hold, SEM publish e SEM linha de transacao", %{conn: conn} do
    m = insert_merchant!()
    bank = insert_bank!()
    _account = insert_account!(m.id, bank.id, 1)
    date = next_business_day()

    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/merchants/#{m.id}/transfers", %{
        "amount" => 10_000,
        "recipientName" => "FULANO",
        "recipientDocument" => "12345678901",
        "recipientBankCode" => "00000000",
        "recipientBranch" => "0001",
        "recipientAccount" => "12345",
        "scheduledDate" => Date.to_iso8601(date)
      })
      |> json_response(202)

    assert body["status"] == "scheduled"
    assert body["settlementDate"] == Date.to_iso8601(date)

    [row] = Repo.all(ScheduledTedRow)
    assert row.status == "pending"
    assert row.amount == 10_000
    # request_params normalizados para o vocabulario do default_dispatch:
    assert row.request_params["recipient_name"] == "FULANO"
    assert row.request_params["recipient_account"] == "12345"

    # nada foi debitado nem publicado agora:
    assert Repo.aggregate(Transaction, :count, :id) == 0
  end

  test "scheduledDate no passado rejeita 422 sem criar nada", %{conn: conn} do
    m = insert_merchant!()
    bank = insert_bank!()
    insert_account!(m.id, bank.id, 1)

    conn
    |> merchant_conn(m.id)
    |> post("/api/merchants/#{m.id}/transfers", %{
      "amount" => 10_000,
      "scheduledDate" => Date.to_iso8601(Date.add(Date.utc_today(), -1))
    })
    |> json_response(422)

    assert Repo.aggregate(ScheduledTedRow, :count, :id) == 0
  end

  test "POST /api/transfers/ted (self) tambem agenda", %{conn: conn} do
    m = insert_merchant!()
    bank = insert_bank!()
    insert_account!(m.id, bank.id, 1)
    date = next_business_day()

    body =
      conn
      |> merchant_conn(m.id)
      |> post("/api/transfers/ted", %{
        "amount" => 5_000,
        "recipientName" => "CICLANO",
        "recipientDocument" => "12345678901",
        "recipientBankCode" => "00000000",
        "recipientBranch" => "0001",
        "recipientAccount" => "999",
        "scheduledDate" => Date.to_iso8601(date)
      })
      |> json_response(202)

    assert body["status"] == "scheduled"
  end
end
```

Nota: o caminho IMEDIATO (sem scheduledDate) toca Wallet/TigerBeetle e não é o alvo destes testes; a regressão dele é a suite existente.

**Step 2:** `mix test test/monetarie_web/controllers/v2/transfer_controller_scheduled_ted_test.exs` - FAIL (hoje debita/publica na hora e responde "accepted").

**Step 3: Implementação**

Em `v2/transfer_controller.ex`, no TOPO de `create/2` (depois do gate de autorização) e de `create_ted/2`:

```elixir
alias Monetarie.UseCases.Spb.ScheduledTed

# dentro do else do unless authorized? em create/2:
case ScheduledTed.classify_settlement(params["scheduledDate"] || params["scheduled_date"]) do
  :immediate -> create_immediate(conn, merchant_id, params)   # corpo atual extraido
  {:schedule, date} -> create_scheduled(conn, merchant_id, params, date)
  {:error, reason} ->
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{error: %{status: 422, message: scheduled_reason_message(reason)}})
end
```

```elixir
defp create_scheduled(conn, merchant_id, params, date) do
  with {:ok, amount_cents} <- validate_amount_cents(params["amount"]),
       {:ok, account_id} <- find_user_account(merchant_id) do
    case ScheduledTed.create(%{
           account_id: account_id,
           merchant_id: merchant_id,
           amount: amount_cents,
           settlement_date: date,
           request_params: normalize_ted_request_params(params)
         }) do
      {:ok, scheduled} ->
        conn
        |> put_status(:accepted)
        |> json(%{
          status: "scheduled",
          scheduledTedId: scheduled.id,
          amount: amount_cents,
          settlementDate: Date.to_iso8601(date),
          message:
            "TED agendada para #{Date.to_iso8601(date)}. Garanta saldo disponivel na data; " <>
              "sem saldo na abertura da grade a TED sera rejeitada por insuficiencia."
        })

      {:error, reason} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{error: %{status: 422, message: "Falha ao agendar TED: #{inspect(reason)}"}})
    end
  else
    {:error, :invalid_amount} ->
      conn |> put_status(:bad_request) |> json(%{error: %{status: 400, message: "Valor invalido"}})

    {:error, :account_not_found} ->
      conn |> put_status(:not_found) |> json(%{error: %{status: 404, message: "Conta nao encontrada"}})
  end
end

# camelCase do v2 -> snake_case que o ScheduledTed.default_dispatch le na data:
defp normalize_ted_request_params(params) do
  %{
    "recipient_name" => params["recipientName"] || params["recipient_name"],
    "recipient_document" => params["recipientDocument"] || params["recipient_document"],
    "recipient_ispb" => params["recipientBankCode"] || params["recipient_ispb"],
    "recipient_agency" => params["recipientBranch"] || params["recipient_agency"],
    "recipient_account" => params["recipientAccount"] || params["recipient_account"],
    "purpose_code" => params["purposeCode"] || params["purpose_code"] || "10",
    "description" => params["description"]
  }
end

defp scheduled_reason_message(:settlement_date_in_past), do: "Data de liquidacao no passado"
defp scheduled_reason_message(:settlement_date_not_business_day), do: "Data de liquidacao nao e dia util"
defp scheduled_reason_message(_), do: "Data de liquidacao invalida"
```

Refatorar o corpo atual de `create/2` para `create_immediate/3` e o de `create_ted/2` para `create_ted_immediate/2` SEM mudar uma linha de lógica (só extração). `create_ted/2` ganha a mesma ramificação. O `"scheduled_date" => params["scheduledDate"]` do `build_spb_ted_payload` (`:672`) SAI do payload imediato (a cabine ignora e agora o agendamento tem trilho próprio).

**Step 4:** `mix test test/monetarie_web/controllers/v2/transfer_controller_scheduled_ted_test.exs test/monetarie/use_cases/spb/scheduled_ted_test.exs` - PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/v2/transfer_controller.ex core/backend/test/monetarie_web/controllers/v2/transfer_controller_scheduled_ted_test.exs
git commit -m "feat(core): TED agendada no v2 (IB) pelo ScheduledTed, sem hold na criacao"
```

---

## Task 9: TED agendada no partner (`POST /transfers/ted`)

**CONTRATO PROVADO:** C5. `partner_v1/transfers_controller.ex:122-202` não agenda. Mesmo padrão da task 8; o partner tem seams `balance_fun/hold_fun/publish_fun` que os testes existentes usam (`test/monetarie_web/controllers/partner_v1/transfers_controller_test.exs`).

**RISCO DE CONFLITO F1:** F1 adiciona rota de devolução de TED possivelmente neste arquivo. Coordenar com o orquestrador ANTES de começar; se F1 já estiver com o arquivo aberto, esta task espera.

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/partner_v1/transfers_controller.ex`
- Modify: `core/backend/test/monetarie_web/controllers/partner_v1/transfers_controller_test.exs`

**Step 1: Teste RED** (no padrão do arquivo existente, com conta partner do setup):

```elixir
describe "POST /api/partner/v1/transfers/ted com settlement_date futura" do
  test "agenda sem hold, sem publish", %{conn: conn} do
    # usar os helpers de conta/oauth do proprio arquivo
    date = next_business_day()

    body =
      conn
      |> partner_conn()
      |> post("/api/partner/v1/transfers/ted", %{
        "account_id" => account.id,
        "amount" => 10_000,
        "recipient_name" => "FULANO",
        "recipient_document" => "12345678901",
        "recipient_ispb" => "00000000",
        "recipient_agency" => "0001",
        "recipient_account" => "123",
        "settlement_date" => Date.to_iso8601(date)
      })
      |> json_response(202)

    assert body["status"] == "scheduled"
    refute_received {:published, _, _}     # publish_fun seam do arquivo
    assert [%{status: "pending"}] = Repo.all(Monetarie.Schemas.Spb.ScheduledTed)
  end

  test "data em dia nao util rejeita 422" do
    # proximo sabado -> 422 settlement_date_not_business_day
  end
end
```

**Step 2:** `mix test test/monetarie_web/controllers/partner_v1/transfers_controller_test.exs` - FAIL.

**Step 3: Implementação** - no `ted/2`, logo após `with_partner_account`, ramificar por `ScheduledTed.classify_settlement(params["settlement_date"] || params["settlementDate"])`: `:immediate` segue o corpo atual; `{:schedule, date}` chama `ScheduledTed.create` com `request_params` já em snake_case (o partner já usa snake; incluir também os aliases camel do arquivo) e responde 202 `status: "scheduled"` + `scheduledTedId` + aviso; `{:error, reason}` -> 422 com mensagem legível. Nenhuma alteração no caminho imediato.

**Step 4:** `mix test test/monetarie_web/controllers/partner_v1/transfers_controller_test.exs` - PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/partner_v1/transfers_controller.ex core/backend/test/monetarie_web/controllers/partner_v1/transfers_controller_test.exs
git commit -m "feat(partner): TED agendada por settlement_date no POST /transfers/ted"
```

---

## Task 10: Listagem/cancelamento de TED agendada + front IB (data no envio, tela de agendadas)

**CONTRATO PROVADO:** C5. `TransferScheduledView.vue:66-69` consulta transações `status=scheduled` (fonte que nunca verá `scheduled_teds`); não existe rota de listagem. `ScheduledTed.cancel/1` existe. Front: `TransferSendView.vue` sem campo de data; payload em `TransferSendConfirmView.vue:94-103`.

**Files:**
- Create: `core/backend/lib/monetarie_web/controllers/v2/scheduled_ted_controller.ex`
- Modify: `core/backend/lib/monetarie_web/router.ex` (self scope, junto da linha 3278): `get "/ted/scheduled", ScheduledTedController, :index_self` e `post "/ted/scheduled/:id/cancel", ScheduledTedController, :cancel_self` DENTRO do `scope "/transfers"` existente
- Test: `core/backend/test/monetarie_web/controllers/v2/scheduled_ted_controller_test.exs`
- Modify (front): `core/apps/banking/src/composables/useTransfer.ts`, `views/transfer/TransferSendView.vue`, `TransferSendConfirmView.vue`, `TransferScheduledView.vue`, i18n dos 5 locales

**Step 1: Teste RED (backend)**

```elixir
defmodule MonetarieWeb.V2.ScheduledTedControllerTest do
  use MonetarieWeb.ConnCase, async: false
  alias Monetarie.UseCases.Spb.ScheduledTed

  test "lista e cancela TED agendada do proprio usuario; nega a de outro", %{conn: conn} do
    m = insert_merchant!()
    other = insert_merchant!()
    bank = insert_bank!()
    account = insert_account!(m.id, bank.id, 1)
    other_account = insert_account!(other.id, bank.id, 1)
    date = next_business_day()

    {:ok, mine} = ScheduledTed.create(%{account_id: account.id, merchant_id: m.id, amount: 10_000, settlement_date: date, request_params: %{"recipient_name" => "F"}})
    {:ok, theirs} = ScheduledTed.create(%{account_id: other_account.id, merchant_id: other.id, amount: 5_000, settlement_date: date, request_params: %{}})

    body =
      conn |> merchant_conn(m.id) |> get("/api/transfers/ted/scheduled") |> json_response(200)

    assert [row] = body["data"]
    assert row["id"] == mine.id
    assert row["amount"] == 10_000              # CENTAVOS na borda
    assert row["settlementDate"] == Date.to_iso8601(date)
    assert row["status"] == "pending"

    conn
    |> recycle() |> merchant_conn(m.id)
    |> post("/api/transfers/ted/scheduled/#{theirs.id}/cancel")
    |> json_response(404)

    body =
      conn
      |> recycle() |> merchant_conn(m.id)
      |> post("/api/transfers/ted/scheduled/#{mine.id}/cancel")
      |> json_response(200)

    assert body["data"]["status"] == "cancelled"
  end
end
```

**Step 2:** FAIL (controller inexistente).

**Step 3: Implementação (backend)** - controller com `index_self` (query `Schemas.Spb.ScheduledTed` por `account_id in (contas do usuario)`, order by settlement_date, limit 200, serializa `{id, amount (centavos, ja e centavos na tabela), settlementDate, status, recipientName: request_params["recipient_name"], description, createdAt}`) e `cancel_self` (SÓ se a linha pertence a uma conta do usuário: buscar por id + account_id in contas; senão 404; depois `ScheduledTed.cancel/1`; `:not_cancellable` -> 422). IDOR-safe por construção (mesma disciplina das rotas self).

**Front:**
- `useTransfer.ts`: `listScheduledTeds()` -> `GET /transfers/ted/scheduled`; `cancelScheduledTed(id)` -> `POST /transfers/ted/scheduled/:id/cancel`; `sendTransfer` passa `scheduledDate` quando presente.
- `TransferSendView.vue`: campo opcional de data (PrimeVue `DatePicker`, `minDate` amanhã) gravado na sessão de transferência.
- `TransferSendConfirmView.vue`: incluir `scheduledDate` no payload quando presente; quando a resposta vier `status: "scheduled"`, mostrar a mensagem de agendamento (com o aviso de saldo na data) em vez do fluxo de sucesso imediato.
- `TransferScheduledView.vue`: trocar a fonte para `listScheduledTeds()`; colunas data de liquidação, valor, favorecido, status (`pending/processing/executed/rejected/failed/cancelled` com labels i18n); ação Cancelar só em `pending`.
- i18n: bloco `transferScheduled*` nos 5 locales.

**Step 4:** `mix test test/monetarie_web/controllers/v2/scheduled_ted_controller_test.exs` PASS; `pnpm --filter @monetarie/banking test:run` PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/v2/scheduled_ted_controller.ex core/backend/lib/monetarie_web/router.ex core/backend/test/monetarie_web/controllers/v2/scheduled_ted_controller_test.exs core/apps/banking/src
git commit -m "feat(ib): agendamento de TED pela tela (data no envio, listagem e cancelamento reais)"
```

---

## Task 11: `GET /limits` real (backend)

**CONTRATO PROVADO:** C6. Fonte: `TransactionLimits.resolve(account_id, :pix_out | :ted)` (subcent) + uso em `account_daily_summaries` (função privada de `LimitCheck`, `:118-141`, expor wrapper público). Borda em CENTAVOS (`div 100`, padrão do partner `accounts_controller.ex:341-360`). O merchant front espera `pixOut.{transactionLimit,dailyLimit,monthlyLimit,nighttimeLimit,dailyUsed,monthlyUsed}`.

**Files:**
- Modify: `core/backend/lib/monetarie/use_cases/limits/limit_check.ex` (expor `usage/1` público sobre a query existente, sem mudar a lógica)
- Create: `core/backend/lib/monetarie_web/controllers/v2/limits_controller.ex`
- Modify: `core/backend/lib/monetarie_web/router.ex` (self scope bare, junto da linha 3281): `get "/limits", LimitsController, :show_self`
- Test: `core/backend/test/monetarie_web/controllers/v2/limits_controller_test.exs`

**Step 1: Teste RED**

```elixir
defmodule MonetarieWeb.V2.LimitsControllerTest do
  use MonetarieWeb.ConnCase, async: false

  test "devolve limites efetivos de pix_out e ted em CENTAVOS + uso do dia", %{conn: conn} do
    m = insert_merchant!()
    bank = insert_bank!()
    account = insert_account!(m.id, bank.id, 1)

    # override de conta em SUBCENT (fonte real do motor): noturno R$ 500,00
    account
    |> Ecto.Changeset.change(%{nighttime_limit: 5_000_000, pix_out_transaction_limit: 100_000_000})
    |> Repo.update!()

    body = conn |> merchant_conn(m.id) |> get("/api/limits") |> json_response(200)

    pix = body["data"]["pixOut"]
    assert pix["nighttimeLimit"] == 50_000          # centavos
    assert pix["transactionLimit"] == 1_000_000
    assert is_integer(pix["dailyLimit"])
    assert is_integer(pix["dailyUsed"])
    assert is_integer(pix["monthlyUsed"])

    ted = body["data"]["ted"]
    assert is_integer(ted["transactionLimit"])

    assert body["data"]["nighttimeWindow"] == %{"start" => "20:00", "end" => "06:00"}
  end

  test "sem conta responde 404", %{conn: conn} do
    m = insert_merchant!()
    conn |> merchant_conn(m.id) |> get("/api/limits") |> json_response(404)
  end
end
```

**Step 2:** FAIL.

**Step 3: Implementação** - `LimitCheck.usage/1` público delegando à query privada existente (renomear a privada, manter chamada interna). Controller:

```elixir
defmodule MonetarieWeb.V2.LimitsController do
  use Phoenix.Controller, formats: [:json]

  import Ecto.Query, only: [from: 2]

  alias Monetarie.Repo
  alias Monetarie.Schemas.Relational.Account
  alias Monetarie.UseCases.Limits.{LimitCheck, TransactionLimits}
  alias MonetarieWeb.Auth.Guardian

  def show_self(conn, _params) do
    user = Guardian.Plug.current_resource(conn)
    user_id = String.to_integer(to_string(user.id))

    case Repo.one(from(a in Account, where: a.user_id == ^user_id and a.kind > 0, order_by: a.id, limit: 1)) do
      nil ->
        conn |> put_status(:not_found) |> json(%{error: %{status: 404, message: "Conta nao encontrada"}})

      account ->
        {daily_used, monthly_used} = LimitCheck.usage(account.id)

        json(conn, %{
          data: %{
            accountId: account.id,
            currency: "BRL",
            pixOut: flow(TransactionLimits.resolve(account.id, :pix_out), daily_used, monthly_used),
            ted: flow(TransactionLimits.resolve(account.id, :ted), daily_used, monthly_used),
            nighttimeWindow: %{start: "20:00", end: "06:00"}
          }
        })
    end
  end

  defp flow(limits, daily_used, monthly_used) do
    %{
      transactionLimit: cents(limits.transaction_limit),
      dailyLimit: cents(limits.daily_limit),
      monthlyLimit: cents(limits.monthly_limit),
      nighttimeLimit: cents(limits.nighttime_limit),
      dailyUsed: cents(daily_used),
      monthlyUsed: cents(monthly_used)
    }
  end

  defp cents(nil), do: nil
  defp cents(v) when is_integer(v), do: div(v, 100)
end
```

Nota honesta: `usage/1` soma TODO outbound (pix+ted juntos), exatamente como o motor conta no gate (`check_daily_monthly` usa a mesma query). É a verdade do enforcement; não separar por tipo aqui.

**Step 4:** `mix test test/monetarie_web/controllers/v2/limits_controller_test.exs test/monetarie/use_cases/limits/` - PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie/use_cases/limits/limit_check.ex core/backend/lib/monetarie_web/controllers/v2/limits_controller.ex core/backend/lib/monetarie_web/router.ex core/backend/test/monetarie_web/controllers/v2/limits_controller_test.exs
git commit -m "feat(core): GET /limits real (limites efetivos + uso, centavos na borda)"
```

---

## Task 12: Front de limites (IB + merchant)

**CONTRATO PROVADO:** C6. Merchant `PixLimitsView.vue:67` já chama `GET /limits` e parseia o shape novo (camel/snake). IB `LimitsView.vue` e `PixLimitsView.vue` estáticas.

**Files:**
- Modify: `core/apps/banking/src/views/pix/PixLimitsView.vue` (buscar `GET /limits`, exibir transaction/daily/monthly/nighttime + uso do dia com barra de progresso; manter o texto explicativo)
- Modify: `core/apps/banking/src/views/LimitsView.vue` (linhas PIX/TED passam a exibir os valores reais do endpoint; linhas regulatórias informativas de boleto/horários continuam i18n)
- Modify: `core/apps/merchant/src/views/pix/PixLimitsView.vue` (remover o fallback `POST /pix/fee-preview` que mascara erro; exibir também `ted`)
- i18n dos dois apps

**Steps 1-4:** vitest RED primeiro para o IB (novo `src/views/pix/__tests__/PixLimitsView.test.ts` no padrão dos view-tests existentes: mock api devolvendo o shape da task 11, assert de que os valores formatados aparecem e de que erro de rede mostra estado de erro, nunca números inventados). Implementar, `pnpm --filter @monetarie/banking test:run` e `--filter @monetarie/merchant-portal test:run` PASS.

**Step 5: Commit**

```bash
git add core/apps/banking/src core/apps/merchant/src
git commit -m "feat(front): limites reais visiveis no IB e no merchant (fim das telas estaticas)"
```

---

## Task 13: TEF real (`/transfers/between-accounts`)

**CONTRATO PROVADO:** C7. Handler de referência: partner `internal/2` -> `OutboundOrchestrator` `%Tef{}`; o orchestrator faz o book transfer com trava e sem gate de limites. Sonda G1 desta task (obrigatória no início): grep no front por `between-accounts` nos dois apps para capturar o payload EXATO que as telas mandam (nomes de campos) e ajustar os aliases abaixo.

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/v2/merchant_portal_controller.ex` (`create_between_accounts_transfer`)
- Modify: `core/backend/test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs`

**Step 1: Teste RED**

```elixir
test "TEF entre contas do MESMO usuario roda pelo orchestrator", %{conn: conn} do
  m = insert_merchant!()
  bank = insert_bank!()
  source = insert_account!(m.id, bank.id, 1)
  dest = insert_account!(m.id, bank.id, 1)

  # seam de orchestrator (mesmo padrao do partner :internal_transfer_fun):
  prev = Application.get_env(:monetarie, :merchant_portal_internal_transfer_fun)
  test_pid = self()
  Application.put_env(:monetarie, :merchant_portal_internal_transfer_fun, fn params ->
    send(test_pid, {:orchestrator, params})
    %Monetarie.UseCases.Payments.OutboundResult{status: :accepted, type: "tef", transaction_id: "TEF1", amount: params.common.amount}
  end)
  on_exit(fn -> restore_env(:merchant_portal_internal_transfer_fun, prev) end)

  body =
    conn
    |> merchant_conn(m.id)
    |> post("/api/transfers/between-accounts", %{
      "sourceAccountId" => source.id, "destinationAccountId" => dest.id, "amount" => 2_500
    })
    |> json_response(202)

  assert body["status"] == "accepted"
  assert_received {:orchestrator, params}
  assert params.common.type == "tef"
  assert params.common.amount == 250_000          # centavos -> base units
  assert params.tef.destination_account_id == to_string(dest.id)
end

test "TEF para conta de OUTRO usuario e 404 e nao chama o orchestrator", %{conn: conn} do
  # destino de outro merchant -> 404; refute_received {:orchestrator, _}
end
```

**Step 2:** FAIL (501 hoje).

**Step 3: Implementação** - espelhar `partner internal/2` (validações: amount centavos > 0 -> `MoneyUnit.from_cents`; source e destination pertencem ao `merchant_id` autenticado; source != destination) e montar `%OutboundPaymentParams{common: %Common{type: "tef", amount: base_units, merchant_id: to_string(merchant_id), current_user: %{id: merchant_id}, description: params["description"]}, tef: %Tef{destination_account_id: to_string(dest.id)}}`. Seam `Application.get_env(:monetarie, :merchant_portal_internal_transfer_fun, &OutboundOrchestrator.execute/1)`. Renderizar `OutboundResult` (202 accepted / 422 failed com errors), copiando `render_internal_result` do partner como referência. IMPORTANTE (G1 da task): copiar a construção EXATA dos structs de `partner_v1/transfers_controller.ex:513-526`, não digitar de memória.

**Step 4:** `mix test test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs` - PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/v2/merchant_portal_controller.ex core/backend/test/monetarie_web/controllers/v2/merchant_portal_controller_test.exs
git commit -m "feat(core): TEF entre contas do usuario pela tela (orchestrator real, fim do 501)"
```

---

## Task 14: Trilho único do PIX Automático (adapter fail-loud, fim do publish para DLQ)

**CONTRATO PROVADO:** C4. O trilho VIVO é o Grupo B (`PixAutomaticoController` -> `UseCases.PixAutomatico` -> `monetarie.spi.recurrence.execute` -> `ExecutionWorker`), e é o ÚNICO que as telas chamam (grep provado nos dois apps). O Grupo A (`PixDomainController` setup/initiate/authorize/mandates) e os `send_camt*/send_pibr001/send_reda` do adapter publicam sem `event` -> DLQ com 202 mentiroso.

**Sonda G1 da task (primeiro passo, antes do RED):** `grep -rn "Provider.setup_recurrence\|Provider.initiate_payment\|Provider.authorize_payment\|Provider.setup_mandate\|Provider.cancel_mandate\|Provider.send_camt\|Provider.send_pibr\|Provider.send_reda\|Provider.send_pix\|Provider.respond_pix\|Provider.return_funds" core/backend/lib` e registrar os chamadores no commit. Esperado: só `pix_domain_controller.ex` (e nenhum front). Se aparecer chamador vivo de verdade (tela/worker), PARAR e escalar: esse chamador precisa de rota própria, não deste tratamento.

**Files:**
- Modify: `core/backend/lib/monetarie/services/pix_providers/in_house/adapter.ex`
- Test: `core/backend/test/monetarie/services/pix_providers/in_house/adapter_dlq_test.exs` (novo)

**Step 1: Teste RED**

```elixir
defmodule Monetarie.Services.PixProviders.InHouse.AdapterDlqTest do
  use Monetarie.DataCase, async: true

  alias Monetarie.Services.PixProviders.InHouse.Adapter

  # Classe de defeito: publicar em monetarie.core.pix.* SEM campo "event" =
  # unrouted na cabine = DLQ com 202 mentiroso ao usuario. Estes caminhos NAO
  # tem consumidor (CoreEventProcessor roteia por "event"; nao existe evento de
  # recorrencia/mandato/camt/pibr/reda). O adapter responde honesto.
  for {fun, args} <- [
        setup_recurrence: [1, %{}, %{}],
        initiate_payment: [1, %{}, %{}],
        authorize_payment: [1, %{}, %{}],
        setup_mandate: [1, %{}, %{}],
        cancel_mandate: [1, %{}, %{}],
        send_camt060: [1, %{}, %{}],
        send_camt055: [1, %{}, %{}],
        send_camt029: [1, %{}, %{}],
        send_pibr001: [1, %{}, %{}]
      ] do
    test "#{fun} nao publica para a DLQ: responde not_supported" do
      assert {:error, :not_supported} = apply(Adapter, unquote(fun), unquote(Macro.escape(args)))
    end
  end

  test "send_reda idem" do
    assert {:error, :not_supported} = Adapter.send_reda(1, "017", %{}, %{})
  end
end
```

**Step 2:** FAIL (hoje devolvem `{:ok, %{status: "accepted"...}}` enfileirando para a DLQ).

**Step 3: Implementação** - substituir os corpos por `{:error, :not_supported}` com comentário apontando o trilho vivo:

```elixir
# ── Recorrencia/mandato/camt/pibr/reda: SEM consumidor na cabine ─────────────
# O CoreEventProcessor roteia pelo campo "event" e NAO existe evento para estes
# assuntos: publicar aqui era 202 mentiroso + DLQ (item 7 do mapeamento
# 2026-07-17). O trilho VIVO do PIX Automatico e o UseCases.PixAutomatico
# (V2.PixAutomaticoController -> monetarie.spi.recurrence.execute). Quando a
# cabine ganhar consumidor real para camt/pibr/reda via provider, reabrir.
@impl true
def setup_recurrence(_entity_id, _params, _config), do: {:error, :not_supported}
# ... idem para os demais 9
```

`send_pix/respond_pix/return_funds`: pela sonda, se os únicos chamadores forem os controllers cujo funil real é o OutboundOrchestrator (nenhuma tela usa), aplicar o MESMO tratamento; caso a sonda mostre uso vivo, deixar intactos e registrar no fecho da task. `PixDomainController` já mapeia `:not_supported` -> 501 (`provider_error`, `pix_domain_controller.ex:287-291`), então as rotas do Grupo A passam a responder 501 honesto sem tocar no controller.

**Step 4:** `mix test test/monetarie/services/pix_providers/in_house/adapter_dlq_test.exs` - PASS. Regressão focada: `mix test test/monetarie_web/controllers/v2/` (nenhum teste existente depende do publish fantasma; se algum quebrar, analisar antes de tocar).

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie/services/pix_providers/in_house/adapter.ex core/backend/test/monetarie/services/pix_providers/in_house/adapter_dlq_test.exs
git commit -m "fix(core): trilho unico do PIX Automatico; adapter nunca mais publica sem event para a DLQ"
```

---

## Task 15: Rotas v1 `/pix/out` pelo funil real e `/pix/in` honesta

**CONTRATO PROVADO:** C4 + C8. Hoje: 202 mentiroso + linha presa em processing + 2 publishes sem `event` -> DLQ (out) e 1 (in). Não existe trilho legítimo para o Core ORIGINAR um PIX-in (entrada nasce no BACEN); para out, o funil real é o `OutboundOrchestrator` (com kill switches, limites, hold), montagem de referência em `v2/transfer_controller.ex:257-307`.

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/pix_controller.ex` (`pix_in`, `pix_out`)
- Test: `core/backend/test/monetarie_web/controllers/pix_controller_v1_routes_test.exs` (novo)

**Step 1: Teste RED**

```elixir
defmodule MonetarieWeb.PixControllerV1RoutesTest do
  use MonetarieWeb.ConnCase, async: false

  # conn autenticada do pipeline v1 (copiar o padrao de autenticacao dos testes
  # v1 existentes deste controller/pipeline; sonda G1: ver como o teste de
  # /pix/status autentica, se existir, senao usar merchant_conn se o pipeline
  # aceitar Guardian igual)

  test "POST /api/v1/pix/in responde 410 e NAO cria transacao nem publica", %{conn: conn} do
    body =
      conn
      |> authed_v1()
      |> post("/api/v1/pix/in", %{"amount" => 100, "payer_name" => "X"})
      |> json_response(410)

    assert body["reason"] == "pix_in_route_removed"
    assert Repo.aggregate(Monetarie.Schemas.Relational.Transaction, :count, :id) == 0
  end

  test "POST /api/v1/pix/out roda pelo OutboundOrchestrator (nunca mais publish sem event)", %{conn: conn} do
    # seam identico ao da task 13 (:v1_pix_out_orchestrator_fun), assert de
    # params.common.type == "pix" e amount em base units; resposta 202 com
    # transactionId/endToEndId do OutboundResult
  end
end
```

Sonda G1 da task: conferir o prefixo real da rota (`router.ex` linha ~240-295: qual scope monta `/pix/in`; ajustar o path do teste ao pipeline verdadeiro, incluindo MFA/idempotência se o pipeline exigir headers).

**Step 2:** FAIL.

**Step 3: Implementação**

```elixir
def pix_in(conn, _params) do
  conn
  |> put_status(:gone)
  |> json(%{
    status: "error",
    reason: "pix_in_route_removed",
    message:
      "PIX de entrada nasce no BACEN e e creditado pelo trilho da cabine (InboundProcessor). " <>
        "Esta rota publicava para um assunto sem consumidor. Em homologacao, use o simulador para gerar PIX-in."
  })
end
```

`pix_out/2`: manter validação de amount (centavos) e resolução de conta atuais; substituir os DOIS `publish_async` + `PaymentTransactions.create` pela montagem de `%OutboundPaymentParams{}` COPIADA de `create_pix_account` (`v2/transfer_controller.ex:268-305`), com seam `Application.get_env(:monetarie, :v1_pix_out_orchestrator_fun, &OutboundOrchestrator.execute/1)`, mapeando os nomes v1 (`recipient_key`, `recipient_key_type`, `recipient_name`, `recipient_document`, `recipient_ispb`, `description`). Resposta: 202 com `transaction_id`/`end_to_end_id`/`amount` (centavos) do `OutboundResult` no shape ATUAL do v1 (snake_case), 422 para failed com os erros do result.

**Step 4:** `mix test test/monetarie_web/controllers/pix_controller_v1_routes_test.exs` - PASS.

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/pix_controller.ex core/backend/test/monetarie_web/controllers/pix_controller_v1_routes_test.exs
git commit -m "fix(core): v1 /pix/out pelo funil real; /pix/in vira 410 honesto (fim do 202 + DLQ)"
```

---

## Task 16: Gate de regressão da frente

**Files:** nenhum novo (só execução).

**Step 1:** Backend focado da frente:

```bash
cd core/backend && mix test \
  test/monetarie/use_cases/pix/qr_generation_test.exs \
  test/monetarie/use_cases/pix/charge_paid_test.exs \
  test/monetarie/use_cases/spb/scheduled_ted_test.exs \
  test/monetarie/services/pix_providers/in_house/adapter_dlq_test.exs \
  test/monetarie_web/controllers/v2/ \
  test/monetarie_web/controllers/partner_v1/transfers_controller_test.exs \
  test/monetarie_web/controllers/ted_controller_hold_test.exs \
  test/monetarie_web/controllers/pix_controller_v1_routes_test.exs
```

Expected: 0 falhas.

**Step 2:** Fronts: `cd core && pnpm --filter @monetarie/shared build && pnpm --filter @monetarie/banking test:run && pnpm --filter @monetarie/merchant-portal test:run && pnpm --filter @monetarie/banking exec vite build && pnpm --filter @monetarie/merchant-portal exec vite build`. Expected: 0 falhas, builds verdes.

**Step 3:** Suite COMPLETA do core roda no gate de merge pelo ORQUESTRADOR (serial), não aqui. GOTCHA de ambiente: túnel SSM em `localhost:15432` sombreia o PG de teste (`lsof -i :15432` antes de rodar).

**Step 4:** Revisão adversarial da frente inteira (revisor fresh, diff completo da worktree) com foco em: unidade nas fronteiras (C9), espelho `qrcodes` nunca criado em conta de outro usuário, nenhum 2xx mascarando erro de cabine, nenhuma rota nova fora dos pipelines autenticados.

**Step 5: Commit final de docs da frente (se houver ajustes).**

---

## Task 17: VALIDAÇÃO VIVA em HML pelas telas (definition of done da frente)

Regra do dono: tela só é validada com SCREENSHOT e zero erro; fixture de caminho feliz nunca valida tela. Executar APÓS o orquestrador deployar a frente em HML (core-api + rebuild dos 2 front UIs). PRD é proibido nesta frente.

**Preparação:**
- Túnel SSM: `awsmon ssm start-session --target i-02ce3a3b6b3ad0d37 --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters '{"host":["internal-monetarie-internal-homolog-45-2129766573.sa-east-1.elb.amazonaws.com"],"portNumber":["80"],"localPortNumber":["18080"]}'` (o ALB roteia por Host header: `ib-h.monetarie.internal`, `merchant-h.monetarie.internal`, `coreapi-h.monetarie.internal`).
- Login IB de teste (usuário do time em HML; senha do reset de 07-02 ou do Secrets Manager, NUNCA gravada neste doc).
- Playwright MCP para navegar/capturar; screenshots em `docs/reports/screenshots/2026-07-18-w1-f2-telas/` com nome `NN-descricao.png`.
- Vigia de DLQ (leitura): na EC2 NATS via SSM, `docker run --rm --network host natsio/nats-box nats stream info MONETARIE_DLQ` ANTES e DEPOIS de cada bateria (a contagem NÃO pode subir por causa das telas).

**Roteiro (cada item = screenshot + anotação do resultado):**
1. IB > PIX > Receber: gerar QR COM valor (ex.: R$ 1,23). Evidência: imagem renderizada + brcode copiado; decodificar o brcode (CRC valida; PoIM 12; URL 26/25) com o decoder do repo ou zxing local. Conferir espelho: `SELECT tx_id, status, amount FROM qrcodes ORDER BY inserted_at DESC LIMIT 1` via rpc read-only no core-api (ECS exec).
2. Pagar a cobrança do item 1 (simulador HML de PIX-in pagando o txid, ou celular real se disponível). Confirmar: tela do merchant/qrcodes mostra status Paga; webhook `pix.charge.paid` entregue ao receptor de teste; `qrcodes.status='paid'` com `payment_end_to_end_id`.
3. IB > Receber SEM valor: QR estático de valor em aberto (decoder: sem tag 54, PoIM 11).
4. IB > Copia e cola: colar um brcode VÁLIDO (o do item 3) -> preview com nome/chave; continuar -> confirmar -> PIX de R$ 0,01 real pelo funil (esperado: rejeição AB09 do recebedor homolog é ACEITÁVEL como prova de trilho, igual 16/07; hold devolvido). Colar um brcode com CRC corrompido -> mensagem de QR inválido (screenshot).
5. IB > TED: enviar com data futura em dia útil -> resposta "agendada" na tela; `SELECT * FROM scheduled_teds ORDER BY inserted_at DESC LIMIT 1` (status pending, SEM linha em transactions, saldo INTOCADO no extrato). Cancelar pela tela de agendadas -> status cancelled. Data no passado -> recusa legível.
6. IB > Limites e merchant > Limites: valores reais na tela; conferir 1 valor ao centavo contra `SELECT nighttime_limit, pix_out_transaction_limit FROM accounts WHERE id=...` (lembrar: banco em subcent, tela em reais).
7. Merchant > TEF entre contas próprias: R$ 0,50 entre duas contas do mesmo usuário -> extrato dos dois lados; tentativa com conta de terceiro -> recusa.
8. PIX Automático (IB): criar recorrência pela tela (trilho B) -> aparece na lista; monitorar `MONETARIE_DLQ` (não pode entrar NADA novo de `core-event`/unrouted durante a bateria inteira).
9. v1: `POST /api/v1/pix/in` via curl autenticado -> 410; `POST /api/v1/pix/out` de R$ 0,01 -> trilho real (mesma prova do item 4).
10. Fechar: contagem da DLQ igual ou menor que o início; zero erro no console do browser nas telas tocadas (validador do Playwright); redigir o fecho da frente com a tabela evidência por item.

**Commit:** screenshots + fecho em `docs/reports/2026-07-18-w1-f2-validacao-viva-hml.md`.

---

## Riscos e conflitos declarados

- **`router.ex` compartilhado com F1** (tasks 6, 10, 11): F2 só toca as regiões v2 (~3100-3370); merge do router é do orquestrador.
- **`partner_v1/transfers_controller.ex`** (task 9): F1 pode adicionar a rota de devolução de TED no mesmo arquivo; sequenciar com o orquestrador.
- **`v2/pix_controller.ex`** (tasks 2 e 6): mesmo arquivo em 2 tasks desta frente; executar em série, nunca em paralelo.
- **`merchant_portal_controller.ex`** (tasks 3 e 13): idem, série.
- Teste existente de 501 do `create_pix_charge` muda de contrato (task 3): o revisor de spec deve conferir que a regra "nunca simular sucesso" foi preservada (erro de cabine = 502).
- `ChargePaid` exige crédito liquidado antes de marcar pago: na validação viva, o webhook pode chegar com segundos de atraso (NAK/reentrega). Não é defeito.
- Front merchant hoje manda QR em REAIS (task 5): até o deploy conjunto back+front, o back novo interpretaria reais como centavos. Deploy da frente é ATÔMICO (back + os 2 fronts na mesma janela).
