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.
- checksums.yaml +7 -0
- data/README.md +58 -0
- data/Rakefile +6 -0
- data/app/models/kount.rb +28 -0
- data/app/models/kount/configuration.rb +13 -0
- data/app/models/kount/khash.rb +44 -0
- data/app/models/kount/ris.rb +5 -0
- data/app/models/kount/ris/address.rb +31 -0
- data/app/models/kount/ris/base.rb +17 -0
- data/app/models/kount/ris/device.rb +122 -0
- data/app/models/kount/ris/inquiry.rb +440 -0
- data/app/models/kount/ris/location.rb +28 -0
- data/app/models/kount/ris/persona.rb +26 -0
- data/app/models/kount/ris/product.rb +30 -0
- data/app/models/kount/ris/response.rb +389 -0
- data/app/models/kount/ris/trigger/counter.rb +19 -0
- data/app/models/kount/ris/trigger/error.rb +17 -0
- data/app/models/kount/ris/trigger/rule.rb +19 -0
- data/app/models/kount/ris/trigger/warning.rb +17 -0
- data/config/initializers/configuration.rb +39 -0
- data/lib/kount/ris.rb +10 -0
- data/lib/kount/ris/engine.rb +21 -0
- data/lib/kount/ris/version.rb +5 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
data/app/models/kount.rb
ADDED
@@ -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,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
|