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 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