minfraud 1.0.4 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +11 -0
  3. data/.github/workflows/release.yml +28 -0
  4. data/.github/workflows/rubocop.yml +18 -0
  5. data/.github/workflows/test.yml +36 -0
  6. data/.gitignore +2 -0
  7. data/.rubocop.yml +90 -0
  8. data/CHANGELOG.md +197 -4
  9. data/CODE_OF_CONDUCT.md +4 -4
  10. data/Gemfile +2 -2
  11. data/LICENSE.txt +2 -1
  12. data/README.dev.md +4 -0
  13. data/README.md +255 -58
  14. data/Rakefile +9 -3
  15. data/bin/console +4 -3
  16. data/dev-bin/release.sh +59 -0
  17. data/lib/minfraud/assessments.rb +127 -53
  18. data/lib/minfraud/components/account.rb +31 -9
  19. data/lib/minfraud/components/addressable.rb +73 -26
  20. data/lib/minfraud/components/base.rb +26 -14
  21. data/lib/minfraud/components/billing.rb +5 -0
  22. data/lib/minfraud/components/credit_card.rb +117 -29
  23. data/lib/minfraud/components/custom_inputs.rb +25 -0
  24. data/lib/minfraud/components/device.rb +51 -10
  25. data/lib/minfraud/components/email.rb +120 -9
  26. data/lib/minfraud/components/event.rb +60 -13
  27. data/lib/minfraud/components/order.rb +59 -22
  28. data/lib/minfraud/components/payment.rb +192 -22
  29. data/lib/minfraud/components/report/transaction.rb +80 -0
  30. data/lib/minfraud/components/shipping.rb +15 -6
  31. data/lib/minfraud/components/shopping_cart.rb +19 -12
  32. data/lib/minfraud/components/shopping_cart_item.rb +42 -13
  33. data/lib/minfraud/enum.rb +22 -8
  34. data/lib/minfraud/error_handler.rb +45 -18
  35. data/lib/minfraud/errors.rb +22 -2
  36. data/lib/minfraud/http_service/response.rb +61 -17
  37. data/lib/minfraud/model/abstract.rb +20 -0
  38. data/lib/minfraud/model/address.rb +52 -0
  39. data/lib/minfraud/model/billing_address.rb +11 -0
  40. data/lib/minfraud/model/credit_card.rb +75 -0
  41. data/lib/minfraud/model/device.rb +51 -0
  42. data/lib/minfraud/model/disposition.rb +42 -0
  43. data/lib/minfraud/model/email.rb +54 -0
  44. data/lib/minfraud/model/email_domain.rb +24 -0
  45. data/lib/minfraud/model/error.rb +28 -0
  46. data/lib/minfraud/model/factors.rb +24 -0
  47. data/lib/minfraud/model/geoip2_location.rb +25 -0
  48. data/lib/minfraud/model/insights.rb +68 -0
  49. data/lib/minfraud/model/ip_address.rb +58 -0
  50. data/lib/minfraud/model/ip_risk_reason.rb +48 -0
  51. data/lib/minfraud/model/issuer.rb +49 -0
  52. data/lib/minfraud/model/score.rb +76 -0
  53. data/lib/minfraud/model/score_ip_address.rb +23 -0
  54. data/lib/minfraud/model/shipping_address.rb +30 -0
  55. data/lib/minfraud/model/subscores.rb +156 -0
  56. data/lib/minfraud/model/warning.rb +63 -0
  57. data/lib/minfraud/report.rb +66 -0
  58. data/lib/minfraud/resolver.rb +25 -16
  59. data/lib/minfraud/validates.rb +187 -0
  60. data/lib/minfraud/version.rb +4 -1
  61. data/lib/minfraud.rb +55 -16
  62. data/minfraud.gemspec +27 -18
  63. metadata +107 -36
  64. data/.travis.yml +0 -5
  65. data/lib/minfraud/http_service/request.rb +0 -37
  66. data/lib/minfraud/http_service.rb +0 -31
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minfraud/model/abstract'
4
+
5
+ module Minfraud
6
+ module Model
7
+ # Warning about the minFraud request.
8
+ #
9
+ # Although more codes may be added in the future, the current warning codes
10
+ # are:
11
+ #
12
+ # * BILLING_CITY_NOT_FOUND - the billing city could not be found in our
13
+ # database.
14
+ # * BILLING_COUNTRY_MISSING - billing address information was provided
15
+ # without providing a billing country.
16
+ # * BILLING_COUNTRY_NOT_FOUND - the billing country could not be found in
17
+ # our database.
18
+ # * BILLING_POSTAL_NOT_FOUND - the billing postal could not be found in our
19
+ # database.
20
+ # * INPUT_INVALID - the value associated with the key does not meet the
21
+ # required constraints, e.g., "United States" in a field that requires a
22
+ # two-letter country code.
23
+ # * INPUT_UNKNOWN - an unknown key was encountered in the request body.
24
+ # * IP_ADDRESS_NOT_FOUND - the IP address could not be geolocated.
25
+ # * SHIPPING_COUNTRY_MISSING - shipping address information was provided
26
+ # without providing a shipping country.
27
+ # * SHIPPING_CITY_NOT_FOUND - the shipping city could not be found in our
28
+ # database.
29
+ # * SHIPPING_COUNTRY_NOT_FOUND - the shipping country could not be found in
30
+ # our database.
31
+ # * SHIPPING_POSTAL_NOT_FOUND - the shipping postal could not be found in
32
+ # our database.
33
+ class Warning < Abstract
34
+ # This value is a machine-readable code identifying the warning.
35
+ #
36
+ # @return [String]
37
+ attr_reader :code
38
+
39
+ # This property provides a human-readable explanation of the warning. The
40
+ # description may change at any time and should not be matched against.
41
+ #
42
+ # @return [String]
43
+ attr_reader :warning
44
+
45
+ # A JSON Pointer to the input field that the warning is associated with.
46
+ # For instance, if the warning was about the billing city, this would be
47
+ # '/billing/city'. If it was for the price in the second shopping cart
48
+ # item, it would be '/shopping_cart/1/price'.
49
+ #
50
+ # @return [String, nil]
51
+ attr_reader :input_pointer
52
+
53
+ # @!visibility private
54
+ def initialize(record)
55
+ super(record)
56
+
57
+ @code = get('code')
58
+ @warning = get('warning')
59
+ @input_pointer = get('input_pointer')
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minfraud
4
+ # Report is used to perform minFraud Report Transaction API requests.
5
+ #
6
+ # @see https://dev.maxmind.com/minfraud/report-a-transaction?lang=en
7
+ class Report
8
+ # The Report::Transaction component.
9
+ #
10
+ # @return [Minfraud::Components::Report::Transaction, nil]
11
+ attr_accessor :transaction
12
+
13
+ # @param params [Hash] Hash of parameters. The only supported key is
14
+ # +:transaction+, which should have a
15
+ # +Minfraud::Components::Report::Transaction+ as its value.
16
+ def initialize(params = {})
17
+ @transaction = params[:transaction]
18
+ end
19
+
20
+ # Perform a request to the minFraud Report Transaction API.
21
+ #
22
+ # @return [nil]
23
+ #
24
+ # @raise [JSON::ParserError] if there was invalid JSON in the response.
25
+ #
26
+ # @raise [Minfraud::AuthorizationError] If there was an authentication
27
+ # problem.
28
+ #
29
+ # @raise [Minfraud::ClientError] If there was a critical problem with one
30
+ # of your inputs.
31
+ #
32
+ # @raise [Minfraud::ServerError] If the server reported an error of some
33
+ # kind.
34
+ def report_transaction
35
+ response = nil
36
+ body = nil
37
+ Minfraud.connection_pool.with do |client|
38
+ response = client.post(
39
+ '/minfraud/v2.0/transactions/report',
40
+ json: @transaction.to_json,
41
+ )
42
+
43
+ body = response.to_s
44
+ end
45
+
46
+ endpoint = nil
47
+ locales = nil
48
+ response = ::Minfraud::HTTPService::Response.new(
49
+ endpoint,
50
+ locales,
51
+ response,
52
+ body,
53
+ )
54
+
55
+ ::Minfraud::ErrorHandler.examine(response)
56
+
57
+ nil
58
+ end
59
+
60
+ private
61
+
62
+ def request
63
+ @request ||= Request.new(::Minfraud::HTTPService.configuration)
64
+ end
65
+ end
66
+ end
@@ -1,10 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Minfraud
4
+ # Resolver provides functionality for setting component attributes.
2
5
  module Resolver
