ipagos_ruby_sdk 0.1.0 → 0.1.1
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 +4 -4
- data/lib/ipagos_ruby_sdk/client.rb +181 -0
- data/lib/ipagos_ruby_sdk/environment.rb +8 -0
- data/lib/ipagos_ruby_sdk/models/amount_details.rb +5 -0
- data/lib/ipagos_ruby_sdk/models/base_model.rb +24 -0
- data/lib/ipagos_ruby_sdk/models/bill_to.rb +7 -0
- data/lib/ipagos_ruby_sdk/models/line_item.rb +16 -0
- data/lib/ipagos_ruby_sdk/types.rb +55 -0
- data/lib/ipagos_ruby_sdk/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfd271bbab63e86184e5ac653608e8a548f988e626fae9d757bac28a4ae3bb46
|
|
4
|
+
data.tar.gz: 1707855b60cad9fe8f6125915ccea1249671f4ca495b356dc7fc2dca205319e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 470403a065245ee9f318b539e6863fecc02fa9e2b2c4aae52ffc64f16c349cdd8027a7b2049e0a1db01ee63644f09a4209cbaa46a17b9c246ed2c038df81bae1
|
|
7
|
+
data.tar.gz: 74b29ec550bbe45c5b1868f0555e16fb7cbe72ec39eb14570f9a5866b4513f1b0a8a23a5c6aa683e390b8b68479e743291bf2c9bfa1f6a158e6a1cb8dcb40d14
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "time"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module IpagosRubySdk
|
|
11
|
+
class Client
|
|
12
|
+
SANDBOX_URL = "https://apitest.cybersource.com"
|
|
13
|
+
PRODUCTION_URL = "https://api.cybersource.com"
|
|
14
|
+
|
|
15
|
+
attr_reader :config
|
|
16
|
+
|
|
17
|
+
def initialize(config)
|
|
18
|
+
@config = config
|
|
19
|
+
@base_url = config.environment == :production ? PRODUCTION_URL : SANDBOX_URL
|
|
20
|
+
|
|
21
|
+
@conn = Faraday.new(url: @base_url) do |f|
|
|
22
|
+
f.headers["Content-Type"] = "application/json"
|
|
23
|
+
f.headers["v-c-merchant-id"] = config.merchant_id
|
|
24
|
+
f.adapter Faraday.default_adapter
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_session(origin)
|
|
29
|
+
path = "/microform/v2/sessions"
|
|
30
|
+
payload = {
|
|
31
|
+
targetOrigins: [origin.chomp("/")],
|
|
32
|
+
allowedCardNetworks: ["VISA", "MASTERCARD", "AMEX"],
|
|
33
|
+
allowedPaymentTypes: ["CARD"],
|
|
34
|
+
clientVersion: "v2"
|
|
35
|
+
}
|
|
36
|
+
send_request(:post, path, payload)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_payment(amount:, bill_to:, token: nil, card_data: nil, reference: nil, line_items: nil)
|
|
40
|
+
path = "/pts/v2/payments"
|
|
41
|
+
|
|
42
|
+
items = if line_items.nil? || line_items.empty?
|
|
43
|
+
[{
|
|
44
|
+
unitPrice: amount.total,
|
|
45
|
+
quantity: 1,
|
|
46
|
+
productName: "Servicio General",
|
|
47
|
+
productSku: "GEN-001"
|
|
48
|
+
}]
|
|
49
|
+
else
|
|
50
|
+
line_items.map(&:to_h)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
payload = {
|
|
54
|
+
clientReferenceInformation: { code: reference || "ORD-#{Time.now.to_i}" },
|
|
55
|
+
processingInformation: { capture: true },
|
|
56
|
+
orderInformation: {
|
|
57
|
+
amountDetails: amount.to_h,
|
|
58
|
+
billTo: bill_to.to_h,
|
|
59
|
+
lineItems: items
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if token
|
|
64
|
+
payload[:tokenInformation] = { transientTokenJwt: token }
|
|
65
|
+
elsif card_data
|
|
66
|
+
payload[:paymentInformation] = {
|
|
67
|
+
card: {
|
|
68
|
+
number: card_data[:number],
|
|
69
|
+
expirationMonth: card_data[:expiration_month],
|
|
70
|
+
expirationYear: card_data[:expiration_year],
|
|
71
|
+
cvv: card_data[:cvv]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
raise ArgumentError, "Se requiere un 'token' o 'card_data' para procesar el pago"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
send_request(:post, path, payload)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def void_payment(id)
|
|
82
|
+
path = "/pts/v2/payments/#{id}/voids"
|
|
83
|
+
payload = { clientReferenceInformation: { code: "void_#{Time.now.to_i}" } }
|
|
84
|
+
send_request(:post, path, payload)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Reembolsar un pago (Refund) - Versión Corregida para CyberSource
|
|
88
|
+
def refund_payment(id, amount)
|
|
89
|
+
path = "/pts/v2/payments/#{id}/refunds"
|
|
90
|
+
|
|
91
|
+
payload = {
|
|
92
|
+
clientReferenceInformation: { code: "refund_#{Time.now.to_i}" },
|
|
93
|
+
orderInformation: {
|
|
94
|
+
amountDetails: {
|
|
95
|
+
totalAmount: amount.total,
|
|
96
|
+
currency: amount.currency
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
send_request(:post, path, payload)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def get_payment(id)
|
|
104
|
+
send_request(:get, "/pts/v2/payments/#{id}")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def get_payment_status(id)
|
|
108
|
+
get_payment(id)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def send_request(method, path, payload = nil)
|
|
114
|
+
headers = generate_auth_headers(method, path, payload)
|
|
115
|
+
body = (payload && method != :get) ? payload.to_json : nil
|
|
116
|
+
response = @conn.run_request(method, path, body, headers)
|
|
117
|
+
handle_response(response)
|
|
118
|
+
rescue JSON::ParserError
|
|
119
|
+
::IpagosRubySdk::Response.error("Respuesta de API inválida (JSON Malformed)", 500)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
::IpagosRubySdk::Response.new(success: false, error_message: "Excepción de Red: #{e.message}", status_code: 500)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def generate_auth_headers(method, path, payload)
|
|
125
|
+
date = Time.now.httpdate
|
|
126
|
+
digest = ""
|
|
127
|
+
|
|
128
|
+
if payload && method != :get
|
|
129
|
+
json_body = payload.to_json
|
|
130
|
+
hash = OpenSSL::Digest::SHA256.digest(json_body)
|
|
131
|
+
digest = "SHA-256=#{Base64.strict_encode64(hash)}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
signature_headers = [
|
|
135
|
+
"(request-target): #{method.to_s.downcase} #{path}",
|
|
136
|
+
"host: #{URI.parse(@base_url).host}",
|
|
137
|
+
"date: #{date}"
|
|
138
|
+
]
|
|
139
|
+
signature_headers << "digest: #{digest}" unless digest.empty?
|
|
140
|
+
signature_headers << "v-c-merchant-id: #{@config.merchant_id}"
|
|
141
|
+
|
|
142
|
+
sig_string = signature_headers.join("\n")
|
|
143
|
+
decoded_secret = Base64.decode64(@config.secret_key)
|
|
144
|
+
hmac = OpenSSL::HMAC.digest("sha256", decoded_secret, sig_string)
|
|
145
|
+
|
|
146
|
+
header_list = "(request-target) host date#{' digest' unless digest.empty?} v-c-merchant-id"
|
|
147
|
+
|
|
148
|
+
headers = {
|
|
149
|
+
"Date" => date,
|
|
150
|
+
"Signature" => "keyid=\"#{@config.key_id}\", algorithm=\"HmacSHA256\", headers=\"#{header_list}\", signature=\"#{Base64.strict_encode64(hmac)}\"",
|
|
151
|
+
"v-c-merchant-id" => @config.merchant_id
|
|
152
|
+
}
|
|
153
|
+
headers["Digest"] = digest unless digest.empty?
|
|
154
|
+
headers
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def handle_response(response)
|
|
158
|
+
success = response.status.between?(200, 299)
|
|
159
|
+
raw_body = response.body.to_s.strip
|
|
160
|
+
|
|
161
|
+
data = begin
|
|
162
|
+
if raw_body.start_with?("{", "[")
|
|
163
|
+
JSON.parse(raw_body)
|
|
164
|
+
elsif raw_body.start_with?("eyJ")
|
|
165
|
+
{ "token" => raw_body }
|
|
166
|
+
else
|
|
167
|
+
{ "raw_response" => raw_body }
|
|
168
|
+
end
|
|
169
|
+
rescue JSON::ParserError
|
|
170
|
+
{ "raw_response" => raw_body }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
::IpagosRubySdk::Response.new(
|
|
174
|
+
success: success,
|
|
175
|
+
data: data,
|
|
176
|
+
error_message: success ? nil : "Error #{response.status}: #{data['message'] || 'Respuesta inválida'}",
|
|
177
|
+
status_code: response.status
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module IpagosRubySdk
|
|
5
|
+
class BaseModel
|
|
6
|
+
def initialize(attributes = {})
|
|
7
|
+
attributes.each do |key, value|
|
|
8
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Este método equivale a tu toArray() de PHP
|
|
13
|
+
def to_h
|
|
14
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
15
|
+
value = instance_variable_get(var)
|
|
16
|
+
# Limpiamos el '@' del nombre de la variable
|
|
17
|
+
key = var.to_s.delete("@")
|
|
18
|
+
|
|
19
|
+
# Si el valor es otro modelo, lo convertimos recursivamente
|
|
20
|
+
hash[key] = value.respond_to?(:to_h) ? value.to_h : value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IpagosRubySdk
|
|
4
|
+
class LineItem < BaseModel
|
|
5
|
+
attr_accessor :unit_price, :quantity, :product_name, :product_sku
|
|
6
|
+
|
|
7
|
+
def to_h
|
|
8
|
+
{
|
|
9
|
+
unitPrice: unit_price,
|
|
10
|
+
quantity: quantity,
|
|
11
|
+
productName: product_name,
|
|
12
|
+
productSku: product_sku
|
|
13
|
+
}.compact
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IpagosRubySdk
|
|
4
|
+
# Configuración (Equivale a IpagosConfig.php)
|
|
5
|
+
# Usamos una estructura con bloque para incluir la validación de presencia
|
|
6
|
+
Configuration = Struct.new(
|
|
7
|
+
:environment,
|
|
8
|
+
:merchant_id,
|
|
9
|
+
:key_id,
|
|
10
|
+
:secret_key,
|
|
11
|
+
keyword_init: true
|
|
12
|
+
) do
|
|
13
|
+
def initialize(*)
|
|
14
|
+
super
|
|
15
|
+
raise ArgumentError, "merchant_id es requerido" if merchant_id.nil?
|
|
16
|
+
raise ArgumentError, "key_id es requerido" if key_id.nil?
|
|
17
|
+
raise ArgumentError, "secret_key es requerido" if secret_key.nil?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Respuesta (Equivale a IpagosResponse.php)
|
|
22
|
+
class Response
|
|
23
|
+
attr_reader :success, :data, :error_message, :status_code
|
|
24
|
+
|
|
25
|
+
def initialize(success:, data: nil, error_message: nil, status_code: 200)
|
|
26
|
+
@success = success
|
|
27
|
+
@data = data
|
|
28
|
+
@error_message = error_message
|
|
29
|
+
@status_code = status_code
|
|
30
|
+
# Hacemos el objeto inmutable
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def success?
|
|
35
|
+
@success
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Obtiene valores del payload soportando Strings o Símbolos
|
|
39
|
+
def get(key, default = nil)
|
|
40
|
+
return default unless @data.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
@data[key.to_s] || @data[key.to_sym] || default
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Factory para respuestas exitosas
|
|
46
|
+
def self.success(data = nil, status_code = 200)
|
|
47
|
+
new(success: true, data: data, status_code: status_code)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Factory para respuestas de error
|
|
51
|
+
def self.error(message, status_code = 400)
|
|
52
|
+
new(success: false, error_message: message, status_code: status_code)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ipagos_ruby_sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Integraciones iPagos
|
|
@@ -81,6 +81,13 @@ files:
|
|
|
81
81
|
- README.md
|
|
82
82
|
- Rakefile
|
|
83
83
|
- lib/ipagos_ruby_sdk.rb
|
|
84
|
+
- lib/ipagos_ruby_sdk/client.rb
|
|
85
|
+
- lib/ipagos_ruby_sdk/environment.rb
|
|
86
|
+
- lib/ipagos_ruby_sdk/models/amount_details.rb
|
|
87
|
+
- lib/ipagos_ruby_sdk/models/base_model.rb
|
|
88
|
+
- lib/ipagos_ruby_sdk/models/bill_to.rb
|
|
89
|
+
- lib/ipagos_ruby_sdk/models/line_item.rb
|
|
90
|
+
- lib/ipagos_ruby_sdk/types.rb
|
|
84
91
|
- lib/ipagos_ruby_sdk/version.rb
|
|
85
92
|
- sig/ipagos_ruby_sdk.rbs
|
|
86
93
|
homepage: https://gitlab.com/ipagos/ipagos-ruby-sdk
|