# Dossie de paridade PROFUNDO — Tarifas (fee) — PIX

Auditoria read-only, zero inferencia. Cada afirmacao tem prova arquivo:linha ou tabela.coluna.
Onde nao ha prova suficiente, o veredito e INCONCLUSIVO.

- Legado decompilado (.NET): `/Users/luizpenha/monetarie/.scratch/legado-pix-decompiled/*.decompiled.cs`
- Legado DB vivo (SQL Server, container `monetarie-bak-mssql`, DB `crk_spi`)
- Legado scripts SQL / Angular: `/Users/luizpenha/monetarie/LegadoPIX/Pix/...`
- Nosso backend Elixir: `/Users/luizpenha/monetarie/pix/backend/apps/{settlement_service,shared}`
- Nosso frontend Vue: `/Users/luizpenha/monetarie/pix/frontend/admin/src`

---

## 1. Resumo executivo

O conceito de "Tarifa" nos dois sistemas tem o MESMO nome mas modela coisas DIFERENTES.

- **LEGADO**: tarifa e o CUSTO INTERBANCARIO do SPI/BACEN por VOLUME de mensagens ISO, por
  TIPO DE MENSAGEM (CdMsg = PACS.008, PACS.004, CAMT.014, ADMI.002...), calculada por BLOCO
  de mensagens (NrQtdeMsgs mensagens por bloco) sobre o AGREGADO DIARIO por (IF, CdMsg, dia).
  E um MONITOR/relatorio: le as mensagens reais (`MessageFull`), calcula on-demand, NAO cobra
  cliente, NAO gera lancamento contabil, NAO persiste "fee_entry". NAO existe conceito de isencao.
  Prova: `SPI.Core.Angular.Infrastructure.decompiled.cs:2377-2395` (calculo por bloco),
  `SpiCadParamTarifa` (`NrQtdeMsgs`, `VlrTarifa`, `DtInicioVigencia`) x `SpiCadParamTarifaXMsg` (`CdMsg`).

- **NOSSO**: tarifa e a TARIFA POR TRANSACAO cobrada do cliente, por FAIXA DE VALOR
  (`min_amount`/`max_amount` em centavos) e por `transaction_type` (PIX_TRANSFER, PIX_QR_*,
  PIX_SAQUE, PIX_TROCO), tipos FIXED/PERCENTAGE/MIN_MAX/TIERED, com ISENCOES por ISPB
  (FULL/PARTIAL/CAPPED) e ciclo de vida de cobranca (PENDING/COLLECTED/WAIVED/REFUNDED).
  Prova: `apps/shared/lib/shared/schemas/settlement/fee.ex` (FeeRule.calculate_fee,
  FeeExemption.apply_exemption), `mon_pix_schema_clean.sql:3316` (`fee_rules.transaction_type`,
  `min_amount`/`max_amount`).

Consequencia: o recurso central do legado (monitor de custo interbancario por TIPO DE MENSAGEM
com calculo por BLOCO) esta AUSENTE no nosso sistema; e o modelo do nosso sistema (cobranca por
faixa de valor + isencoes) esta AUSENTE no legado. Alem disso, no nosso sistema a cobranca por
transacao NAO esta ligada ao money-path (funcoes de calculo/isencao nao sao chamadas em codigo
de producao; o endpoint de isencao retorna 501).

---

## 2. LEGADO — comportamento com prova

### 2.1 Modelo de dados

DDL: `/Users/luizpenha/monetarie/LegadoPIX/Pix/SPI/Database-Inicial/Base CRK_SPI/Tabelas/Evolucao/20200714_Tarifas.sql`

```
SpiCadParamTarifa(
  IdParametroTarifa INT IDENTITY PK,
  DtInicioVigencia  DATE NOT NULL,     -- vigencia (data de inicio)
  NrQtdeMsgs        INT  NOT NULL,     -- tamanho do BLOCO (qtde de mensagens por bloco)
  VlrTarifa         NUMERIC(14,5) NOT NULL  -- preco por BLOCO
)
SpiCadParamTarifaXMsg(
  IdParametroTarifa INT NOT NULL,
  CdMsg VARCHAR(10) NOT NULL,          -- tipo de mensagem ISO (PACS.008, CAMT.014, ...)
  PK (IdParametroTarifa, CdMsg),
  FK -> SpiCadParamTarifa ON DELETE CASCADE
)
```