3
6
  class << self
4
- # @param [Object] context an object for variable assignment
5
- # @param [Hash] params a hash of parameters
6
- # @return [Array] a list of supplied params
7
- # @note Raises RequestFormatError once unpermitted key is met
7
+ # Set keys on the context based on the provided parameters.
8
+ #
9
+ # @param context [Object] An object for variable assignment.
10
+ #
11
+ # @param params [Hash] A hash of parameters.
12
+ #
13
+ # @return [Array]
14
+ #
15
+ # @raise [Minfraud::RequestFormatError] If an unexpected key is found.
8
16
  def assign(context, params)
9
17
  Array(params).each do |key, value|
10
18
  raise RequestFormatError, "#{key} does not belong to request document format" unless MAPPING[key]
@@ -15,18 +23,19 @@ module Minfraud
15
23
  end
16
24
  end
17
25
 
18
- # Mapping between components & minFraud request keys
26
+ # @!visibility private
19
27
  MAPPING = {
20
- account: ::Minfraud::Components::Account,
21
- billing: ::Minfraud::Components::Billing,
22
- credit_card: ::Minfraud::Components::CreditCard,
23
- device: ::Minfraud::Components::Device,
24
- email: ::Minfraud::Components::Email,
25
- event: ::Minfraud::Components::Event,
26
- order: ::Minfraud::Components::Order,
27
- payment: ::Minfraud::Components::Payment,
28
- shipping: ::Minfraud::Components::Shipping,
29
- shopping_cart: ::Minfraud::Components::ShoppingCart
30
- }
28
+ account: ::Minfraud::Components::Account,
29
+ billing: ::Minfraud::Components::Billing,
30
+ credit_card: ::Minfraud::Components::CreditCard,
31
+ custom_inputs: ::Minfraud::Components::CustomInputs,
32
+ device: ::Minfraud::Components::Device,
33
+ email: ::Minfraud::Components::Email,
34
+ event: ::Minfraud::Components::Event,
35
+ order: ::Minfraud::Components::Order,
36
+ payment: ::Minfraud::Components::Payment,
37
+ shipping: ::Minfraud::Components::Shipping,
38
+ shopping_cart: ::Minfraud::Components::ShoppingCart,
39
+ }.freeze
31
40
  end
