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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FocusNfe
|
|
4
|
+
# Raiz de acesso à API. Cada cliente carrega sua própria {Configuration} e
|
|
5
|
+
# mantém duas {HTTP::Connection} memoizadas, uma por token: a de empresa
|
|
6
|
+
# (emissão/consulta de documentos) e a de conta (consultas auxiliares e gestão
|
|
7
|
+
# de empresas). Permite coexistência de várias empresas no mesmo processo sem
|
|
8
|
+
# estado compartilhado. Expõe os recursos da API instanciados preguiçosamente.
|
|
9
|
+
class Client
|
|
10
|
+
# @return [FocusNfe::Configuration] configuração validada deste cliente
|
|
11
|
+
attr_reader :configuration
|
|
12
|
+
|
|
13
|
+
# @overload initialize(configuration)
|
|
14
|
+
# @param configuration [FocusNfe::Configuration] configuração já montada
|
|
15
|
+
# @overload initialize(token_empresa:, token_conta:, environment:, **options)
|
|
16
|
+
# @param token_empresa [String] token da empresa (emissão/consulta de documentos)
|
|
17
|
+
# @param token_conta [String] token da conta (consultas auxiliares e gestão de empresas)
|
|
18
|
+
# @param environment [Symbol] :producao ou :homologacao
|
|
19
|
+
# @param options [Hash] demais opções da {Configuration} (timeout, headers, …)
|
|
20
|
+
# @raise [FocusNfe::Errors::ConfigurationError] se a configuração for inválida
|
|
21
|
+
def initialize(configuration = nil, **)
|
|
22
|
+
@configuration = (configuration || Configuration.new(**)).tap(&:validate!)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [FocusNfe::HTTP::Connection] conexão da empresa, memoizada
|
|
26
|
+
# @raise [FocusNfe::Errors::ConfigurationError] se +token_empresa+ estiver ausente
|
|
27
|
+
def connection
|
|
28
|
+
@connection ||= build_connection(:empresa)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [FocusNfe::HTTP::Connection] conexão da conta, memoizada
|
|
32
|
+
# @raise [FocusNfe::Errors::ConfigurationError] se +token_conta+ estiver ausente
|
|
33
|
+
def connection_conta
|
|
34
|
+
@connection_conta ||= build_connection(:conta)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [FocusNfe::Recursos::Nfe] recurso de NF-e, memoizado
|
|
38
|
+
def nfe
|
|
39
|
+
@nfe ||= Recursos::Nfe.new(connection)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [FocusNfe::Recursos::Nfce] recurso de NFC-e, memoizado
|
|
43
|
+
def nfce
|
|
44
|
+
@nfce ||= Recursos::Nfce.new(connection)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [FocusNfe::Recursos::Nfse] recurso de NFS-e, memoizado
|
|
48
|
+
def nfse
|
|
49
|
+
@nfse ||= Recursos::Nfse.new(connection)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [FocusNfe::Recursos::NfseNacional] recurso de NFS-e nacional, memoizado
|
|
53
|
+
def nfse_nacional
|
|
54
|
+
@nfse_nacional ||= Recursos::NfseNacional.new(connection)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [FocusNfe::Recursos::Cte] recurso de CT-e, memoizado
|
|
58
|
+
def cte
|
|
59
|
+
@cte ||= Recursos::Cte.new(connection)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [FocusNfe::Recursos::CteOs] recurso de CT-e OS, memoizado
|
|
63
|
+
def cte_os
|
|
64
|
+
@cte_os ||= Recursos::CteOs.new(connection)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [FocusNfe::Recursos::Mdfe] recurso de MDF-e, memoizado
|
|
68
|
+
def mdfe
|
|
69
|
+
@mdfe ||= Recursos::Mdfe.new(connection)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [FocusNfe::Recursos::Nfcom] recurso de NFCom, memoizado
|
|
73
|
+
def nfcom
|
|
74
|
+
@nfcom ||= Recursos::Nfcom.new(connection)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [FocusNfe::Recursos::Dce] recurso de DC-e, memoizado
|
|
78
|
+
def dce
|
|
79
|
+
@dce ||= Recursos::Dce.new(connection)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [FocusNfe::Recursos::Nfgas] recurso de NFGas, memoizado
|
|
83
|
+
def nfgas
|
|
84
|
+
@nfgas ||= Recursos::Nfgas.new(connection)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [FocusNfe::Recursos::NfesRecebidas] recurso de NF-e recebidas, memoizado
|
|
88
|
+
def nfes_recebidas
|
|
89
|
+
@nfes_recebidas ||= Recursos::NfesRecebidas.new(connection)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [FocusNfe::Recursos::CtesRecebidas] recurso de CT-e recebidos, memoizado
|
|
93
|
+
def ctes_recebidas
|
|
94
|
+
@ctes_recebidas ||= Recursos::CtesRecebidas.new(connection)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [FocusNfe::Recursos::NfsesNacionaisRecebidas] recurso de NFS-e nacionais recebidas, memoizado
|
|
98
|
+
def nfses_nacionais_recebidas
|
|
99
|
+
@nfses_nacionais_recebidas ||= Recursos::NfsesNacionaisRecebidas.new(connection)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [FocusNfe::Recursos::Ceps] recurso de consulta de CEP, memoizado
|
|
103
|
+
def ceps
|
|
104
|
+
@ceps ||= Recursos::Ceps.new(connection_conta)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [FocusNfe::Recursos::Municipios] recurso de consulta de municípios, memoizado
|
|
108
|
+
def municipios
|
|
109
|
+
@municipios ||= Recursos::Municipios.new(connection_conta)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [FocusNfe::Recursos::Cfops] recurso de consulta de CFOP, memoizado
|
|
113
|
+
def cfops
|
|
114
|
+
@cfops ||= Recursos::Cfops.new(connection_conta)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [FocusNfe::Recursos::Cnaes] recurso de consulta de CNAE, memoizado
|
|
118
|
+
def cnaes
|
|
119
|
+
@cnaes ||= Recursos::Cnaes.new(connection_conta)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [FocusNfe::Recursos::Ncms] recurso de consulta de NCM, memoizado
|
|
123
|
+
def ncms
|
|
124
|
+
@ncms ||= Recursos::Ncms.new(connection_conta)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [FocusNfe::Recursos::Cnpjs] recurso de consulta de CNPJ, memoizado
|
|
128
|
+
def cnpjs
|
|
129
|
+
@cnpjs ||= Recursos::Cnpjs.new(connection_conta)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [FocusNfe::Recursos::Empresas] recurso de gestão de empresas, memoizado
|
|
133
|
+
def empresas
|
|
134
|
+
@empresas ||= Recursos::Empresas.new(connection_conta)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [FocusNfe::Recursos::Webhooks] recurso de gestão de webhooks, memoizado
|
|
138
|
+
def webhooks
|
|
139
|
+
@webhooks ||= Recursos::Webhooks.new(connection)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @return [FocusNfe::Recursos::EmailsBloqueados] recurso de e-mails bloqueados, memoizado
|
|
143
|
+
def emails_bloqueados
|
|
144
|
+
@emails_bloqueados ||= Recursos::EmailsBloqueados.new(connection)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @return [FocusNfe::Recursos::Backups] recurso de backups de XML, memoizado
|
|
148
|
+
def backups
|
|
149
|
+
@backups ||= Recursos::Backups.new(connection)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# @param escopo [Symbol] :empresa ou :conta
|
|
155
|
+
# @return [FocusNfe::HTTP::Connection] conexão autenticada com o token do escopo
|
|
156
|
+
# @raise [FocusNfe::Errors::ConfigurationError] se o token do escopo estiver ausente
|
|
157
|
+
def build_connection(escopo)
|
|
158
|
+
configuration.validate_token!(escopo)
|
|
159
|
+
HTTP::Connection.new(configuration, token: configuration.token_de(escopo).to_s)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FocusNfe
|
|
4
|
+
# Guarda as opções de uso da gem: os dois tokens da Focus NFe (+token_empresa+,
|
|
5
|
+
# que identifica a empresa emitente/consultada, e +token_conta+, usado nas
|
|
6
|
+
# consultas auxiliares e na gestão de empresas), o ambiente (que resolve a URL
|
|
7
|
+
# base), timeouts, logger, adaptador HTTP e cabeçalhos extras. Serve tanto ao
|
|
8
|
+
# modo global (FocusNfe.configure) quanto ao Client explícito (multi-empresa).
|
|
9
|
+
class Configuration
|
|
10
|
+
# @return [Hash{Symbol=>String}] ambiente => URL base da API (sem o prefixo /v2)
|
|
11
|
+
BASE_URLS = {
|
|
12
|
+
producao: "https://api.focusnfe.com.br",
|
|
13
|
+
homologacao: "https://homologacao.focusnfe.com.br"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# @return [Hash{Symbol=>Symbol}] escopo => atributo de token correspondente
|
|
17
|
+
ESCOPOS_TOKEN = { empresa: :token_empresa, conta: :token_conta }.freeze
|
|
18
|
+
|
|
19
|
+
# @return [Symbol] ambiente padrão quando nenhum é informado
|
|
20
|
+
DEFAULT_ENVIRONMENT = :homologacao
|
|
21
|
+
# @return [Integer] timeout de leitura padrão, em segundos
|
|
22
|
+
DEFAULT_TIMEOUT = 30
|
|
23
|
+
# @return [Integer] timeout de conexão padrão, em segundos
|
|
24
|
+
DEFAULT_OPEN_TIMEOUT = 10
|
|
25
|
+
|
|
26
|
+
attr_accessor :token_empresa, :token_conta, :environment, :timeout,
|
|
27
|
+
:open_timeout, :logger, :http_adapter, :headers
|
|
28
|
+
|
|
29
|
+
# @param token_empresa [String, nil] token da empresa (emissão/consulta de documentos)
|
|
30
|
+
# @param token_conta [String, nil] token da conta (consultas auxiliares e gestão de empresas)
|
|
31
|
+
# @param environment [Symbol] :producao ou :homologacao
|
|
32
|
+
# @param timeout [Integer] timeout de leitura, em segundos
|
|
33
|
+
# @param open_timeout [Integer] timeout de conexão, em segundos
|
|
34
|
+
# @param logger [_Logger, nil] logger plugável usado pela {HTTP::Connection}; deve responder a
|
|
35
|
+
# +debug+/+info+/+warn+/+error+ (compatível com o +Logger+ da stdlib, +Rails.logger+ etc.).
|
|
36
|
+
# +nil+ (padrão) desliga o logging. O +Authorization+ é sempre redigido — ver {HTTP::Logging}.
|
|
37
|
+
# @param http_adapter [FocusNfe::HTTP::Adapter, nil] instância pronta (nil => Connection cria a default)
|
|
38
|
+
# @param headers [Hash] cabeçalhos extras enviados em toda requisição
|
|
39
|
+
def initialize(token_empresa: nil, token_conta: nil, environment: DEFAULT_ENVIRONMENT,
|
|
40
|
+
timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT,
|
|
41
|
+
logger: nil, http_adapter: nil, headers: {})
|
|
42
|
+
@token_empresa = token_empresa
|
|
43
|
+
@token_conta = token_conta
|
|
44
|
+
@environment = environment
|
|
45
|
+
@timeout = timeout
|
|
46
|
+
@open_timeout = open_timeout
|
|
47
|
+
@logger = logger
|
|
48
|
+
@http_adapter = http_adapter
|
|
49
|
+
@headers = headers
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [String] URL base correspondente ao ambiente atual
|
|
53
|
+
# @raise [FocusNfe::Errors::ConfigurationError] se o ambiente for desconhecido
|
|
54
|
+
def base_url
|
|
55
|
+
validate_environment!
|
|
56
|
+
BASE_URLS.fetch(environment)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param escopo [Symbol] :empresa ou :conta
|
|
60
|
+
# @return [String, nil] token correspondente ao escopo
|
|
61
|
+
def token_de(escopo)
|
|
62
|
+
public_send(ESCOPOS_TOKEN.fetch(escopo))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Valida o ambiente e a presença de ao menos um token, falhando cedo quando a
|
|
66
|
+
# configuração é inutilizável em qualquer escopo.
|
|
67
|
+
#
|
|
68
|
+
# @return [self]
|
|
69
|
+
# @raise [FocusNfe::Errors::ConfigurationError] ambiente inválido ou nenhum token presente
|
|
70
|
+
def validate!
|
|
71
|
+
validate_environment!
|
|
72
|
+
return self if ESCOPOS_TOKEN.keys.any? { |escopo| token_presente?(escopo) }
|
|
73
|
+
|
|
74
|
+
raise Errors::ConfigurationError, "informe token_empresa e/ou token_conta"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Valida o ambiente e a presença do token de um escopo específico, no momento
|
|
78
|
+
# em que um recurso daquele escopo é efetivamente usado.
|
|
79
|
+
#
|
|
80
|
+
# @param escopo [Symbol] :empresa ou :conta
|
|
81
|
+
# @return [self]
|
|
82
|
+
# @raise [FocusNfe::Errors::ConfigurationError] ambiente inválido ou token do escopo ausente
|
|
83
|
+
def validate_token!(escopo)
|
|
84
|
+
validate_environment!
|
|
85
|
+
return self if token_presente?(escopo)
|
|
86
|
+
|
|
87
|
+
raise Errors::ConfigurationError, "#{ESCOPOS_TOKEN.fetch(escopo)} é obrigatório para esta operação"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @raise [FocusNfe::Errors::ConfigurationError] se o ambiente não for reconhecido
|
|
93
|
+
def validate_environment!
|
|
94
|
+
return if BASE_URLS.key?(environment)
|
|
95
|
+
|
|
96
|
+
raise Errors::ConfigurationError,
|
|
97
|
+
"ambiente inválido: #{environment.inspect} (use :producao ou :homologacao)"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def token_presente?(escopo)
|
|
101
|
+
!token_de(escopo).to_s.strip.empty?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FocusNfe
|
|
4
|
+
# Raiz de toda a hierarquia de exceções da gem. Permite ao integrador capturar
|
|
5
|
+
# qualquer falha do `focus_nfe` com um único `rescue FocusNfe::Error`.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Exceções tipadas: falhas HTTP (por faixa de status), de configuração
|
|
9
|
+
# (client-side) e de conexão (transporte). Reúne também o mapeamento
|
|
10
|
+
# status → classe e a construção da exceção a partir de uma `Response`.
|
|
11
|
+
module Errors
|
|
12
|
+
# Falha HTTP retornada pela API, carregando o status, o corpo parseado com
|
|
13
|
+
# as mensagens de erro da API e a {FocusNfe::HTTP::Response} original.
|
|
14
|
+
class HttpError < Error
|
|
15
|
+
# @return [Integer, nil] código de status HTTP da resposta
|
|
16
|
+
attr_reader :status
|
|
17
|
+
|
|
18
|
+
# @return [Object, nil] corpo parseado com as mensagens de erro da API
|
|
19
|
+
attr_reader :body
|
|
20
|
+
|
|
21
|
+
# @return [FocusNfe::HTTP::Response, nil] resposta original que originou o erro
|
|
22
|
+
attr_reader :response
|
|
23
|
+
|
|
24
|
+
# @param message [String, nil] mensagem da exceção
|
|
25
|
+
# @param status [Integer, nil] código de status HTTP
|
|
26
|
+
# @param body [Object, nil] corpo parseado da resposta
|
|
27
|
+
# @param response [FocusNfe::HTTP::Response, nil] resposta original
|
|
28
|
+
def initialize(message = nil, status: nil, body: nil, response: nil)
|
|
29
|
+
@status = status
|
|
30
|
+
@body = body
|
|
31
|
+
@response = response
|
|
32
|
+
super(message)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String, nil] código de erro da API, quando o corpo é estruturado
|
|
36
|
+
def codigo
|
|
37
|
+
body["codigo"] if body.is_a?(Hash)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Array<Hash>] erros no formato +{ "campo" => String?, "mensagem" => String }+;
|
|
41
|
+
# vazio quando o corpo não traz erros estruturados
|
|
42
|
+
def erros
|
|
43
|
+
return [] unless body.is_a?(Hash)
|
|
44
|
+
return body["erros"] if body["erros"].is_a?(Array)
|
|
45
|
+
|
|
46
|
+
body["mensagem"] ? [{ "campo" => nil, "mensagem" => body["mensagem"] }] : []
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 400 — requisição malformada.
|
|
51
|
+
class BadRequest < HttpError; end
|
|
52
|
+
|
|
53
|
+
# 401 — token ausente ou inválido.
|
|
54
|
+
class Unauthorized < HttpError; end
|
|
55
|
+
|
|
56
|
+
# 403 — autenticado, porém sem permissão para o recurso.
|
|
57
|
+
class Forbidden < HttpError; end
|
|
58
|
+
|
|
59
|
+
# 404 — recurso inexistente.
|
|
60
|
+
class NotFound < HttpError; end
|
|
61
|
+
|
|
62
|
+
# 409 — conflito de estado (ex.: `ref` já utilizada).
|
|
63
|
+
class Conflict < HttpError; end
|
|
64
|
+
|
|
65
|
+
# 422 — erro de validação dos campos enviados.
|
|
66
|
+
class ValidationError < HttpError; end
|
|
67
|
+
|
|
68
|
+
# 429 — limite de requisições excedido.
|
|
69
|
+
class RateLimited < HttpError; end
|
|
70
|
+
|
|
71
|
+
# 5xx — erro interno do servidor da Focus NFe.
|
|
72
|
+
class ServerError < HttpError; end
|
|
73
|
+
|
|
74
|
+
# Status não-2xx sem mapeamento específico (ex.: 418, 451).
|
|
75
|
+
class UnexpectedResponse < HttpError; end
|
|
76
|
+
|
|
77
|
+
# Erro client-side de configuração (token ausente, ambiente inválido) —
|
|
78
|
+
# não envolve resposta HTTP.
|
|
79
|
+
class ConfigurationError < Error; end
|
|
80
|
+
|
|
81
|
+
# Falha de transporte (timeout, conexão recusada, excesso de redirects).
|
|
82
|
+
class ConnectionError < Error; end
|
|
83
|
+
|
|
84
|
+
# Falha ao processar um webhook inbound (corpo malformado) — client-side,
|
|
85
|
+
# não envolve resposta HTTP.
|
|
86
|
+
class WebhookError < Error; end
|
|
87
|
+
|
|
88
|
+
# @return [Hash{Integer=>Class}] status HTTP específico => classe de exceção
|
|
89
|
+
BY_STATUS = {
|
|
90
|
+
400 => BadRequest,
|
|
91
|
+
401 => Unauthorized,
|
|
92
|
+
403 => Forbidden,
|
|
93
|
+
404 => NotFound,
|
|
94
|
+
409 => Conflict,
|
|
95
|
+
422 => ValidationError,
|
|
96
|
+
429 => RateLimited
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
module_function
|
|
100
|
+
|
|
101
|
+
# Resolve a classe de exceção correspondente a um status HTTP.
|
|
102
|
+
#
|
|
103
|
+
# @param status [Integer] código de status HTTP não-2xx
|
|
104
|
+
# @return [Class] subclasse de {HttpError}; qualquer 5xx vira {ServerError}
|
|
105
|
+
# e qualquer status sem mapeamento específico vira {UnexpectedResponse}
|
|
106
|
+
def class_for(status)
|
|
107
|
+
BY_STATUS[status] || (status.between?(500, 599) ? ServerError : UnexpectedResponse)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Constrói a exceção tipada já preenchida a partir de uma resposta.
|
|
111
|
+
#
|
|
112
|
+
# @param response [FocusNfe::HTTP::Response] resposta não-2xx recebida
|
|
113
|
+
# @return [HttpError] instância da classe certa com status/corpo/resposta
|
|
114
|
+
def from_response(response)
|
|
115
|
+
class_for(response.status).new(
|
|
116
|
+
"requisição falhou com status #{response.status}",
|
|
117
|
+
status: response.status,
|
|
118
|
+
body: response.body,
|
|
119
|
+
response: response
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module FocusNfe
|
|
6
|
+
module Esquemas
|
|
7
|
+
# Um campo de um {Esquema} de emissão, derivado das definições de
|
|
8
|
+
# +campos.focusnfe.com.br+. Conhece o nome do campo, sua obrigatoriedade e
|
|
9
|
+
# sabe parsear o tipo fiscal (+String[1-60]+, +Integer[1-9]+, +Decimal[13.2]+,
|
|
10
|
+
# +Date+, +DateTime+, enum, coleção) em restrições aplicáveis a um valor.
|
|
11
|
+
class Campo
|
|
12
|
+
# @return [Regexp] captura base e tamanho de um escalar de comprimento (ex.: +String[1-60]+)
|
|
13
|
+
ESCALAR = /\A(?<base>String|Integer)\[(?<inicio>\d+)(?:-(?<fim>\d+))?\]/
|
|
14
|
+
|
|
15
|
+
# @return [Regexp] captura os códigos declarados em um enum (+* +0+: …+ ou +*+0+: …+)
|
|
16
|
+
CODIGO_ENUM = /\*\s*\+([^+]+)\+/
|
|
17
|
+
|
|
18
|
+
# @param definicao [Hash] entrada do schema ({ "name", "type", "required", "collection", … })
|
|
19
|
+
def initialize(definicao)
|
|
20
|
+
@definicao = definicao
|
|
21
|
+
parsear_tipo
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [String] nome do campo no payload de emissão
|
|
25
|
+
def nome = @definicao["name"]
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] descrição do campo conforme +campos.focusnfe.com.br+
|
|
28
|
+
def descricao = @definicao["description"]
|
|
29
|
+
|
|
30
|
+
# @return [String, nil] tipo bruto como documentado (ex.: +"String[1-60]"+)
|
|
31
|
+
def tipo_bruto = @definicao["type"]
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] enumeração dos valores aceitos, como documentada
|
|
34
|
+
def enum = @definicao["enum"]
|
|
35
|
+
|
|
36
|
+
# @return [Array<String>] códigos aceitos extraídos do {#enum} (vazio se não houver)
|
|
37
|
+
def valores_enum
|
|
38
|
+
@valores_enum ||= enum.to_s.scan(CODIGO_ENUM).flatten
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean] se o campo declara um conjunto de valores aceitos
|
|
42
|
+
def enum? = !valores_enum.empty?
|
|
43
|
+
|
|
44
|
+
# @return [String, nil] tag XML subjacente do campo
|
|
45
|
+
def tag = @definicao["tag"]
|
|
46
|
+
|
|
47
|
+
# Representação serializável do campo, para introspecção externa (devs e
|
|
48
|
+
# ferramentas automatizadas). Coleções aninham a descrição dos subcampos em
|
|
49
|
+
# +:colecao+, em profundidade arbitrária; campos escalares têm +:colecao+ nil.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] descrição estruturada do campo
|
|
52
|
+
def to_h
|
|
53
|
+
{
|
|
54
|
+
nome: nome, descricao: descricao, tipo: tipo, tipo_bruto: tipo_bruto,
|
|
55
|
+
obrigatorio: obrigatorio?, tamanho_minimo: tamanho_minimo, tamanho_maximo: tamanho_maximo,
|
|
56
|
+
decimal: decimal&.to_h, enum: enum, tag: tag, colecao: esquema_colecao&.descrever
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] se o campo é obrigatório na emissão
|
|
61
|
+
def obrigatorio? = @definicao["required"] == true
|
|
62
|
+
|
|
63
|
+
# @return [Boolean] se o campo é uma coleção de subitens
|
|
64
|
+
def colecao? = tipo == :colecao
|
|
65
|
+
|
|
66
|
+
# @return [Symbol] tipo parseado (+:string+, +:integer+, +:decimal+, +:date+,
|
|
67
|
+
# +:datetime+, +:enum+, +:colecao+ ou +:desconhecido+)
|
|
68
|
+
attr_reader :tipo
|
|
69
|
+
|
|
70
|
+
# @return [Esquema, nil] esquema dos subcampos da coleção, ou +nil+ se o campo
|
|
71
|
+
# não for coleção ou não declarar +object_attributes+
|
|
72
|
+
def esquema_colecao
|
|
73
|
+
return unless colecao?
|
|
74
|
+
|
|
75
|
+
@esquema_colecao ||= (atributos = @definicao.dig("collection", "object_attributes")) && Esquema.new(atributos)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Integer, nil] tamanho/quantidade de dígitos mínimo (escalares)
|
|
79
|
+
attr_reader :tamanho_minimo
|
|
80
|
+
|
|
81
|
+
# @return [Integer, nil] tamanho/quantidade de dígitos máximo (escalares)
|
|
82
|
+
attr_reader :tamanho_maximo
|
|
83
|
+
|
|
84
|
+
# @return [Decimal, nil] especificação decimal do campo, ou +nil+ se não for decimal
|
|
85
|
+
attr_reader :decimal
|
|
86
|
+
|
|
87
|
+
# Valida um valor contra o tipo/tamanho e o conjunto de enum do campo.
|
|
88
|
+
# Coleções e tipos desconhecidos não restringem nesta etapa.
|
|
89
|
+
#
|
|
90
|
+
# @param valor [Object] valor informado para o campo
|
|
91
|
+
# @return [String, nil] mensagem de erro ou +nil+ se válido
|
|
92
|
+
def validar_valor(valor)
|
|
93
|
+
erro = validar_tipo(valor)
|
|
94
|
+
return erro if erro
|
|
95
|
+
|
|
96
|
+
validar_enum(valor) if enum?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def parsear_tipo
|
|
102
|
+
bruto = @definicao["type"]
|
|
103
|
+
escalar = bruto && ESCALAR.match(bruto)
|
|
104
|
+
return parsear_escalar(escalar) if escalar
|
|
105
|
+
|
|
106
|
+
@decimal = Decimal.parsear(bruto)
|
|
107
|
+
return @tipo = :decimal if @decimal
|
|
108
|
+
|
|
109
|
+
@tipo = tipo_nao_escalar(bruto)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def tipo_nao_escalar(bruto)
|
|
113
|
+
return :colecao if @definicao.key?("collection") || bruto.to_s.start_with?("Coleção")
|
|
114
|
+
return @definicao["enum"] ? :enum : :desconhecido if bruto.nil?
|
|
115
|
+
return :datetime if bruto.start_with?("DateTime")
|
|
116
|
+
return :date if bruto.start_with?("Date")
|
|
117
|
+
|
|
118
|
+
:desconhecido
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parsear_escalar(match)
|
|
122
|
+
@tipo = { "String" => :string, "Integer" => :integer }[match[:base].to_s]
|
|
123
|
+
@tamanho_minimo = Integer(match[:inicio])
|
|
124
|
+
@tamanho_maximo = match[:fim] ? Integer(match[:fim]) : @tamanho_minimo
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validar_tipo(valor)
|
|
128
|
+
case tipo
|
|
129
|
+
when :string then validar_string(valor)
|
|
130
|
+
when :integer then validar_integer(valor)
|
|
131
|
+
when :decimal then validar_decimal(valor)
|
|
132
|
+
when :date then validar_data(valor, Date)
|
|
133
|
+
when :datetime then validar_data(valor, DateTime)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def validar_string(valor)
|
|
138
|
+
comprimento = valor.to_s.length
|
|
139
|
+
return if comprimento.between?(tamanho_minimo, tamanho_maximo)
|
|
140
|
+
|
|
141
|
+
"#{nome}: tamanho #{comprimento} fora do intervalo #{tamanho_minimo}-#{tamanho_maximo}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def validar_integer(valor)
|
|
145
|
+
digitos = valor.to_s
|
|
146
|
+
return "#{nome}: deve conter apenas dígitos" unless digitos.match?(/\A\d+\z/)
|
|
147
|
+
return if digitos.length.between?(tamanho_minimo, tamanho_maximo)
|
|
148
|
+
|
|
149
|
+
"#{nome}: #{digitos.length} dígitos fora do intervalo #{tamanho_minimo}-#{tamanho_maximo}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def validar_decimal(valor)
|
|
153
|
+
mensagem = decimal&.validar(valor)
|
|
154
|
+
"#{nome}: #{mensagem}" if mensagem
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def validar_data(valor, classe)
|
|
158
|
+
classe.iso8601(valor.to_s)
|
|
159
|
+
nil
|
|
160
|
+
rescue ArgumentError, TypeError
|
|
161
|
+
"#{nome}: data inválida (esperado ISO 8601)"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def validar_enum(valor)
|
|
165
|
+
return if valores_enum.include?(valor.to_s)
|
|
166
|
+
|
|
167
|
+
"#{nome}: valor #{valor.inspect} fora do conjunto permitido (#{valores_enum.join(", ")})"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FocusNfe
|
|
4
|
+
# Introspecção pública dos esquemas empacotados: lista os documentos com schema
|
|
5
|
+
# e descreve seus campos como dado serializável, para devs e ferramentas
|
|
6
|
+
# automatizadas — sem token nem conexão.
|
|
7
|
+
module Esquemas
|
|
8
|
+
# @return [String] glob dos arquivos de schema empacotados em +data/schemas/+
|
|
9
|
+
GLOB_SCHEMAS = "schema_*.json"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Nomes dos documentos com schema empacotado, ordenados. Inclui os
|
|
13
|
+
# sub-schemas auxiliares (ex.: +"nfe_item"+, +"cte_transporte_aereo"+), pois
|
|
14
|
+
# também são introspectáveis via {descrever}.
|
|
15
|
+
#
|
|
16
|
+
# @return [Array<String>] nomes de documento aceitos por {descrever}
|
|
17
|
+
def disponiveis
|
|
18
|
+
Dir.glob(File.join(Esquema::DIRETORIO, GLOB_SCHEMAS))
|
|
19
|
+
.map { |caminho| File.basename(caminho, ".json").delete_prefix("schema_") }
|
|
20
|
+
.sort
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Descrição estruturada dos campos de um documento, para devs e ferramentas
|
|
24
|
+
# automatizadas conhecerem nomes, tipos e obrigatoriedade sem token nem
|
|
25
|
+
# conexão. Coleções aninham seus subcampos (ver {Campo#to_h}).
|
|
26
|
+
#
|
|
27
|
+
# @param nome [String] nome do documento (ver {disponiveis})
|
|
28
|
+
# @return [Array<Hash>, nil] descrição de cada campo, ou +nil+ se não houver schema
|
|
29
|
+
def descrever(nome)
|
|
30
|
+
Esquema.carregar(nome)&.descrever
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FocusNfe
|
|
4
|
+
module Esquemas
|
|
5
|
+
# Especificação decimal de um {Campo} (ex.: +Decimal[13.2]+, +Decimal[13.2-4]+):
|
|
6
|
+
# até +inteiros+ dígitos inteiros e de +casas_minimas+ a +casas_maximas+ casas
|
|
7
|
+
# decimais. Sabe validar um valor informado contra esses limites.
|
|
8
|
+
class Decimal
|
|
9
|
+
# @return [Regexp] captura inteiros e a faixa de casas (ex.: +Decimal[13.2-4]+)
|
|
10
|
+
ESPEC = /\ADecimal\[(?<inteiros>\d+)(?:\.(?<casas_min>\d*)(?:-(?<casas_max>\d+))?)?\]/
|
|
11
|
+
|
|
12
|
+
# @return [Regexp] decompõe um valor em parte inteira e fracionária
|
|
13
|
+
VALOR = /\A-?(?<inteira>\d+)(?:[.,](?<fracionaria>\d+))?\z/
|
|
14
|
+
|
|
15
|
+
# @param bruto [String, nil] tipo bruto do campo
|
|
16
|
+
# @return [Decimal, nil] a especificação parseada, ou +nil+ se +bruto+ não for decimal
|
|
17
|
+
def self.parsear(bruto)
|
|
18
|
+
match = bruto && ESPEC.match(bruto)
|
|
19
|
+
match && new(match)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Integer] quantidade máxima de dígitos inteiros
|
|
23
|
+
attr_reader :inteiros
|
|
24
|
+
|
|
25
|
+
# @return [Integer] quantidade mínima de casas decimais
|
|
26
|
+
attr_reader :casas_minimas
|
|
27
|
+
|
|
28
|
+
# @return [Integer] quantidade máxima de casas decimais
|
|
29
|
+
attr_reader :casas_maximas
|
|
30
|
+
|
|
31
|
+
# @param match [MatchData] captura de {ESPEC}
|
|
32
|
+
def initialize(match)
|
|
33
|
+
@inteiros = Integer(match[:inteiros])
|
|
34
|
+
@casas_minimas = match[:casas_min].to_s.empty? ? 0 : Integer(match[:casas_min])
|
|
35
|
+
@casas_maximas = match[:casas_max] ? Integer(match[:casas_max]) : @casas_minimas
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Valida um valor decimal. Lenient: aceita menos casas que o mínimo (zeros à
|
|
39
|
+
# direita são equivalentes) — rejeita apenas o claramente inválido.
|
|
40
|
+
#
|
|
41
|
+
# @param valor [Object] valor informado
|
|
42
|
+
# @return [String, nil] mensagem de erro (sem o nome do campo) ou +nil+ se válido
|
|
43
|
+
def validar(valor)
|
|
44
|
+
match = VALOR.match(valor.to_s.strip)
|
|
45
|
+
return "valor decimal inválido" unless match
|
|
46
|
+
|
|
47
|
+
erro_de_tamanho(match[:inteira].to_s, match[:fracionaria])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Hash] descrição serializável da especificação
|
|
51
|
+
def to_h
|
|
52
|
+
{ inteiros: inteiros, casas_minimas: casas_minimas, casas_maximas: casas_maximas }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def erro_de_tamanho(inteira, fracionaria)
|
|
58
|
+
digitos = inteira.sub(/\A0+(?=\d)/, "").length
|
|
59
|
+
casas = fracionaria&.length || 0
|
|
60
|
+
return "#{digitos} dígitos inteiros excedem o máximo de #{inteiros}" if digitos > inteiros
|
|
61
|
+
|
|
62
|
+
"#{casas} casas decimais excedem o máximo de #{casas_maximas}" if casas > casas_maximas
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|