Entidades no dominio: `SPI.Core.Domain.decompiled.cs:817-828` (`ParametroTarifa`:
IdParametroTarifa, DtInicioVigencia, NrQtdeMsgs, VlrTarifa, `List<TarifaXMsg> Msgs`) e
`SPI.Core.Domain.decompiled.cs:1204-1208` (`TarifaXMsg`: IdParametroTarifa, CdMsg).
Config EF: `SPI.Core.Angular.Infrastructure.decompiled.cs:5911-5926` (ToTable "SpiCadParamTarifa",
VlrTarifa required, DtInicioVigencia DATE).

**Nao ha coluna de faixa de VALOR, nem de transaction_type, nem de isencao, nem de status.**

### 2.2 Dados vivos (crk_spi.dbo) — confirmam o modelo

`SpiCadParamTarifa` = 25 linhas, `SpiCadParamTarifaXMsg` = 37 linhas (COUNT vivo).
Amostra (IdParametroTarifa | DtInicioVigencia | NrQtdeMsgs | VlrTarifa):

```
1    2020-07-01  12  0.25000
5    2020-07-17  10  0.01000
3    2020-07-19  20  0.55000
6    2020-11-02  11  1.00000
1007 2020-11-18   1 10.00000
1017 2021-10-20  20  0.05000
1019 2022-11-03  10  0.03000
1020 2024-01-10  10  0.10000
```

Mapeamento CdMsg (amostra): `1 -> PACS.008`; `2 -> PACS.004, PACS.008`;
`3 -> PACS.002, PACS.004, PACS.008, PIBR.001`; `4 -> CAMT.053, CAMT.054, CAMT.060`;
`1007 -> ADMI.002, ADMI.004, CAMT.014`; `1019 -> CAMT.060`.
Confirma: a tarifa e por TIPO DE MENSAGEM ISO, com bloco variando por vigencia.

### 2.3 Calculo (o coracao da regra) — por BLOCO, agregado diario

`SPI.Core.Angular.Infrastructure.decompiled.cs:2358-2374` (`FiltroTarifa`): agrupa as mensagens
reais (`MessageFull`, base corrente + historico) por `{ IdInstFinanc, CdMsg, DtMovto }` e conta
`QtdeMsgs = d.Count()`.

`SPI.Core.Angular.Infrastructure.decompiled.cs:2377-2395` (`ComplementaListaParcialTarifaDiaria`):

```
tarifaAplicar = params where DtInicioVigencia <= DtMovto and CdMsg == d.CdMsg
                orderby DtInicioVigencia DESCENDING  -> FirstOrDefault()   // vigencia mais recente vale
QtdeBlocos     = Trunc(QtdeMsgs / NrQtdeMsgs)          // divisao inteira (blocos exatos)
QtdeMsgsResto  = Trunc(QtdeMsgs % NrQtdeMsgs)          // resto
VlrBlocosMsgs  = QtdeBlocos * VlrTarifa
VlrResto       = QtdeMsgsResto * (VlrTarifa / NrQtdeMsgs)   // fracao do bloco pelo resto
VlrTotal       = VlrBlocosMsgs + VlrResto                    // SPI.Core.Angular.Application:7048-7069
```

Doc dos campos: `SPI.Core.Angular.Application.decompiled.cs:7013-7069` (`ParcialTarifaDiaria`).
`TarifaAplicar` (projeção usada no calculo): `SPI.Core.General.decompiled.cs:14397-14405`
(DtInicioVigencia, NrQtdeMsgs, VlrTarifa, CdMsg).

Resolucao de vigencia (leitura): `ListAtivasParaValidacao` projeta 1 `TarifaAplicar` por
(param x CdMsg); a escolha por dia usa DtInicioVigencia <= DtMovto mais recente
(`SPI.Core.Angular.Infrastructure.decompiled.cs:2381-2384`). Sem `DtFimVigencia` (aberta).