32
41
  end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'ipaddr'
5
+ require 'uri'
6
+
7
+ # rubocop:disable Metrics/ModuleLength
8
+ module Minfraud
9
+ # @!visibility private
10
+ module Validates
11
+ def validate_string(field, length, value)
12
+ return if !value
13
+
14
+ if value.to_s.length > length
15
+ raise InvalidInputError, "The #{field} value is not valid. The maximum length is #{length}."
16
+ end
17
+ end
18
+
19
+ def validate_md5(field, value)
20
+ return if !value
21
+
22
+ if !value.to_s.match(/\A[a-fA-F0-9]{32}\z/)
23
+ raise InvalidInputError, "The #{field} value is not valid. It must be an MD5 hash as a string."
24
+ end
25
+ end
26
+
27
+ def validate_subdivision_code(field, value)
28
+ return if !value
29
+
30
+ if !value.to_s.match(/\A[0-9A-Z]{1,4}\z/)
31
+ raise InvalidInputError, "The #{field} value is not valid. It must be an ISO 3166-2 subdivision code."
32
+ end
33
+ end
34
+
35
+ def validate_country_code(field, value)
36
+ return if !value
37
+
38
+ if !value.to_s.match(/\A[A-Z]{2}\z/)
39
+ raise InvalidInputError, "The #{field} value is not valid. It must be an ISO 3166-1 alpha-2 country code."
40
+ end
41
+ end
42
+
43
+ def validate_telephone_country_code(field, value)
44
+ return if !value
45
+
46
+ if !value.to_s.match(/\A[0-9]{1,4}\z/)
47
+ raise InvalidInputError, "The #{field} value is not valid. It must be at most 4 digits."
48
+ end
49
+ end
50
+
51
+ def validate_regex(field, regex, value)
52
+ return if !value
53
+
54
+ if !regex.match(value.to_s)
55
+ raise InvalidInputError, "The #{field} value is not valid. It must match the pattern #{regex}."
56
+ end
57
+ end
58
+
59
+ def validate_credit_card_token(field, value)
60
+ return if !value
61
+
62
+ s = value.to_s
63
+
64
+ if !/\A[\x21-\x7E]{1,255}\z/.match(s)
65
+ raise InvalidInputError, "The #{field} value is not valid. It must contain only non-space printable ASCII characters."
66
+ end
67
+
68
+ if /\A[0-9]{1,19}\z/.match(s)
69
+ raise InvalidInputError, "The #{field} value is not valid. If it is all digits, it must be longer than 19 characters."
70
+ end
71
+ end
72
+
73
+ def validate_custom_input_value(field, value)
74
+ return if !value
75
+
76
+ if [true, false].include?(value)
77
+ return
78
+ end
79
+
80
+ if value.is_a? Numeric
81
+ if value < -1e13 + 1 || value > 1e13 - 1
82
+ raise InvalidInputError, "The #{field} value is not valid. Numeric values must be between -1e13 and 1e13."
83
+ end
84
+
85
+ return
86
+ end
87
+
88
+ validate_string(field, 255, value)
89
+ end
90
+
91
+ def validate_ip(field, value)
92
+ return if !value
93
+
94
+ s = value.to_s
95
+ if s.include? '/'
96
+ raise InvalidInputError, "The #{field} value is not valid. It must be an individual IP address."
97
+ end
98
+
99
+ # rubocop:disable Style/RescueStandardError
100
+ begin
101
+ IPAddr.new(s)
102
+ rescue
103
+ raise InvalidInputError, "The #{field} value is not valid. It must be an IPv4 or IPv6 address."
104
+ end
105
+ # rubocop:enable Style/RescueStandardError
106
+
107
+ nil
108
+ end
109
+
110
+ def validate_nonnegative_number(field, value)
111
+ return if !value
112
+
113
+ if !value.is_a? Numeric
114
+ raise InvalidInputError, "The #{field} value is not valid. It must be numeric."
115
+ end
116
+
117
+ if value.negative? || value > 1e13 - 1
118
+ raise InvalidInputError, "The #{field} value is not valid. It must be at least 0 and at most 1e13 - 1."
119
+ end
120
+ end
121
+
122
+ def validate_nonnegative_integer(field, value)
123
+ return if !value
124
+
125
+ if !value.is_a? Integer
126
+ raise InvalidInputError, "The #{field} is not valid. It must be an integer."
127
+ end
128
+
129
+ if value.negative? || value > 1e13 - 1
130
+ raise InvalidInputError, "The #{field} is not valid. It must be at least 0 and at most 1e13 - 1."
131
+ end
132
+ end
133
+
134
+ def validate_email(field, value)
135
+ return if !value
136
+
137
+ if /.@./.match(value)
138
+ validate_string(field, 255, value)
139
+ return
140
+ end
141
+
142
+ validate_md5(field, value)
143
+ end
144
+
145
+ def validate_rfc3339(field, value)
146
+ return if !value
147
+
148
+ # rubocop:disable Style/RescueStandardError
149
+ begin
150
+ DateTime.rfc3339(value)
151
+ rescue
152
+ raise InvalidInputError, "The #{field} value is not valid. It must be in the RFC 3339 date-time format."
153
+ end
154
+ # rubocop:enable Style/RescueStandardError
155
+
156
+ nil
157
+ end
158
+
159
+ def validate_boolean(field, value)
160
+ return if !value
161
+
162
+ if ![false, true].include? value
163
+ raise InvalidInputError, "The #{field} value is not valid. It must be boolean."
164
+ end
165
+ end
166
+
167
+ def validate_uri(field, value)
168
+ return if !value
169
+
170
+ if value.to_s.length > 1_024
171
+ raise InvalidInputError, "The #{field} value is not valid. It must not exceed 1024 characters."
172
+ end
173
+
174
+ # rubocop:disable Style/RescueStandardError
175
+ begin
176
+ u = URI(value)
177
+ if !u.scheme
178
+ raise InvalidInputError
179
+ end
180
+ rescue
181
+ raise InvalidInputError, "The #{field} value is not valid. It must be an absolute URI."
182
+ end
183
+ # rubocop:enable Style/RescueStandardError
184
+ end
185
+ end
186
+ end
187
+ # rubocop:enable Metrics/ModuleLength
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Minfraud
2
- VERSION = '1.0.4'
4
+ # The Gem version.
5
+ VERSION = '2.4.0'
3
6
  end
