diff --git a/CHANGELOG.md b/CHANGELOG.md index aea2f8b..285d7ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v2.7.0-beta.1 + +* Added support for the new risk reasons outputs in minFraud Factors. The risk + reasons output codes and reasons are currently in beta and are subject to + change. We recommend that you use these beta outputs with caution and avoid + relying on them for critical applications. + ## v2.6.0 (2024-07-08) * Updated the validation for the Report Transactions API to make the diff --git a/lib/minfraud/model/factors.rb b/lib/minfraud/model/factors.rb index cb2a93c..38ed4ee 100644 --- a/lib/minfraud/model/factors.rb +++ b/lib/minfraud/model/factors.rb @@ -2,11 +2,21 @@ require 'minfraud/model/insights' require 'minfraud/model/subscores' +require 'minfraud/model/risk_score_reason' module Minfraud module Model # Model representing the Factors response. class Factors < Insights + # This field contains RiskScoreReason objects that describe risk score reasons + # for a given transaction that change the risk score significantly. + # Risk score reasons are usually only returned for medium to high risk transactions. + # If there were no significant changes to the risk score due to these reasons, + # then this array will be empty. + # + # @return [Array] + attr_reader :risk_score_reasons + # An object containing scores for many of the individual risk factors # that are used to calculate the overall risk score. # @@ -17,6 +27,13 @@ class Factors < Insights def initialize(record, locales) super + @risk_score_reasons = [] + if record&.key?('risk_score_reasons') + record['risk_score_reasons'].each do |r| + @risk_score_reasons << Minfraud::Model::RiskScoreReason.new(r) + end + end + @subscores = Minfraud::Model::Subscores.new(get('subscores')) end end diff --git a/lib/minfraud/model/reason.rb b/lib/minfraud/model/reason.rb new file mode 100644 index 0000000..5e3bc67 --- /dev/null +++ b/lib/minfraud/model/reason.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'minfraud/model/abstract' + +module Minfraud + module Model + # The risk score reason for the multiplier. + # + # This class provides both a machine-readable code and a human-readable + # explanation of the reason for the risk score, see + # https://dev.maxmind.com/minfraud/api-documentation/responses/schema--response--risk-score-reason--multiplier-reason. + # Although more codes may be added in the future, the current codes are: + # + # * BROWSER_LANGUAGE - Riskiness of the browser user-agent and + # language associated with the request. + # * BUSINESS_ACTIVITY - Riskiness of business activity + # associated with the request. + # * COUNTRY - Riskiness of the country associated with the request. + # * CUSTOMER_ID - Riskiness of a customer's activity. + # * EMAIL_DOMAIN - Riskiness of email domain. + # * EMAIL_DOMAIN_NEW - Riskiness of newly-sighted email domain. + # * EMAIL_ADDRESS_NEW - Riskiness of newly-sighted email address. + # * EMAIL_LOCAL_PART - Riskiness of the local part of the email address. + # * EMAIL_VELOCITY - Velocity on email - many requests on same email + # over short period of time. + # * ISSUER_ID_NUMBER_COUNTRY_MISMATCH - Riskiness of the country mismatch + # between IP, billing, shipping and IIN country. + # * ISSUER_ID_NUMBER_ON_SHOP_ID - Risk of Issuer ID Number for the shop ID. + # * ISSUER_ID_NUMBER_LAST_DIGITS_ACTIVITY - Riskiness of many recent requests + # and previous high-risk requests on the IIN and last digits of the credit card. + # * ISSUER_ID_NUMBER_SHOP_ID_VELOCITY - Risk of recent Issuer ID Number activity + # for the shop ID. + # * INTRACOUNTRY_DISTANCE - Risk of distance between IP, billing, + # and shipping location. + # * ANONYMOUS_IP - Risk due to IP being an Anonymous IP. + # * IP_BILLING_POSTAL_VELOCITY - Velocity of distinct billing postal code + # on IP address. + # * IP_EMAIL_VELOCITY - Velocity of distinct email address on IP address. + # * IP_HIGH_RISK_DEVICE - High-risk device sighted on IP address. + # * IP_ISSUER_ID_NUMBER_VELOCITY - Velocity of distinct IIN on IP address. + # * IP_ACTIVITY - Riskiness of IP based on minFraud network activity. + # * LANGUAGE - Riskiness of browser language. + # * MAX_RECENT_EMAIL - Riskiness of email address + # based on past minFraud risk scores on email. + # * MAX_RECENT_PHONE - Riskiness of phone number + # based on past minFraud risk scores on phone. + # * MAX_RECENT_SHIP - Riskiness of email address + # based on past minFraud risk scores on ship address. + # * MULTIPLE_CUSTOMER_ID_ON_EMAIL - Riskiness of email address + # having many customer IDs. + # * ORDER_AMOUNT - Riskiness of the order amount. + # * ORG_DISTANCE_RISK - Risk of ISP and distance between + # billing address and IP location. + # * PHONE - Riskiness of the phone number or related numbers. + # * CART - Riskiness of shopping cart contents. + # * TIME_OF_DAY - Risk due to local time of day. + # * TRANSACTION_REPORT_EMAIL - Risk due to transaction reports + # on the email address. + # * TRANSACTION_REPORT_IP - Risk due to transaction reports on the IP address. + # * TRANSACTION_REPORT_PHONE - Risk due to transaction reports + # on the phone number. + # * TRANSACTION_REPORT_SHIP - Risk due to transaction reports + # on the shipping address. + # * EMAIL_ACTIVITY - Riskiness of the email address + # based on minFraud network activity. + # * PHONE_ACTIVITY - Riskiness of the phone number + # based on minFraud network activity. + # * SHIP_ACTIVITY - Riskiness of ship address based on minFraud network activity. + class Reason < Abstract + # This value is a machine-readable code identifying the reason. + # + # @return [String] + attr_reader :code + + # This property provides a human-readable explanation of the reason. The + # description may change at any time and should not be matched against. + # + # @return [String] + attr_reader :reason + + # @!visibility private + def initialize(record) + super + + @code = get('code') + @reason = get('reason') + end + end + end +end diff --git a/lib/minfraud/model/risk_score_reason.rb b/lib/minfraud/model/risk_score_reason.rb new file mode 100644 index 0000000..b48e6d4 --- /dev/null +++ b/lib/minfraud/model/risk_score_reason.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'minfraud/model/reason' + +module Minfraud + module Model + # The risk score multiplier and the reasons for that multiplier. + class RiskScoreReason < Abstract + # The factor by which the risk score is increased (if the value is greater than 1) + # or decreased (if the value is less than 1) for given risk reason(s). + # Multipliers greater than 1.5 and less than 0.66 are considered significant + # and lead to risk reason(s) being present. + # + # @return [Float] + attr_reader :multiplier + + # This field contains Risk objects that describe one of the reasons for the multiplier. + # + # @return [Array] + attr_reader :reasons + + # @!visibility private + def initialize(record) + super + + @multiplier = get('multiplier') + + @reasons = [] + if record&.key?('reasons') + record['reasons'].each do |r| + @reasons << Minfraud::Model::Reason.new(r) + end + end + end + end + end +end diff --git a/minfraud.gemspec b/minfraud.gemspec index eae8241..fcd327c 100644 --- a/minfraud.gemspec +++ b/minfraud.gemspec @@ -22,10 +22,10 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_runtime_dependency 'connection_pool', '~> 2.2' - spec.add_runtime_dependency 'http', '>= 4.3', '< 6.0' - spec.add_runtime_dependency 'maxmind-geoip2', '~> 1.2' - spec.add_runtime_dependency 'simpleidn', '~> 0.1', '>= 0.1.1' + spec.add_dependency 'connection_pool', '~> 2.2' + spec.add_dependency 'http', '>= 4.3', '< 6.0' + spec.add_dependency 'maxmind-geoip2', '~> 1.2' + spec.add_dependency 'simpleidn', '~> 0.1', '>= 0.1.1' spec.add_development_dependency 'bundler', '~> 2.2' spec.add_development_dependency 'rake', '~> 13.0' diff --git a/spec/fixtures/files/factors-response1.json b/spec/fixtures/files/factors-response1.json index a7b3c9a..b5e5e1f 100644 --- a/spec/fixtures/files/factors-response1.json +++ b/spec/fixtures/files/factors-response1.json @@ -206,5 +206,43 @@ "input_pointer": "/account/username_md5", "warning": "Encountered value at /account/username_md5 that does meet the required constraints" } + ], + "risk_score_reasons": [ + { + "multiplier": 45.0, + "reasons": [ + { + "code": "ANONYMOUS_IP", + "reason": "Risk due to IP being an Anonymous IP" + } + ] + }, + { + "multiplier": 1.8, + "reasons": [ + { + "code": "TIME_OF_DAY", + "reason": "Risk due to local time of day" + } + ] + }, + { + "multiplier": 1.6, + "reasons": [ + { + "reason": "Riskiness of newly-sighted email domain", + "code": "EMAIL_DOMAIN_NEW" + } + ] + }, + { + "multiplier": 0.34, + "reasons": [ + { + "code": "EMAIL_ADDRESS_NEW", + "reason": "Riskiness of newly-sighted email address" + } + ] + } ] } diff --git a/spec/model/factors_spec.rb b/spec/model/factors_spec.rb index 23cad14..ca722e5 100644 --- a/spec/model/factors_spec.rb +++ b/spec/model/factors_spec.rb @@ -31,6 +31,22 @@ expect(m.subscores.shipping_address).to eq 0.2 expect(m.subscores.shipping_address_distance_to_ip_location).to eq 0.16 expect(m.subscores.time_of_day).to eq 0.17 + + expect(m.risk_score_reasons.length).to eq 4 + expect(m.risk_score_reasons[0].multiplier).to eq 45.0 + expect(m.risk_score_reasons[0].reasons.length).to eq 1 + expect(m.risk_score_reasons[0].reasons[0].code).to eq 'ANONYMOUS_IP' + expect(m.risk_score_reasons[0].reasons[0].reason).to eq 'Risk due to IP being an Anonymous IP' + end + + it 'checks absence of risk score reasons' do + buf = File.read('spec/fixtures/files/factors-response1.json') + record = JSON.parse(buf) + record.delete('risk_score_reasons') + + m = Minfraud::Model::Factors.new(record, ['en']) + + expect(m.risk_score_reasons).to eq [] end end end