kount-ris 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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