data/lib/minfraud.rb CHANGED
@@ -1,49 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'connection_pool'
4
+ require 'http'
1
5
  require 'minfraud'
2
6
  require 'minfraud/enum'
7
+ require 'minfraud/validates'
3
8
  require 'minfraud/components/base'
4
9
  require 'minfraud/components/account'
5
10
  require 'minfraud/components/addressable'
6
11
  require 'minfraud/components/billing'
7
12
  require 'minfraud/components/credit_card'
13
+ require 'minfraud/components/custom_inputs'
8
14
  require 'minfraud/components/device'
9
15
  require 'minfraud/components/email'
10
16
  require 'minfraud/components/event'
11
17
  require 'minfraud/components/order'
12
18
  require 'minfraud/components/payment'
13
- require 'minfraud/components/shopping_cart_item'
19
+ require 'minfraud/components/report/transaction'
14
20
  require 'minfraud/components/shipping'
15
21
  require 'minfraud/components/shopping_cart'
16
- require 'minfraud/components/device'
22
+ require 'minfraud/components/shopping_cart_item'
17
23
  require 'minfraud/resolver'
18
24
  require 'minfraud/version'
19
25
  require 'minfraud/errors'
20
- require 'minfraud/http_service'
21
- require 'minfraud/http_service/request'
22
26
  require 'minfraud/http_service/response'