### 2.4 Natureza: MONITOR/relatorio, NAO cobranca

`List`/`ListPaged` leem de `MessageFull` (corrente) UNION `MessageFull` (historico) e calculam
on-demand: `SPI.Core.Angular.Infrastructure.decompiled.cs:2398-2438`. Agregados de tela:
`VlrTotalGeral`, `VlrTotalPorBlocos` (linhas 2435-2438). **Nao ha INSERT de fee_entry, nao ha
debito de conta de cliente, nao ha lancamento contabil.** (Confirmado tambem no mapa de contexto
`12-verificacao-antifraude-dict-contabil.md:132` "Tarifas: nenhum hook contabil".)

### 2.5 Validacoes de cadastro (Gravar)

`SPI.Core.Angular.Infrastructure.decompiled.cs:1348-1405`:
- `!item.Msgs.Any()` -> "Informe ao menos uma mensagem."
- `VlrTarifa <= 0` -> "Informe o valor da tarifa."
- `DtInicioVigencia == MinValue/MaxValue` -> "Data invalida."
- `NrQtdeMsgs <= 0` -> "Informe a quantidade de mensagens a agrupar."
- `IdParametroTarifa < 0` -> "Id da tarifa invalido na alteracao."
- Na ALTERACAO (Id>0): bloqueia se ja existe outro parametro com a MESMA DtInicioVigencia
  compartilhando alguma das mensagens: "Ja existe parametrizacao para a mesma data com alguma
  das mensagens selecionadas." (linha 1378).

### 2.6 API + UI

- `MonitorTarifaController` (Mensagens.Api): `SPI.Web.Angular.Mensagens.Api.decompiled.cs:491-542`
  (`ListPaged`, `ListPagedHist`, `List`).
- `ParametroController` (Web.Angular.Api): `SPI.Web.Angular.Api.decompiled.cs:1462-1493`
  (`listParamTarifas`, `manTarifa`).
- Angular: `LegadoPIX/Pix/SPI/angular/src/app/pages/spi/parametrizacoes/param_tarifas/edit/edit.component.ts`
  (form: txtInicioVigencia [required], txtQtdMensagem [min 0], txtValorTarifa [min 0.01],
  cboCdMsg [required, multi]; log de atividade cod 3031).

### 2.7 Nao ha isencao no legado (fee)

`grep -liE "isen[cç]|isento" *.decompiled.cs` retorna apenas Multiliquidacao (isentoIof,
isentoJurAdp/Saq/Con no CADASTRO DE CONTA de credito —
`Multiliquidacao.ValidacaoCredito.Infrastructure.decompiled.cs:5270`), que e isencao de
IOF/juros de CONTRATO DE CONTA, NAO de tarifa PIX. Nenhuma isencao no dominio de tarifa SPI.

---

## 3. NOSSO — comportamento com prova

### 3.1 Ha DOIS subsistemas de fee sobrepostos (ambos apontam para `fee_tables`)

**(A) FeeSchedule / FeeCalculation / FeeReport** — contexto `SettlementService.Fees`
(`apps/settlement_service/lib/settlement_service/fees.ex`).
- `FeeSchedule` mapeia `monetarie_settlement.fee_tables` com colunas
  fee_type/calculation_method/flat_fee/percentage_rate/min_fee/max_fee/min_amount/max_amount/
  participant_ispb/tiers/valid_from/valid_until
  (`apps/settlement_service/lib/settlement_service/fees/fee_schedule.ex:17-35`).
- Calculo por FAIXA DE VALOR + metodo flat/percentage/tiered:
  `fees.ex:330-360` (`calculate_fee_from_schedule`), `fees.ex:301-320` (`get_applicable_schedule`
  filtra por `min_amount`/`max_amount` e participant_ispb).
- Fee default quando sem schedule: 0.01% com minimo R$0,01 (`fees.ex:290-300`).
- Persiste `FeeCalculation` (por transacao de SESSAO de liquidacao) e emite evento NATS
  `monetarie.settlement.fees.calculated` (`fees.ex:33-75`).

