azul-sdk 0.5.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/.rubocop.yml +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +259 -0
- data/Rakefile +8 -0
- data/lib/azul/configuration.rb +72 -0
- data/lib/azul/errors.rb +52 -0
- data/lib/azul/http_client.rb +69 -0
- data/lib/azul/request.rb +85 -0
- data/lib/azul/resources/base.rb +112 -0
- data/lib/azul/resources/payment.rb +31 -0
- data/lib/azul/resources/refund.rb +11 -0
- data/lib/azul/resources/transaction.rb +15 -0
- data/lib/azul/response.rb +108 -0
- data/lib/azul/url_builder.rb +36 -0
- data/lib/azul/version.rb +5 -0
- data/lib/azul.rb +30 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 06b284006deac2dc97f3f2621adba6f610be97d3e753ab02da1570cb9ded95c8
|
4
|
+
data.tar.gz: 92123bef51ec5147e0a4c60f73e48536ccb4dd38ae09d9cd81a48ad3d1c61b1f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a8d005327eda8ad543667a87d03309a6d8cd6cce73b1206570a03b4d27a23fdb88f2133f5b5247e9663f539c2abdd74826583de3d91b2894e99789eb47ab32b9
|
7
|
+
data.tar.gz: e8d2fa4f154c7c9fd21e7840b722e0fa5954c59df4f9f9c854e5eda2a8c7e699f9123344e3bc1f990513d0bbe1201ce0c1d5ffef6b727d73146befb467aebc34
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.1
|
3
|
+
NewCops: enable
|
4
|
+
SuggestExtensions: false
|
5
|
+
|
6
|
+
Style/StringLiterals:
|
7
|
+
EnforcedStyle: double_quotes
|
8
|
+
|
9
|
+
Style/StringLiteralsInInterpolation:
|
10
|
+
EnforcedStyle: double_quotes
|
11
|
+
|
12
|
+
Style/Documentation:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Naming/MemoizedInstanceVariableName:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Naming/VariableNumber:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Style/SymbolArray:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Style/WordArray:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Metrics/AbcSize:
|
28
|
+
Max: 25
|
29
|
+
|
30
|
+
Layout/ElseAlignment:
|
31
|
+
Enabled: False
|
32
|
+
|
33
|
+
Layout/EndAlignment:
|
34
|
+
Enabled: False
|
35
|
+
|
36
|
+
Layout/IndentationWidth:
|
37
|
+
Enabled: False
|
38
|
+
|
39
|
+
Lint/SymbolConversion:
|
40
|
+
Enabled: false
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Rafael Montas
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
# Azul SDK
|
2
|
+
|
3
|
+
Azul SDK provee acceso a la implementación de Webservices Azul.
|
4
|
+
|
5
|
+
## Instalación
|
6
|
+
|
7
|
+
Para utilizar esta librería puedes correr el siguiente comando:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
gem install azul-sdk
|
11
|
+
```
|
12
|
+
|
13
|
+
Si utilizas Bundler, puedes agregar la siguiente línea a tu archivo `Gemfile`:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem "azul-sdk", require: "azul"
|
17
|
+
```
|
18
|
+
|
19
|
+
## Requerimientos
|
20
|
+
|
21
|
+
- Ruby 3.1 o superior
|
22
|
+
|
23
|
+
## Configuración
|
24
|
+
|
25
|
+
Esta librería necesita ser configurada con las credenciales facilitadas por Servicions Digitales Azul. Los siguientes parametros son configurables:
|
26
|
+
|
27
|
+
|
28
|
+
| Parametro | Tipo | Descripción |
|
29
|
+
| --- | --- | --- |
|
30
|
+
| merchant_id | `String` | Identificador del comercio. |
|
31
|
+
| environment | `String` `Symbol` | Ambiente a utilizar. De este valor dependen los URLs del API a utilizar. <br><br> **Posibles valores: `:development`, `:production`** |
|
32
|
+
| auth_1 | `String` | Valor de autenticación enviado en el header del requerimiento. |
|
33
|
+
| auth_2 | `String` | Valor de autenticación enviado en el header del requerimiento. |
|
34
|
+
| client_certificate | `String` | Certificado del cliente emitido por Azul para la autenticación mutua. |
|
35
|
+
| client_key | `String` | Llave privada del Certificate Signing Request (CSR) utilizado para la emisión del certificado. |
|
36
|
+
| client_certificate_path | `String` | Ruta del certificado del cliente en el sistema en caso de preferir utilizar el archivo en lugar del valor. |
|
37
|
+
| client_key_path | `String` | Ruta de la llave privada del Certificate Signing Request (CSR) en el sistema en caso de preferir utilizar el archivo en lugar del valor. |
|
38
|
+
| timeout | `Integer` | Tiempo de espera en segundos para las solicitudes. Este valor establece el tiempo máximo que se esperará en las siguientes configuraciones: `ssl_timeout`, `open_timeout`, `read_timeout`, `write_timeout`. <br><br> **Valor por defecto: `120` (120 segundos)** |
|
39
|
+
|
40
|
+
Puedes configurar la librería en un initializer llamado `azul.rb`, por ejemplo, si estas utilizando Rails en `config/initializers/azul.rb`:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require "azul"
|
44
|
+
|
45
|
+
Azul.configure do |config|
|
46
|
+
config.merchant_id = ENV.fetch("merchant_id")
|
47
|
+
config.auth_1 = ENV.fetch("auth_1")
|
48
|
+
config.auth_2 = ENV.fetch("auth_2")
|
49
|
+
config.client_certificate = ENV.fetch("client_certificate")
|
50
|
+
config.client_key = ENV.fetch("client_key")
|
51
|
+
config.environment = "development"
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
### Ambientes
|
56
|
+
|
57
|
+
Basado en la configuración del ambiente, la librería utilizará los endpoints y credenciales apropiados para cada caso.
|
58
|
+
|
59
|
+
- development: `https://pruebas.azul.com.do/WebServices/JSON/Default.aspx`
|
60
|
+
- production: `https://pagos.azul.com.do/WebServices/JSON/Default.aspx`
|
61
|
+
|
62
|
+
## Uso
|
63
|
+
|
64
|
+
Los métodos definidos en esta librería permiten interactuar con los servicios web de Azul de manera sencilla. A continuación se presentan algunos ejemplos de uso:
|
65
|
+
|
66
|
+
### Transacciones de venta
|
67
|
+
|
68
|
+
Esta es la transacción principal utilizada para someter una autorización de una tarjeta.
|
69
|
+
|
70
|
+
Las ventas realizadas con la transacción “Sale” son capturadas automáticamente para su liquidación, por lo que sólo pueden ser anuladas con una transacción de “Void” en un lapso de no más de 20 minutos luego de recibir respuesta de aprobación (ver Método ProcessVoid).
|
71
|
+
|
72
|
+
Luego de transcurridos estos 20 minutos, la transacción será liquidada y se debe realizar una transacción de “Refund” o devolución para devolver los fondos a la tarjeta.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
response = Azul::Payment.sale({
|
76
|
+
card_number: "411111******1111",
|
77
|
+
expiration: "202812",
|
78
|
+
cvc: "123",
|
79
|
+
amount: 100000,
|
80
|
+
itbis: 18000,
|
81
|
+
})
|
82
|
+
```
|
83
|
+
|
84
|
+
### Transacciones de preautorización (hold)
|
85
|
+
|
86
|
+
Se puede separar la autorización del posteo o captura en dos mensajes distintos:
|
87
|
+
|
88
|
+
- Hold: pre-autorización y reserva de los fondos en la tarjeta del cliente.
|
89
|
+
- Post: se hace la captura o el “posteo” de la transacción.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
response = Azul::Payment.hold({
|
93
|
+
card_number: "411111******1111",
|
94
|
+
expiration: "202812",
|
95
|
+
cvc: "123",
|
96
|
+
amount: 100000,
|
97
|
+
itbis: 18000,
|
98
|
+
})
|
99
|
+
```
|
100
|
+
|
101
|
+
### Transacciones de captura (posteo) de una preautorización (hold)
|
102
|
+
|
103
|
+
Permita capurar una preautorización (hold) previamente realizada para su liquidación.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
response = Azul::Payment.capture({
|
107
|
+
azul_order_id: "44772511",
|
108
|
+
amount: 100000,
|
109
|
+
itbis: 18000,
|
110
|
+
})
|
111
|
+
```
|
112
|
+
|
113
|
+
### Transacciones para anular ventas, capturas (posteo) o preautorizaciones (hold)
|
114
|
+
|
115
|
+
Las transacciones de venta o post se pueden anular antes de los 20 minutos de haber recibido la respuesta de aprobación. Las transacciones de hold que no han sido posteadas no tienen límite de tiempo para anularse.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
response = Azul::Payment.void({ azul_order_id: "44772511" })
|
119
|
+
```
|
120
|
+
|
121
|
+
### Transacciones de devolución (refund)
|
122
|
+
|
123
|
+
La devolución (refund) permite reembolsarle los fondos a una tarjeta luego de haberse
|
124
|
+
liquidado la transacción.
|
125
|
+
|
126
|
+
Para poder realizar una devolución se debe haber procesado exitosamente una transacción de venta o captura y se deben utilizar los datos de la transacción original para enviar la devolución.
|
127
|
+
|
128
|
+
- El monto a devolver puede ser el mismo o menor.
|
129
|
+
- Se permite hacer una devolución, múltiples devoluciones o devoluciones parciales para cada transacción realizada.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
response = Azul::Refund.create({
|
133
|
+
azul_order_id: "44772511",
|
134
|
+
amount: 100000,
|
135
|
+
itbis: 18000,
|
136
|
+
})
|
137
|
+
```
|
138
|
+
|
139
|
+
### Verificación de transacciones (verify)
|
140
|
+
|
141
|
+
Este método permite verificar la respuesta enviada por el webservice de una transacción anterior, identificada por el campo `custom_order_id`.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
response = Azul::Transaction.verify({ custom_order_id: "123456789" })
|
145
|
+
```
|
146
|
+
|
147
|
+
### Consulta de transacciones (search)
|
148
|
+
|
149
|
+
Este método permite extraer los detalles de una o varias transacciones anteriormente procesadas de un rango de fechas previamente establecido.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
response = Azul::Transaction.search({
|
153
|
+
date_from: "2025-08-01",
|
154
|
+
date_to: "2025-08-31"
|
155
|
+
})
|
156
|
+
```
|
157
|
+
|
158
|
+
### Objeto de respuesta y atributos (response)
|
159
|
+
|
160
|
+
Todos estos métodos devuelven una instancia [`Azul::Response`](lib/azul/response.rb) que representa la respuesta del API y contiene información sobre la transacción procesada.
|
161
|
+
|
162
|
+
Se puede utilizar `accessors` para leer los atributos de la respuesta de manera sencilla. Si el atributo no existe, retornará `nil`.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
|
166
|
+
response = Azul::Payment.sale({
|
167
|
+
card_number: "411111******1111",
|
168
|
+
expiration: "202812",
|
169
|
+
cvc: "123",
|
170
|
+
amount: 100000,
|
171
|
+
itbis: 18000,
|
172
|
+
custom_order_id: "xyz11001"
|
173
|
+
})
|
174
|
+
|
175
|
+
puts response.azul_order_id # "44772544"
|
176
|
+
puts response.response_code # "ISO8583"
|
177
|
+
puts response.response_message # "APROBADA"
|
178
|
+
puts response.raw_response # <Net::HTTPOK:0x0000ffff603cc458>
|
179
|
+
puts response.body
|
180
|
+
|
181
|
+
# {
|
182
|
+
# "AuthorizationCode"=>"OK2930",
|
183
|
+
# "AzulOrderId"=>"44772544",
|
184
|
+
# "CountryCode"=>"SGP",
|
185
|
+
# "CustomOrderId"=>"xyz11001",
|
186
|
+
# "DateTime"=>"20250808155452",
|
187
|
+
# "ErrorDescription"=>"",
|
188
|
+
# "IsoCode"=>"00",
|
189
|
+
# "LotNumber"=>"",
|
190
|
+
# "RRN"=>"2025080815545644772544",
|
191
|
+
# "ResponseCode"=>"ISO8583",
|
192
|
+
# "ResponseCodeThreeDS"=>"4",
|
193
|
+
# "ResponseMessage"=>"APROBADA",
|
194
|
+
# "Ticket"=>"1"
|
195
|
+
# }
|
196
|
+
```
|
197
|
+
|
198
|
+
### Objeto de requerimiento (request)
|
199
|
+
|
200
|
+
A través del objeto de respuesta `response.request` se puede acceder a los datos enviados en la solicitud original para su revisión.
|
201
|
+
|
202
|
+
El objeto de requerimiento (request) contiene las siguientes propiedades:
|
203
|
+
|
204
|
+
| Nombre | Tipo | Descripción | Ejemplo |
|
205
|
+
| --- | --- | --- | --- |
|
206
|
+
| api_url | `String` | URL del API al que se envió la solicitud. |
|
207
|
+
| headers | `Hash` | Encabezados HTTP enviados en la solicitud. |
|
208
|
+
| method | `Symbol` | Método HTTP utilizado en la solicitud. |
|
209
|
+
| params | `Hash` | Parámetros enviados en la solicitud. |
|
210
|
+
| payment_method_metadata | `Hash` | Metadatos del método de pago utilizado. |
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
puts response.request.api_url
|
214
|
+
# https://pruebas.azul.com.do/WebServices/JSON/Default.aspx
|
215
|
+
|
216
|
+
puts response.request.headers
|
217
|
+
# { "Content-Type": "application/json", "User-Agent"=>"Ruby" }, "Auth1"=>"...", "Auth2"=>"..."
|
218
|
+
|
219
|
+
puts response.request.method
|
220
|
+
# :post
|
221
|
+
|
222
|
+
puts response.request.params
|
223
|
+
# { "card_number": "4111********1111", "expiration": "[FILTERED]", "cvc": "[FILTERED]", "amount": 100000, "itbis": 18000, "custom_order_id": "xyz11001" }
|
224
|
+
|
225
|
+
puts response.request.payment_method_metadata
|
226
|
+
# {:last4=>"0117", :brand=>"Visa", :exp_month=>"12", :exp_year=>"2028"}
|
227
|
+
```
|
228
|
+
|
229
|
+
### Requerimientos exitosos
|
230
|
+
|
231
|
+
Para considerar una respuesta como exitosa, se considera que el atributo `iso_code` debe ser igual a `"00"`, `"3D"` o `"3D2METHOD"` o que el attributo `response_message` sea igual a `"APROBADA"`.
|
232
|
+
|
233
|
+
En el caso de consultas de transacciones (search), se considera exitosa si el attributo `response_code` es igual a `"SEARCHED"`.
|
234
|
+
|
235
|
+
### Errores (Errors)
|
236
|
+
|
237
|
+
Cuando el API retorna un error o el atributo `iso_code` no es considerado exitoso, se lanza una excepción basada en los valores retornados en los campos `iso_code` y `error_description`.
|
238
|
+
|
239
|
+
| Códigos de error | Descripción | Clase de Excepción |
|
240
|
+
| --- | --- | --- |
|
241
|
+
| "03", "04", "05", "07", "12", "13", "14", "41", "43", "46", "51", "54", "57", "59", "61", "62", "63", "82", "83", "91" | Códigos de respuesta del banco emisor o procesador | [`Azul::DeclineError`](lib/azul/errors.rb) |
|
242
|
+
| "99" | Error de procesamiento | [`Azul::ProcessingError`](lib/azul/errors.rb) |
|
243
|
+
| "08", "3D" | Error de autenticación | [`Azul::AuthenticationError`](lib/azul/errors.rb) |
|
244
|
+
| "" | Error sin `iso_code` | [`Azul::ApiError`](lib/azul/errors.rb) |
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
begin
|
248
|
+
response = Azul::Payment.sale({
|
249
|
+
card_number: "411111******1111",
|
250
|
+
expiration: "202812",
|
251
|
+
amount: 100000,
|
252
|
+
itbis: 18000,
|
253
|
+
})
|
254
|
+
rescue Azul::ApiError => e
|
255
|
+
puts e.message # "Azul API Error: VALIDATION_ERROR:CVC - "
|
256
|
+
puts e.response.body # Azul API response parsed from JSON
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
class Configuration
|
5
|
+
attr_writer :merchant_id, :environment, :auth_1, :auth_2, :client_certificate, :client_key
|
6
|
+
attr_accessor :timeout, :client_certificate_path, :client_key_path
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@merchant_id = nil
|
10
|
+
@environment = nil
|
11
|
+
@auth_1 = nil
|
12
|
+
@auth_2 = nil
|
13
|
+
@client_certificate = nil
|
14
|
+
@client_key = nil
|
15
|
+
@client_certificate_path = nil
|
16
|
+
@client_key_path = nil
|
17
|
+
@timeout = 120
|
18
|
+
end
|
19
|
+
|
20
|
+
def merchant_id
|
21
|
+
return @merchant_id if @merchant_id
|
22
|
+
|
23
|
+
error_message = "Azul merchant id es requerido!"
|
24
|
+
raise ConfigurationError, error_message
|
25
|
+
end
|
26
|
+
|
27
|
+
def environment
|
28
|
+
if [:production, :development].include?(@environment&.to_sym)
|
29
|
+
@environment&.to_sym
|
30
|
+
else
|
31
|
+
error_message = "Azul environment (:production or :development) es requerido!"
|
32
|
+
raise ConfigurationError, error_message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def auth_1
|
37
|
+
return @auth_1 if @auth_1
|
38
|
+
|
39
|
+
error_message = "Azul Auth1 es requerido!"
|
40
|
+
raise ConfigurationError, error_message
|
41
|
+
end
|
42
|
+
|
43
|
+
def auth_2
|
44
|
+
return @auth_2 if @auth_2
|
45
|
+
|
46
|
+
error_message = "Azul Auth2 es requerido!"
|
47
|
+
raise ConfigurationError, error_message
|
48
|
+
end
|
49
|
+
|
50
|
+
def client_certificate
|
51
|
+
if @client_certificate_path && File.exist?(@client_certificate_path)
|
52
|
+
File.read(@client_certificate_path)
|
53
|
+
elsif @client_certificate
|
54
|
+
@client_certificate
|
55
|
+
else
|
56
|
+
error_message = "Azul client certificate es requerido para autenticación mTLS!"
|
57
|
+
raise ConfigurationError, error_message
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def client_key
|
62
|
+
if @client_key_path && File.exist?(@client_key_path)
|
63
|
+
File.read(@client_key_path)
|
64
|
+
elsif @client_key
|
65
|
+
@client_key
|
66
|
+
else
|
67
|
+
error_message = "Azul client key es requerido para autenticación mTLS!"
|
68
|
+
raise ConfigurationError, error_message
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/azul/errors.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
module Errors
|
5
|
+
class ConfigurationError < StandardError; end
|
6
|
+
|
7
|
+
class ApiError < StandardError
|
8
|
+
attr_reader :response
|
9
|
+
|
10
|
+
def initialize(response)
|
11
|
+
@response = response
|
12
|
+
super(build_error_message(response))
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def build_error_message(response)
|
18
|
+
"Azul API Error: #{response.error_description} - #{response.response_message}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class DeclineError < ApiError; end
|
23
|
+
class ProcessingError < ApiError; end
|
24
|
+
class AuthenticationError < ApiError; end
|
25
|
+
|
26
|
+
class Error < StandardError
|
27
|
+
DECLINE_CODES = %w[03 04 05 07 12 13 14 41 43 46 51 54 57 59 61 62 63 82 83 91].freeze
|
28
|
+
PROCESSING_CODES = %w[99].freeze
|
29
|
+
AUTH_CODES = %w[08 3D].freeze
|
30
|
+
|
31
|
+
def self.from_response(response)
|
32
|
+
case response.iso_code
|
33
|
+
when *DECLINE_CODES
|
34
|
+
DeclineError.new(response)
|
35
|
+
when *PROCESSING_CODES
|
36
|
+
ProcessingError.new(response)
|
37
|
+
when *AUTH_CODES
|
38
|
+
AuthenticationError.new(response)
|
39
|
+
else
|
40
|
+
ApiError.new(response)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
ConfigurationError = Errors::ConfigurationError
|
47
|
+
ApiError = Errors::ApiError
|
48
|
+
DeclineError = Errors::DeclineError
|
49
|
+
ProcessingError = Errors::ProcessingError
|
50
|
+
AuthenticationError = Errors::AuthenticationError
|
51
|
+
Error = Errors::Error
|
52
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
class HttpClient
|
5
|
+
def initialize
|
6
|
+
@config = Azul.configuration
|
7
|
+
@url_builder = UrlBuilder.new(@config.environment)
|
8
|
+
end
|
9
|
+
|
10
|
+
def post(params = {}, action: nil)
|
11
|
+
required_params = { "Channel": "EC", "PosInputMode": "E-Commerce", "Store": @config.merchant_id }
|
12
|
+
api_url = @url_builder.build(action: action)
|
13
|
+
|
14
|
+
perform_request(:post, api_url, params.merge(required_params))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def perform_request(method, api_url, params)
|
20
|
+
uri = URI(api_url)
|
21
|
+
http = create_http_client(uri)
|
22
|
+
net_request = build_net_request(method, uri, params)
|
23
|
+
|
24
|
+
request = build_request_object(method, api_url, net_request, params)
|
25
|
+
response = http.request(net_request)
|
26
|
+
|
27
|
+
Response.new(response, request)
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_http_client(uri)
|
31
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
32
|
+
http.use_ssl = true
|
33
|
+
http.ssl_timeout = @config.timeout
|
34
|
+
http.open_timeout = @config.timeout
|
35
|
+
http.read_timeout = @config.timeout
|
36
|
+
http.write_timeout = @config.timeout
|
37
|
+
|
38
|
+
# Configure SSL
|
39
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
40
|
+
http.cert = OpenSSL::X509::Certificate.new(@config.client_certificate)
|
41
|
+
http.key = OpenSSL::PKey::RSA.new(@config.client_key)
|
42
|
+
|
43
|
+
http
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_net_request(method, uri, params)
|
47
|
+
case method
|
48
|
+
when :get
|
49
|
+
Net::HTTP::Get.new(uri.request_uri)
|
50
|
+
when :post
|
51
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
52
|
+
req.body = params.to_json
|
53
|
+
req
|
54
|
+
end.tap { |req| add_headers(req) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_headers(request)
|
58
|
+
request["Auth1"] = @config.auth_1
|
59
|
+
request["Auth2"] = @config.auth_2
|
60
|
+
request["Content-Type"] = "application/json"
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_request_object(method, api_url, net_request, params)
|
64
|
+
headers = {}
|
65
|
+
net_request.each_header { |k, v| headers[k] = v }
|
66
|
+
Request.new(method, api_url, headers, params)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/azul/request.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
class Request
|
5
|
+
attr_reader :method, :api_url, :headers, :params, :payment_method_metadata
|
6
|
+
|
7
|
+
def initialize(method, api_url, headers, params)
|
8
|
+
@method = method
|
9
|
+
@api_url = api_url
|
10
|
+
@headers = headers
|
11
|
+
|
12
|
+
# Extract payment method details before filtering
|
13
|
+
@payment_method_metadata = extract_payment_metadata(params)
|
14
|
+
|
15
|
+
@params = filter_sensitive_params(params)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def extract_payment_metadata(params)
|
21
|
+
return {} if params["CardNumber"].blank?
|
22
|
+
|
23
|
+
clean_number = params["CardNumber"].to_s.gsub(/\D/, "")
|
24
|
+
|
25
|
+
{
|
26
|
+
last4: clean_number[-4..],
|
27
|
+
brand: identify_card_brand(clean_number),
|
28
|
+
exp_month: extract_exp_month(params["Expiration"]),
|
29
|
+
exp_year: extract_exp_year(params["Expiration"])
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def identify_card_brand(card_number)
|
34
|
+
case card_number
|
35
|
+
when /^4/
|
36
|
+
"Visa"
|
37
|
+
when /^5[1-5]/, /^222[1-9]/, /^22[3-9]/, /^2[3-6]/, /^27[0-1]/, /^2720/
|
38
|
+
"Mastercard"
|
39
|
+
when /^3[47]/
|
40
|
+
"American Express"
|
41
|
+
when /^6011/, /^622126/, /^62212[7-9]/, /^6221[3-9]/, /^622[2-8]/, /^6229[0-1]/, /^62292[0-5]/, /^64[4-9]/, /^65/
|
42
|
+
"Discover"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_exp_month(expiration)
|
47
|
+
return if expiration.blank?
|
48
|
+
|
49
|
+
# Format: YYYYMM
|
50
|
+
expiration.to_s[-2..]
|
51
|
+
end
|
52
|
+
|
53
|
+
def extract_exp_year(expiration)
|
54
|
+
return if expiration.blank?
|
55
|
+
|
56
|
+
# Format: YYYYMM
|
57
|
+
expiration.to_s[0..3]
|
58
|
+
end
|
59
|
+
|
60
|
+
def filter_sensitive_params(params)
|
61
|
+
return params unless params.is_a?(Hash)
|
62
|
+
|
63
|
+
params.each do |key, value|
|
64
|
+
case key
|
65
|
+
when "CardNumber"
|
66
|
+
params[key] = mask_card_number(value) if value.present?
|
67
|
+
when "Expiration", "CVC"
|
68
|
+
params[key] = "[FILTERED]" if value.present?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
params
|
73
|
+
end
|
74
|
+
|
75
|
+
def mask_card_number(card_number)
|
76
|
+
return card_number unless card_number.is_a?(String) && card_number.length >= 8
|
77
|
+
|
78
|
+
first_four = card_number[0..3]
|
79
|
+
last_four = card_number[-4..]
|
80
|
+
masked_middle = "*" * (card_number.length - 8)
|
81
|
+
|
82
|
+
"#{first_four}#{masked_middle}#{last_four}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
module Resources
|
5
|
+
class Base
|
6
|
+
attr_reader :params, :response
|
7
|
+
|
8
|
+
PARAM_MAPPING = {
|
9
|
+
card_number: "CardNumber",
|
10
|
+
expiration: "Expiration",
|
11
|
+
cvc: "CVC",
|
12
|
+
amount: "Amount",
|
13
|
+
itbis: "Itbis",
|
14
|
+
trx_type: "TrxType",
|
15
|
+
order_number: "OrderNumber",
|
16
|
+
customer_service_phone: "CustomerServicePhone",
|
17
|
+
ecommerce_url: "ECommerceURL",
|
18
|
+
custom_order_id: "CustomOrderId",
|
19
|
+
alt_merchant_name: "AltMerchantName",
|
20
|
+
apple_pay: "ApplePay",
|
21
|
+
payment_token: "PaymentToken",
|
22
|
+
cryptogram: "Cryptogram",
|
23
|
+
eci_indicator: "ECIIndicator",
|
24
|
+
google_pay: "GooglePay",
|
25
|
+
data_vault_token: "DataVaultToken",
|
26
|
+
save_to_data_vault: "SaveToDataVault",
|
27
|
+
force_no_3ds: "ForceNo3DS",
|
28
|
+
three_ds_auth: "ThreeDSAuth",
|
29
|
+
term_url: "TermUrl", # Nested in three_ds_auth
|
30
|
+
method_notification_url: "MethodNotificationUrl", # Nested in three_ds_auth
|
31
|
+
requestor_challenge_indicator: "RequestorChallengeIndicator", # Nested in three_ds_auth
|
32
|
+
card_holder_info: "CardHolderInfo",
|
33
|
+
name: "Name", # Nested in card_holder_info
|
34
|
+
email: "Email", # Nested in card_holder_info
|
35
|
+
phone_mobile: "PhoneMobile", # Nested in card_holder_info
|
36
|
+
browser_info: "BrowserInfo",
|
37
|
+
accept_header: "AcceptHeader", # Nested in browser_info
|
38
|
+
ip_address: "IPAddress", # Nested in browser_info
|
39
|
+
user_agent: "UserAgent", # Nested in browser_info
|
40
|
+
language: "Language", # Nested in browser_info
|
41
|
+
color_depth: "ColorDepth", # Nested in browser_info
|
42
|
+
screen_width: "ScreenWidth", # Nested in browser_info
|
43
|
+
screen_height: "ScreenHeight", # Nested in browser_info
|
44
|
+
time_zone: "TimeZone", # Nested in browser_info
|
45
|
+
javascript_enable: "JavaScriptEnable", # Nested in browser_info
|
46
|
+
method_notification_status: "MethodNotificationStatus",
|
47
|
+
cres: "CRes",
|
48
|
+
original_date: "OriginalDate",
|
49
|
+
original_trx_ticket_nr: "OriginalTrxTicketNr",
|
50
|
+
azul_order_id: "AzulOrderId",
|
51
|
+
response_code: "ResponseCode",
|
52
|
+
rrn: "RRN",
|
53
|
+
date_from: "DateFrom",
|
54
|
+
date_to: "DateTo",
|
55
|
+
acquirer_ref_data: "AcquirerRefData"
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
def initialize(params = {})
|
59
|
+
@params = params
|
60
|
+
end
|
61
|
+
|
62
|
+
def create(action: nil)
|
63
|
+
@http_client ||= HttpClient.new
|
64
|
+
@response = @http_client.post(request_params, action: action)
|
65
|
+
|
66
|
+
if @response.success?
|
67
|
+
@response
|
68
|
+
else
|
69
|
+
error = Error.from_response(@response)
|
70
|
+
raise error
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def request_params
|
77
|
+
mapped = map_parameters_to_api_format
|
78
|
+
|
79
|
+
# If data_vault_token is present, add empty card_number and expiration
|
80
|
+
if params[:data_vault_token] || params["data_vault_token"]
|
81
|
+
mapped["CardNumber"] ||= ""
|
82
|
+
mapped["Expiration"] ||= ""
|
83
|
+
end
|
84
|
+
|
85
|
+
mapped
|
86
|
+
end
|
87
|
+
|
88
|
+
def map_parameters_to_api_format
|
89
|
+
params.each_with_object({}) do |(key, value), result|
|
90
|
+
api_param = PARAM_MAPPING[key.to_sym] || to_pascal_case(key.to_s)
|
91
|
+
result[api_param] = value.is_a?(Hash) ? process_nested_params(value) : value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_nested_params(hash)
|
96
|
+
result = {}
|
97
|
+
|
98
|
+
hash.each do |key, value|
|
99
|
+
api_param = PARAM_MAPPING[key.to_sym] || to_pascal_case(key.to_s)
|
100
|
+
|
101
|
+
result[api_param] = value.is_a?(Hash) ? process_nested_params(value) : value
|
102
|
+
end
|
103
|
+
|
104
|
+
result
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_pascal_case(string)
|
108
|
+
string.split("_").map(&:capitalize).join
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
module Resources
|
5
|
+
class Payment < Base
|
6
|
+
def self.sale(params = {})
|
7
|
+
new(params.merge(trx_type: "Sale")).create
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.hold(params = {})
|
11
|
+
new(params.merge(trx_type: "Hold", acquirer_ref_data: "1")).create
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.capture(params = {})
|
15
|
+
new(params).create(action: :post)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.void(params = {})
|
19
|
+
new(params).create(action: :void)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.process_threeds_method(params = {})
|
23
|
+
new(params).create(action: :process_threeds_method)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.process_threeds_challenge(params = {})
|
27
|
+
new(params).create(action: :process_threeds_challenge)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
module Resources
|
5
|
+
class Transaction < Base
|
6
|
+
def self.search(params = {})
|
7
|
+
new(params).create(action: :search)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.verify(params = {})
|
11
|
+
new(params).create(action: :verify)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
class Response
|
5
|
+
attr_reader :raw_response, :request
|
6
|
+
|
7
|
+
def initialize(raw_response, request)
|
8
|
+
@raw_response = raw_response
|
9
|
+
@request = request
|
10
|
+
end
|
11
|
+
|
12
|
+
def success?
|
13
|
+
http_success? && azul_success?
|
14
|
+
end
|
15
|
+
|
16
|
+
def body
|
17
|
+
@body ||= parse_body
|
18
|
+
end
|
19
|
+
|
20
|
+
def iso_code
|
21
|
+
body["IsoCode"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def response_code
|
25
|
+
body["ResponseCode"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def response_message
|
29
|
+
body["ResponseMessage"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_description
|
33
|
+
body["ErrorDescription"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def authorization_code
|
37
|
+
body["AuthorizationCode"]
|
38
|
+
end
|
39
|
+
|
40
|
+
def azul_order_id
|
41
|
+
body["AzulOrderId"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def country_code
|
45
|
+
body["CountryCode"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def custom_order_id
|
49
|
+
body["CustomOrderId"]
|
50
|
+
end
|
51
|
+
|
52
|
+
def date_time
|
53
|
+
body["DateTime"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def lot_number
|
57
|
+
body["LotNumber"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def rrn
|
61
|
+
body["RRN"]
|
62
|
+
end
|
63
|
+
|
64
|
+
def ticket
|
65
|
+
body["Ticket"]
|
66
|
+
end
|
67
|
+
|
68
|
+
def threeds_method
|
69
|
+
body["ThreeDSMethod"]
|
70
|
+
end
|
71
|
+
|
72
|
+
def threeds_challenge
|
73
|
+
body["ThreeDSChallenge"]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Token related methods
|
77
|
+
def data_vault_token
|
78
|
+
body["DataVaultToken"]
|
79
|
+
end
|
80
|
+
|
81
|
+
def data_vault_expiration
|
82
|
+
body["DataVaultExpiration"]
|
83
|
+
end
|
84
|
+
|
85
|
+
def data_vault_brand
|
86
|
+
body["DataVaultBrand"]
|
87
|
+
end
|
88
|
+
|
89
|
+
# Payment method details from request metadata
|
90
|
+
def payment_method_details
|
91
|
+
@request.payment_method_metadata
|
92
|
+
end
|
93
|
+
|
94
|
+
def http_success?
|
95
|
+
@raw_response.is_a?(Net::HTTPSuccess)
|
96
|
+
end
|
97
|
+
|
98
|
+
def azul_success?
|
99
|
+
["00", "3D2METHOD", "3D"].include?(iso_code) || response_message == "APROBADA" || response_code == "SEARCHED"
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_body
|
103
|
+
return {} if raw_response.body.blank?
|
104
|
+
|
105
|
+
JSON.parse(raw_response.body)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Azul
|
4
|
+
class UrlBuilder
|
5
|
+
ENDPOINTS = {
|
6
|
+
development: "https://pruebas.azul.com.do/WebServices/JSON/Default.aspx",
|
7
|
+
production: "https://pagos.azul.com.do/WebServices/JSON/Default.aspx"
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
ACTIONS = {
|
11
|
+
post: "ProcessPost",
|
12
|
+
void: "ProcessVoid",
|
13
|
+
verify: "VerifyPayment",
|
14
|
+
search: "SearchPayments",
|
15
|
+
process_threeds_method: "processthreedsmethod",
|
16
|
+
process_threeds_challenge: "processthreedschallenge"
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
def initialize(environment)
|
20
|
+
@environment = environment
|
21
|
+
end
|
22
|
+
|
23
|
+
def build(action: nil)
|
24
|
+
base_url = ENDPOINTS[@environment]
|
25
|
+
|
26
|
+
return base_url unless action
|
27
|
+
|
28
|
+
uri = URI(base_url)
|
29
|
+
query_param = ACTIONS[action]
|
30
|
+
|
31
|
+
uri.query = query_param if query_param
|
32
|
+
|
33
|
+
uri.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/azul/version.rb
ADDED
data/lib/azul.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "azul/configuration"
|
4
|
+
require_relative "azul/errors"
|
5
|
+
require_relative "azul/url_builder"
|
6
|
+
require_relative "azul/http_client"
|
7
|
+
require_relative "azul/request"
|
8
|
+
require_relative "azul/response"
|
9
|
+
require_relative "azul/resources/base"
|
10
|
+
require_relative "azul/resources/payment"
|
11
|
+
require_relative "azul/resources/refund"
|
12
|
+
require_relative "azul/resources/transaction"
|
13
|
+
require_relative "azul/version"
|
14
|
+
|
15
|
+
module Azul
|
16
|
+
class << self
|
17
|
+
attr_accessor :configuration
|
18
|
+
|
19
|
+
def configure
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
yield(@configuration)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Aliasing the resource classes at the module level
|
26
|
+
Base = Resources::Base
|
27
|
+
Payment = Resources::Payment
|
28
|
+
Refund = Resources::Refund
|
29
|
+
Transaction = Resources::Transaction
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: azul-sdk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rafael Montas
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-09 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Implementación de Azul Webservices API.
|
14
|
+
email:
|
15
|
+
- rafaelmontas1@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rubocop.yml"
|
21
|
+
- LICENSE.txt
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- lib/azul.rb
|
25
|
+
- lib/azul/configuration.rb
|
26
|
+
- lib/azul/errors.rb
|
27
|
+
- lib/azul/http_client.rb
|
28
|
+
- lib/azul/request.rb
|
29
|
+
- lib/azul/resources/base.rb
|
30
|
+
- lib/azul/resources/payment.rb
|
31
|
+
- lib/azul/resources/refund.rb
|
32
|
+
- lib/azul/resources/transaction.rb
|
33
|
+
- lib/azul/response.rb
|
34
|
+
- lib/azul/url_builder.rb
|
35
|
+
- lib/azul/version.rb
|
36
|
+
homepage: https://github.com/rafaelmontas/azul-sdk
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata:
|
40
|
+
homepage_uri: https://github.com/rafaelmontas/azul-sdk
|
41
|
+
source_code_uri: https://github.com/rafaelmontas/azul-sdk
|
42
|
+
rubygems_mfa_required: 'true'
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 3.1.0
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubygems_version: 3.5.16
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: Implementación de Azul Webservices API.
|
62
|
+
test_files: []
|