23
27
  require 'minfraud/error_handler'
24
28
  require 'minfraud/assessments'
29
+ require 'minfraud/report'
25
30
 
31
+ # This class holds global configuration parameters and provides a namespace
32
+ # for the gem's classes.
26
33
  module Minfraud
27
34
  class << self
28
- # @!attribute user_id
29
- # @return [String] MaxMind username that is used for authorization
30
- attr_accessor :user_id
35
+ # The MaxMind account ID that is used for authorization.
36
+ #
37
+ # @return [Integer, nil]
38
+ attr_accessor :account_id
39
+
40
+ # Enable client side validation. This is disabled by default.
41
+ #
42
+ # @return [Boolean, nil]
43
+ attr_accessor :enable_validation
31
44
 
32
- # @!attribute license_key
33
- # @return [String] MaxMind license key that is used for authorization
45
+ # The host to use when connecting to the web service.
46
+ # By default, the client connects to the production host. However,
47
+ # during testing and development, you can set this option to
48
+ # 'sandbox.maxmind.com' to use the Sandbox environment's host. The
49
+ # sandbox allows you to experiment with the API without affecting your
50
+ # production data.
51
+ #
52
+ # @return [String, nil]
53
+ attr_accessor :host
54
+
55
+ # The MaxMind license key that is used for authorization.
56
+ #
57
+ # @return [String, nil]
34
58
  attr_accessor :license_key
