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 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
@@ -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
@@ -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
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Azul
4
+ module Resources
5
+ class Refund < Base
6
+ def self.create(params = {})
7
+ new(params.merge(trx_type: "Refund")).create
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Azul
4
+ VERSION = "0.5.0"
5
+ end
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: []