paycertify 0.0.7 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/paycertify.rb +2 -0
- data/lib/paycertify/confirmation.rb +94 -0
- data/lib/paycertify/gateway.rb +17 -17
- data/lib/paycertify/gateway/attribute_mapping.rb +49 -6
- data/lib/paycertify/gateway/base.rb +8 -0
- data/lib/paycertify/gateway/base/resource.rb +87 -0
- data/lib/paycertify/gateway/base/validation.rb +105 -0
- data/lib/paycertify/gateway/charge.rb +58 -0
- data/lib/paycertify/gateway/client.rb +11 -1
- data/lib/paycertify/gateway/credit_card.rb +65 -0
- data/lib/paycertify/gateway/customer.rb +60 -0
- data/lib/paycertify/gateway/response.rb +18 -15
- data/lib/paycertify/gateway/transaction.rb +97 -35
- data/lib/paycertify/insurance.rb +118 -0
- data/lib/paycertify/three_ds.rb +1 -1
- data/lib/paycertify/three_ds/callback.rb +8 -0
- data/lib/paycertify/three_ds/client.rb +5 -0
- data/lib/paycertify/three_ds/form.rb +24 -26
- metadata +23 -2
- data/lib/paycertify/gateway/validation.rb +0 -174
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4853d8c8ab842f2530d63b7c7880e51b0131c6cb
|
4
|
+
data.tar.gz: 65aa6d612b3c3c7154595a7d59cb5f31278f4fb9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: accec71a0359c602880883d965b315fd65f2bb9189719ad9003e33145f3ec079c78983e6d864f6ead27f453edb42dfb4ef34c998ca416093601f3e44b7a1241e
|
7
|
+
data.tar.gz: df636c0cd6d851677d7fc2e4428be57c5b9b426c2ad1de1f86122daf6756fd9368628b3f890ca1ae4dcd6be22aa63316e0fa3a77a5976cc360e6f985bb6618dd
|
data/lib/paycertify.rb
CHANGED
@@ -0,0 +1,94 @@
|
|
1
|
+
module PayCertify
|
2
|
+
class Confirmation
|
3
|
+
|
4
|
+
class NoCredentialsError < StandardError; end
|
5
|
+
|
6
|
+
API_ENDPOINT = 'https://www.paycertify.com/'
|
7
|
+
|
8
|
+
MANDATORY_FIELDS = [
|
9
|
+
:transaction_id, :cc_last_four_digits, :name, :email,
|
10
|
+
:phone, :amount, :currency, :payment_gateway
|
11
|
+
]
|
12
|
+
|
13
|
+
OPTIONAL_FIELDS = [
|
14
|
+
:status, :transaction_date, :order_description, :card_type, :name_on_card,
|
15
|
+
:address, :city, :zip, :state, :country, :confirmation_type, :fraud_score_processing, :scheduled_messages,
|
16
|
+
:thank_you_page_url, :metadata
|
17
|
+
]
|
18
|
+
|
19
|
+
attr_accessor :attributes, :errors, :response
|
20
|
+
|
21
|
+
delegate :api_key, to: :class
|
22
|
+
|
23
|
+
def initialize(attributes)
|
24
|
+
raise NoCredentialsError, 'No api key provided.' unless api_key.present?
|
25
|
+
|
26
|
+
self.attributes = HashWithIndifferentAccess.new(attributes)
|
27
|
+
self.errors = {}
|
28
|
+
|
29
|
+
validate!
|
30
|
+
end
|
31
|
+
|
32
|
+
def success?
|
33
|
+
response.success?
|
34
|
+
end
|
35
|
+
|
36
|
+
def start!
|
37
|
+
data = attributes.slice *[MANDATORY_FIELDS + OPTIONAL_FIELDS].flatten
|
38
|
+
|
39
|
+
api_response = connection.post do |request|
|
40
|
+
request.url path_for('merchant/transactions')
|
41
|
+
request.headers['Content-Type'] = 'application/json'
|
42
|
+
request.headers['PAYCERTIFYKEY'] = api_key
|
43
|
+
request.body = JSON.generate(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
self.response = Response.new(api_response)
|
47
|
+
self.errors = errors.merge(response) unless response.success?
|
48
|
+
|
49
|
+
response
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
attr_accessor :api_key
|
54
|
+
|
55
|
+
def configure(&block)
|
56
|
+
yield self if block_given?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Response < HashWithIndifferentAccess
|
61
|
+
attr_accessor :original_response
|
62
|
+
|
63
|
+
def initialize(response)
|
64
|
+
self.original_response = response
|
65
|
+
super JSON.parse(response.body)
|
66
|
+
end
|
67
|
+
|
68
|
+
def success?
|
69
|
+
original_response.status < 400
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def validate!
|
75
|
+
MANDATORY_FIELDS.each do |field|
|
76
|
+
unless attributes[field].present?
|
77
|
+
self.errors[field] = "Required attribute not present"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def connection
|
83
|
+
@connection ||= Faraday.new(url: API_ENDPOINT, ssl: {verify: false}) do |faraday|
|
84
|
+
faraday.request :url_encoded
|
85
|
+
faraday.response :logger
|
86
|
+
faraday.adapter Faraday.default_adapter
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def path_for(path)
|
91
|
+
return "api/v1/" + path
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/paycertify/gateway.rb
CHANGED
@@ -1,34 +1,34 @@
|
|
1
1
|
require_relative './gateway/attribute_mapping'
|
2
|
+
require_relative './gateway/base'
|
3
|
+
require_relative './gateway/charge'
|
2
4
|
require_relative './gateway/client'
|
5
|
+
require_relative './gateway/credit_card'
|
6
|
+
require_relative './gateway/customer'
|
3
7
|
require_relative './gateway/response'
|
4
8
|
require_relative './gateway/transaction'
|
5
|
-
require_relative './gateway/validation'
|
6
9
|
|
7
10
|
require 'json'
|
8
11
|
|
9
12
|
module PayCertify
|
10
13
|
class Gateway
|
11
|
-
|
12
|
-
attr_accessor :client, :attributes
|
13
|
-
|
14
|
-
delegate :api_key, :mode, to: :class
|
15
|
-
|
16
|
-
def initialize(attributes)
|
17
|
-
self.attributes = attributes
|
18
|
-
self.client = PayCertify::Gateway::Client.new(api_key: api_key, mode: mode)
|
19
|
-
end
|
20
|
-
|
21
|
-
|
22
|
-
def process_transaction!
|
23
|
-
transaction = PayCertify::Gateway::Transaction.new(client, attributes)
|
24
|
-
transaction.process
|
25
|
-
end
|
14
|
+
CREDENTIALS_PATH = '/ws/encgateway2.asmx/GetCredential'
|
26
15
|
|
27
16
|
class << self
|
28
|
-
|
17
|
+
attr_accessor :api_key, :mode, :vendor
|
29
18
|
|
30
19
|
def configure(&block)
|
31
20
|
yield self if block_given?
|
21
|
+
|
22
|
+
client = PayCertify::Gateway::Client.new(api_key: api_key, mode: mode)
|
23
|
+
response = client.get(path: CREDENTIALS_PATH)
|
24
|
+
|
25
|
+
self.vendor = response['response']['vendor']
|
26
|
+
|
27
|
+
return {
|
28
|
+
api_key: api_key,
|
29
|
+
mode: mode,
|
30
|
+
vendor: vendor
|
31
|
+
}
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
@@ -2,7 +2,7 @@ module PayCertify
|
|
2
2
|
class Gateway
|
3
3
|
module AttributeMapping
|
4
4
|
module_function
|
5
|
-
def
|
5
|
+
def transaction
|
6
6
|
{
|
7
7
|
'Amount' => :amount,
|
8
8
|
'Currency' => :currency,
|
@@ -25,8 +25,40 @@ module PayCertify
|
|
25
25
|
'Email' => :email,
|
26
26
|
'Description' => :order_description,
|
27
27
|
'CustomerID' => :customer_id,
|
28
|
-
'ServerID' => :ip
|
29
|
-
|
28
|
+
'ServerID' => :ip
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def customer
|
33
|
+
{
|
34
|
+
'CustomerID' => :app_customer_id,
|
35
|
+
'CustomerKey' => :customer_id,
|
36
|
+
'CustomerName' => :name,
|
37
|
+
'Street1' => :address,
|
38
|
+
'City' => :city,
|
39
|
+
'StateID' => :state,
|
40
|
+
'Zip' => :zip,
|
41
|
+
'MobilePhone' => :phone,
|
42
|
+
'Fax' => :fax,
|
43
|
+
'Email' => :email,
|
44
|
+
'Status' => :status
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def credit_card
|
49
|
+
{
|
50
|
+
'CardNum' => :card_number,
|
51
|
+
'NameOnCard' => :name_on_card,
|
52
|
+
'CustomerKey' => :customer_id,
|
53
|
+
'PostalCode' => :zip
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def charge
|
58
|
+
{
|
59
|
+
'CardToken' => :credit_card_id,
|
60
|
+
'Amount' => :amount,
|
61
|
+
'PNRef' => :transaction_id
|
30
62
|
}
|
31
63
|
end
|
32
64
|
|
@@ -34,9 +66,20 @@ module PayCertify
|
|
34
66
|
{ 'ExpDate' => [transaction.expiration_month, transaction.expiration_year].join }
|
35
67
|
end
|
36
68
|
|
37
|
-
def
|
38
|
-
|
69
|
+
def type(object)
|
70
|
+
case object.class.name
|
71
|
+
when /.*Transaction.*/, /.*Charge.*/
|
72
|
+
{ 'TransType' => object.type.to_s.capitalize }
|
73
|
+
when /.*Customer.*/
|
74
|
+
{ 'TransType' => object.type.to_s.upcase }
|
75
|
+
else
|
76
|
+
{}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def status(customer)
|
81
|
+
object.status.to_s.upcase
|
39
82
|
end
|
40
83
|
end
|
41
84
|
end
|
42
|
-
end
|
85
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module PayCertify
|
2
|
+
class Gateway
|
3
|
+
module Base
|
4
|
+
class Resource
|
5
|
+
attr_accessor :client, :original_attributes, :response, :errors
|
6
|
+
|
7
|
+
delegate :api_key, :mode, to: PayCertify::Gateway
|
8
|
+
|
9
|
+
def initialize(attributes)
|
10
|
+
self.original_attributes = attributes
|
11
|
+
self.client = PayCertify::Gateway::Client.new(api_key: api_key, mode: mode)
|
12
|
+
|
13
|
+
if validatable?
|
14
|
+
# Validate + attribute assignment
|
15
|
+
validation.attributes.each do |key, value|
|
16
|
+
self.send("#{key}=", value)
|
17
|
+
end
|
18
|
+
|
19
|
+
self.errors = validation.errors
|
20
|
+
else
|
21
|
+
# Attribute assignment only
|
22
|
+
self.class.const_get('ATTRIBUTES').each do |key|
|
23
|
+
self.send("#{key}=", attributes[key])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def success?
|
29
|
+
errors.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def validatable?
|
33
|
+
self.class.const_get('Validation')
|
34
|
+
true
|
35
|
+
rescue NameError
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
def validation
|
40
|
+
@validation ||= self.class.const_get('Validation').new(original_attributes)
|
41
|
+
end
|
42
|
+
|
43
|
+
def attributes
|
44
|
+
{}.tap do |attributes|
|
45
|
+
self.class.const_get('ATTRIBUTES').each do |attribute|
|
46
|
+
value = self.send(attribute)
|
47
|
+
attributes[attribute] = value if value.present?
|
48
|
+
end
|
49
|
+
|
50
|
+
if response.present?
|
51
|
+
attributes['gateway_response'] = response
|
52
|
+
end
|
53
|
+
|
54
|
+
attributes
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_json
|
59
|
+
JSON.generate(attributes)
|
60
|
+
end
|
61
|
+
|
62
|
+
def save!
|
63
|
+
self.response = client.post(
|
64
|
+
path: self.class.const_get('API_ENDPOINT'),
|
65
|
+
data: attributes_to_gateway_format
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def attributes_to_gateway_format
|
70
|
+
{}.tap do |formatted|
|
71
|
+
attribute_mapping = PayCertify::Gateway::AttributeMapping
|
72
|
+
mapping_name = self.class.name.underscore.split('/').last
|
73
|
+
|
74
|
+
attribute_mapping.send(mapping_name).each do |key, value|
|
75
|
+
[value].flatten.tap do |method_chain|
|
76
|
+
new_value = method_chain.map { |method_name| self.send(method_name) }.join
|
77
|
+
formatted[key] = new_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
formatted
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module PayCertify
|
2
|
+
class Gateway
|
3
|
+
module Base
|
4
|
+
class Validation
|
5
|
+
|
6
|
+
EMAIL_REGEX = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
|
7
|
+
CREDIT_CARD_REGEX = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/i
|
8
|
+
|
9
|
+
attr_accessor :attributes, :errors
|
10
|
+
|
11
|
+
def initialize(attributes)
|
12
|
+
self.attributes = attributes
|
13
|
+
self.errors = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def no_validation(_); end
|
17
|
+
|
18
|
+
def presence_validation(attribute)
|
19
|
+
if value_for(attribute).blank?
|
20
|
+
add_error(attribute, "Required attribute not present")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def email_validation(attribute)
|
25
|
+
unless value_for(attribute) =~ EMAIL_REGEX
|
26
|
+
add_error(attribute, "Doesn't validate as an email.")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def zip_validation(attribute)
|
31
|
+
set_attribute(attribute, Integer(value_for(attribute)).to_s)
|
32
|
+
|
33
|
+
unless value_for(attribute).length == 5
|
34
|
+
add_error(attribute, "Must be a 5-digit string that can evaluate to a number.")
|
35
|
+
end
|
36
|
+
|
37
|
+
rescue
|
38
|
+
add_error(attribute, "Must be a 5-digit string that can evaluate to a number.")
|
39
|
+
end
|
40
|
+
|
41
|
+
def card_number_validation(attribute)
|
42
|
+
# Non decimal numbers should be stripped to match.
|
43
|
+
set_attribute(attribute, value_for(attribute).gsub(/\D/, ''))
|
44
|
+
|
45
|
+
unless value_for(attribute) =~ CREDIT_CARD_REGEX
|
46
|
+
add_error(attribute, "Doesn't validate as a credit card.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def expiration_month_validation(attribute)
|
51
|
+
# if a string, check if length = 2 and smaller than int 12
|
52
|
+
# if int, transform into string with zero pad and check if smaller than int 12
|
53
|
+
integer = Integer(value_for(attribute))
|
54
|
+
|
55
|
+
if integer > 12
|
56
|
+
add_error(attribute, "Must be smaller than 12.")
|
57
|
+
end
|
58
|
+
|
59
|
+
set_attribute(attribute, integer.to_s.rjust(2, '0'))
|
60
|
+
|
61
|
+
rescue ArgumentError
|
62
|
+
add_error(attribute, "Must be an integer.")
|
63
|
+
end
|
64
|
+
|
65
|
+
def expiration_year_validation(attribute)
|
66
|
+
# if length = 4, strip to 2;
|
67
|
+
# if a string, check if length = 2 and smaller than int 12
|
68
|
+
# if int, transform into string with zero pad and check if smaller than int 12
|
69
|
+
|
70
|
+
|
71
|
+
is_four_digit = if value_for(attribute).is_a?(String)
|
72
|
+
value_for(attribute).length == 4
|
73
|
+
else
|
74
|
+
value_for(attribute) > 999
|
75
|
+
end
|
76
|
+
|
77
|
+
integer_value = Integer(value_for(attribute))
|
78
|
+
|
79
|
+
set_attribute(attribute, integer_value.to_s.last(2))
|
80
|
+
rescue
|
81
|
+
add_error(attribute, "Must be a 2 to 4-digit string.")
|
82
|
+
end
|
83
|
+
|
84
|
+
def amount_validation(attribute)
|
85
|
+
set_attribute(attribute, Float(value_for(attribute)))
|
86
|
+
rescue ArgumentError
|
87
|
+
add_error(attribute, "Must be a float, integer or decimal")
|
88
|
+
end
|
89
|
+
|
90
|
+
def value_for(attribute)
|
91
|
+
attributes[attribute[:name]]
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_attribute(attribute, value)
|
95
|
+
self.attributes[attribute[:name]] = value
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_error(attribute, message)
|
99
|
+
self.errors[attribute[:name]] ||= []
|
100
|
+
self.errors[attribute[:name]] << message
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module PayCertify
|
2
|
+
class Gateway
|
3
|
+
class Charge < PayCertify::Gateway::Base::Resource
|
4
|
+
|
5
|
+
API_ENDPOINT = '/ws/cardsafe.asmx/ProcessStoredCard'
|
6
|
+
|
7
|
+
ATTRIBUTES = [
|
8
|
+
:transaction_id, :app_transaction_id, :type, :amount,
|
9
|
+
:credit_card_id, :gateway_response
|
10
|
+
]
|
11
|
+
|
12
|
+
attr_accessor *ATTRIBUTES
|
13
|
+
|
14
|
+
alias :execute! :save!
|
15
|
+
|
16
|
+
def type
|
17
|
+
'sale'
|
18
|
+
end
|
19
|
+
|
20
|
+
def success?
|
21
|
+
response['response']['transaction_result']['result'] == '0'
|
22
|
+
end
|
23
|
+
|
24
|
+
def save!
|
25
|
+
super
|
26
|
+
self.transaction_id = response['response']['transaction_result']['pn_ref']
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes_to_gateway_format
|
31
|
+
formatted = super
|
32
|
+
attribute_mapping = PayCertify::Gateway::AttributeMapping
|
33
|
+
|
34
|
+
formatted.merge! attribute_mapping.type(self)
|
35
|
+
formatted.merge!({'TokenMode' => 'DEFAULT'})
|
36
|
+
|
37
|
+
formatted
|
38
|
+
end
|
39
|
+
|
40
|
+
class Validation < PayCertify::Gateway::Base::Validation
|
41
|
+
ALL_VALIDATIONS = [
|
42
|
+
{ name: :credit_card_id, validation: :no_validation, required: true },
|
43
|
+
{ name: :amount, validation: :amount_validation, required: true },
|
44
|
+
{ name: :transaction_id, validation: :no_validation, required: true },
|
45
|
+
]
|
46
|
+
|
47
|
+
def initialize(attributes={})
|
48
|
+
super(attributes)
|
49
|
+
|
50
|
+
ALL_VALIDATIONS.each do |attribute|
|
51
|
+
presence_validation(attribute) if attribute[:required]
|
52
|
+
send(attribute[:validation], attribute) if value_for(attribute).present?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -19,8 +19,14 @@ module PayCertify
|
|
19
19
|
@api_endpoint ||= 'https://'+ (live?? 'gateway' : 'demo') +'.paycertify.net'
|
20
20
|
end
|
21
21
|
|
22
|
+
def get(path:, data: {})
|
23
|
+
data.merge!(token_payload)
|
24
|
+
response = connection.get(path, data)
|
25
|
+
respond_with response
|
26
|
+
end
|
27
|
+
|
22
28
|
def post(path:, data:)
|
23
|
-
body = data.merge(
|
29
|
+
body = data.merge(token_payload)
|
24
30
|
|
25
31
|
response = connection.post do |request|
|
26
32
|
request.url path
|
@@ -47,6 +53,10 @@ module PayCertify
|
|
47
53
|
end
|
48
54
|
end
|
49
55
|
|
56
|
+
def token_payload
|
57
|
+
{ 'ApiToken' => api_key }
|
58
|
+
end
|
59
|
+
|
50
60
|
def respond_with(response)
|
51
61
|
self.response = PayCertify::Gateway::Response.new(response)
|
52
62
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module PayCertify
|
2
|
+
class Gateway
|
3
|
+
class CreditCard < PayCertify::Gateway::Base::Resource
|
4
|
+
|
5
|
+
API_ENDPOINT = '/ws/cardsafe.asmx/StoreCard'
|
6
|
+
SAFE_CARD_REGEX = /\<CardSafeToken>(.*)<\/CardSafeToken>/
|
7
|
+
|
8
|
+
ATTRIBUTES = [
|
9
|
+
:credit_card_id, :card_number, :expiration_month, :expiration_year,
|
10
|
+
:customer_id, :name_on_card, :zip
|
11
|
+
]
|
12
|
+
|
13
|
+
attr_accessor *ATTRIBUTES
|
14
|
+
|
15
|
+
def success?
|
16
|
+
super && response['response']['result'] == '0'
|
17
|
+
end
|
18
|
+
|
19
|
+
def save!
|
20
|
+
super
|
21
|
+
self.credit_card_id = get_credit_card_id
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def attributes_to_gateway_format
|
26
|
+
formatted = super
|
27
|
+
attribute_mapping = PayCertify::Gateway::AttributeMapping
|
28
|
+
|
29
|
+
formatted.merge! attribute_mapping.expiration_date(self)
|
30
|
+
formatted.merge!({'TokenMode' => 'DEFAULT'})
|
31
|
+
|
32
|
+
formatted
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_credit_card_id
|
36
|
+
response['response']['ext_data'].match(SAFE_CARD_REGEX)
|
37
|
+
credit_card_id = $1
|
38
|
+
credit_card_id.presence || response['response']['ext_data']['safe_card_token']
|
39
|
+
end
|
40
|
+
|
41
|
+
class Validation < PayCertify::Gateway::Base::Validation
|
42
|
+
ALL_VALIDATIONS = [
|
43
|
+
# Mandatory fields
|
44
|
+
{ name: :card_number, validation: :card_number_validation, required: true },
|
45
|
+
{ name: :expiration_month, validation: :expiration_month_validation, required: true },
|
46
|
+
{ name: :expiration_year, validation: :expiration_year_validation, required: true },
|
47
|
+
{ name: :name_on_card, validation: :no_validation, required: true },
|
48
|
+
{ name: :customer_id, validation: :no_validation, required: true },
|
49
|
+
|
50
|
+
# Optional fields
|
51
|
+
{ name: :zip, validation: :zip_validation, required: false }
|
52
|
+
]
|
53
|
+
|
54
|
+
def initialize(attributes={})
|
55
|
+
super(attributes)
|
56
|
+
|
57
|
+
ALL_VALIDATIONS.each do |attribute|
|
58
|
+
presence_validation(attribute) if attribute[:required]
|
59
|
+
send(attribute[:validation], attribute) if value_for(attribute).present?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module PayCertify
|
2
|
+
class Gateway
|
3
|
+
class Customer < PayCertify::Gateway::Base::Resource
|
4
|
+
|
5
|
+
API_ENDPOINT = '/ws/recurring.asmx/ManageCustomer'
|
6
|
+
|
7
|
+
ATTRIBUTES = [
|
8
|
+
:app_customer_id, :name, :customer_id, :type, :address, :city,
|
9
|
+
:state, :zip, :phone, :fax, :email, :status
|
10
|
+
]
|
11
|
+
|
12
|
+
attr_accessor *ATTRIBUTES
|
13
|
+
|
14
|
+
def save!
|
15
|
+
super
|
16
|
+
self.customer_id = self.response['response']['customer_key']
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def attributes_to_gateway_format
|
21
|
+
formatted = super
|
22
|
+
attribute_mapping = PayCertify::Gateway::AttributeMapping
|
23
|
+
formatted.merge! attribute_mapping.type(self)
|
24
|
+
formatted
|
25
|
+
end
|
26
|
+
|
27
|
+
class Validation < PayCertify::Gateway::Base::Validation
|
28
|
+
ALL_VALIDATIONS = [
|
29
|
+
# Mandatory fields
|
30
|
+
{ name: :app_customer_id, validation: :no_validation, required: true },
|
31
|
+
{ name: :type, validation: :type_validation, required: true },
|
32
|
+
{ name: :name, validation: :no_validation, required: true },
|
33
|
+
|
34
|
+
# Optional fields
|
35
|
+
{ name: :zip, validation: :zip_validation, required: false },
|
36
|
+
{ name: :email, validation: :email_validation, required: false },
|
37
|
+
{ name: :status, validation: :status_validation, required: false }
|
38
|
+
]
|
39
|
+
|
40
|
+
ALLOWED_TYPES = %w(add update delete)
|
41
|
+
ALLOWED_STATUSES = %w(active inactive pending closed)
|
42
|
+
|
43
|
+
def initialize(attributes={})
|
44
|
+
super(attributes)
|
45
|
+
|
46
|
+
ALL_VALIDATIONS.each do |attribute|
|
47
|
+
presence_validation(attribute) if attribute[:required]
|
48
|
+
send(attribute[:validation], attribute) if value_for(attribute).present?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def type_validation(attribute)
|
53
|
+
unless value_for(attribute).try(:to_s).in?(ALLOWED_TYPES)
|
54
|
+
add_error(attribute, "Must be one of #{ALLOWED_TYPES.join(', ')}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -8,24 +8,27 @@ module PayCertify
|
|
8
8
|
|
9
9
|
def initialize(response)
|
10
10
|
self.status = response.status
|
11
|
-
self.original_body = Hash.from_xml(response.body)
|
11
|
+
self.original_body = Hash.from_xml(response.body)
|
12
12
|
|
13
|
-
super(
|
14
|
-
id: original_body['PNRef'],
|
15
|
-
status: response.status,
|
16
|
-
status_string: original_body['Result'] == APPROVED ? 'approved' : 'declined',
|
17
|
-
message: [original_body['Message'], original_body['Message1'], original_body['Message2']].compact.join(' / '),
|
18
|
-
amount: original_body['Amount'].to_d,
|
19
|
-
auth_code: original_body['AuthCode'],
|
20
|
-
payment_type: original_body['PaymentType'],
|
21
|
-
card_number: original_body['Account'],
|
22
|
-
created_at: DateTime.strptime([original_body['TransDate'], original_body['TransTime']].join, '%m%d%Y%H%M%s')
|
23
|
-
)
|
13
|
+
super(convert_hash_keys(original_body))
|
24
14
|
end
|
25
15
|
|
26
|
-
|
27
|
-
|
28
|
-
|
16
|
+
private
|
17
|
+
def underscore_key(k)
|
18
|
+
k.to_s.underscore.to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def convert_hash_keys(value)
|
22
|
+
case value
|
23
|
+
when Array
|
24
|
+
value.map { |v| convert_hash_keys(v) }
|
25
|
+
# or `value.map(&method(:convert_hash_keys))`
|
26
|
+
when Hash
|
27
|
+
Hash[value.map { |k, v| [underscore_key(k), convert_hash_keys(v)] }]
|
28
|
+
else
|
29
|
+
value
|
30
|
+
end
|
31
|
+
end
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
@@ -1,54 +1,116 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
1
3
|
module PayCertify
|
2
4
|
class Gateway
|
3
|
-
class Transaction
|
4
|
-
|
5
|
-
TRANSACTION_PATH = '/ws/encgateway2.asmx/ProcessCreditCard'
|
6
|
-
|
7
|
-
attr_accessor :client, :original_attributes
|
5
|
+
class Transaction < PayCertify::Gateway::Base::Resource
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
:
|
7
|
+
API_ENDPOINT = '/ws/encgateway2.asmx/ProcessCreditCard'
|
8
|
+
|
9
|
+
ATTRIBUTES = [
|
10
|
+
:transaction_id, :type, :amount, :currency, :card_number, :expiration_month, :expiration_year,
|
11
|
+
:name_on_card, :cvv, :billing_address, :billing_city, :billing_state, :billing_country,
|
12
|
+
:billing_zip, :shipping_address, :shipping_city, :shipping_state, :shipping_country, :shipping_zip,
|
13
|
+
:email, :phone, :ip, :order_description, :customer_id
|
14
|
+
]
|
13
15
|
|
14
|
-
|
15
|
-
self.original_attributes = attributes
|
16
|
-
self.client = client
|
16
|
+
attr_accessor *ATTRIBUTES
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
def save!
|
19
|
+
super
|
20
|
+
self.transaction_id = response['response']['pn_ref']
|
21
|
+
self
|
21
22
|
end
|
22
23
|
|
23
|
-
def
|
24
|
-
|
24
|
+
def success?
|
25
|
+
super && response['response']['result'] == '0'
|
25
26
|
end
|
26
27
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
)
|
28
|
+
def attributes_to_gateway_format
|
29
|
+
formatted = super
|
30
|
+
attribute_mapping = PayCertify::Gateway::AttributeMapping
|
31
|
+
formatted.merge! attribute_mapping.expiration_date(self)
|
32
|
+
formatted.merge! attribute_mapping.type(self)
|
33
|
+
formatted
|
32
34
|
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
class Validation < PayCertify::Gateway::Base::Validation
|
37
|
+
ALL_VALIDATIONS = [
|
38
|
+
# Mandatory fields
|
39
|
+
{ name: :type, validation: :type_validation, required: true },
|
40
|
+
{ name: :amount, validation: :amount_validation, required: true },
|
41
|
+
{ name: :currency, validation: :currency_validation, required: true },
|
42
|
+
{ name: :card_number, validation: :card_number_validation, required: true },
|
43
|
+
{ name: :expiration_month, validation: :expiration_month_validation, required: true },
|
44
|
+
{ name: :expiration_year, validation: :expiration_year_validation, required: true },
|
45
|
+
{ name: :name_on_card, validation: :no_validation, required: true },
|
46
|
+
{ name: :cvv, validation: :no_validation, required: true },
|
47
|
+
{ name: :transaction_id, validation: :no_validation, required: true },
|
48
|
+
{ name: :billing_city, validation: :no_validation, required: true },
|
49
|
+
{ name: :billing_state, validation: :no_validation, required: true },
|
50
|
+
{ name: :billing_country, validation: :no_validation, required: true },
|
51
|
+
{ name: :billing_zip, validation: :zip_validation, required: true },
|
52
|
+
|
53
|
+
# Optional fields
|
54
|
+
{ name: :shipping_zip, validation: :zip_validation, required: false },
|
55
|
+
{ name: :email, validation: :email_validation, required: false },
|
56
|
+
{ name: :ip, validation: :ip_validation, required: false }
|
57
|
+
]
|
58
|
+
|
59
|
+
CAPTURE_VALIDATIONS = [
|
60
|
+
{ name: :type, validation: :type_validation, required: true },
|
61
|
+
{ name: :amount, validation: :amount_validation, required: true },
|
62
|
+
{ name: :transaction_id, validation: :no_validation, required: true }
|
63
|
+
]
|
38
64
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
65
|
+
VOID_VALIDATIONS = [
|
66
|
+
{ name: :type, validation: :type_validation, required: true },
|
67
|
+
# { name: :amount, validation: :amount_validation, required: true },
|
68
|
+
{ name: :transaction_id, validation: :no_validation, required: true }
|
69
|
+
]
|
45
70
|
|
46
|
-
|
47
|
-
|
71
|
+
ALLOWED_TYPES = %w(sale auth return void force recurring)
|
72
|
+
ALLOWED_CURRENCIES = %w(USD EUR)
|
48
73
|
|
49
|
-
|
74
|
+
def initialize(attributes={})
|
75
|
+
super(attributes)
|
76
|
+
|
77
|
+
validations.each do |attribute|
|
78
|
+
presence_validation(attribute) if attribute[:required]
|
79
|
+
send(attribute[:validation], attribute) if value_for(attribute).present?
|
50
80
|
end
|
51
81
|
end
|
82
|
+
|
83
|
+
def validations
|
84
|
+
case attributes[:type]
|
85
|
+
when 'force'
|
86
|
+
CAPTURE_VALIDATIONS
|
87
|
+
when 'void', 'return'
|
88
|
+
VOID_VALIDATIONS
|
89
|
+
else
|
90
|
+
ALL_VALIDATIONS
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def type_validation(attribute)
|
95
|
+
unless value_for(attribute).try(:to_s).in?(ALLOWED_TYPES)
|
96
|
+
add_error(attribute, "Must be one of #{ALLOWED_TYPES.join(', ')}")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def currency_validation(attribute)
|
101
|
+
set_attribute(attribute, value_for(attribute).upcase)
|
102
|
+
|
103
|
+
unless value_for(attribute).try(:to_s).in?(ALLOWED_CURRENCIES)
|
104
|
+
add_error(attribute, "Must be one of #{ALLOWED_CURRENCIES.join(', ')}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def ip_validation(attribute)
|
109
|
+
IPAddr.new(value_for(attribute))
|
110
|
+
rescue IPAddr::InvalidAddressError
|
111
|
+
add_error(attribute, "Doesn't validate as an IP.")
|
112
|
+
end
|
113
|
+
end
|
52
114
|
end
|
53
115
|
end
|
54
116
|
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module PayCertify
|
2
|
+
class Insurance
|
3
|
+
|
4
|
+
class NoCredentialsError < StandardError; end
|
5
|
+
|
6
|
+
API_ENDPOINT = 'https://connect.paycertify.com/'
|
7
|
+
|
8
|
+
MANDATORY_FIELDS = [
|
9
|
+
:firstname, :lastname, :email, :order_number, :items_ordered, :charge_amount,
|
10
|
+
:billing_address, :billing_address2, :billing_city, :billing_state, :billing_country, :billing_zip_code
|
11
|
+
]
|
12
|
+
|
13
|
+
OPTIONAL_FIELDS = [
|
14
|
+
:phone, :shipping_address, :shipping_address2, :shipping_city,
|
15
|
+
:shipping_state, :shipping_country, :shipping_zip_code, :shipping_carrier, :tracking_number
|
16
|
+
]
|
17
|
+
|
18
|
+
attr_accessor :attributes, :errors, :response
|
19
|
+
|
20
|
+
delegate :client_id, :token, to: :class
|
21
|
+
|
22
|
+
def initialize(attributes)
|
23
|
+
raise NoCredentialsError, 'No token found for api_client/secret/client_id combination.' unless token.present?
|
24
|
+
|
25
|
+
self.attributes = HashWithIndifferentAccess.new(attributes)
|
26
|
+
self.errors = {}
|
27
|
+
|
28
|
+
validate!
|
29
|
+
end
|
30
|
+
|
31
|
+
def success?
|
32
|
+
response.success?
|
33
|
+
end
|
34
|
+
|
35
|
+
def save!
|
36
|
+
data = attributes.slice *[MANDATORY_FIELDS + OPTIONAL_FIELDS].flatten
|
37
|
+
data[:ship_to_billing_addr] = true unless data[:shipping_address].present?
|
38
|
+
|
39
|
+
api_response = connection.post do |request|
|
40
|
+
request.url path_for('orders')
|
41
|
+
request.headers['Content-Type'] = 'application/json'
|
42
|
+
request.headers['Authorization'] = "JWT #{token}"
|
43
|
+
request.body = JSON.generate(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
self.response = Response.new(api_response)
|
47
|
+
self.errors = errors.merge(response) unless response.success?
|
48
|
+
|
49
|
+
response
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
attr_accessor :api_public_key, :api_secret_key, :client_id, :token
|
54
|
+
|
55
|
+
def configure(&block)
|
56
|
+
yield self if block_given?
|
57
|
+
|
58
|
+
connection = Faraday.new(url: API_ENDPOINT, ssl: {verify: false}) do |faraday|
|
59
|
+
faraday.request :url_encoded
|
60
|
+
faraday.response :logger
|
61
|
+
faraday.adapter Faraday.default_adapter
|
62
|
+
end
|
63
|
+
|
64
|
+
response = connection.get do |request|
|
65
|
+
request.url 'api/v1/token'
|
66
|
+
request.headers['api-public-key'] = api_public_key
|
67
|
+
request.headers['api-secret-key'] = api_secret_key
|
68
|
+
request.headers['api-client-id'] = client_id
|
69
|
+
end
|
70
|
+
|
71
|
+
json = JSON.parse(response.body)
|
72
|
+
|
73
|
+
self.token = json['jwt']
|
74
|
+
|
75
|
+
return {
|
76
|
+
api_public_key: api_public_key,
|
77
|
+
api_secret_key: api_secret_key,
|
78
|
+
client_id: client_id,
|
79
|
+
token: token
|
80
|
+
}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Response < HashWithIndifferentAccess
|
85
|
+
attr_accessor :original_response
|
86
|
+
|
87
|
+
def initialize(response)
|
88
|
+
self.original_response = response
|
89
|
+
super JSON.parse(response.body)
|
90
|
+
end
|
91
|
+
|
92
|
+
def success?
|
93
|
+
original_response.status < 400
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def validate!
|
99
|
+
MANDATORY_FIELDS.each do |field|
|
100
|
+
unless attributes[field].present?
|
101
|
+
self.errors[field] = "Required attribute not present"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def connection
|
107
|
+
@connection ||= Faraday.new(url: API_ENDPOINT, ssl: {verify: false}) do |faraday|
|
108
|
+
faraday.request :url_encoded
|
109
|
+
faraday.response :logger
|
110
|
+
faraday.adapter Faraday.default_adapter
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def path_for(path)
|
115
|
+
return "api/v1/#{client_id}/#{path}/"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/lib/paycertify/three_ds.rb
CHANGED
@@ -19,6 +19,14 @@ module PayCertify
|
|
19
19
|
self['_frictionless_3ds_callback'].present?
|
20
20
|
end
|
21
21
|
|
22
|
+
def handshake
|
23
|
+
self.slice('cavv', 'eci', 'cavv_algorithm', 'xid')
|
24
|
+
end
|
25
|
+
|
26
|
+
def redirect_to(location)
|
27
|
+
"<script>window.location.href = '#{location}'</script>"
|
28
|
+
end
|
29
|
+
|
22
30
|
def authenticate!
|
23
31
|
PayCertify::ThreeDS.authenticate!(settings: session, callback_params: params)
|
24
32
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'faraday'
|
2
|
+
require 'faraday_curl'
|
2
3
|
|
3
4
|
module PayCertify
|
4
5
|
class ThreeDS
|
@@ -56,12 +57,16 @@ module PayCertify
|
|
56
57
|
def connection
|
57
58
|
@connection ||= Faraday.new(url: API_ENDPOINT, ssl: {verify: false}) do |faraday|
|
58
59
|
faraday.request :url_encoded
|
60
|
+
faraday.request :curl, Logger.new(STDOUT), :warn
|
59
61
|
faraday.response :logger
|
60
62
|
faraday.adapter Faraday.default_adapter
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
64
66
|
def signature(path, data)
|
67
|
+
10.times{puts}
|
68
|
+
puts "#{api_key}#{path_for(path)}#{data}#{api_secret}"
|
69
|
+
10.times{puts}
|
65
70
|
Digest::SHA256.hexdigest "#{api_key}#{path_for(path)}#{data}#{api_secret}"
|
66
71
|
end
|
67
72
|
|
@@ -46,7 +46,7 @@ module PayCertify
|
|
46
46
|
</script>
|
47
47
|
HTML
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
def frictionless
|
51
51
|
html = <<-HTML.squish
|
52
52
|
<style> #frame { display: none; } </style>
|
@@ -69,42 +69,40 @@ module PayCertify
|
|
69
69
|
var frame = document.getElementById('frame');
|
70
70
|
var form = document.getElementById('callback-form');
|
71
71
|
var interval = 500;
|
72
|
-
var timeout = interval *
|
73
|
-
var counter = 0;
|
72
|
+
var timeout = interval * 15;
|
74
73
|
|
75
74
|
frame.contentDocument.write('#{form}');
|
76
75
|
frame.contentDocument.form3ds.submit();
|
77
76
|
|
78
|
-
setInterval(function() {
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
try {
|
83
|
-
var frameContent = frame.contentDocument;
|
84
|
-
var frameDoc = frameContent.documentElement;
|
77
|
+
var interval = setInterval(function() {
|
78
|
+
try {
|
79
|
+
var frameContent = frame.contentDocument;
|
80
|
+
var frameDoc = frameContent.documentElement;
|
85
81
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
var input;
|
82
|
+
var text = frameContent.body.innerHTML || frameDoc.textContent || frameDoc.innerText;
|
83
|
+
var json = JSON.parse(text);
|
90
84
|
|
91
|
-
|
92
|
-
input = document.createElement('input');
|
93
|
-
input.type = 'hidden';
|
94
|
-
input.name = key;
|
95
|
-
input.value = json[key];
|
85
|
+
var input;
|
96
86
|
|
97
|
-
|
98
|
-
|
87
|
+
for(key in json) {
|
88
|
+
input = document.createElement('input');
|
89
|
+
input.type = 'hidden';
|
90
|
+
input.name = key;
|
91
|
+
input.value = json[key];
|
99
92
|
|
100
|
-
form.
|
101
|
-
} catch(e) {
|
102
|
-
return false;
|
93
|
+
form.appendChild(input);
|
103
94
|
};
|
104
|
-
|
95
|
+
|
96
|
+
clearInterval(interval);
|
105
97
|
form.submit();
|
106
|
-
}
|
98
|
+
} catch(e) {
|
99
|
+
return false;
|
100
|
+
};
|
107
101
|
}, interval);
|
102
|
+
|
103
|
+
setTimeout(function() {
|
104
|
+
form.submit();
|
105
|
+
}, timeout);
|
108
106
|
})();
|
109
107
|
</script>
|
110
108
|
HTML
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paycertify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- PayCertify Engineering Team
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday_curl
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
description: Interact with the Gateway, 3DS, Kount, and FraudPortal
|
42
56
|
email: engineering@paycertify.com
|
43
57
|
executables: []
|
@@ -45,12 +59,19 @@ extensions: []
|
|
45
59
|
extra_rdoc_files: []
|
46
60
|
files:
|
47
61
|
- lib/paycertify.rb
|
62
|
+
- lib/paycertify/confirmation.rb
|
48
63
|
- lib/paycertify/gateway.rb
|
49
64
|
- lib/paycertify/gateway/attribute_mapping.rb
|
65
|
+
- lib/paycertify/gateway/base.rb
|
66
|
+
- lib/paycertify/gateway/base/resource.rb
|
67
|
+
- lib/paycertify/gateway/base/validation.rb
|
68
|
+
- lib/paycertify/gateway/charge.rb
|
50
69
|
- lib/paycertify/gateway/client.rb
|
70
|
+
- lib/paycertify/gateway/credit_card.rb
|
71
|
+
- lib/paycertify/gateway/customer.rb
|
51
72
|
- lib/paycertify/gateway/response.rb
|
52
73
|
- lib/paycertify/gateway/transaction.rb
|
53
|
-
- lib/paycertify/
|
74
|
+
- lib/paycertify/insurance.rb
|
54
75
|
- lib/paycertify/three_ds.rb
|
55
76
|
- lib/paycertify/three_ds/callback.rb
|
56
77
|
- lib/paycertify/three_ds/client.rb
|
@@ -1,174 +0,0 @@
|
|
1
|
-
require 'ipaddr'
|
2
|
-
|
3
|
-
module PayCertify
|
4
|
-
class Gateway
|
5
|
-
class Validation
|
6
|
-
|
7
|
-
ATTRIBUTES = [
|
8
|
-
# Mandatory fields
|
9
|
-
{ name: :type, validation: :type_validation, required: true },
|
10
|
-
{ name: :amount, validation: :amount_validation, required: true },
|
11
|
-
{ name: :currency, validation: :currency_validation, required: true },
|
12
|
-
{ name: :card_number, validation: :card_number_validation, required: true },
|
13
|
-
{ name: :expiration_month, validation: :expiration_month_validation, required: true },
|
14
|
-
{ name: :expiration_year, validation: :expiration_year_validation, required: true },
|
15
|
-
{ name: :name_on_card, validation: :no_validation, required: true },
|
16
|
-
{ name: :cvv, validation: :no_validation, required: true },
|
17
|
-
{ name: :transaction_id, validation: :no_validation, required: true },
|
18
|
-
{ name: :billing_city, validation: :no_validation, required: true },
|
19
|
-
{ name: :billing_state, validation: :no_validation, required: true },
|
20
|
-
{ name: :billing_country, validation: :no_validation, required: true },
|
21
|
-
{ name: :billing_zip, validation: :zip_validation, required: true },
|
22
|
-
|
23
|
-
# Optional fields
|
24
|
-
{ name: :billing_address, validation: :no_validation, required: false },
|
25
|
-
{ name: :shipping_address, validation: :no_validation, required: false },
|
26
|
-
{ name: :shipping_city, validation: :no_validation, required: false },
|
27
|
-
{ name: :shipping_state, validation: :no_validation, required: false },
|
28
|
-
{ name: :shipping_country, validation: :no_validation, required: false },
|
29
|
-
{ name: :shipping_zip, validation: :zip_validation, required: false },
|
30
|
-
{ name: :email, validation: :email_validation, required: false },
|
31
|
-
{ name: :phone, validation: :no_validation, required: false },
|
32
|
-
{ name: :ip, validation: :ip_validation, required: false },
|
33
|
-
{ name: :order_description, validation: :no_validation, required: false },
|
34
|
-
{ name: :customer_id, validation: :no_validation, required: false },
|
35
|
-
{ name: :metadata, validation: :no_validation, required: false }
|
36
|
-
]
|
37
|
-
|
38
|
-
ALLOWED_TYPES = %w(sale auth refund void force recurring)
|
39
|
-
ALLOWED_CURRENCIES = %w(USD EUR)
|
40
|
-
CREDIT_CARD_REGEX = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/i
|
41
|
-
EMAIL_REGEX = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
|
42
|
-
|
43
|
-
class ValidationError < StandardError; end
|
44
|
-
|
45
|
-
attr_accessor :attributes, :errors
|
46
|
-
|
47
|
-
def initialize(attributes={})
|
48
|
-
self.attributes = attributes
|
49
|
-
|
50
|
-
ATTRIBUTES.each do |attribute|
|
51
|
-
presence_validation(attribute) if attribute[:required]
|
52
|
-
send(attribute[:validation], attribute) if value_for(attribute).present?
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
protected
|
57
|
-
def presence_validation(attribute)
|
58
|
-
if value_for(attribute).blank?
|
59
|
-
raise ValidationError, "Required attribute not present: #{attribute[:name]}."
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def no_validation(_); end
|
64
|
-
|
65
|
-
def type_validation(attribute)
|
66
|
-
unless value_for(attribute).try(:to_s).in?(ALLOWED_TYPES)
|
67
|
-
raise ValidationError, error_message(attribute, "Must be one of #{ALLOWED_TYPES.join(', ')}")
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def amount_validation(attribute)
|
72
|
-
set_attribute(attribute, Float(value_for(attribute)))
|
73
|
-
rescue ArgumentError
|
74
|
-
raise ValidationError, error_message(attribute, "Must be a float, integer or decimal")
|
75
|
-
end
|
76
|
-
|
77
|
-
def currency_validation(attribute)
|
78
|
-
set_attribute(attribute, value_for(attribute).upcase)
|
79
|
-
|
80
|
-
unless value_for(attribute).try(:to_s).in?(ALLOWED_CURRENCIES)
|
81
|
-
raise ValidationError, error_message(attribute, "Must be one of #{ALLOWED_CURRENCIES.join(', ')}")
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def card_number_validation(attribute)
|
86
|
-
# Non decimal numbers should be stripped to match.
|
87
|
-
set_attribute(attribute, value_for(attribute).gsub(/\D/, ''))
|
88
|
-
|
89
|
-
unless value_for(attribute) =~ CREDIT_CARD_REGEX
|
90
|
-
raise ValidationError, error_message(attribute, "Doesn't validate as a credit card.")
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def zip_validation
|
95
|
-
# Check if it's a number
|
96
|
-
set_attribute(attribute, Integer(value_for(attribute)).to_s)
|
97
|
-
|
98
|
-
if value_for(attribute).length > 5
|
99
|
-
# Raise validation error
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def expiration_month_validation(attribute)
|
104
|
-
# if a string, check if length = 2 and smaller than int 12
|
105
|
-
# if int, transform into string with zero pad and check if smaller than int 12
|
106
|
-
integer = Integer(value_for(attribute))
|
107
|
-
|
108
|
-
if integer > 12
|
109
|
-
raise ValidationError, error_message(attribute, "Must be smaller than 12.")
|
110
|
-
end
|
111
|
-
|
112
|
-
set_attribute(attribute, integer.to_s.rjust(2, '0'))
|
113
|
-
|
114
|
-
rescue ArgumentError
|
115
|
-
raise ValidationError, error_message(attribute, "Must be an integer.")
|
116
|
-
end
|
117
|
-
|
118
|
-
def expiration_year_validation(attribute)
|
119
|
-
# if length = 4, strip to 2;
|
120
|
-
# if a string, check if length = 2 and smaller than int 12
|
121
|
-
# if int, transform into string with zero pad and check if smaller than int 12
|
122
|
-
|
123
|
-
|
124
|
-
is_four_digit = if value_for(attribute).is_a?(String)
|
125
|
-
value_for(attribute).length == 4
|
126
|
-
else
|
127
|
-
value_for(attribute) > 999
|
128
|
-
end
|
129
|
-
|
130
|
-
integer_value = Integer(value_for(attribute))
|
131
|
-
|
132
|
-
set_attribute(attribute, integer_value.to_s.last(2))
|
133
|
-
rescue
|
134
|
-
raise ValidationError, error_message(attribute, "Must be a 2 to 4-digit string.")
|
135
|
-
end
|
136
|
-
|
137
|
-
def zip_validation(attribute)
|
138
|
-
set_attribute(attribute, Integer(value_for(attribute)).to_s)
|
139
|
-
|
140
|
-
unless value_for(attribute).length == 5
|
141
|
-
raise ValidationError, error_message(attribute, "Must be a 5-digit string that can evaluate to a number.")
|
142
|
-
end
|
143
|
-
|
144
|
-
rescue
|
145
|
-
raise ValidationError, error_message(attribute, "Must be a 5-digit string that can evaluate to a number.")
|
146
|
-
end
|
147
|
-
|
148
|
-
def email_validation(attribute)
|
149
|
-
unless value_for(attribute) =~ EMAIL_REGEX
|
150
|
-
raise ValidationError, error_message(attribute, "Doesn't validate as an email.")
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def ip_validation(attribute)
|
155
|
-
IPAddr.new(value_for(attribute))
|
156
|
-
rescue IPAddr::InvalidAddressError
|
157
|
-
raise ValidationError, error_message(attribute, "Doesn't validate as an IP.")
|
158
|
-
end
|
159
|
-
|
160
|
-
private
|
161
|
-
def value_for(attribute)
|
162
|
-
attributes[attribute[:name]]
|
163
|
-
end
|
164
|
-
|
165
|
-
def set_attribute(attribute, value)
|
166
|
-
self.attributes[attribute[:name]] = value
|
167
|
-
end
|
168
|
-
|
169
|
-
def error_message(attribute, message)
|
170
|
-
"Attribute #{attribute[:name]} passed as: #{value_for(attribute)}. #{message}"
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|