35
59
 
36
- # @yield [self] to accept configuration settings
60
+ # @!visibility private
61
+ attr_reader :connection_pool
62
+
63
+ # Yield self to accept configuration settings.
64
+ #
65
+ # @yield [self]
37
66
  def configure
38
67
  yield self
68
+
69
+ pool_size = 5
70
+ host = @host.nil? ? 'minfraud.maxmind.com' : @host
71
+ @connection_pool = ConnectionPool.new(size: pool_size) do
72
+ make_http_client.persistent("https://#{host}")
73
+ end
39
74
  end
40
75
 
41
- # @return [Hash] current Minfraud configuration
42
- def configuration
43
- {
44
- user_id: @user_id,
45
- license_key: @license_key
46
- }
76
+ private
77
+
78
+ def make_http_client
79
+ HTTP.basic_auth(
80
+ user: @account_id,
81
+ pass: @license_key,
82
+ ).headers(
83
+ accept: 'application/json',
84
+ user_agent: "minfraud-api-ruby/#{Minfraud::VERSION} ruby/#{RUBY_VERSION} http/#{HTTP::VERSION}",
85
+ )
47
86
  end
48
87
  end
49
88
  end
data/minfraud.gemspec CHANGED
@@ -1,29 +1,38 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
4
6
  require 'minfraud/version'
5
7
 
6
8
  Gem::Specification.new do |spec|
7
- spec.name = "minfraud"
9
+ spec.name = 'minfraud'
8
10
  spec.version = Minfraud::VERSION
9
- spec.authors = ["kushnir.yb"]
10
- spec.email = ["kushnir.yb@gmail.com"]
11
+ spec.authors = ['kushnir.yb', 'William Storey']
12
+ spec.email = ['support@maxmind.com']
11
13
 
12
- spec.summary = %q{Ruby interface to the MaxMind minFraud v2.0 API services}
13
- spec.homepage = "https://github.com/kushniryb/minfraud-api-v2"
14
- spec.license = "MIT"
14
+ spec.summary = 'Ruby API for the minFraud Score, Insights, Factors, and Report Transactions services'
15
+ spec.homepage = 'https://github.com/maxmind/minfraud-api-ruby'
16
+ spec.license = 'MIT'
15
17
 
16
- spec.required_ruby_version = '>= 1.9'
18
+ spec.required_ruby_version = '>= 2.7.0'
17
19
 
18
20
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
- spec.bindir = "exe"
21
+ spec.bindir = 'exe'
20
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
- spec.require_paths = ["lib"]
22
-
23
- spec.add_runtime_dependency 'faraday', '~> 0.9', '>= 0.9.1'
24
- spec.add_runtime_dependency 'faraday_middleware', '~> 0.9', '>= 0.9.1'
25
- spec.add_runtime_dependency 'hashie', '~> 3.0'
26
- spec.add_development_dependency "bundler", "~> 1.12"
27
- spec.add_development_dependency "rake", "~> 10.0"
28
- spec.add_development_dependency "rspec", "~> 3.0"
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_runtime_dependency 'connection_pool', '~> 2.2'
26
+ spec.add_runtime_dependency 'http', '>= 4.3', '< 6.0'
27
+ spec.add_runtime_dependency 'maxmind-geoip2', '~> 1.2'
28
+ spec.add_runtime_dependency 'simpleidn', '~> 0.1', '>= 0.1.1'
29
+
30
+ spec.add_development_dependency 'bundler', '~> 2.2'
31
+ spec.add_development_dependency 'rake', '~> 13.0'
32
+ spec.add_development_dependency 'rspec', '~> 3.0'
33
+ spec.add_development_dependency 'rubocop', '~> 1.23'
34
+ spec.add_development_dependency 'webmock', '~> 3.14'
35
+ spec.metadata = {
36
+ 'rubygems_mfa_required' => 'true'
37
+ }
29
38
  end