paycertify 0.0.7 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 40f7fc2a6ff69e2391ef10a9d2f2339c233790ea
4
- data.tar.gz: 95161a2a11a02aa3c7b9a780c2d32b5f19aac3f7
3
+ metadata.gz: 4853d8c8ab842f2530d63b7c7880e51b0131c6cb
4
+ data.tar.gz: 65aa6d612b3c3c7154595a7d59cb5f31278f4fb9
5
5
  SHA512:
6
- metadata.gz: 00a5764ed1ede47fec3395647cb80f1a2f561e9864311de6abdc97c4771e9a2c619a8fbaaacfde9e60d627505f99383a6f9700958717e0ddae8c6d52e3265e9b
7
- data.tar.gz: f35887568dd908a566f90c7474ff79e671832e1d13c44b089c3c0ed7742ddbdad2a1744a9a4907ded979956cfa1d62d6565f578fbc0440d3ff8e77bf50350e6e
6
+ metadata.gz: accec71a0359c602880883d965b315fd65f2bb9189719ad9003e33145f3ec079c78983e6d864f6ead27f453edb42dfb4ef34c998ca416093601f3e44b7a1241e
7
+ data.tar.gz: df636c0cd6d851677d7fc2e4428be57c5b9b426c2ad1de1f86122daf6756fd9368628b3f890ca1ae4dcd6be22aa63316e0fa3a77a5976cc360e6f985bb6618dd
@@ -3,5 +3,7 @@ require 'active_support/core_ext'
3
3
 
4
4
  require 'paycertify/three_ds'
5
5
  require 'paycertify/gateway'
6
+ require 'paycertify/confirmation'
7
+ require 'paycertify/insurance'
6
8
 
7
9
  module PayCertify; end
@@ -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
@@ -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
- cattr_accessor :api_key, :mode
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 wrapper_to_gateway
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
- 'ExtData' => :metadata
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 transaction_type(transaction)
38
- { 'TransType' => transaction.type.to_s.capitalize }
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,8 @@
1
+ require_relative './base/resource'
2
+ require_relative './base/validation'
3
+
4
+ module PayCertify
5
+ class Gateway
6
+ module Base; end
7
+ end
8
+ 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('ApiToken' => api_key)
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)['Response']
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
- def success?
27
- self['status_string'] == 'approved'
28
- end
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
- attr_accessor :type, :amount, :currency, :card_number, :expiration_month, :expiration_year,
10
- :name_on_card, :cvv, :transaction_id, :billing_address, :billing_city, :billing_state, :billing_country, :billing_zip,
11
- :shipping_address, :shipping_city, :shipping_state, :shipping_country,
12
- :shipping_zip, :email, :phone, :ip, :order_description, :customer_id, :metadata
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
- def initialize(client, attributes)
15
- self.original_attributes = attributes
16
- self.client = client
16
+ attr_accessor *ATTRIBUTES
17
17
 
18
- validation.attributes.each do |key, value|
19
- self.send("#{key}=", value)
20
- end
18
+ def save!
19
+ super
20
+ self.transaction_id = response['response']['pn_ref']
21
+ self
21
22
  end
22
23
 
23
- def validation
24
- @validation ||= PayCertify::Gateway::Validation.new(original_attributes)
24
+ def success?
25
+ super && response['response']['result'] == '0'
25
26
  end
26
27
 
27
- def process
28
- client.post(
29
- path: TRANSACTION_PATH,
30
- data: attributes_to_gateway_format
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
- private
35
- def attributes_to_gateway_format
36
- {}.tap do |formatted|
37
- attribute_mapping = PayCertify::Gateway::AttributeMapping
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
- attribute_mapping.wrapper_to_gateway.each do |key, value|
40
- [value].flatten.tap do |method_chain|
41
- new_value = method_chain.map { |method_name| self.send(method_name) }.join
42
- formatted[key] = new_value
43
- end
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
- formatted.merge! attribute_mapping.expiration_date(self)
47
- formatted.merge! attribute_mapping.transaction_type(self)
71
+ ALLOWED_TYPES = %w(sale auth return void force recurring)
72
+ ALLOWED_CURRENCIES = %w(USD EUR)
48
73
 
49
- formatted
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
@@ -63,7 +63,7 @@ module PayCertify
63
63
  end
64
64
 
65
65
  class << self
66
- cattr_accessor :api_key, :api_secret, :mode
66
+ attr_accessor :api_key, :api_secret, :mode
67
67
 
68
68
  def configure(&block)
69
69
  yield self if block_given?
@@ -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 * 30;
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
- counter = counter + interval;
80
-
81
- if (counter < timeout) {
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
- var text = frameContent.body.innerHTML || frameDoc.textContent || frameDoc.innerText;
87
- var json = JSON.parse(text);
88
-
89
- var input;
82
+ var text = frameContent.body.innerHTML || frameDoc.textContent || frameDoc.innerText;
83
+ var json = JSON.parse(text);
90
84
 
91
- for(key in json) {
92
- input = document.createElement('input');
93
- input.type = 'hidden';
94
- input.name = key;
95
- input.value = json[key];
85
+ var input;
96
86
 
97
- form.appendChild(input);
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.submit();
101
- } catch(e) {
102
- return false;
93
+ form.appendChild(input);
103
94
  };
104
- } else {
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.7
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/gateway/validation.rb
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