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