kount-ris 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 51d8bf9de3c132036eec7a8a69f4ecf77bccefab
4
+ data.tar.gz: 2097106f86b04f3ed9a8e28375e66ff86f4a08bd
5
+ SHA512:
6
+ metadata.gz: 031156bd644f58898a830b67ed8845390f79afb862070631099dba7e19058046273ffd0f93bbd6d82a5d3b11483ef6e4e96c95b713a3effc20a335fc1d23a8ac
7
+ data.tar.gz: a8bb0dfc941deb9a0a13abbcb9d61d17d10a40c8ef25bb7875902869b8450f2053b7e2922c77f52024c9441be5ccb4ca171e04aa2a0cb746cb6b8cc02402441f
@@ -0,0 +1,58 @@
1
+ # Kount::RIS
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/kount/ris`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'kount-ris'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install kount-ris
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/kount-ris. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
37
+ ## Code of Conduct
38
+
39
+ Everyone interacting in the Kount::RIS project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/kount-ris/blob/master/CODE_OF_CONDUCT.md).
40
+
41
+ ## To-Do
42
+
43
+ 1. cleanup configuration
44
+ - default Kount configuration options should be set by an initializer in the main application
45
+ - this could be loaded from yml or any other method perferd by the application
46
+ - some default defaults could be included in the configuration
47
+ - Kount.configuration should store/return an instance of Kount::Configuration
48
+
49
+ 2. consolidate custom validations
50
+ - validating nested/associated objects on a Inquiry or Response object is completed using custom validation methods
51
+ - these methods are all very similar and could be consolidated into some common code libraries
52
+ - this could probably be added into Kount::RIS::Base
53
+
54
+ 3. Add support for Kount.configurations
55
+ - this class method would hold a hash of named Kount::Configuration objects
56
+ - an Inquiry should have an association to a Kount::Configuration, either the default Kount.configuration, a named Kount::Configuration, or a passed Kount::Configuration object
57
+
58
+ 4. store response TIMEZONE value in Device#timezone as a DateTime offset
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,28 @@
1
+ module Kount
2
+ class << self
3
+ def configure(conf={})
4
+ Configuration.set(conf)
5
+ end
6
+
7
+ def configuration
8
+ {
9
+ merchant_id: Kount::Configuration.merchant_id,
10
+ company_server_url: Kount::Configuration.company_server_url,
11
+ api_key: Kount::Configuration.api_key,
12
+ gateway_id: Kount::Configuration.gateway_id,
13
+ site_id: Kount::Configuration.site_id,
14
+ api_version: Kount::Configuration.api_version || '0630',
15
+ response_format: Kount::Configuration.response_format,
16
+ payment_type: Kount::Configuration.payment_type,
17
+ currency_code: Kount::Configuration.currency_code,
18
+ shipping_method: Kount::Configuration.shipping_method,
19
+ logger: Kount::Configuration.logger,
20
+ simple_log_level: Kount::Configuration.simple_log_level,
21
+ simple_log_file: Kount::Configuration.simple_log_file,
22
+ simple_log_path: Kount::Configuration.simple_log_path,
23
+ salt: Kount::Configuration.salt,
24
+ api_host: Kount::Configuration.api_host,
25
+ }.compact
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ module Kount
2
+ class Configuration
3
+ mattr_accessor :merchant_id, :company_server_url, :api_key, :gateway_id, :site_id, :salt, :api_host
4
+ mattr_accessor :api_version, :response_format, :payment_type, :currency_code, :shipping_method
5
+ mattr_accessor :logger, :simple_log_level, :simple_log_file, :simple_log_path
6
+
7
+ class << self
8
+ def set(conf = {})
9
+ conf.each { |setting, value| self.send("#{setting}=", value) }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ require 'digest/sha1'
2
+
3
+ module Kount
4
+ class KHASH
5
+ PREFIX_LENGTH = 6
6
+ SUFFIX_LENGTH = 16
7
+
8
+ attr_accessor :payment_type, :salt, :merchant_id
9
+ attr_reader :original_value, :value
10
+
11
+ def initialize(attrs = {})
12
+ attrs = { value: attrs } unless attrs.is_a?(Hash)
13
+ attrs.symbolize_keys!
14
+
15
+ self.payment_type = RIS::Inquiry::PAYMENT_TYPES.key(attrs[:payment_type]) || attrs[:payment_type] || 'card'
16
+ self.salt = attrs[:salt] || Kount::Configuration.salt
17
+ self.merchant_id = attrs[:merchant_id] || Kount::Configuration.merchant_id
18
+ self.original_value = attrs[:value]
19
+ end
20
+
21
+ def original_value=(value)
22
+ @original_value = value.to_s
23
+ hash_value!
24
+ @original_value
25
+ end
26
+
27
+ def to_s
28
+ @value
29
+ end
30
+
31
+ private
32
+
33
+ def hash_value!
34
+ prefix = payment_type.to_s.downcase == 'card' ? original_value.first(PREFIX_LENGTH) : merchant_id.to_s
35
+
36
+ character_array = (0...36).map{ |i| i.to_s(36).upcase }.join
37
+ digest = Digest::SHA1.hexdigest("#{original_value}.#{salt}")
38
+ suffix = SUFFIX_LENGTH.times.collect { |i| character_array[digest[(i*2)..((i*2)+6)].to_i(16) % 36] }.join
39
+ @value = prefix.concat(suffix)
40
+ end
41
+ end
42
+ end
43
+
44
+
@@ -0,0 +1,5 @@
1
+ module Kount
2
+ module RIS
3
+
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ module Kount
2
+ module RIS
3
+ class Address < Base
4
+ attr_accessor :address_1, :address_2, :premise, :thoroughfare, :city, :region, :postal_code, :country_code, :phone
5
+
6
+ validates :address_1, length: { maximum: 256 } # address line 1
7
+ validates :address_2, length: { maximum: 256 } # address line 2
8
+ validates :premise, length: { maximum: 256 } # Premise (used in EU addresses)
9
+ validates :thoroughfare, length: { maximum: 256 } # Thoroughfare (used in EU addresses)
10
+ validates :city, length: { maximum: 256 } # country code
11
+ validates :region, length: { maximum: 256 } # state / region / province
12
+ validates :postal_code, length: { maximum: 20 } # postal code
13
+ validates :country_code, length: 2 # country code
14
+ validates :phone, length: { maximum: 32 } # phone number
15
+
16
+ def to_h
17
+ {
18
+ address_1: address_1,
19
+ address_2: address_2,
20
+ premise: premise,
21
+ thoroughfare: thoroughfare,
22
+ city: city,
23
+ region: region,
24
+ postal_code: postal_code,
25
+ country_code: country_code,
26
+ phone: phone,
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module Kount
2
+ module RIS
3
+ class Base
4
+ include ActiveModel::Model
5
+
6
+ COUNTRY_CODES = %w[AF AX AL DZ AS AD AO AI AQ AG AR AM AW AU AT AZ BS BH BD BB BY BE BZ
7
+ BJ BM BT BO BA BW BV BR VG IO BN BG BF BI KH CM CA CV KY CF TD CL CN HK MO CX CC CO
8
+ KM CG CD CK CR CI HR CU CY CZ DK DJ DM DO EC EG SV GQ ER EE ET FK FO FJ FI FR GF PF
9
+ TF GA GM GE DE GH GI GR GL GD GP GU GT GG GN GW GY HT HM VA HN HU IS IN ID IR IQ IE
10
+ IM IL IT JM JP JE JO KZ KE KI KP KR KW KG LA LV LB LS LR LY LI LT LU MK MG MW MY MV
11
+ ML MT MH MQ MR MU YT MX FM MD MC MN ME MS MA MZ MM NA NR NP NL AN NC NZ NI NE NG NU
12
+ NF MP NO OM PK PW PS PA PG PY PE PH PN PL PT PR QA RE RO RU RW BL SH KN LC MF PM VC
13
+ WS SM ST SA SN RS SC SL SG SK SI SB SO ZA GS SS ES LK SD SR SJ SZ SE CH SY TW TJ TZ
14
+ TH TL TG TK TO TT TN TR TM TC TV UG UA AE GB US UM UY UZ VU VE VN VI WF EH YE ZM ZW]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,122 @@
1
+ module Kount
2
+ module RIS
3
+ class Device < Base
4
+ attr_accessor :browser, :cookies_enabled, :first_seen_at, :layers, :screen_resolution, :fingerprint,
5
+ :flash_enabled, :configured_country_code, :javascript_enabled, :captured, :language_country_code,
6
+ :localtime, :using_forwarder, :mobile_type, :operating_system, :remote_access, :configured_region,
7
+ :timezone, :user_agent, :voice_enabled, :region, :country_code
8
+
9
+ alias_method :cookies_enabled?, :cookies_enabled
10
+ alias_method :flash_enabled?, :flash_enabled
11
+ alias_method :javascript_enabled?, :javascript_enabled
12
+ alias_method :captured?, :captured
13
+ alias_method :using_forwarder?, :using_forwarder
14
+ alias_method :remote_access?, :remote_access
15
+ alias_method :voice_enabled?, :voice_enabled
16
+
17
+ NETWORK_TYPES = {
18
+ 'a' => 'Anonymous',
19
+ 'h' => 'High School',
20
+ 'l' => 'Library',
21
+ 'n' => 'Normal',
22
+ 'o' => 'Open Proxy',
23
+ 'p' => 'Prison',
24
+ 's' => 'Satellite',
25
+ }
26
+
27
+ class << self
28
+ def network_types
29
+ NETWORK_TYPES
30
+ end
31
+ end
32
+
33
+ def network_types
34
+ self.class.network_types
35
+ end
36
+
37
+ def network_type=(value)
38
+ return @network_type = nil if value.nil?
39
+
40
+ network_type_value = network_types.detect { |k,v| k if v.downcase == value.to_s.downcase }&.first || value.to_s.downcase
41
+ raise ArgumentError, "'#{value}' is not a valid network type" unless network_types.include?(network_type_value)
42
+ @network_type = network_type_value
43
+ end
44
+
45
+ def network_type
46
+ network_types[@network_type]
47
+ end
48
+
49
+ validates :browser, length: { maximum: 64 } # web browser name
50
+ validates :cookies_enabled, inclusion: { in: [true, false] }, allow_nil: true # browser has cookies enabled
51
+ validates :layers, length: { maximum: 55 } # string built from device operating system, browser, javascript, cookie, & flash settings
52
+ validates :screen_resolution, length: { maximum: 10 } # device screen resolution
53
+ validates :fingerprint, length: { maximum: 32 } # device uuid
54
+ validates :flash_enabled, inclusion: { in: [true, false] }, allow_nil: true # browser has flash installed/enabled
55
+ validates :configured_country_code, length: { maximum: 2 } # country configured for the device
56
+ validates :javascript_enabled, inclusion: { in: [true, false] }, allow_nil: true # browser has javascript enabled
57
+ validates :captured, inclusion: { in: [true, false] }, allow_nil: true # was device data captured?
58
+ validates :language_country_code, length: { maximum: 2 } # device configured language
59
+ validates :using_forwarder, inclusion: { in: [true, false] }, allow_nil: true # If device is mobile, is it using a forwarder to process the carrier’s service
60
+ validates :mobile_type, length: { maximum: 32 } # mobile device name
61
+ validates :network_type, inclusion: { in: network_types.values }, allow_nil: true # network type
62
+ validates :operating_system, length: { maximum: 64 } # operating system
63
+ validates :remote_access, inclusion: { in: [true, false] }, allow_nil: true # is the device accessable remotely
64
+ validates :configured_region, length: { maximum: 64 } # device configured region
65
+ validates :timezone, length: { maximum: 6 } # device configured timezone
66
+ validates :user_agent, length: { maximum: 1024 } # browser user agent
67
+ validates :voice_enabled, inclusion: { in: [true, false] }, allow_nil: true # is the device voice enabled
68
+ validates :region, length: { maximum: 64 } # device location region
69
+ validates :country_code, length: { maximum: 2 } # iso country code where the device is located
70
+
71
+ validate :first_seen_at_is_date, if: lambda { |obj| obj.first_seen_at.present? } # date the device was first seen
72
+ validate :localtime_is_datetime, if: lambda { |obj| obj.localtime.present? } # device localtime
73
+
74
+ def mobile?
75
+ mobile_type.present?
76
+ end
77
+
78
+ def to_h
79
+ {
80
+ browser: browser,
81
+ cookies_enabled: cookies_enabled,
82
+ first_seen_at: first_seen_at,
83
+ layers: layers,
84
+ screen_resolution: screen_resolution,
85
+ fingerprint: fingerprint,
86
+ flash_enabled: flash_enabled,
87
+ configured_country_code: configured_country_code,
88
+ javascript_enabled: javascript_enabled,
89
+ captured: captured,
90
+ language_country_code: language_country_code,
91
+ localtime: localtime,
92
+ using_forwarder: using_forwarder,
93
+ mobile_type: mobile_type,
94
+ operating_system: operating_system,
95
+ remote_access: remote_access,
96
+ configured_region: configured_region,
97
+ timezone: timezone,
98
+ user_agent: user_agent,
99
+ voice_enabled: voice_enabled,
100
+ region: region,
101
+ country_code: country_code,
102
+ }
103
+ end
104
+
105
+ private
106
+
107
+ def first_seen_at_is_date
108
+ self.first_seen_at = first_seen_at.to_date if first_seen_at.is_a?(Date) || first_seen_at.is_a?(DateTime)
109
+
110
+ errors.add(:first_seen_at, 'must be a Date object') unless first_seen_at.is_a?(Date)
111
+ end
112
+
113
+ def localtime_is_datetime
114
+ self.localtime = localtime.to_datetime if localtime.is_a?(Date) || localtime.is_a?(Time)
115
+
116
+ errors.add(:localtime, 'must be a DateTime object') unless localtime.is_a?(DateTime)
117
+ end
118
+
119
+ end
120
+ end
121
+ end
122
+
@@ -0,0 +1,440 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module Kount
5
+ module RIS
6
+ class Inquiry < Base
7
+ attr_accessor :id, :authorized, :email, :ip_address, :acknowledged, :merchant_id, :products, :session_id,
8
+ :site_id, :total, :kount_id, :api_version, :gateway_id, :avs_code, :shipping_address, :customer_id,
9
+ :billing_address, :recipient_email, :recipient_name, :last_four, :amount, :cvv_code, :order_number,
10
+ :shipping_method, :birthdate, :account_created_at, :user_agent, :name, :api_host, :card_type, :api_key
11
+ attr_reader :mode, :response_format, :gender, :response, :card_number
12
+
13
+ alias_method :authorized?, :authorized
14
+ alias_method :acknowledged?, :acknowledged
15
+
16
+ PAYMENT_TYPES = {
17
+ 'apay' => 'Apple Pay',
18
+ 'card' => 'Credit Card',
19
+ 'pypl' => 'PayPal',
20
+ 'chek' => 'Check',
21
+ 'none' => 'None',
22
+ 'gdmp' => 'Green Dot Money Pack',
23
+ 'goog' => 'Google Checkout',
24
+ 'blml' => 'Bill Me Later',
25
+ 'gift' => 'Gift Card',
26
+ 'bpay' => 'BPAY',
27
+ 'netellar' => 'Netellar',
28
+ 'giropay' => 'GiroPay',
29
+ 'elv' => 'ELV',
30
+ 'mercade_pago' => 'Mercade Pago',
31
+ 'sepa' => 'Single Euro Payments Area',
32
+ 'interac' => 'Interac',
33
+ 'poli' => 'POLi',
34
+ 'skrill' => 'Skrill/Moneybookers',
35
+ 'sofort' => 'Sofort',
36
+ 'token' => 'Token',
37
+ }.freeze
38
+ SHIPPING_METHODS = {
39
+ 'sd' => 'Same Day',
40
+ 'nd' => 'Next Day',
41
+ '2d' => 'Second Day',
42
+ 'st' => 'Standard',
43
+ }.freeze
44
+ MODES = %w[q u p x e j w].freeze
45
+ RESPONSE_FORMATS = %w[sdk xml json yaml].freeze
46
+ GENDERS = %w[male female].freeze
47
+ CARD_TYPES = %w[visa mastercard amex discover]
48
+
49
+ class << self
50
+ def currency_codes
51
+ Kount::RIS::Base::COUNTRY_CODES
52
+ end
53
+
54
+ def payment_types
55
+ PAYMENT_TYPES
56
+ end
57
+
58
+ def shipping_methods
59
+ SHIPPING_METHODS
60
+ end
61
+
62
+ def modes
63
+ MODES
64
+ end
65
+
66
+ def response_formats
67
+ RESPONSE_FORMATS
68
+ end
69
+
70
+ def genders
71
+ GENDERS
72
+ end
73
+
74
+ def card_types
75
+ CARD_TYPES
76
+ end
77
+ end
78
+
79
+ def currency_codes
80
+ self.class.currency_codes
81
+ end
82
+
83
+ def payment_types
84
+ self.class.payment_types
85
+ end
86
+
87
+ def shipping_methods
88
+ self.class.shipping_methods
89
+ end
90
+
91
+ def modes
92
+ self.class.modes
93
+ end
94
+
95
+ def response_formats
96
+ self.class.response_formats
97
+ end
98
+
99
+ def genders
100
+ self.class.genders
101
+ end
102
+
103
+ def card_types
104
+ self.class.card_types
105
+ end
106
+
107
+ validates :id, presence: { if: lambda { |obj| obj.mode?(:p) } }, length: { maximum: 32 } # automatic number identification
108
+ validates :authorized, presence: { if: lambda { |obj| obj.mode?(:p, :q) } }, inclusion: { in: [true, false] } # authorization status
109
+ validates :currency_code, inclusion: { in: currency_codes }, presence: { if: lambda { |obj| obj.mode?(:p, :q) } } # Country of currency submitted on order
110
+ validates :email, presence: { if: lambda { |obj| obj.mode?(:q) } }, length: { maximum: 64 } # customer email address
111
+ validates :ip_address, presence: { if: lambda { |obj| obj.mode?(:p, :q) } }, length: { maximum: 16 } # customer ip address
112
+ validates :acknowledged, presence: { if: lambda { |obj| obj.mode?(:p, :q) } }, inclusion: { in: [true, false] } # Merchants acknowledgement to ship/process the order
113
+ validates :merchant_id, presence: { if: lambda { |obj| obj.mode?(:p, :q, :u, :x) } }, length: { maximum: 6 } # Merchant ID assigned to the merchant by Kount
114
+ validates :mode, inclusion: { in: modes }, presence: true # RIS inquiry mode
115
+ validates :payment_type, inclusion: { in: payment_types.values }, presence: { if: lambda { |obj| obj.mode?(:p, :q) } }
116
+ validates :session_id, presence: { if: lambda { |obj| obj.mode?(:p, :q, :u, :x) } }, length: { maximum: 32 } # GUID session id
117
+ validates :site_id, presence: { if: lambda { |obj| obj.mode?(:p, :q) } }, length: { maximum: 8 } # website identifier configured in Web Agent Console
118
+ validates :total, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true # products total, automatically calculated if not provided
119
+ validates :kount_id, presence: { if: lambda { |obj| obj.mode?(:u, :x) } } # kount issued transaction id, required if updating
120
+ validates :api_version, presence: { if: lambda { |obj| obj.mode?(:p, :q, :u, :x) } }, length: { maximum: 4 } # api version
121
+ validates :gateway_id, presence: { if: lambda { |obj| obj.mode?(:j, :w) } }, length: { maximum: 32 } # used only for Kount Central Merchants
122
+ validates :recipient_email, length: { maximum: 64 } # Recipient Email Address
123
+ validates :recipient_name, length: { maximum: 64 } # Recipient Name
124
+ validates :shipping_method, inclusion: { in: shipping_methods.values }, allow_nil: true # Shipping Method
125
+ validates :customer_id, length: { maximum: 32 } # unique customer identifier
126
+ validates :user_agent, length: { maximum: 1024 } # HTTP header user agent
127
+ validates :amount, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true # transaction amount
128
+ validates :response_format, inclusion: { in: response_formats }, presence: true # response_format
129
+ validates :last_four, format: { with: /\A[0-9]{4}\z/ }, length: { is: 4 }, allow_nil: true
130
+ validates :name, length: { maximum: 64 } # cardholder's name
131
+ validates :gender, inclusion: { in: genders }, allow_nil: true # gender
132
+ validates :order_number, length: { maximum: 32 } # cardholder's name
133
+ validates :card_type, inclusion: { in: card_types }, allow_nil: true # Shipping Method
134
+ validate :products_are_valid
135
+ validate :shipping_address_is_valid, if: lambda { |obj| obj.shipping_address.present? }
136
+ validate :billing_address_is_valid, if: lambda { |obj| obj.billing_address.present? }
137
+ validate :birthdate_is_date, if: lambda { |obj| obj.birthdate.present? }
138
+ validate :account_created_at_is_datetime, if: lambda { |obj| obj.account_created_at.present? }
139
+ validate :real_world_ip_address, unless: lambda { |obj| obj.mode?(:p) }
140
+
141
+ def currency_code=(value)
142
+ @currency_code = value.to_s.downcase
143
+ end
144
+
145
+ def currency_code
146
+ @currency_code.upcase if @currency_code
147
+ end
148
+
149
+ def payment_type=(value)
150
+ return @payment_type = nil if value.nil?
151
+
152
+ payment_type_value = payment_types.detect { |k,v| k if v.downcase == value.to_s.downcase }&.first || value.to_s.downcase
153
+ raise ArgumentError, "'#{value}' is not a valid payment type" unless payment_types.include?(payment_type_value)
154
+ @payment_type = payment_type_value
155
+ end
156
+
157
+ def payment_type
158
+ payment_types[@payment_type]
159
+ end
160
+
161
+ def shipping_method=(value)
162
+ return @shipping_method = nil if value.nil?
163
+
164
+ shipping_method_value = shipping_methods.detect { |k,v| k if v.downcase == value.to_s.downcase }&.first || value.to_s.downcase
165
+ raise ArgumentError, "'#{value}' is not a valid shipping method" unless shipping_methods.include?(shipping_method_value)
166
+ @shipping_method = shipping_method_value
167
+ end
168
+
169
+ def shipping_method
170
+ shipping_methods[@shipping_method]
171
+ end
172
+
173
+ def mode=(value)
174
+ return @mode = nil if value.nil?
175
+
176
+ mode_value = value.to_s.downcase
177
+ raise ArgumentError, "'#{value}' is not a valid mode" unless modes.include?(mode_value)
178
+ @mode = mode_value
179
+ end
180
+
181
+ def response_format=(value)
182
+ return @response_format = nil if value.nil?
183
+
184
+ response_format_value = value.to_s.downcase
185
+ raise ArgumentError, "'#{value}' is not a valid response format" unless response_formats.include?(response_format_value)
186
+ @response_format = response_format_value
187
+ end
188
+
189
+ def gender=(value)
190
+ return @gender = nil if value.nil?
191
+
192
+ gender_value = value.to_s.downcase
193
+ raise ArgumentError, "'#{value}' is not a valid gender" unless genders.include?(gender_value)
194
+ @gender = gender_value
195
+ end
196
+
197
+ def card_number=(value)
198
+ @card_number = value.to_s
199
+ @last_four = value.to_s.last(4)
200
+ @card_type = case value.to_s.first
201
+ when '3' then 'amex'
202
+ when '4' then 'visa'
203
+ when '5' then 'mastercard'
204
+ when '6' then 'discover'
205
+ end
206
+ end
207
+
208
+ def initialize(attrs = {})
209
+ attrs.symbolize_keys!
210
+ attrs.reverse_merge!(Kount.configuration.except(:company_server_url, :salt))
211
+ attrs.reverse_merge!({
212
+ authorized: true,
213
+ acknowledged: true,
214
+ email: 'noemail@kount.com',
215
+ ip_address: '10.0.0.1',
216
+ mode: :q,
217
+ })
218
+ if attrs[:mode] == 'p'
219
+ attrs.reverse_merge!({ id: '0123456789' })
220
+ attrs.merge!({ ip_address: '10.0.0.1' })
221
+ end
222
+
223
+ super(attrs.except(:products_attributes, :shipping_address_attributes, :billing_address_attributes))
224
+
225
+ self.products = Array(attrs[:products_attributes]).collect { |product_attributes| Product.new(product_attributes) }
226
+ self.shipping_address = Address.new(attrs[:shipping_address_attributes]) if attrs.include?(:shipping_address_attributes)
227
+ self.billing_address = Address.new(attrs[:billing_address_attributes]) if attrs.include?(:billing_address_attributes)
228
+
229
+ self
230
+ end
231
+
232
+ def payment_token
233
+ card_number =~ /x/i ? card_number : KHASH.new(card_number).to_s
234
+ end
235
+
236
+ def avs_match_street
237
+ {
238
+ a: { visa: true, mastercard: true, discover: true, amex: true },
239
+ b: { visa: true },
240
+ d: { visa: true },
241
+ f: { visa: true, amex: true },
242
+ m: { visa: true, amex: true },
243
+ n: { visa: false, mastercard: false, discover: false, amex: false },
244
+ o: { amex: true },
245
+ t: { discover: false },
246
+ w: { visa: false, mastercard: false, discover: false, amex: false },
247
+ x: { visa: true, mastercard: true, discover: true },
248
+ y: { visa: true, mastercard: true, discover: true, amex: true },
249
+ z: { visa: false, mastercard: false, discover: false, amex: false },
250
+ }.dig(avs_code.to_s.downcase.to_sym, card_type.to_sym) if avs_code.present? && card_type.present?
251
+ end
252
+ alias_method :avs_match_street?, :avs_match_street
253
+
254
+ def avs_match_zip
255
+ {
256
+ a: { visa: false, mastercard: true, discover: false, amex: true },
257
+ d: { visa: true },
258
+ f: { visa: true },
259
+ l: { amex: true },
260
+ m: { visa: true, amex: true },
261
+ n: { visa: false, mastercard: false, discover: false, amex: false },
262
+ p: { visa: true },
263
+ t: { discover: true },
264
+ w: { visa: true, mastercard: true, discover: true, amex: false },
265
+ x: { visa: true, mastercard: true, discover: true },
266
+ y: { visa: true, mastercard: true, discover: true, amex: true },
267
+ z: { visa: true, mastercard: true, discover: true, amex: true },
268
+ }.dig(avs_code.to_s.downcase.to_sym, card_type.to_sym) if avs_code.present? && card_type.present?
269
+ end
270
+ alias_method :avs_match_zip?, :avs_match_zip
271
+
272
+ def cvv_match
273
+ case cvv_code.to_s.to_sym
274
+ when :m, :y, :'0' then true
275
+ when :n, :'1' then false
276
+ end if cvv_code.present?
277
+ end
278
+ alias_method :cvv_match?, :cvv_match
279
+
280
+ def mode?(*modes)
281
+ Array(modes).collect(&:to_s).include? mode
282
+ end
283
+
284
+ def to_query_params
285
+ query_params = {
286
+ ANID: id,
287
+ AUTH: ((authorized? ? 'A' : 'D') unless authorized.nil?),
288
+ CURR: currency_code,
289
+ EMAL: email,
290
+ IPAD: ip_address,
291
+ MACK: ('Y' if acknowledged?),
292
+ MERC: merchant_id,
293
+ MODE: (mode.to_s.upcase unless mode.nil?),
294
+ PROD_DESC: products.collect(&:description),
295
+ PROD_ITEM: products.collect(&:id),
296
+ PROD_PRICE: products.collect(&:price),
297
+ PROD_QUANT: products.collect(&:quantity),
298
+ PROD_TYPE: products.collect(&:category),
299
+ PTOK: payment_token,
300
+ PTYP: @payment_type,
301
+ SESS: session_id,
302
+ SITE: site_id,
303
+ TOTL: (total || products.sum { |product| product.price * product.quantity }),
304
+ TRAN: kount_id,
305
+ VERS: api_version,
306
+ CUSTOMER_ID: gateway_id,
307
+ AVST: (avs_match_street.nil? ? 'X' : (avs_match_street? ? 'M' : 'N')),
308
+ AVSZ: (avs_match_zip.nil? ? 'X' : (avs_match_zip? ? 'M' : 'N')),
309
+ CASH: amount,
310
+ CVVR: (cvv_match.nil? ? 'X' : (cvv_match? ? 'M' : 'N')),
311
+ S2EM: recipient_email,
312
+ S2NM: recipient_name,
313
+ SHTP: @shipping_method,
314
+ UNIQ: customer_id,
315
+ UAGT: user_agent,
316
+ DOB: (birthdate.strftime('%Y-%m-%d') unless birthdate.nil?),
317
+ EPOC: (account_created_at.to_i unless account_created_at.nil?),
318
+ FRMT: (response_format.to_s.upcase unless response_format.nil?),
319
+ GENDER: gender,
320
+ LAST4: last_four,
321
+ NAME: name,
322
+ ORDR: order_number,
323
+ }
324
+
325
+ query_params.merge!({
326
+ B2A1: shipping_address.address_1,
327
+ B2A2: shipping_address.address_2,
328
+ B2CC: shipping_address.country_code,
329
+ B2CI: shipping_address.city,
330
+ B2PC: shipping_address.postal_code,
331
+ B2PN: shipping_address.phone,
332
+ B2ST: shipping_address.region,
333
+ BPREMISE: shipping_address.premise,
334
+ BSTREET: shipping_address.thoroughfare,
335
+ }) if shipping_address.present?
336
+
337
+ query_params.merge!({
338
+ S2A1: billing_address.address_1,
339
+ S2A2: billing_address.address_2,
340
+ S2CC: billing_address.country_code,
341
+ S2CI: billing_address.city,
342
+ S2PC: billing_address.postal_code,
343
+ S2PN: billing_address.phone,
344
+ S2ST: billing_address.region,
345
+ SPREMISE: billing_address.premise,
346
+ SSTREET: billing_address.thoroughfare,
347
+ }) if billing_address.present?
348
+
349
+ # validate total length of URL including domain - 4k limit
350
+
351
+ query_params.compact!
352
+ end
353
+
354
+ def to_query
355
+ to_query_params.to_query
356
+ end
357
+
358
+ def submit!
359
+ uri = URI.parse("https://#{api_host}")
360
+ http = Net::HTTP.new(uri.host, uri.port)
361
+ http.use_ssl = true
362
+
363
+ request = Net::HTTP::Post.new(uri.request_uri)
364
+ request["X-Kount-Api-Key"] = api_key
365
+ request.set_form_data(to_query_params)
366
+
367
+ response = http.request(request)
368
+
369
+ response_params = response.body.split("\n").collect { |params| Rack::Utils.parse_nested_query(params) }.reduce({}, :merge)
370
+
371
+ @response = Response.new(response_params)
372
+ end
373
+
374
+ private
375
+
376
+ def products_are_valid
377
+ unless products.is_a?(Array)
378
+ errors.add(:products, 'must be an Array of Product objects')
379
+ return false
380
+ end
381
+
382
+ if products.none? && mode?(:p, :q)
383
+ errors.add(:products, 'can\'t be empty')
384
+ return false
385
+ end
386
+
387
+ products.each.with_index(1) do |product, i|
388
+ unless product.valid?
389
+ product.errors.full_messages.each do |error|
390
+ errors.add(:products, "element #{i} #{error}")
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ def shipping_address_is_valid
397
+ unless shipping_address.is_a?(Address)
398
+ errors.add(:shipping_address, 'must be an Address object')
399
+ return false
400
+ end
401
+
402
+ unless shipping_address.valid?
403
+ shipping_address.errors.full_messages.each do |error|
404
+ errors.add(:shipping_address, error)
405
+ end
406
+ end
407
+ end
408
+
409
+ def billing_address_is_valid
410
+ unless billing_address.is_a?(Address)
411
+ errors.add(:billing_address, 'must be an Address object')
412
+ return false
413
+ end
414
+
415
+ unless billing_address.valid?
416
+ billing_address.errors.full_messages.each do |error|
417
+ errors.add(:billing_address, error)
418
+ end
419
+ end
420
+ end
421
+
422
+ def birthdate_is_date
423
+ self.birthdate = birthdate.to_date if birthdate.is_a?(Date) || birthdate.is_a?(DateTime)
424
+
425
+ errors.add(:birthdate, 'must be a Date object') unless birthdate.is_a?(Date)
426
+ end
427
+
428
+ def account_created_at_is_datetime
429
+ self.account_created_at = account_created_at.to_datetime if account_created_at.is_a?(Date) || account_created_at.is_a?(Time)
430
+
431
+ errors.add(:account_created_at, 'must be a DateTime object') unless account_created_at.is_a?(DateTime)
432
+ end
433
+
434
+ def real_world_ip_address
435
+ errors.add(:ip_address, 'may not be in a private scope') if ip_address.to_s =~ /\A(10|172|192)\./
436
+ end
437
+
438
+ end
439
+ end
440
+ end