focus_nfe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.git-hooks/pre_push/steep.rb +18 -0
- data/.git-hooks/pre_push/yard_doc.rb +18 -0
- data/.gitattributes +1 -0
- data/.overcommit.yml +69 -0
- data/.rspec +3 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +77 -0
- data/CLAUDE.md +118 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +348 -0
- data/Rakefile +105 -0
- data/data/schemas/schema_cte.json +2793 -0
- data/data/schemas/schema_cte_os.json +1335 -0
- data/data/schemas/schema_cte_os_transporte_rodoviario.json +109 -0
- data/data/schemas/schema_cte_transporte_aereo.json +115 -0
- data/data/schemas/schema_cte_transporte_aquaviario.json +174 -0
- data/data/schemas/schema_cte_transporte_dutoviario.json +65 -0
- data/data/schemas/schema_cte_transporte_ferroviario.json +144 -0
- data/data/schemas/schema_cte_transporte_multimodal.json +45 -0
- data/data/schemas/schema_cte_transporte_rodoviario.json +78 -0
- data/data/schemas/schema_dce.json +549 -0
- data/data/schemas/schema_mdfe.json +1102 -0
- data/data/schemas/schema_mdfe_transporte_aereo.json +44 -0
- data/data/schemas/schema_mdfe_transporte_aquaviario.json +209 -0
- data/data/schemas/schema_mdfe_transporte_ferroviario.json +99 -0
- data/data/schemas/schema_mdfe_transporte_rodoviario.json +628 -0
- data/data/schemas/schema_nfcom.json +1859 -0
- data/data/schemas/schema_nfe.json +4750 -0
- data/data/schemas/schema_nfe_forma_pagamento.json +97 -0
- data/data/schemas/schema_nfe_item.json +2574 -0
- data/data/schemas/schema_nfgas.json +2316 -0
- data/data/schemas/schema_nfse_nacional.json +1847 -0
- data/data/schemas/schema_nfse_recebida.json +548 -0
- data/lib/focus_nfe/client.rb +162 -0
- data/lib/focus_nfe/configuration.rb +104 -0
- data/lib/focus_nfe/errors.rb +123 -0
- data/lib/focus_nfe/esquemas/campo.rb +171 -0
- data/lib/focus_nfe/esquemas/catalogo.rb +34 -0
- data/lib/focus_nfe/esquemas/decimal.rb +66 -0
- data/lib/focus_nfe/esquemas/esquema.rb +72 -0
- data/lib/focus_nfe/esquemas/validador.rb +87 -0
- data/lib/focus_nfe/http/adapter.rb +25 -0
- data/lib/focus_nfe/http/adapters/net_http.rb +99 -0
- data/lib/focus_nfe/http/authentication.rb +23 -0
- data/lib/focus_nfe/http/connection.rb +118 -0
- data/lib/focus_nfe/http/logging.rb +100 -0
- data/lib/focus_nfe/http/response.rb +75 -0
- data/lib/focus_nfe/modelos/documento.rb +113 -0
- data/lib/focus_nfe/modelos/inutilizacao.rb +75 -0
- data/lib/focus_nfe/modelos/pagina.rb +54 -0
- data/lib/focus_nfe/recursos/backups.rb +17 -0
- data/lib/focus_nfe/recursos/base.rb +91 -0
- data/lib/focus_nfe/recursos/ceps.rb +16 -0
- data/lib/focus_nfe/recursos/cfops.rb +16 -0
- data/lib/focus_nfe/recursos/cnaes.rb +16 -0
- data/lib/focus_nfe/recursos/cnpjs.rb +13 -0
- data/lib/focus_nfe/recursos/concerns/baixavel.rb +41 -0
- data/lib/focus_nfe/recursos/concerns/baixavel_eventos.rb +34 -0
- data/lib/focus_nfe/recursos/concerns/cancelavel.rb +25 -0
- data/lib/focus_nfe/recursos/concerns/conciliavel.rb +66 -0
- data/lib/focus_nfe/recursos/concerns/consultavel.rb +26 -0
- data/lib/focus_nfe/recursos/concerns/corrigivel.rb +45 -0
- data/lib/focus_nfe/recursos/concerns/corrigivel_cte.rb +60 -0
- data/lib/focus_nfe/recursos/concerns/emitivel.rb +51 -0
- data/lib/focus_nfe/recursos/concerns/enviavel.rb +40 -0
- data/lib/focus_nfe/recursos/concerns/eventavel.rb +46 -0
- data/lib/focus_nfe/recursos/concerns/inutilizavel.rb +96 -0
- data/lib/focus_nfe/recursos/concerns/listavel.rb +22 -0
- data/lib/focus_nfe/recursos/concerns/localizavel.rb +22 -0
- data/lib/focus_nfe/recursos/concerns/notificavel.rb +23 -0
- data/lib/focus_nfe/recursos/concerns/removivel.rb +20 -0
- data/lib/focus_nfe/recursos/concerns/visualizavel.rb +28 -0
- data/lib/focus_nfe/recursos/cte.rb +35 -0
- data/lib/focus_nfe/recursos/cte_os.rb +29 -0
- data/lib/focus_nfe/recursos/ctes_recebidas.rb +38 -0
- data/lib/focus_nfe/recursos/dce.rb +16 -0
- data/lib/focus_nfe/recursos/emails_bloqueados.rb +31 -0
- data/lib/focus_nfe/recursos/empresas.rb +35 -0
- data/lib/focus_nfe/recursos/mdfe.rb +78 -0
- data/lib/focus_nfe/recursos/municipios.rb +60 -0
- data/lib/focus_nfe/recursos/ncms.rb +16 -0
- data/lib/focus_nfe/recursos/nfce.rb +19 -0
- data/lib/focus_nfe/recursos/nfcom.rb +16 -0
- data/lib/focus_nfe/recursos/nfe.rb +106 -0
- data/lib/focus_nfe/recursos/nfes_recebidas.rb +56 -0
- data/lib/focus_nfe/recursos/nfgas.rb +16 -0
- data/lib/focus_nfe/recursos/nfse.rb +18 -0
- data/lib/focus_nfe/recursos/nfse_nacional.rb +16 -0
- data/lib/focus_nfe/recursos/nfses_nacionais_recebidas.rb +21 -0
- data/lib/focus_nfe/recursos/webhooks.rb +22 -0
- data/lib/focus_nfe/version.rb +6 -0
- data/lib/focus_nfe/webhook.rb +68 -0
- data/lib/focus_nfe.rb +124 -0
- data/sig/focus_nfe/client.rbs +38 -0
- data/sig/focus_nfe/configuration.rbs +29 -0
- data/sig/focus_nfe/errors.rbs +59 -0
- data/sig/focus_nfe/esquemas/campo.rbs +47 -0
- data/sig/focus_nfe/esquemas/catalogo.rbs +8 -0
- data/sig/focus_nfe/esquemas/decimal.rbs +25 -0
- data/sig/focus_nfe/esquemas/esquema.rbs +30 -0
- data/sig/focus_nfe/esquemas/validador.rbs +20 -0
- data/sig/focus_nfe/http/adapter.rbs +7 -0
- data/sig/focus_nfe/http/adapters/net_http.rbs +25 -0
- data/sig/focus_nfe/http/authentication.rbs +9 -0
- data/sig/focus_nfe/http/connection.rbs +32 -0
- data/sig/focus_nfe/http/logging.rbs +30 -0
- data/sig/focus_nfe/http/response.rbs +28 -0
- data/sig/focus_nfe/modelos/documento.rbs +34 -0
- data/sig/focus_nfe/modelos/inutilizacao.rbs +24 -0
- data/sig/focus_nfe/modelos/pagina.rbs +21 -0
- data/sig/focus_nfe/recursos/backups.rbs +7 -0
- data/sig/focus_nfe/recursos/base.rbs +25 -0
- data/sig/focus_nfe/recursos/ceps.rbs +10 -0
- data/sig/focus_nfe/recursos/cfops.rbs +10 -0
- data/sig/focus_nfe/recursos/cnaes.rbs +10 -0
- data/sig/focus_nfe/recursos/cnpjs.rbs +7 -0
- data/sig/focus_nfe/recursos/concerns/baixavel.rbs +12 -0
- data/sig/focus_nfe/recursos/concerns/baixavel_eventos.rbs +14 -0
- data/sig/focus_nfe/recursos/concerns/cancelavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/conciliavel.rbs +15 -0
- data/sig/focus_nfe/recursos/concerns/consultavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/corrigivel.rbs +15 -0
- data/sig/focus_nfe/recursos/concerns/corrigivel_cte.rbs +17 -0
- data/sig/focus_nfe/recursos/concerns/emitivel.rbs +14 -0
- data/sig/focus_nfe/recursos/concerns/enviavel.rbs +15 -0
- data/sig/focus_nfe/recursos/concerns/eventavel.rbs +12 -0
- data/sig/focus_nfe/recursos/concerns/inutilizavel.rbs +20 -0
- data/sig/focus_nfe/recursos/concerns/listavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/localizavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/notificavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/removivel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/visualizavel.rbs +9 -0
- data/sig/focus_nfe/recursos/cte.rbs +17 -0
- data/sig/focus_nfe/recursos/cte_os.rbs +17 -0
- data/sig/focus_nfe/recursos/ctes_recebidas.rbs +14 -0
- data/sig/focus_nfe/recursos/dce.rbs +10 -0
- data/sig/focus_nfe/recursos/emails_bloqueados.rbs +12 -0
- data/sig/focus_nfe/recursos/empresas.rbs +12 -0
- data/sig/focus_nfe/recursos/mdfe.rbs +21 -0
- data/sig/focus_nfe/recursos/municipios.rbs +19 -0
- data/sig/focus_nfe/recursos/ncms.rbs +10 -0
- data/sig/focus_nfe/recursos/nfce.rbs +12 -0
- data/sig/focus_nfe/recursos/nfcom.rbs +10 -0
- data/sig/focus_nfe/recursos/nfe.rbs +23 -0
- data/sig/focus_nfe/recursos/nfes_recebidas.rbs +16 -0
- data/sig/focus_nfe/recursos/nfgas.rbs +10 -0
- data/sig/focus_nfe/recursos/nfse.rbs +11 -0
- data/sig/focus_nfe/recursos/nfse_nacional.rbs +10 -0
- data/sig/focus_nfe/recursos/nfses_nacionais_recebidas.rbs +11 -0
- data/sig/focus_nfe/recursos/webhooks.rbs +11 -0
- data/sig/focus_nfe/webhook.rbs +11 -0
- data/sig/focus_nfe.rbs +10 -0
- data/spec/focus_nfe/client_spec.rb +208 -0
- data/spec/focus_nfe/configuration_spec.rb +121 -0
- data/spec/focus_nfe/errors_mapping_spec.rb +68 -0
- data/spec/focus_nfe/errors_spec.rb +107 -0
- data/spec/focus_nfe/esquemas/campo_spec.rb +291 -0
- data/spec/focus_nfe/esquemas/decimal_spec.rb +54 -0
- data/spec/focus_nfe/esquemas/esquema_spec.rb +73 -0
- data/spec/focus_nfe/esquemas/validador_spec.rb +167 -0
- data/spec/focus_nfe/esquemas_spec.rb +42 -0
- data/spec/focus_nfe/http/adapter_spec.rb +8 -0
- data/spec/focus_nfe/http/adapters/net_http_spec.rb +181 -0
- data/spec/focus_nfe/http/authentication_spec.rb +24 -0
- data/spec/focus_nfe/http/connection_spec.rb +255 -0
- data/spec/focus_nfe/http/logging_spec.rb +83 -0
- data/spec/focus_nfe/http/response_spec.rb +161 -0
- data/spec/focus_nfe/modelos/documento_spec.rb +150 -0
- data/spec/focus_nfe/modelos/inutilizacao_spec.rb +91 -0
- data/spec/focus_nfe/modelos/pagina_spec.rb +77 -0
- data/spec/focus_nfe/recursos/backups_spec.rb +29 -0
- data/spec/focus_nfe/recursos/base_spec.rb +56 -0
- data/spec/focus_nfe/recursos/ceps_spec.rb +16 -0
- data/spec/focus_nfe/recursos/cfops_spec.rb +16 -0
- data/spec/focus_nfe/recursos/cnaes_spec.rb +20 -0
- data/spec/focus_nfe/recursos/cnpjs_spec.rb +11 -0
- data/spec/focus_nfe/recursos/concerns/emitivel_validacao_spec.rb +158 -0
- data/spec/focus_nfe/recursos/cte_os_spec.rb +9 -0
- data/spec/focus_nfe/recursos/cte_spec.rb +9 -0
- data/spec/focus_nfe/recursos/ctes_recebidas_spec.rb +56 -0
- data/spec/focus_nfe/recursos/dce_spec.rb +8 -0
- data/spec/focus_nfe/recursos/emails_bloqueados_spec.rb +29 -0
- data/spec/focus_nfe/recursos/empresas_spec.rb +45 -0
- data/spec/focus_nfe/recursos/mdfe_spec.rb +100 -0
- data/spec/focus_nfe/recursos/municipios_spec.rb +58 -0
- data/spec/focus_nfe/recursos/ncms_spec.rb +16 -0
- data/spec/focus_nfe/recursos/nfce_spec.rb +10 -0
- data/spec/focus_nfe/recursos/nfcom_spec.rb +8 -0
- data/spec/focus_nfe/recursos/nfe_spec.rb +262 -0
- data/spec/focus_nfe/recursos/nfes_recebidas_spec.rb +87 -0
- data/spec/focus_nfe/recursos/nfgas_spec.rb +8 -0
- data/spec/focus_nfe/recursos/nfse_nacional_spec.rb +8 -0
- data/spec/focus_nfe/recursos/nfse_spec.rb +9 -0
- data/spec/focus_nfe/recursos/nfses_nacionais_recebidas_spec.rb +17 -0
- data/spec/focus_nfe/recursos/webhooks_spec.rb +22 -0
- data/spec/focus_nfe/webhook_spec.rb +66 -0
- data/spec/focus_nfe_global_configuration_spec.rb +70 -0
- data/spec/focus_nfe_require_spec.rb +87 -0
- data/spec/focus_nfe_spec.rb +11 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/shared_examples/recurso_fiscal.rb +445 -0
- data/spec/support/shared_examples/recurso_leitura.rb +217 -0
- data/tools/pull_fields.rb +62 -0
- metadata +420 -0
data/README.md
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# FocusNfe
|
|
2
|
+
|
|
3
|
+
Cliente Ruby **não-oficial** para a API da [Focus NFe](https://focusnfe.com.br) —
|
|
4
|
+
serviço brasileiro de emissão de documentos fiscais eletrônicos (NFe, NFCe, NFSe,
|
|
5
|
+
CTe, MDFe, NFCom, DCe e outros).
|
|
6
|
+
|
|
7
|
+
> ⚠️ **Não-oficial.** Esta gem não tem vínculo com a Focus NFe. A autoridade
|
|
8
|
+
> final sobre campos, regras e validações fiscais é sempre a API da Focus/SEFAZ.
|
|
9
|
+
|
|
10
|
+
A gem é uma camada fina sobre HTTP: transporta JSON, autentica, trata os status
|
|
11
|
+
HTTP em **erros tipados** e devolve objetos Ruby úteis. Não reimplementa regras
|
|
12
|
+
fiscais. Não tem dependências de runtime (usa apenas a stdlib).
|
|
13
|
+
|
|
14
|
+
Cobre:
|
|
15
|
+
|
|
16
|
+
- **Documentos emitidos** — `nfe`, `nfce`, `nfse`, `nfse_nacional`, `cte`,
|
|
17
|
+
`cte_os`, `mdfe`, `nfcom`, `dce`, `nfgas`.
|
|
18
|
+
- **Documentos recebidos** — `nfes_recebidas`, `ctes_recebidas`,
|
|
19
|
+
`nfses_nacionais_recebidas` (listagem com sincronização incremental,
|
|
20
|
+
consulta, downloads, manifestação e eventos).
|
|
21
|
+
- **APIs auxiliares** (somente leitura, autenticadas pelo **token da conta**) —
|
|
22
|
+
`ceps`, `municipios`, `cfops`, `cnaes`, `ncms`, `cnpjs`.
|
|
23
|
+
- **APIs de gestão** — `empresas` (token da conta); `webhooks`,
|
|
24
|
+
`emails_bloqueados`, `backups` (token da empresa).
|
|
25
|
+
|
|
26
|
+
## Instalação
|
|
27
|
+
|
|
28
|
+
Adicione a gem ao `Gemfile` da aplicação:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle add focus_nfe
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Ou instale diretamente:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
gem install focus_nfe
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuração
|
|
41
|
+
|
|
42
|
+
### Os dois tokens da Focus NFe
|
|
43
|
+
|
|
44
|
+
A API usa **dois tokens distintos**, e a gem os separa:
|
|
45
|
+
|
|
46
|
+
- **`token_empresa`** — identifica a empresa que emite/consulta o documento.
|
|
47
|
+
Autentica todos os documentos (`nfe`, `nfce`, …, e as recebidas) e as APIs
|
|
48
|
+
de gestão por empresa (`webhooks`, `emails_bloqueados`, `backups`).
|
|
49
|
+
- **`token_conta`** — token da conta. Autentica as consultas auxiliares (`ceps`,
|
|
50
|
+
`municipios`, `cfops`, `cnaes`, `ncms`, `cnpjs`) e a gestão de empresas
|
|
51
|
+
(`empresas`).
|
|
52
|
+
|
|
53
|
+
Configure só o que for usar: um cliente só com `token_empresa` emite documentos;
|
|
54
|
+
acessar um recurso de conta sem `token_conta` levanta `ConfigurationError` (e
|
|
55
|
+
vice-versa), antes de qualquer ida à rede.
|
|
56
|
+
|
|
57
|
+
Há dois modos de uso, que coexistem.
|
|
58
|
+
|
|
59
|
+
### Global — para aplicações de uma empresa só
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
FocusNfe.configure do |config|
|
|
63
|
+
config.token_empresa = ENV["FOCUS_NFE_TOKEN_EMPRESA"]
|
|
64
|
+
config.token_conta = ENV["FOCUS_NFE_TOKEN_CONTA"] # opcional (consultas auxiliares/empresas)
|
|
65
|
+
config.environment = :producao # ou :homologacao (padrão)
|
|
66
|
+
config.timeout = 30
|
|
67
|
+
config.logger = Rails.logger
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
client = FocusNfe.client # usa a config global
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Explícito — várias empresas no mesmo processo
|
|
74
|
+
|
|
75
|
+
O `token_empresa` é por empresa; cada `Client` carrega seus próprios tokens e
|
|
76
|
+
ambiente, sem estado compartilhado. O `token_conta`, quando usado, é o mesmo da
|
|
77
|
+
conta que agrupa as empresas.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
loja = FocusNfe::Client.new(token_empresa: "TOKEN_LOJA", environment: :producao)
|
|
81
|
+
filial = FocusNfe::Client.new(token_empresa: "TOKEN_FILIAL", environment: :homologacao)
|
|
82
|
+
|
|
83
|
+
# Consultas auxiliares e gestão de empresas usam o token da conta:
|
|
84
|
+
conta = FocusNfe::Client.new(token_conta: "TOKEN_CONTA", environment: :producao)
|
|
85
|
+
conta.cnpjs.consultar("12345678000123")
|
|
86
|
+
conta.empresas.criar(dados: dados_empresa, dry_run: true)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
O ambiente resolve a URL base (o prefixo `/v2` é interno):
|
|
90
|
+
|
|
91
|
+
- `:producao` → `https://api.focusnfe.com.br`
|
|
92
|
+
- `:homologacao` → `https://homologacao.focusnfe.com.br`
|
|
93
|
+
|
|
94
|
+
### Logger
|
|
95
|
+
|
|
96
|
+
Logging é opt-in: por padrão `config.logger` é `nil` e nada é emitido. Plugue qualquer
|
|
97
|
+
logger compatível com o `Logger` da stdlib (responde a `debug`/`info`/`warn`/`error`),
|
|
98
|
+
como `Rails.logger` ou `Logger.new($stdout)`:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
config.logger = Logger.new($stdout)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A gem registra cada requisição (`debug`), resposta (`info`/`warn`) e falha (`error`). O
|
|
105
|
+
`Authorization` é sempre redigido (`[FILTERED]`) e o corpo da requisição nunca é logado —
|
|
106
|
+
dados sensíveis não vazam.
|
|
107
|
+
|
|
108
|
+
## Uso
|
|
109
|
+
|
|
110
|
+
### Emissão e ciclo assíncrono
|
|
111
|
+
|
|
112
|
+
A emissão é assíncrona na maioria dos documentos. A `ref` é a referência única do
|
|
113
|
+
documento na sua aplicação (validada client-side como alfanumérica antes do
|
|
114
|
+
envio). As respostas de emissão e consulta são encapsuladas em
|
|
115
|
+
`FocusNfe::Modelos::Documento`.
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
doc = client.nfe.emitir(ref: "pedido-42", dados: payload_nfe)
|
|
119
|
+
doc.status # => "processando_autorizacao"
|
|
120
|
+
doc.processando? # => true
|
|
121
|
+
doc.ref # => "pedido-42"
|
|
122
|
+
|
|
123
|
+
# Acompanhamento por polling (ou via webhooks — ver Gestão).
|
|
124
|
+
doc = client.nfe.consultar("pedido-42")
|
|
125
|
+
if doc.autorizado?
|
|
126
|
+
doc.chave_nfe
|
|
127
|
+
doc.caminho_xml_nota_fiscal
|
|
128
|
+
doc.caminho_danfe
|
|
129
|
+
elsif doc.erro?
|
|
130
|
+
doc.status_sefaz
|
|
131
|
+
doc.mensagem_sefaz
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Predicados de status disponíveis: `autorizado?`, `cancelado?`, `processando?`,
|
|
136
|
+
`erro?`, `denegado?`. Campos não mapeados continuam acessíveis via `doc["campo"]`
|
|
137
|
+
ou `doc.dados`.
|
|
138
|
+
|
|
139
|
+
A NFC-e é **síncrona** — o resultado já vem na própria chamada de emissão:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
nota = client.nfce.emitir(ref: "venda-1001", dados: payload_nfce)
|
|
143
|
+
nota.autorizado? # => true/false na mesma chamada
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Cancelamento
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
client.nfe.cancelar("pedido-42", justificativa: "Cliente desistiu da compra.")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Documentos recebidos e sincronização incremental
|
|
153
|
+
|
|
154
|
+
`listar` devolve uma `FocusNfe::Modelos::Pagina` (enumerável). O cabeçalho
|
|
155
|
+
`X-Max-Version` é exposto em `versao_maxima`, para retomar a sincronização do
|
|
156
|
+
ponto onde parou.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
pagina = client.nfes_recebidas.listar(cnpj: "12345678000123", versao: ultima_versao)
|
|
160
|
+
pagina.cada { |nfe| processar(nfe) }
|
|
161
|
+
proxima_versao = pagina.versao_maxima
|
|
162
|
+
|
|
163
|
+
# Consulta, downloads e manifestação do destinatário:
|
|
164
|
+
client.nfes_recebidas.consultar(chave, completa: true)
|
|
165
|
+
xml = client.nfes_recebidas.download_xml(chave)
|
|
166
|
+
pdf = client.nfes_recebidas.download_pdf(chave)
|
|
167
|
+
client.nfes_recebidas.manifestar(chave, tipo: "confirmacao")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### APIs auxiliares
|
|
171
|
+
|
|
172
|
+
Autenticadas pelo `token_conta` (ver [Configuração](#configuração)):
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
client.ceps.consultar("69909032")
|
|
176
|
+
client.cnpjs.consultar("12345678000123")
|
|
177
|
+
client.ncms.consultar("01012100")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### APIs de gestão
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# Cadastro de empresa (apenas produção); dry_run valida sem persistir.
|
|
184
|
+
client.empresas.criar(dados: dados_empresa, dry_run: true)
|
|
185
|
+
|
|
186
|
+
# Webhooks (a gem registra o gatilho e processa a chamada de volta).
|
|
187
|
+
client.webhooks.criar(dados: {
|
|
188
|
+
event: "nfe",
|
|
189
|
+
url: "https://meu.app/hooks/nfe",
|
|
190
|
+
cnpj: "12345678000123",
|
|
191
|
+
authorization_header: "X-Focus-Authorization", # header que a Focus enviará no callback
|
|
192
|
+
authorization: "um-segredo-forte" # valor esperado nesse header
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Recebendo webhooks (inbound)
|
|
197
|
+
|
|
198
|
+
Quando a Focus muda o status de um documento, ela chama a URL cadastrada. A gem
|
|
199
|
+
converte o corpo recebido no mesmo `Modelos::Documento` de emissão/consulta e
|
|
200
|
+
autentica a chamada comparando o header com o `authorization` do gatilho:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# Em um controller Rails:
|
|
204
|
+
def focus_callback
|
|
205
|
+
autenticado = FocusNfe::Webhook.autenticado?(
|
|
206
|
+
headers: request.headers,
|
|
207
|
+
authorization: ENV.fetch("FOCUS_WEBHOOK_AUTH"),
|
|
208
|
+
authorization_header: "X-Focus-Authorization"
|
|
209
|
+
)
|
|
210
|
+
return head(:unauthorized) unless autenticado
|
|
211
|
+
|
|
212
|
+
documento = FocusNfe::Webhook.parse(request.raw_post)
|
|
213
|
+
AtualizaNota.call(ref: documento.ref) if documento.autorizado?
|
|
214
|
+
head :ok
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`parse` aceita a String crua ou um `Hash` já parseado e levanta
|
|
219
|
+
`FocusNfe::Errors::WebhookError` se o corpo não for JSON válido.
|
|
220
|
+
|
|
221
|
+
## Erros tipados
|
|
222
|
+
|
|
223
|
+
Cada faixa de status HTTP vira uma exceção específica, todas descendentes de
|
|
224
|
+
`FocusNfe::Error`. Cada exceção carrega `status`, `body` (mensagens da API) e a
|
|
225
|
+
`response` original.
|
|
226
|
+
|
|
227
|
+
| Status | Exceção | Significado |
|
|
228
|
+
| ------ | ----------------------------------- | -------------------------------------- |
|
|
229
|
+
| 400 | `FocusNfe::Errors::BadRequest` | Requisição malformada |
|
|
230
|
+
| 401 | `FocusNfe::Errors::Unauthorized` | Token ausente ou inválido |
|
|
231
|
+
| 403 | `FocusNfe::Errors::Forbidden` | Sem permissão |
|
|
232
|
+
| 404 | `FocusNfe::Errors::NotFound` | Recurso inexistente |
|
|
233
|
+
| 409 | `FocusNfe::Errors::Conflict` | Conflito de estado (ex.: `ref` em uso) |
|
|
234
|
+
| 422 | `FocusNfe::Errors::ValidationError` | Erro de validação dos campos |
|
|
235
|
+
| 429 | `FocusNfe::Errors::RateLimited` | Limite de requisições excedido |
|
|
236
|
+
| 5xx | `FocusNfe::Errors::ServerError` | Falha no servidor da Focus/SEFAZ |
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
begin
|
|
240
|
+
client.nfe.emitir(ref: "pedido-42", dados: payload)
|
|
241
|
+
rescue FocusNfe::Errors::ValidationError => e
|
|
242
|
+
e.status # => 422
|
|
243
|
+
e.body # => mensagens de erro da API
|
|
244
|
+
rescue FocusNfe::Error => e
|
|
245
|
+
# captura qualquer falha da gem
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Há ainda `ConfigurationError` (token/ambiente inválidos, client-side) e
|
|
250
|
+
`ConnectionError` (timeout, conexão recusada, excesso de redirects).
|
|
251
|
+
|
|
252
|
+
## Validação opt-in por schemas
|
|
253
|
+
|
|
254
|
+
Os campos de emissão derivam dos schemas documentados em
|
|
255
|
+
`campos.focusnfe.com.br` (empacotados em `data/schemas/`). A validação
|
|
256
|
+
client-side é **opcional e desligada por padrão** — a Focus é a autoridade final
|
|
257
|
+
e os campos mudam (ex.: Reforma Tributária em transição).
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
client.nfe.emitir(ref: "pedido-42", dados: payload, validar: true)
|
|
261
|
+
# => levanta FocusNfe::Esquemas::ErroDeValidacao se faltar obrigatório
|
|
262
|
+
# ou o tipo/tamanho de um campo escalar não bater.
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A validação é **recursiva**: campos de coleção (`Coleção[...]`, como `itens`) têm
|
|
266
|
+
cada item validado contra o schema da coleção, em qualquer profundidade. Os erros
|
|
267
|
+
vêm com o caminho até o campo — a posição do item é base 1:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
payload = {
|
|
271
|
+
natureza_operacao: "Venda",
|
|
272
|
+
itens: [
|
|
273
|
+
{ numero_item: 1, descricao: "Produto A" },
|
|
274
|
+
{ numero_item: 2 } # falta a descrição obrigatória
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
begin
|
|
279
|
+
client.nfe.emitir(ref: "pedido-42", dados: payload, validar: true)
|
|
280
|
+
rescue FocusNfe::Esquemas::ErroDeValidacao => e
|
|
281
|
+
e.erros # => ["itens[2].descricao: campo obrigatório ausente", ...]
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Documentos sem schema próprio são emitidos sem validar (pulam silenciosamente).
|
|
286
|
+
|
|
287
|
+
### Introspecção dos schemas
|
|
288
|
+
|
|
289
|
+
Os mesmos schemas empacotados ficam acessíveis como dado, para você (ou uma
|
|
290
|
+
ferramenta automatizada) descobrir quais campos e tipos um documento aceita — sem
|
|
291
|
+
token nem conexão:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
FocusNfe::Esquemas.disponiveis
|
|
295
|
+
# => ["cte", "cte_os", "dce", "mdfe", "nfcom", "nfe", "nfe_item", "nfgas", ...]
|
|
296
|
+
|
|
297
|
+
FocusNfe::Esquemas.descrever("nfe")
|
|
298
|
+
# => [
|
|
299
|
+
# { nome: "natureza_operacao", descricao: "Descrição da natureza de operação.",
|
|
300
|
+
# tipo: :string, tipo_bruto: "String[1-60]", obrigatorio: true,
|
|
301
|
+
# tamanho_minimo: 1, tamanho_maximo: 60, enum: nil, tag: "natOp", colecao: nil },
|
|
302
|
+
# ...
|
|
303
|
+
# ]
|
|
304
|
+
# => nil para documento sem schema
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Cada campo vira um `Hash` serializável. Campos de coleção (`Coleção[...]`) aninham
|
|
308
|
+
a descrição dos subcampos em `:colecao`, em qualquer profundidade; enums trazem os
|
|
309
|
+
valores aceitos em `:enum`. `disponiveis` também lista os sub-schemas auxiliares
|
|
310
|
+
(`nfe_item`, `cte_transporte_aereo`, …), que igualmente podem ser descritos.
|
|
311
|
+
|
|
312
|
+
## Desenvolvimento
|
|
313
|
+
|
|
314
|
+
Após clonar o repositório, rode `bin/setup`: ele instala as dependências,
|
|
315
|
+
configura os git hooks ([overcommit](https://github.com/sds/overcommit) —
|
|
316
|
+
pre-commit/pre-push/commit-msg) e roda o `rake` default como verificação de
|
|
317
|
+
ambiente. `bin/console` abre um IRB com a gem carregada.
|
|
318
|
+
|
|
319
|
+
O projeto é desenvolvido **test-first (TDD)** com RSpec + WebMock — nenhuma
|
|
320
|
+
classe/método/branch nasce sem um spec falhando que o exija. O `rake` default
|
|
321
|
+
roda **RSpec + RuboCop** e precisa estar verde antes de cada commit:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
bundle exec rake # RSpec + RuboCop
|
|
325
|
+
bin/rspec # apenas a suíte
|
|
326
|
+
bin/rubocop -a # estilo, com auto-correção
|
|
327
|
+
bundle exec rake pull_fields # regenera data/schemas/ a partir de campos.focusnfe.com.br
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Os arquivos em `data/schemas/` são **gerados automaticamente** por
|
|
331
|
+
`rake pull_fields` — não os edite à mão. Para atualizá-los, rode o script e
|
|
332
|
+
faça commit do resultado. O CI verifica em cada PR se os schemas estão em dia.
|
|
333
|
+
|
|
334
|
+
Para instalar a gem localmente, rode `bundle exec rake install`. Para publicar
|
|
335
|
+
uma nova versão, atualize o número em `version.rb` e rode `bundle exec rake
|
|
336
|
+
release`, que cria a tag git, sobe os commits + tag e publica o `.gem` no
|
|
337
|
+
[rubygems.org](https://rubygems.org).
|
|
338
|
+
|
|
339
|
+
## Contribuindo
|
|
340
|
+
|
|
341
|
+
Bug reports e pull requests são bem-vindos no GitHub em
|
|
342
|
+
https://github.com/wilfison/focus_nfe. Espera-se que os participantes sigam o
|
|
343
|
+
[código de conduta](https://github.com/wilfison/focus_nfe/blob/main/CODE_OF_CONDUCT.md).
|
|
344
|
+
|
|
345
|
+
## Licença
|
|
346
|
+
|
|
347
|
+
Disponível como código aberto sob os termos da
|
|
348
|
+
[licença MIT](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "rubocop/rake_task"
|
|
6
|
+
require "yard"
|
|
7
|
+
|
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
9
|
+
RuboCop::RakeTask.new
|
|
10
|
+
|
|
11
|
+
YARD::Rake::YardocTask.new(:yard)
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
require "steep/rake_task"
|
|
15
|
+
Steep::RakeTask.new(:steep)
|
|
16
|
+
rescue LoadError
|
|
17
|
+
# ambiente sem dependências de desenvolvimento
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
task default: %i[spec rubocop]
|
|
21
|
+
|
|
22
|
+
desc "Roda localmente as mesmas verificações do CI (.github/workflows/ci.yml)"
|
|
23
|
+
task :ci do
|
|
24
|
+
etapas = [
|
|
25
|
+
["Specs (RSpec)", -> { Rake::Task["spec"].invoke }],
|
|
26
|
+
["Lint (RuboCop)", -> { Rake::Task["rubocop"].invoke }],
|
|
27
|
+
["Tipos (Steep)", -> { sh "bundle exec steep check" }],
|
|
28
|
+
["Docs (YARD)", -> { sh "bundle exec yard doc --no-output --fail-on-warning" }],
|
|
29
|
+
["Cobertura de docs", -> { Rake::Task["docs:coverage"].invoke }]
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
falhas = []
|
|
33
|
+
etapas.each do |nome, acao|
|
|
34
|
+
puts "\n\e[1m▶ #{nome}\e[0m"
|
|
35
|
+
acao.call
|
|
36
|
+
puts "\e[32m✓ #{nome}\e[0m"
|
|
37
|
+
rescue SystemExit, StandardError => e
|
|
38
|
+
falhas << nome
|
|
39
|
+
puts "\e[31m✗ #{nome} (#{e.class}: #{e.message})\e[0m"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "\n\e[1mResumo do CI local\e[0m"
|
|
43
|
+
etapas.each do |etapa|
|
|
44
|
+
marcador = falhas.include?(etapa[0]) ? "\e[31m✗\e[0m" : "\e[32m✓\e[0m"
|
|
45
|
+
puts " #{marcador} #{etapa[0]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
abort "\n#{falhas.size} verificação(ões) falharam: #{falhas.join(", ")}" if falhas.any?
|
|
49
|
+
|
|
50
|
+
puts "\n\e[32mTudo verde — pronto para enviar ao GitHub.\e[0m"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "Pull fields from FocusNFe API and save to JSON files"
|
|
54
|
+
task :pull_fields do
|
|
55
|
+
sh "ruby #{File.join(__dir__, "tools", "pull_fields.rb")}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
COBERTURA_DOCS_MINIMA = 93.0
|
|
59
|
+
|
|
60
|
+
namespace :docs do
|
|
61
|
+
desc "Gera a documentação YARD em docs/ e abre no navegador"
|
|
62
|
+
task open: :yard do
|
|
63
|
+
report = File.join(__dir__, "docs", "index.html")
|
|
64
|
+
abort "Documentação não encontrada. Rode `bundle exec rake yard` primeiro." unless File.exist?(report)
|
|
65
|
+
|
|
66
|
+
sh browser_opener, report
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc "Falha se a cobertura de documentação YARD ficar abaixo de #{COBERTURA_DOCS_MINIMA}%"
|
|
70
|
+
task :coverage do
|
|
71
|
+
saida = `yard stats --list-undoc`
|
|
72
|
+
puts saida
|
|
73
|
+
|
|
74
|
+
cobertura = saida[/([\d.]+)% documented/, 1]&.to_f
|
|
75
|
+
abort "Não foi possível ler a cobertura no resultado do `yard stats`." if cobertura.nil?
|
|
76
|
+
|
|
77
|
+
if cobertura < COBERTURA_DOCS_MINIMA
|
|
78
|
+
abort "Cobertura de documentação #{cobertura}% abaixo do mínimo de #{COBERTURA_DOCS_MINIMA}%."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
puts "Cobertura de documentação: #{cobertura}% (mínimo #{COBERTURA_DOCS_MINIMA}%)."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
desc "Sobe o servidor YARD (http://localhost:8808) com refresh automático"
|
|
85
|
+
task :serve do
|
|
86
|
+
sh "yard", "server", "--reload"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def browser_opener
|
|
91
|
+
if RUBY_PLATFORM.include?("darwin") then "open"
|
|
92
|
+
elsif RUBY_PLATFORM.match?(/mswin|mingw|cygwin/) then "start"
|
|
93
|
+
else "xdg-open"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
namespace :coverage do
|
|
98
|
+
desc "Abre o relatório de cobertura do SimpleCov no navegador"
|
|
99
|
+
task :open do
|
|
100
|
+
report = File.join(__dir__, "coverage", "index.html")
|
|
101
|
+
abort "Relatório não encontrado. Rode `bundle exec rake spec` primeiro." unless File.exist?(report)
|
|
102
|
+
|
|
103
|
+
sh browser_opener, report
|
|
104
|
+
end
|
|
105
|
+
end
|