**(B) Fee / FeeTable / FeeRule / FeeExemption** — schemas em
`apps/shared/lib/shared/schemas/settlement/fee.ex`.
- `FeeRule`: `min_amount`/`max_amount` (centavos), `fee_type` FIXED/PERCENTAGE/MIN_MAX/TIERED,
  `fee_value` (centavos ou basis points), `min_fee`/`max_fee`, `priority` (linhas ~205-230).
  Calculo: `FeeRule.calculate_fee` (FIXED/PERCENTAGE=amount*bp/10000/MIN_MAX/TIERED) (~262-285).
- `FeeTable`: name, status ACTIVE/INACTIVE/DRAFT/ARCHIVED, `effective_date`/`expiry_date`,
  institution_id, is_default (~150-195).
- `FeeExemption`: institution_ispb (8 chars), exemption_type FULL/PARTIAL/CAPPED,
  exemption_value, valid_from/valid_until; apply: FULL->0, PARTIAL->amount-(amount*bp/10000),
  CAPPED->min(amount,max) (~300-420).
- `Fee` (fee_entries): fee_amount, base_amount, fee_rate (bp), status
  PENDING/COLLECTED/WAIVED/REFUNDED, waive_changeset/collect_changeset/refund_changeset (~1-140).

### 3.2 DDL canonica (`apps/shared/priv/repo/sql/mon_pix_schema_clean.sql`)

- `fee_rules:3316` -> `fee_table_id, rule_id, transaction_type NOT NULL, fee_type,
  calculation_type, fixed_amount, percentage, min_amount, max_amount, tier_from, tier_to`.
  Dimensao = `transaction_type` (PIX/TED...) + FAIXA DE VALOR. **Nao ha coluna de tipo de
  mensagem ISO, nem NrQtdeMsgs/bloco.**
- `fee_exemptions:3275` -> `ispb, fee_type, transaction_type, exemption_type, percentage,
  max_amount, reason, effective_from, effective_until, status`.
- `fee_tables:3357` -> `table_id, name, effective_from, effective_until, status, is_default`.
- `fee_entries:3234` -> `entry_id, transaction_id, end_to_end_id, ispb, fee_type, fee_amount,
  transaction_amount, fee_rule_id, status(pending), billed_at, paid_at`.
- `fee_calculations`/`fee_reports`: `apps/shared/priv/repo/migrations/20260618120000_create_settlement_runtime_tables.exs:229,256`.

Nota de colisao de schema: `FeeSchedule` e `FeeTable` mapeiam a MESMA tabela `fee_tables` com
conjuntos de colunas diferentes; a migration `20260617194000_align_settlement_fee_schema.exs`
adiciona `effective_date/expiry_date/institution_id/...` e nao mapeia `transaction_type` no
Ecto FeeRule (a coluna existe no DB com DEFAULT 'PIX' mas o schema Ecto a ignora —
`fee.ex` FeeRule nao tem campo transaction_type).

### 3.3 Wiring no money-path — AUSENTE/manual

- `SettlementService.Fees.calculate_fees/apply_fee_schedule/generate_fee_report` sao chamadas
  APENAS pelo `FeeController` (API manual):
  `apps/settlement_service/lib/settlement_service_web/controllers/fee_controller.ex:21,29,56`.
  `calculate_fees(session_id)` opera sobre `Sessions.Transaction` (transacoes de SESSAO de
  liquidacao, status :pending), NAO sobre a transacao PIX real do cliente (`fees.ex:36-45`).
