nxgate 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/LICENSE +21 -0
- data/README.md +320 -0
- data/lib/nxgate/client.rb +292 -0
- data/lib/nxgate/error.rb +55 -0
- data/lib/nxgate/hmac_signer.rb +82 -0
- data/lib/nxgate/token_manager.rb +109 -0
- data/lib/nxgate/types.rb +129 -0
- data/lib/nxgate/webhook.rb +121 -0
- data/lib/nxgate.rb +35 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 26aaf6e56c28024006e8b39d22220851836bceaf0dc5a094ca8ba6879323fcd5
|
|
4
|
+
data.tar.gz: b96af5c8e0a12381144bce2d9b2a8df0d5603480898d7607bdecbfe606fdbe4b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6befe2c59740cd39eb9be1139c294473e440c133dfbb97263308cb60000612a3dc5c4d113dcc030c66ee13f951c2e2adc84b36fe93eb1a3dd255e2824d3dae95
|
|
7
|
+
data.tar.gz: c7339b2ea6be7b4416382d1492d8538ab75ab2e5decf46acea9840791b1d5aa4c66ec87bc5a5f8f624ef9f48c8613202c7786f7c83685cfdd98e4c367ed68f90
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NXGATE
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# NXGATE PIX SDK para Ruby
|
|
2
|
+
|
|
3
|
+
SDK oficial da NXGATE para integracoes PIX em Ruby. Permite gerar cobrancas (cash-in), realizar saques (cash-out), consultar saldo e transacoes, alem de processar webhooks.
|
|
4
|
+
|
|
5
|
+
## Requisitos
|
|
6
|
+
|
|
7
|
+
- Ruby 3.0 ou superior
|
|
8
|
+
- Sem dependencias externas (utiliza apenas `net/http`, `json` e `openssl` da stdlib)
|
|
9
|
+
|
|
10
|
+
## Instalacao
|
|
11
|
+
|
|
12
|
+
Adicione ao seu `Gemfile`:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'nxgate'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
E execute:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Ou instale diretamente:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gem install nxgate
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuracao
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
require 'nxgate'
|
|
34
|
+
|
|
35
|
+
# Configuracao basica
|
|
36
|
+
nx = NXGate::Client.new(
|
|
37
|
+
client_id: 'nxgate_xxx',
|
|
38
|
+
client_secret: 'secret'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Com assinatura HMAC (opcional)
|
|
42
|
+
nx = NXGate::Client.new(
|
|
43
|
+
client_id: 'nxgate_xxx',
|
|
44
|
+
client_secret: 'secret',
|
|
45
|
+
hmac_secret: 'sua_chave_hmac'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Com URL customizada e timeout
|
|
49
|
+
nx = NXGate::Client.new(
|
|
50
|
+
client_id: 'nxgate_xxx',
|
|
51
|
+
client_secret: 'secret',
|
|
52
|
+
base_url: 'https://sandbox.nxgate.com.br',
|
|
53
|
+
timeout: 60
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Uso
|
|
58
|
+
|
|
59
|
+
### Gerar Cobranca PIX (Cash-in)
|
|
60
|
+
|
|
61
|
+
Gera uma cobranca PIX e retorna o QR Code para pagamento.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
charge = nx.pix_generate(
|
|
65
|
+
valor: 100.00,
|
|
66
|
+
nome_pagador: 'Joao da Silva',
|
|
67
|
+
documento_pagador: '12345678901',
|
|
68
|
+
webhook: 'https://meusite.com/webhook'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
puts charge.status # "OK"
|
|
72
|
+
puts charge.id_transaction # "tx_abc123"
|
|
73
|
+
puts charge.payment_code # Codigo PIX copia e cola
|
|
74
|
+
puts charge.payment_code_base64 # QR Code em Base64
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Parametros opcionais
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
charge = nx.pix_generate(
|
|
81
|
+
valor: 250.00,
|
|
82
|
+
nome_pagador: 'Maria Santos',
|
|
83
|
+
documento_pagador: '98765432100',
|
|
84
|
+
forcar_pagador: true,
|
|
85
|
+
email_pagador: 'maria@email.com',
|
|
86
|
+
celular: '11999999999',
|
|
87
|
+
descricao: 'Pedido #12345',
|
|
88
|
+
webhook: 'https://meusite.com/webhook',
|
|
89
|
+
magic_id: 'pedido_12345',
|
|
90
|
+
api_key: 'chave_api',
|
|
91
|
+
split_users: [
|
|
92
|
+
{ username: 'lojista1', percentage: 70 },
|
|
93
|
+
{ username: 'lojista2', percentage: 30 }
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Saque PIX (Cash-out)
|
|
99
|
+
|
|
100
|
+
Realiza uma transferencia PIX para a chave informada.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
withdrawal = nx.pix_withdraw(
|
|
104
|
+
valor: 50.0,
|
|
105
|
+
chave_pix: 'joao@email.com',
|
|
106
|
+
tipo_chave: 'EMAIL'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
puts withdrawal.status # "OK"
|
|
110
|
+
puts withdrawal.message # "Saque realizado"
|
|
111
|
+
puts withdrawal.internal_reference # "ref_xyz"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Tipos de chave aceitos
|
|
115
|
+
|
|
116
|
+
| Tipo | Descricao |
|
|
117
|
+
|---------|----------------------|
|
|
118
|
+
| `CPF` | CPF do destinatario |
|
|
119
|
+
| `CNPJ` | CNPJ do destinatario |
|
|
120
|
+
| `PHONE` | Telefone celular |
|
|
121
|
+
| `EMAIL` | Endereco de e-mail |
|
|
122
|
+
| `RANDOM`| Chave aleatoria |
|
|
123
|
+
|
|
124
|
+
### Consultar Saldo
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
balance = nx.get_balance
|
|
128
|
+
|
|
129
|
+
puts balance.balance # Saldo total
|
|
130
|
+
puts balance.blocked # Saldo bloqueado
|
|
131
|
+
puts balance.available # Saldo disponivel
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Consultar Transacao
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
tx = nx.get_transaction(type: 'cash-in', txid: 'tx_abc123')
|
|
138
|
+
|
|
139
|
+
puts tx.id_transaction # "tx_abc123"
|
|
140
|
+
puts tx.status # "PAID"
|
|
141
|
+
puts tx.amount # 100.0
|
|
142
|
+
puts tx.paid_at # "2025-01-15T10:30:00Z"
|
|
143
|
+
puts tx.end_to_end # "e2e_xyz"
|
|
144
|
+
puts tx.raw # Hash completo da resposta
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Webhooks
|
|
148
|
+
|
|
149
|
+
### Processar Evento de Webhook
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Em um controller Rails, Sinatra, etc.
|
|
153
|
+
event = NXGate::Webhook.parse(request.body.read)
|
|
154
|
+
|
|
155
|
+
# Ou a partir de um Hash
|
|
156
|
+
event = NXGate::Webhook.parse(params)
|
|
157
|
+
|
|
158
|
+
puts event.type # Tipo do evento
|
|
159
|
+
puts event.data # Dados do evento
|
|
160
|
+
|
|
161
|
+
# Verificacoes de tipo
|
|
162
|
+
event.cash_in? # true se for evento de cash-in
|
|
163
|
+
event.cash_out? # true se for evento de cash-out
|
|
164
|
+
event.success? # true se for pagamento/saque bem-sucedido
|
|
165
|
+
event.refunded? # true se houve reembolso
|
|
166
|
+
event.error? # true se houve erro (apenas cash-out)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Tipos de Evento
|
|
170
|
+
|
|
171
|
+
#### Cash-in (QR Code)
|
|
172
|
+
|
|
173
|
+
| Tipo | Descricao |
|
|
174
|
+
|-------------------------------------|-------------------------|
|
|
175
|
+
| `QR_CODE_COPY_AND_PASTE_PAID` | Pagamento confirmado |
|
|
176
|
+
| `QR_CODE_COPY_AND_PASTE_REFUNDED` | Pagamento reembolsado |
|
|
177
|
+
|
|
178
|
+
#### Cash-out (Saque)
|
|
179
|
+
|
|
180
|
+
| Tipo | Descricao |
|
|
181
|
+
|-------------------------|-------------------------|
|
|
182
|
+
| `PIX_CASHOUT_SUCCESS` | Saque realizado |
|
|
183
|
+
| `PIX_CASHOUT_ERROR` | Erro no saque |
|
|
184
|
+
| `PIX_CASHOUT_REFUNDED` | Saque reembolsado |
|
|
185
|
+
|
|
186
|
+
### Exemplo com Sinatra
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
require 'sinatra'
|
|
190
|
+
require 'nxgate'
|
|
191
|
+
|
|
192
|
+
post '/webhook' do
|
|
193
|
+
payload = request.body.read
|
|
194
|
+
|
|
195
|
+
begin
|
|
196
|
+
event = NXGate::Webhook.parse(payload)
|
|
197
|
+
|
|
198
|
+
if event.cash_in? && event.success?
|
|
199
|
+
# Pagamento confirmado
|
|
200
|
+
puts "Pagamento recebido: R$ #{event.data[:amount]}"
|
|
201
|
+
puts "Transacao: #{event.data[:tx_id]}"
|
|
202
|
+
elsif event.cash_out? && event.success?
|
|
203
|
+
# Saque realizado
|
|
204
|
+
puts "Saque realizado: R$ #{event.data[:amount]}"
|
|
205
|
+
elsif event.error?
|
|
206
|
+
# Erro no saque
|
|
207
|
+
puts "Erro no saque: #{event.data[:id_transaction]}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
status 200
|
|
211
|
+
'OK'
|
|
212
|
+
rescue NXGate::Error => e
|
|
213
|
+
status 400
|
|
214
|
+
e.message
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Assinatura HMAC
|
|
220
|
+
|
|
221
|
+
Quando o `hmac_secret` e fornecido na inicializacao do client, todas as requisicoes sao automaticamente assinadas com HMAC-SHA256.
|
|
222
|
+
|
|
223
|
+
### Headers adicionados
|
|
224
|
+
|
|
225
|
+
| Header | Descricao |
|
|
226
|
+
|---------------------|----------------------------------------------|
|
|
227
|
+
| `X-Client-ID` | ID do cliente |
|
|
228
|
+
| `X-HMAC-Signature` | Assinatura HMAC-SHA256 em Base64 |
|
|
229
|
+
| `X-HMAC-Timestamp` | Timestamp ISO 8601 da requisicao |
|
|
230
|
+
| `X-HMAC-Nonce` | String unica por requisicao (UUID) |
|
|
231
|
+
|
|
232
|
+
### Payload assinado
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Verificacao de assinatura (para webhooks)
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
signer = NXGate::HmacSigner.new(
|
|
242
|
+
client_id: 'nxgate_xxx',
|
|
243
|
+
hmac_secret: 'sua_chave_hmac'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
valido = signer.verify(
|
|
247
|
+
method: 'POST',
|
|
248
|
+
path: '/webhook',
|
|
249
|
+
timestamp: request.env['HTTP_X_HMAC_TIMESTAMP'],
|
|
250
|
+
nonce: request.env['HTTP_X_HMAC_NONCE'],
|
|
251
|
+
body: request.body.read,
|
|
252
|
+
signature: request.env['HTTP_X_HMAC_SIGNATURE']
|
|
253
|
+
)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Tratamento de Erros
|
|
257
|
+
|
|
258
|
+
Todos os erros herdam de `NXGate::Error` e incluem informacoes detalhadas.
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
begin
|
|
262
|
+
nx.pix_generate(valor: 100.0, nome_pagador: 'Joao', documento_pagador: '123')
|
|
263
|
+
rescue NXGate::AuthenticationError => e
|
|
264
|
+
# Erro de autenticacao (401)
|
|
265
|
+
puts "Autenticacao falhou: #{e.message}"
|
|
266
|
+
puts "HTTP Status: #{e.http_status}"
|
|
267
|
+
rescue NXGate::ServiceUnavailableError => e
|
|
268
|
+
# Servico indisponivel (503) - apos retentativas
|
|
269
|
+
puts "Servico indisponivel: #{e.message}"
|
|
270
|
+
rescue NXGate::TimeoutError => e
|
|
271
|
+
# Timeout na conexao
|
|
272
|
+
puts "Timeout: #{e.message}"
|
|
273
|
+
rescue NXGate::Error => e
|
|
274
|
+
# Outros erros da API
|
|
275
|
+
puts "Erro: #{e.code} - #{e.title}"
|
|
276
|
+
puts "Detalhe: #{e.description}"
|
|
277
|
+
puts "HTTP: #{e.http_status}"
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Hierarquia de Erros
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
NXGate::Error (StandardError)
|
|
285
|
+
|-- NXGate::AuthenticationError # 401 - Falha na autenticacao
|
|
286
|
+
|-- NXGate::TimeoutError # Timeout na conexao
|
|
287
|
+
|-- NXGate::ServiceUnavailableError # 503 - Servico indisponivel
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Retentativas Automaticas
|
|
291
|
+
|
|
292
|
+
Requisicoes que retornam HTTP 503 sao automaticamente retentadas ate 2 vezes com backoff exponencial:
|
|
293
|
+
|
|
294
|
+
- 1a retentativa: aguarda 1 segundo
|
|
295
|
+
- 2a retentativa: aguarda 2 segundos
|
|
296
|
+
- Apos 2 retentativas falhas, lanca `NXGate::ServiceUnavailableError`
|
|
297
|
+
|
|
298
|
+
## Gerenciamento de Token
|
|
299
|
+
|
|
300
|
+
O token OAuth2 e gerenciado automaticamente:
|
|
301
|
+
|
|
302
|
+
- Obtido na primeira requisicao
|
|
303
|
+
- Cacheado em memoria
|
|
304
|
+
- Renovado automaticamente 60 segundos antes da expiracao
|
|
305
|
+
- Invalidado e renovado em caso de erro 401
|
|
306
|
+
- Thread-safe (utiliza Mutex)
|
|
307
|
+
|
|
308
|
+
## Testes
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Executar todos os testes
|
|
312
|
+
bundle exec rake test
|
|
313
|
+
|
|
314
|
+
# Ou diretamente
|
|
315
|
+
ruby -Ilib -Itest test/test_client.rb
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Licenca
|
|
319
|
+
|
|
320
|
+
Distribuido sob a licenca MIT. Consulte o arquivo [LICENSE](LICENSE) para mais informacoes.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module NXGate
|
|
8
|
+
# Cliente principal da SDK NXGATE PIX.
|
|
9
|
+
#
|
|
10
|
+
# Gerencia autenticacao, assinatura HMAC, retentativas e todas
|
|
11
|
+
# as operacoes da API PIX.
|
|
12
|
+
#
|
|
13
|
+
# @example Uso basico
|
|
14
|
+
# client = NXGate::Client.new(
|
|
15
|
+
# client_id: "nxgate_xxx",
|
|
16
|
+
# client_secret: "secret"
|
|
17
|
+
# )
|
|
18
|
+
# charge = client.pix_generate(valor: 100.0, nome_pagador: "Joao", documento_pagador: "12345678901")
|
|
19
|
+
#
|
|
20
|
+
class Client
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.nxgate.com.br"
|
|
22
|
+
DEFAULT_TIMEOUT = 30
|
|
23
|
+
MAX_RETRIES = 2
|
|
24
|
+
RETRY_BASE_DELAY = 1.0 # segundos
|
|
25
|
+
|
|
26
|
+
attr_reader :base_url
|
|
27
|
+
|
|
28
|
+
# @param client_id [String] ID do cliente NXGATE
|
|
29
|
+
# @param client_secret [String] Segredo do cliente NXGATE
|
|
30
|
+
# @param hmac_secret [String, nil] Segredo HMAC (opcional). Quando fornecido, todas as requisicoes serao assinadas.
|
|
31
|
+
# @param base_url [String] URL base da API (padrao: https://api.nxgate.com.br)
|
|
32
|
+
# @param timeout [Integer] Timeout em segundos para requisicoes HTTP (padrao: 30)
|
|
33
|
+
def initialize(client_id:, client_secret:, hmac_secret: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
34
|
+
@client_id = client_id
|
|
35
|
+
@client_secret = client_secret
|
|
36
|
+
@base_url = base_url.chomp("/")
|
|
37
|
+
@timeout = timeout
|
|
38
|
+
|
|
39
|
+
@hmac_signer = if hmac_secret
|
|
40
|
+
HmacSigner.new(client_id: client_id, hmac_secret: hmac_secret)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@token_manager = TokenManager.new(
|
|
44
|
+
base_url: @base_url,
|
|
45
|
+
client_id: client_id,
|
|
46
|
+
client_secret: client_secret,
|
|
47
|
+
hmac_signer: @hmac_signer
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Gera uma cobranca PIX (cash-in) e retorna o QR Code.
|
|
52
|
+
#
|
|
53
|
+
# @param valor [Float] Valor da cobranca (obrigatorio)
|
|
54
|
+
# @param nome_pagador [String] Nome do pagador (obrigatorio)
|
|
55
|
+
# @param documento_pagador [String] CPF ou CNPJ do pagador (obrigatorio)
|
|
56
|
+
# @param forcar_pagador [Boolean, nil] Forcar dados do pagador
|
|
57
|
+
# @param email_pagador [String, nil] Email do pagador
|
|
58
|
+
# @param celular [String, nil] Celular do pagador
|
|
59
|
+
# @param descricao [String, nil] Descricao da cobranca
|
|
60
|
+
# @param webhook [String, nil] URL de webhook para notificacoes
|
|
61
|
+
# @param magic_id [String, nil] ID magico para rastreamento
|
|
62
|
+
# @param api_key [String, nil] Chave de API adicional
|
|
63
|
+
# @param split_users [Array<Hash>, nil] Lista de split de pagamento [{username:, percentage:}]
|
|
64
|
+
# @return [NXGate::PixChargeResponse] Resposta com QR Code e dados da cobranca
|
|
65
|
+
# @raise [NXGate::Error] Se a requisicao falhar
|
|
66
|
+
def pix_generate(valor:, nome_pagador:, documento_pagador:, forcar_pagador: nil,
|
|
67
|
+
email_pagador: nil, celular: nil, descricao: nil, webhook: nil,
|
|
68
|
+
magic_id: nil, api_key: nil, split_users: nil)
|
|
69
|
+
body = {
|
|
70
|
+
valor: valor,
|
|
71
|
+
nome_pagador: nome_pagador,
|
|
72
|
+
documento_pagador: documento_pagador
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
body[:forcar_pagador] = forcar_pagador unless forcar_pagador.nil?
|
|
76
|
+
body[:email_pagador] = email_pagador if email_pagador
|
|
77
|
+
body[:celular] = celular if celular
|
|
78
|
+
body[:descricao] = descricao if descricao
|
|
79
|
+
body[:webhook] = webhook if webhook
|
|
80
|
+
body[:magic_id] = magic_id if magic_id
|
|
81
|
+
body[:api_key] = api_key if api_key
|
|
82
|
+
|
|
83
|
+
if split_users
|
|
84
|
+
body[:split_users] = split_users.map do |user|
|
|
85
|
+
if user.is_a?(SplitUser)
|
|
86
|
+
user.to_h
|
|
87
|
+
else
|
|
88
|
+
{ "username" => user[:username] || user["username"],
|
|
89
|
+
"percentage" => user[:percentage] || user["percentage"] }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
data = post("/pix/gerar", body)
|
|
95
|
+
PixChargeResponse.from_hash(data)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Realiza um saque PIX (cash-out).
|
|
99
|
+
#
|
|
100
|
+
# @param valor [Float] Valor do saque (obrigatorio)
|
|
101
|
+
# @param chave_pix [String] Chave PIX de destino (obrigatorio)
|
|
102
|
+
# @param tipo_chave [String] Tipo da chave: CPF, CNPJ, PHONE, EMAIL ou RANDOM (obrigatorio)
|
|
103
|
+
# @param documento [String, nil] Documento do destinatario
|
|
104
|
+
# @param webhook [String, nil] URL de webhook para notificacoes
|
|
105
|
+
# @param magic_id [String, nil] ID magico para rastreamento
|
|
106
|
+
# @param api_key [String, nil] Chave de API adicional
|
|
107
|
+
# @return [NXGate::PixWithdrawResponse] Resposta do saque
|
|
108
|
+
# @raise [NXGate::Error] Se a requisicao falhar
|
|
109
|
+
def pix_withdraw(valor:, chave_pix:, tipo_chave:, documento: nil, webhook: nil,
|
|
110
|
+
magic_id: nil, api_key: nil)
|
|
111
|
+
valid_key_types = %w[CPF CNPJ PHONE EMAIL RANDOM]
|
|
112
|
+
unless valid_key_types.include?(tipo_chave.to_s.upcase)
|
|
113
|
+
raise Error.new(
|
|
114
|
+
code: "INVALID_KEY_TYPE",
|
|
115
|
+
title: "Tipo de chave invalido",
|
|
116
|
+
description: "tipo_chave deve ser um de: #{valid_key_types.join(', ')}. Recebido: #{tipo_chave}"
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
body = {
|
|
121
|
+
valor: valor,
|
|
122
|
+
chave_pix: chave_pix,
|
|
123
|
+
tipo_chave: tipo_chave.to_s.upcase
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
body[:documento] = documento if documento
|
|
127
|
+
body[:webhook] = webhook if webhook
|
|
128
|
+
body[:magic_id] = magic_id if magic_id
|
|
129
|
+
body[:api_key] = api_key if api_key
|
|
130
|
+
|
|
131
|
+
data = post("/pix/sacar", body)
|
|
132
|
+
PixWithdrawResponse.from_hash(data)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Consulta o saldo da conta.
|
|
136
|
+
#
|
|
137
|
+
# @return [NXGate::BalanceResponse] Saldo disponivel, bloqueado e total
|
|
138
|
+
# @raise [NXGate::Error] Se a requisicao falhar
|
|
139
|
+
def get_balance
|
|
140
|
+
data = get("/v1/balance")
|
|
141
|
+
BalanceResponse.from_hash(data)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Consulta uma transacao especifica.
|
|
145
|
+
#
|
|
146
|
+
# @param type [String] Tipo da transacao: "cash-in" ou "cash-out"
|
|
147
|
+
# @param txid [String] ID da transacao
|
|
148
|
+
# @return [NXGate::TransactionResponse] Dados da transacao
|
|
149
|
+
# @raise [NXGate::Error] Se a requisicao falhar
|
|
150
|
+
def get_transaction(type:, txid:)
|
|
151
|
+
path = "/v1/transactions?type=#{uri_encode(type)}&txid=#{uri_encode(txid)}"
|
|
152
|
+
data = get(path)
|
|
153
|
+
TransactionResponse.from_hash(data)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def get(path)
|
|
159
|
+
request_with_retry(:get, path)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def post(path, body)
|
|
163
|
+
request_with_retry(:post, path, body)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def request_with_retry(method, path, body = nil)
|
|
167
|
+
attempts = 0
|
|
168
|
+
|
|
169
|
+
loop do
|
|
170
|
+
attempts += 1
|
|
171
|
+
|
|
172
|
+
begin
|
|
173
|
+
return execute_request(method, path, body)
|
|
174
|
+
rescue ServiceUnavailableError => e
|
|
175
|
+
raise e if attempts > MAX_RETRIES
|
|
176
|
+
|
|
177
|
+
delay = RETRY_BASE_DELAY * (2**(attempts - 1))
|
|
178
|
+
sleep(delay)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def execute_request(method, path, body = nil)
|
|
184
|
+
uri = URI("#{@base_url}#{path}")
|
|
185
|
+
http = build_http(uri)
|
|
186
|
+
|
|
187
|
+
request = build_request(method, uri, body)
|
|
188
|
+
response = http.request(request)
|
|
189
|
+
|
|
190
|
+
handle_response(response)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def build_http(uri)
|
|
194
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
195
|
+
http.use_ssl = (uri.scheme == "https")
|
|
196
|
+
http.open_timeout = @timeout
|
|
197
|
+
http.read_timeout = @timeout
|
|
198
|
+
http
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def build_request(method, uri, body = nil)
|
|
202
|
+
path = uri.request_uri
|
|
203
|
+
json_body = body ? JSON.generate(body) : ""
|
|
204
|
+
|
|
205
|
+
request = case method
|
|
206
|
+
when :get
|
|
207
|
+
Net::HTTP::Get.new(path)
|
|
208
|
+
when :post
|
|
209
|
+
req = Net::HTTP::Post.new(path)
|
|
210
|
+
req.body = json_body
|
|
211
|
+
req
|
|
212
|
+
else
|
|
213
|
+
raise ArgumentError, "Metodo HTTP nao suportado: #{method}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
request["Content-Type"] = "application/json"
|
|
217
|
+
request["Accept"] = "application/json"
|
|
218
|
+
request["Authorization"] = "Bearer #{@token_manager.access_token}"
|
|
219
|
+
request["User-Agent"] = "nxgate-ruby/#{NXGate::VERSION}"
|
|
220
|
+
|
|
221
|
+
if @hmac_signer
|
|
222
|
+
hmac_headers = @hmac_signer.sign(
|
|
223
|
+
method: method.to_s.upcase,
|
|
224
|
+
path: uri.path,
|
|
225
|
+
body: json_body
|
|
226
|
+
)
|
|
227
|
+
hmac_headers.each { |key, value| request[key] = value }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
request
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def handle_response(response)
|
|
234
|
+
status = response.code.to_i
|
|
235
|
+
|
|
236
|
+
case status
|
|
237
|
+
when 200..299
|
|
238
|
+
parse_success(response)
|
|
239
|
+
when 401
|
|
240
|
+
@token_manager.invalidate!
|
|
241
|
+
error_data = parse_error_body(response.body)
|
|
242
|
+
raise AuthenticationError.new(
|
|
243
|
+
code: error_data[:code],
|
|
244
|
+
title: error_data[:title],
|
|
245
|
+
description: error_data[:description],
|
|
246
|
+
http_status: status
|
|
247
|
+
)
|
|
248
|
+
when 503
|
|
249
|
+
error_data = parse_error_body(response.body)
|
|
250
|
+
raise ServiceUnavailableError.new(
|
|
251
|
+
code: error_data[:code] || "SERVICE_UNAVAILABLE",
|
|
252
|
+
title: error_data[:title] || "Servico indisponivel",
|
|
253
|
+
description: error_data[:description] || "A API esta temporariamente indisponivel",
|
|
254
|
+
http_status: status
|
|
255
|
+
)
|
|
256
|
+
else
|
|
257
|
+
error_data = parse_error_body(response.body)
|
|
258
|
+
raise Error.new(
|
|
259
|
+
code: error_data[:code],
|
|
260
|
+
title: error_data[:title],
|
|
261
|
+
description: error_data[:description],
|
|
262
|
+
http_status: status
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def parse_success(response)
|
|
268
|
+
return {} if response.body.nil? || response.body.empty?
|
|
269
|
+
|
|
270
|
+
JSON.parse(response.body)
|
|
271
|
+
rescue JSON::ParserError
|
|
272
|
+
{ "raw" => response.body }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def parse_error_body(body)
|
|
276
|
+
return { code: nil, title: nil, description: nil } if body.nil? || body.empty?
|
|
277
|
+
|
|
278
|
+
data = JSON.parse(body)
|
|
279
|
+
{
|
|
280
|
+
code: data["code"] || data["error"],
|
|
281
|
+
title: data["title"] || data["error_description"],
|
|
282
|
+
description: data["description"] || data["message"] || body
|
|
283
|
+
}
|
|
284
|
+
rescue JSON::ParserError
|
|
285
|
+
{ code: nil, title: nil, description: body }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def uri_encode(str)
|
|
289
|
+
URI.encode_www_form_component(str.to_s)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
data/lib/nxgate/error.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NXGate
|
|
4
|
+
# Erro personalizado para respostas da API NXGATE.
|
|
5
|
+
#
|
|
6
|
+
# Atributos:
|
|
7
|
+
# code - Codigo interno retornado pela API (ex.: "INVALID_TOKEN")
|
|
8
|
+
# title - Titulo curto do erro
|
|
9
|
+
# description - Descricao detalhada
|
|
10
|
+
# http_status - Codigo HTTP da resposta (ex.: 401, 422, 503)
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
attr_reader :code, :title, :description, :http_status
|
|
13
|
+
|
|
14
|
+
def initialize(message = nil, code: nil, title: nil, description: nil, http_status: nil)
|
|
15
|
+
@code = code
|
|
16
|
+
@title = title
|
|
17
|
+
@description = description
|
|
18
|
+
@http_status = http_status
|
|
19
|
+
|
|
20
|
+
full_message = build_message(message)
|
|
21
|
+
super(full_message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
code: @code,
|
|
27
|
+
title: @title,
|
|
28
|
+
description: @description,
|
|
29
|
+
http_status: @http_status
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_message(message)
|
|
36
|
+
return message if message
|
|
37
|
+
|
|
38
|
+
parts = []
|
|
39
|
+
parts << "[#{@code}]" if @code
|
|
40
|
+
parts << @title if @title
|
|
41
|
+
parts << "- #{@description}" if @description
|
|
42
|
+
parts << "(HTTP #{@http_status})" if @http_status
|
|
43
|
+
parts.empty? ? "NXGate API Error" : parts.join(" ")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Erro de autenticacao (401)
|
|
48
|
+
class AuthenticationError < Error; end
|
|
49
|
+
|
|
50
|
+
# Erro de timeout na conexao
|
|
51
|
+
class TimeoutError < Error; end
|
|
52
|
+
|
|
53
|
+
# Erro quando o servico esta indisponivel (503) apos retentativas
|
|
54
|
+
class ServiceUnavailableError < Error; end
|
|
55
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module NXGate
|
|
9
|
+
# Responsavel por gerar assinaturas HMAC-SHA256 para requisicoes autenticadas.
|
|
10
|
+
#
|
|
11
|
+
# O payload assinado segue o formato:
|
|
12
|
+
# "METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY"
|
|
13
|
+
#
|
|
14
|
+
# Headers gerados:
|
|
15
|
+
# X-Client-ID - Identificador do cliente
|
|
16
|
+
# X-HMAC-Signature - Assinatura HMAC-SHA256 em Base64
|
|
17
|
+
# X-HMAC-Timestamp - Timestamp ISO 8601
|
|
18
|
+
# X-HMAC-Nonce - String unica por requisicao
|
|
19
|
+
class HmacSigner
|
|
20
|
+
# @param client_id [String] ID do cliente NXGATE
|
|
21
|
+
# @param hmac_secret [String] Chave secreta para HMAC
|
|
22
|
+
def initialize(client_id:, hmac_secret:)
|
|
23
|
+
@client_id = client_id
|
|
24
|
+
@hmac_secret = hmac_secret
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Gera os headers de assinatura HMAC para uma requisicao.
|
|
28
|
+
#
|
|
29
|
+
# @param method [String] Metodo HTTP (GET, POST, etc.)
|
|
30
|
+
# @param path [String] Caminho da requisicao (ex.: "/pix/gerar")
|
|
31
|
+
# @param body [String] Corpo da requisicao (string vazia para GET)
|
|
32
|
+
# @return [Hash] Headers de assinatura
|
|
33
|
+
def sign(method:, path:, body: "")
|
|
34
|
+
timestamp = Time.now.utc.iso8601
|
|
35
|
+
nonce = SecureRandom.uuid
|
|
36
|
+
|
|
37
|
+
payload = "#{method.upcase}\n#{path}\n#{timestamp}\n#{nonce}\n#{body}"
|
|
38
|
+
|
|
39
|
+
digest = OpenSSL::Digest.new("SHA256")
|
|
40
|
+
hmac = OpenSSL::HMAC.digest(digest, @hmac_secret, payload)
|
|
41
|
+
signature = Base64.strict_encode64(hmac)
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
"X-Client-ID" => @client_id,
|
|
45
|
+
"X-HMAC-Signature" => signature,
|
|
46
|
+
"X-HMAC-Timestamp" => timestamp,
|
|
47
|
+
"X-HMAC-Nonce" => nonce
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Verifica se uma assinatura HMAC recebida e valida.
|
|
52
|
+
# Util para validar webhooks assinados.
|
|
53
|
+
#
|
|
54
|
+
# @param method [String] Metodo HTTP
|
|
55
|
+
# @param path [String] Caminho da requisicao
|
|
56
|
+
# @param timestamp [String] Timestamp ISO 8601 recebido
|
|
57
|
+
# @param nonce [String] Nonce recebido
|
|
58
|
+
# @param body [String] Corpo da requisicao
|
|
59
|
+
# @param signature [String] Assinatura Base64 recebida
|
|
60
|
+
# @return [Boolean] true se a assinatura e valida
|
|
61
|
+
def verify(method:, path:, timestamp:, nonce:, body:, signature:)
|
|
62
|
+
payload = "#{method.upcase}\n#{path}\n#{timestamp}\n#{nonce}\n#{body}"
|
|
63
|
+
|
|
64
|
+
digest = OpenSSL::Digest.new("SHA256")
|
|
65
|
+
hmac = OpenSSL::HMAC.digest(digest, @hmac_secret, payload)
|
|
66
|
+
expected = Base64.strict_encode64(hmac)
|
|
67
|
+
|
|
68
|
+
secure_compare(expected, signature)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Comparacao em tempo constante para evitar timing attacks.
|
|
74
|
+
def secure_compare(a, b)
|
|
75
|
+
return false unless a.bytesize == b.bytesize
|
|
76
|
+
|
|
77
|
+
result = 0
|
|
78
|
+
a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
|
|
79
|
+
result.zero?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module NXGate
|
|
8
|
+
# Gerencia tokens OAuth2 com cache automatico e renovacao.
|
|
9
|
+
#
|
|
10
|
+
# O token e cacheado e renovado automaticamente 60 segundos antes
|
|
11
|
+
# da expiracao para evitar falhas por token expirado.
|
|
12
|
+
class TokenManager
|
|
13
|
+
# Margem de seguranca em segundos antes da expiracao para renovar o token.
|
|
14
|
+
EXPIRY_MARGIN = 60
|
|
15
|
+
|
|
16
|
+
# @param base_url [String] URL base da API
|
|
17
|
+
# @param client_id [String] ID do cliente
|
|
18
|
+
# @param client_secret [String] Segredo do cliente
|
|
19
|
+
# @param hmac_signer [HmacSigner, nil] Assinador HMAC opcional
|
|
20
|
+
def initialize(base_url:, client_id:, client_secret:, hmac_signer: nil)
|
|
21
|
+
@base_url = base_url
|
|
22
|
+
@client_id = client_id
|
|
23
|
+
@client_secret = client_secret
|
|
24
|
+
@hmac_signer = hmac_signer
|
|
25
|
+
@token = nil
|
|
26
|
+
@expires_at = nil
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Retorna um token valido, renovando se necessario.
|
|
31
|
+
# Thread-safe via Mutex.
|
|
32
|
+
#
|
|
33
|
+
# @return [String] Token de acesso Bearer
|
|
34
|
+
# @raise [NXGate::AuthenticationError] Se a autenticacao falhar
|
|
35
|
+
def access_token
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
fetch_token if token_expired?
|
|
38
|
+
@token
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Forca a renovacao do token na proxima chamada.
|
|
43
|
+
def invalidate!
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@token = nil
|
|
46
|
+
@expires_at = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def token_expired?
|
|
53
|
+
@token.nil? || @expires_at.nil? || Time.now >= @expires_at
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def fetch_token
|
|
57
|
+
uri = URI("#{@base_url}/oauth2/token")
|
|
58
|
+
body = JSON.generate({
|
|
59
|
+
grant_type: "client_credentials",
|
|
60
|
+
client_id: @client_id,
|
|
61
|
+
client_secret: @client_secret
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
65
|
+
http.use_ssl = (uri.scheme == "https")
|
|
66
|
+
http.open_timeout = 10
|
|
67
|
+
http.read_timeout = 15
|
|
68
|
+
|
|
69
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
70
|
+
request["Content-Type"] = "application/json"
|
|
71
|
+
request["Accept"] = "application/json"
|
|
72
|
+
|
|
73
|
+
if @hmac_signer
|
|
74
|
+
hmac_headers = @hmac_signer.sign(method: "POST", path: uri.path, body: body)
|
|
75
|
+
hmac_headers.each { |key, value| request[key] = value }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
request.body = body
|
|
79
|
+
|
|
80
|
+
response = http.request(request)
|
|
81
|
+
|
|
82
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
83
|
+
parsed = parse_error_body(response.body)
|
|
84
|
+
raise AuthenticationError.new(
|
|
85
|
+
code: parsed[:code] || "AUTH_FAILED",
|
|
86
|
+
title: parsed[:title] || "Falha na autenticacao",
|
|
87
|
+
description: parsed[:description] || response.body,
|
|
88
|
+
http_status: response.code.to_i
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
data = JSON.parse(response.body)
|
|
93
|
+
@token = data["access_token"]
|
|
94
|
+
expires_in = (data["expires_in"] || 3600).to_i
|
|
95
|
+
@expires_at = Time.now + expires_in - EXPIRY_MARGIN
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_error_body(body)
|
|
99
|
+
data = JSON.parse(body)
|
|
100
|
+
{
|
|
101
|
+
code: data["code"] || data["error"],
|
|
102
|
+
title: data["title"] || data["error_description"],
|
|
103
|
+
description: data["description"] || data["message"]
|
|
104
|
+
}
|
|
105
|
+
rescue JSON::ParserError
|
|
106
|
+
{ code: nil, title: nil, description: body }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/nxgate/types.rb
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NXGate
|
|
4
|
+
# Resposta de autenticacao OAuth2.
|
|
5
|
+
TokenResponse = Struct.new(
|
|
6
|
+
:access_token,
|
|
7
|
+
:token_type,
|
|
8
|
+
:expires_in,
|
|
9
|
+
keyword_init: true
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Resposta de geracao de cobranca PIX (cash-in).
|
|
13
|
+
PixChargeResponse = Struct.new(
|
|
14
|
+
:status,
|
|
15
|
+
:message,
|
|
16
|
+
:payment_code,
|
|
17
|
+
:id_transaction,
|
|
18
|
+
:payment_code_base64,
|
|
19
|
+
keyword_init: true
|
|
20
|
+
) do
|
|
21
|
+
def self.from_hash(hash)
|
|
22
|
+
new(
|
|
23
|
+
status: hash["status"],
|
|
24
|
+
message: hash["message"],
|
|
25
|
+
payment_code: hash["paymentCode"],
|
|
26
|
+
id_transaction: hash["idTransaction"],
|
|
27
|
+
payment_code_base64: hash["paymentCodeBase64"]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Resposta de saque PIX (cash-out).
|
|
33
|
+
PixWithdrawResponse = Struct.new(
|
|
34
|
+
:status,
|
|
35
|
+
:message,
|
|
36
|
+
:internal_reference,
|
|
37
|
+
keyword_init: true
|
|
38
|
+
) do
|
|
39
|
+
def self.from_hash(hash)
|
|
40
|
+
new(
|
|
41
|
+
status: hash["status"],
|
|
42
|
+
message: hash["message"],
|
|
43
|
+
internal_reference: hash["internalreference"]
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Resposta de consulta de saldo.
|
|
49
|
+
BalanceResponse = Struct.new(
|
|
50
|
+
:balance,
|
|
51
|
+
:blocked,
|
|
52
|
+
:available,
|
|
53
|
+
keyword_init: true
|
|
54
|
+
) do
|
|
55
|
+
def self.from_hash(hash)
|
|
56
|
+
new(
|
|
57
|
+
balance: hash["balance"],
|
|
58
|
+
blocked: hash["blocked"],
|
|
59
|
+
available: hash["available"]
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Resposta de consulta de transacao.
|
|
65
|
+
TransactionResponse = Struct.new(
|
|
66
|
+
:id_transaction,
|
|
67
|
+
:status,
|
|
68
|
+
:amount,
|
|
69
|
+
:paid_at,
|
|
70
|
+
:end_to_end,
|
|
71
|
+
:raw,
|
|
72
|
+
keyword_init: true
|
|
73
|
+
) do
|
|
74
|
+
def self.from_hash(hash)
|
|
75
|
+
new(
|
|
76
|
+
id_transaction: hash["idTransaction"],
|
|
77
|
+
status: hash["status"],
|
|
78
|
+
amount: hash["amount"],
|
|
79
|
+
paid_at: hash["paidAt"],
|
|
80
|
+
end_to_end: hash["endToEnd"],
|
|
81
|
+
raw: hash
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Evento de webhook recebido.
|
|
87
|
+
WebhookEvent = Struct.new(
|
|
88
|
+
:type,
|
|
89
|
+
:data,
|
|
90
|
+
:raw,
|
|
91
|
+
keyword_init: true
|
|
92
|
+
) do
|
|
93
|
+
# Retorna true se o evento e de cash-in (QR Code).
|
|
94
|
+
def cash_in?
|
|
95
|
+
type&.start_with?("QR_CODE_")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Retorna true se o evento e de cash-out (saque).
|
|
99
|
+
def cash_out?
|
|
100
|
+
type&.start_with?("PIX_CASHOUT_")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Retorna true se o pagamento foi confirmado com sucesso.
|
|
104
|
+
def success?
|
|
105
|
+
type == "QR_CODE_COPY_AND_PASTE_PAID" || type == "PIX_CASHOUT_SUCCESS"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Retorna true se houve reembolso.
|
|
109
|
+
def refunded?
|
|
110
|
+
type == "QR_CODE_COPY_AND_PASTE_REFUNDED" || type == "PIX_CASHOUT_REFUNDED"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Retorna true se houve erro (apenas cash-out).
|
|
114
|
+
def error?
|
|
115
|
+
type == "PIX_CASHOUT_ERROR"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Dados de split de pagamento.
|
|
120
|
+
SplitUser = Struct.new(
|
|
121
|
+
:username,
|
|
122
|
+
:percentage,
|
|
123
|
+
keyword_init: true
|
|
124
|
+
) do
|
|
125
|
+
def to_h
|
|
126
|
+
{ "username" => username, "percentage" => percentage }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module NXGate
|
|
6
|
+
# Parser de eventos de webhook da NXGATE.
|
|
7
|
+
#
|
|
8
|
+
# Suporta eventos de cash-in (QR Code) e cash-out (saque PIX).
|
|
9
|
+
#
|
|
10
|
+
# Tipos de evento cash-in:
|
|
11
|
+
# - QR_CODE_COPY_AND_PASTE_PAID - Pagamento confirmado
|
|
12
|
+
# - QR_CODE_COPY_AND_PASTE_REFUNDED - Pagamento reembolsado
|
|
13
|
+
#
|
|
14
|
+
# Tipos de evento cash-out:
|
|
15
|
+
# - PIX_CASHOUT_SUCCESS - Saque realizado com sucesso
|
|
16
|
+
# - PIX_CASHOUT_ERROR - Erro no saque
|
|
17
|
+
# - PIX_CASHOUT_REFUNDED - Saque reembolsado
|
|
18
|
+
module Webhook
|
|
19
|
+
CASH_IN_TYPES = %w[
|
|
20
|
+
QR_CODE_COPY_AND_PASTE_PAID
|
|
21
|
+
QR_CODE_COPY_AND_PASTE_REFUNDED
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
CASH_OUT_TYPES = %w[
|
|
25
|
+
PIX_CASHOUT_SUCCESS
|
|
26
|
+
PIX_CASHOUT_ERROR
|
|
27
|
+
PIX_CASHOUT_REFUNDED
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
ALL_TYPES = (CASH_IN_TYPES + CASH_OUT_TYPES).freeze
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Faz o parse de um payload de webhook.
|
|
34
|
+
#
|
|
35
|
+
# @param payload [String, Hash] Payload JSON (string) ou Hash ja parseado
|
|
36
|
+
# @return [NXGate::WebhookEvent] Evento parseado
|
|
37
|
+
# @raise [NXGate::Error] Se o payload for invalido
|
|
38
|
+
def parse(payload)
|
|
39
|
+
data = normalize_payload(payload)
|
|
40
|
+
event_type = data["type"]
|
|
41
|
+
|
|
42
|
+
unless event_type && ALL_TYPES.include?(event_type)
|
|
43
|
+
raise Error.new(
|
|
44
|
+
code: "INVALID_WEBHOOK",
|
|
45
|
+
title: "Webhook invalido",
|
|
46
|
+
description: "Tipo de evento desconhecido: #{event_type.inspect}"
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if CASH_IN_TYPES.include?(event_type)
|
|
51
|
+
parse_cash_in(event_type, data)
|
|
52
|
+
else
|
|
53
|
+
parse_cash_out(event_type, data)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Verifica se um tipo de evento e valido.
|
|
58
|
+
#
|
|
59
|
+
# @param type [String] Tipo do evento
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def valid_type?(type)
|
|
62
|
+
ALL_TYPES.include?(type)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def normalize_payload(payload)
|
|
68
|
+
case payload
|
|
69
|
+
when String
|
|
70
|
+
JSON.parse(payload)
|
|
71
|
+
when Hash
|
|
72
|
+
payload.transform_keys(&:to_s)
|
|
73
|
+
else
|
|
74
|
+
raise Error.new(
|
|
75
|
+
code: "INVALID_PAYLOAD",
|
|
76
|
+
title: "Payload invalido",
|
|
77
|
+
description: "Esperado String ou Hash, recebido #{payload.class}"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
rescue JSON::ParserError => e
|
|
81
|
+
raise Error.new(
|
|
82
|
+
code: "INVALID_JSON",
|
|
83
|
+
title: "JSON invalido",
|
|
84
|
+
description: "Erro ao parsear JSON do webhook: #{e.message}"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_cash_in(event_type, raw)
|
|
89
|
+
event_data = raw["data"] || {}
|
|
90
|
+
event_data = event_data.transform_keys(&:to_s) if event_data.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
WebhookEvent.new(
|
|
93
|
+
type: event_type,
|
|
94
|
+
data: {
|
|
95
|
+
amount: event_data["amount"],
|
|
96
|
+
status: event_data["status"],
|
|
97
|
+
worked: event_data["worked"],
|
|
98
|
+
tag: event_data["tag"],
|
|
99
|
+
tx_id: event_data["tx_id"],
|
|
100
|
+
end_to_end: event_data["end_to_end"]
|
|
101
|
+
},
|
|
102
|
+
raw: raw
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_cash_out(event_type, raw)
|
|
107
|
+
WebhookEvent.new(
|
|
108
|
+
type: event_type,
|
|
109
|
+
data: {
|
|
110
|
+
amount: raw["amount"],
|
|
111
|
+
status: raw["status"],
|
|
112
|
+
worked: raw["worked"],
|
|
113
|
+
id_transaction: raw["idTransaction"],
|
|
114
|
+
key: raw["key"]
|
|
115
|
+
},
|
|
116
|
+
raw: raw
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/nxgate.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "nxgate/error"
|
|
4
|
+
require_relative "nxgate/types"
|
|
5
|
+
require_relative "nxgate/hmac_signer"
|
|
6
|
+
require_relative "nxgate/token_manager"
|
|
7
|
+
require_relative "nxgate/webhook"
|
|
8
|
+
require_relative "nxgate/client"
|
|
9
|
+
|
|
10
|
+
# SDK oficial NXGATE para operacoes PIX.
|
|
11
|
+
#
|
|
12
|
+
# Oferece integracoes completas para:
|
|
13
|
+
# - Gerar cobranças PIX (cash-in) com QR Code
|
|
14
|
+
# - Realizar saques PIX (cash-out)
|
|
15
|
+
# - Consultar saldo
|
|
16
|
+
# - Consultar transacoes
|
|
17
|
+
# - Receber e processar webhooks
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# require 'nxgate'
|
|
21
|
+
#
|
|
22
|
+
# client = NXGate::Client.new(
|
|
23
|
+
# client_id: 'nxgate_xxx',
|
|
24
|
+
# client_secret: 'secret'
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# charge = client.pix_generate(
|
|
28
|
+
# valor: 100.00,
|
|
29
|
+
# nome_pagador: 'Joao da Silva',
|
|
30
|
+
# documento_pagador: '12345678901'
|
|
31
|
+
# )
|
|
32
|
+
#
|
|
33
|
+
module NXGate
|
|
34
|
+
VERSION = "1.0.0"
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nxgate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- NXGATE
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: SDK Ruby para integracao com a API NXGATE PIX. Suporta geracao de cobranças
|
|
14
|
+
(cash-in), saques (cash-out), consulta de saldo, transacoes e webhooks. Inclui autenticacao
|
|
15
|
+
OAuth2, assinatura HMAC e retentativas automaticas.
|
|
16
|
+
email:
|
|
17
|
+
- dev@nxgate.com.br
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/nxgate.rb
|
|
25
|
+
- lib/nxgate/client.rb
|
|
26
|
+
- lib/nxgate/error.rb
|
|
27
|
+
- lib/nxgate/hmac_signer.rb
|
|
28
|
+
- lib/nxgate/token_manager.rb
|
|
29
|
+
- lib/nxgate/types.rb
|
|
30
|
+
- lib/nxgate/webhook.rb
|
|
31
|
+
homepage: https://github.com/nxgate/nxgate-sdk-ruby
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
homepage_uri: https://github.com/nxgate/nxgate-sdk-ruby
|
|
36
|
+
source_code_uri: https://github.com/nxgate/nxgate-sdk-ruby
|
|
37
|
+
changelog_uri: https://github.com/nxgate/nxgate-sdk-ruby/blob/main/CHANGELOG.md
|
|
38
|
+
bug_tracker_uri: https://github.com/nxgate/nxgate-sdk-ruby/issues
|
|
39
|
+
post_install_message:
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 3.0.0
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 3.4.19
|
|
55
|
+
signing_key:
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: SDK oficial NXGATE para operacoes PIX
|
|
58
|
+
test_files: []
|