- `FeeRule.calculate_fee`, `FeeExemption.apply_exemption`, `Fee.changeset`, `waive_changeset`,
  `collect_changeset` (subsistema B) NAO tem NENHUM chamador em codigo de producao
  (grep em apps/*/lib excluindo testes e as proprias definicoes = 0 resultados uteis).
- `FeeExemptionController` retorna 501 Not Implemented para TODAS as acoes:
  `apps/settlement_service/lib/settlement_service_web/controllers/fee_exemption_controller.ex:4-13`.
- Rotas: `router.ex:233-256` (`/fees/schedules`, `/fees/tables`, `/fees/calculate`,
  `/fees/sessions/:id/calculate`, `/fees/reports`, `/fees/exemptions`).

### 3.4 Seeds (dados semeados)

`apps/shared/priv/repo/seeds.exs:815-895` cria 1 `FeeTable` "PIX Default Fee Table - Seed"
(effective_date 2026-01-01, is_default) + 5 `FeeRule` por FAIXA DE VALOR:
0..R$100 FIXED R$0,10; R$100..500 0,15%; R$500..1000 MIN_MAX 0,10% [R$0,50..R$1,00];
R$1000..10000 0,08%; >R$10000 MIN_MAX 0,05% [R$0,50..R$10,00].
`seeds.exs:900-955` cria 2 `FeeExemption`: BACEN `00000000` FULL, MONETARIE `46026562` CAPPED
(valor 100). (Este subsistema semeado e o que NAO esta wired ao money-path.)

### 3.5 Frontend Vue

- `views/monitors/TariffMonitorView.vue`: CRUD de "tarifas" com `feeType` =
  transfer/qrcode/withdrawal/change/dict/billing e `amount` unico + effectiveDate/expirationDate/
  status; chama `GET/POST/PUT /api/v1/fees/schedules` (services/monitors.ts:95-124).
  **Defeito conhecido documentado no proprio codigo** (`TariffMonitorView.vue:38-42` e
  `services/monitors.ts:110-116`): o form manda camelCase (feeType, amount, effectiveDate,
  expirationDate, status) mas o changeset `FeeSchedule` so aceita snake_case
  (fee_type, flat_fee, active, valid_from, valid_until) e DESCARTA as demais chaves em silencio.
- `views/accounting/FeeManagementView.vue`: gestao de schedules/tables/calculadora com
  `transaction_type` = PIX_TRANSFER/PIX_QR_STATIC/PIX_QR_DYNAMIC/PIX_SAQUE/PIX_TROCO e tipos
  FIXED/PERCENTAGE/TIERED (linhas 33-56).
- `views/reports/FeeReportView.vue`: relatorio por participante/periodo (subsistema A).

**Nenhuma tela reproduz o monitor por TIPO DE MENSAGEM ISO com calculo por BLOCO do legado.**

---

## 4. GAPS (legado x nosso)

Ver objeto estruturado. Resumo:

- G1 (divergencia/alto): modelo de calculo — bloco-por-qtde-de-mensagem (legado) x faixa-de-valor (nosso).
- G2 (ausente/alto): tarifa por TIPO DE MENSAGEM ISO (CdMsg) inexistente no nosso.
- G3 (ausente/alto): monitor de custo interbancario diario (relatorio sobre mensagens reais) inexistente.
- G4 (divergencia/alto): proposito — custo interbancario/relatorio (legado) x cobranca do cliente (nosso).
- G5 (parcial/alto): cobranca por transacao nao ligada ao money-path (funcoes nao chamadas; endpoint 501).
- G6 (divergencia/medio): "faixa" = NrQtdeMsgs (bloco) x min/max_amount (valor).
- G7 (divergencia/medio): resolucao de vigencia (mais-recente aberta x janela effective/expiry).
- G8 (divergencia/medio): isencoes — legado nao tem; nosso tem mas nao wired.
- G9 (divergencia/medio): frontend TariffMonitor descarta campos (camelCase x snake_case).
- G10 (divergencia/medio): dois subsistemas de fee sobrepostos na mesma tabela (colisao/duplicidade).
- G11 (parcial/baixo): validacao de duplicidade de vigencia por mensagem ausente no nosso.
- G12 (parcial/baixo): precisao — VlrTarifa NUMERIC(14,5) e fracao do bloco (legado) x centavos/bp inteiros (nosso).

### Itens COBERTOS/parciais (confianca)
- C1: existe armazenamento de parametro de tarifa com vigencia e valor no nosso lado (fee_tables/fee_rules).
- C2: existe uma dimensao de "tipo" no nosso lado (transaction_type), embora taxonomia diferente do CdMsg.
- C3: existe UI administrativa de tarifas no nosso lado (FeeManagementView/TariffMonitorView).
- C4: existe conceito de vigencia no nosso lado (effective_date/expiry_date; valid_from/valid_until).
