diff --git a/CHANGELOG.md b/CHANGELOG.md index cb709cd..8c33876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v2.11.0 + +* Added the `phone_verification_method`, + `phone_was_verification_successful`, and `phone_verification_time` + attributes to `Minfraud::Components::Billing`. They describe the most + recent verification of the billing phone number. +* Boolean inputs are now always sent as JSON booleans, and `false` values + are no longer dropped from the request. Previously, `false` was omitted + for every boolean input. `true` was sent as the string `"true"` for + `/credit_card/was_3d_secure_successful` and for boolean custom inputs. + The `/order/has_gift_message`, `/order/is_gift`, and + `/payment/was_authorized` inputs already sent `true` as a JSON boolean. + ## v2.10.0 (2026-07-21) * Added the `residential` attribute to the `anonymizer` object on diff --git a/README.md b/README.md index 605b912..5d4c7bd 100644 --- a/README.md +++ b/README.md @@ -102,17 +102,20 @@ assessment = Minfraud::Assessments.new( domain: 'maxmind.com', }, billing: { - first_name: 'First', - last_name: 'Last', - company: 'Company', - address: '101 Address Rd.', - address_2: 'Unit 5', - city: 'New Haven', - region: 'CT', - country: 'US', - postal: '06510', - phone_number: '123-456-7890', - phone_country_code: '1', + first_name: 'First', + last_name: 'Last', + company: 'Company', + address: '101 Address Rd.', + address_2: 'Unit 5', + city: 'New Haven', + region: 'CT', + country: 'US', + postal: '06510', + phone_number: '123-456-7890', + phone_country_code: '1', + phone_verification_method: :delivered_code, + phone_verification_time: '2026-10-01T14:30:00Z', + phone_was_verification_successful: true, }, shipping: { first_name: 'ShipFirst', diff --git a/lib/minfraud/components/base.rb b/lib/minfraud/components/base.rb index 06ccc4f..8400f3b 100644 --- a/lib/minfraud/components/base.rb +++ b/lib/minfraud/components/base.rb @@ -26,27 +26,22 @@ def to_json(*_args) # # @return [Hash] def populate!(hash, v_sym) - return hash unless (value = instance_variable_get(v_sym)) + value = instance_variable_get(v_sym) + return hash if value.nil? key = v_sym.to_s.gsub(/@/, '') - hash.merge!(key => represent(key, value)) + hash.merge!(key => represent(value)) end - # Return the value according to the request format. - # - # @param key [Symbol] An instance variable symbol. + # Return the value according to the request format. Booleans stay + # booleans. Other values become strings. # # @param value [Object] An instance variable value. # # @return [Object] - def represent(key, value) - BOOLS.include?(key) ? value : value.to_s + def represent(value) + [true, false].include?(value) ? value : value.to_s end - - # Keys that have to remain boolean - BOOLS = %w[was_authorized is_gift has_gift_message].freeze - - private_constant :BOOLS end end end diff --git a/lib/minfraud/components/billing.rb b/lib/minfraud/components/billing.rb index eb760c6..3fc24bd 100644 --- a/lib/minfraud/components/billing.rb +++ b/lib/minfraud/components/billing.rb @@ -5,6 +5,54 @@ module Components # Billing corresponds to the billing object of a minFraud request. # # @see https://dev.maxmind.com/minfraud/api-documentation/requests/?lang=en#schema--request--billing - class Billing < Addressable; end + class Billing < Addressable + include ::Minfraud::Enum + + # The most recent method used to verify the billing phone number. This + # must be one of +:delivered_code+, +:network+, or +:other+. + # +:delivered_code+ is a code delivered to the phone, such as by SMS, + # voice call, or messaging app. +:network+ is verification through the + # mobile network operator, such as silent network authentication. + # + # @!attribute phone_verification_method + # + # @return [Symbol, nil] + enum_accessor :phone_verification_method, %i[delivered_code network other] + + # Whether the most recent verification of the billing phone number + # succeeded. Do not include this field if no verification was + # attempted. + # + # @return [Boolean, nil] + attr_accessor :phone_was_verification_successful + + # The date and time of the most recent verification of the billing phone + # number. The string must be in the RFC 3339 date-time format, e.g., + # "2012-04-12T23:20:50.52Z". + # + # @see https://datatracker.ietf.org/doc/html/rfc3339 + # + # @return [String, nil] + attr_accessor :phone_verification_time + + # @param params [Hash] Hash of parameters. Each key/value should + # correspond to one of the available attributes. + def initialize(params = {}) + self.phone_verification_method = params[:phone_verification_method] + @phone_was_verification_successful = params[:phone_was_verification_successful] + @phone_verification_time = params[:phone_verification_time] + super + end + + private + + def validate + super + return if !Minfraud.enable_validation + + validate_boolean('phone_was_verification_successful', @phone_was_verification_successful) + validate_rfc3339('phone_verification_time', @phone_verification_time) + end + end end end diff --git a/spec/components/base_spec.rb b/spec/components/base_spec.rb index dac2448..59cbebc 100644 --- a/spec/components/base_spec.rb +++ b/spec/components/base_spec.rb @@ -29,6 +29,28 @@ base_component.instance_variable_set(:@null, nil) expect(base_component.to_json).to eq(expected) end + + it 'returns json with boolean values kept as booleans' do + base_component.instance_variable_set(:@yes, true) + base_component.instance_variable_set(:@no, false) + expect(base_component.to_json).to eq( + expected.merge('yes' => true, 'no' => false), + ) + end + end + end + + describe 'boolean inputs' do + [ + [Minfraud::Components::CreditCard, :was_3d_secure_successful], + [Minfraud::Components::Order, :has_gift_message], + [Minfraud::Components::Order, :is_gift], + [Minfraud::Components::Payment, :was_authorized], + [Minfraud::Components::CustomInputs, :boolean_input], + ].product([true, false]).each do |(component, field), value| + it "sends #{component}##{field} #{value} as a JSON boolean" do + expect(component.new(field => value).to_json).to eq(field.to_s => value) + end end end end diff --git a/spec/components/billing_spec.rb b/spec/components/billing_spec.rb index 2ddee4c..fc5243b 100644 --- a/spec/components/billing_spec.rb +++ b/spec/components/billing_spec.rb @@ -3,6 +3,37 @@ require 'spec_helper' describe Minfraud::Components::Billing do + describe '#initialize' do + it 'accepts each phone_verification_method value' do + %i[delivered_code network other].each do |method| + expect(described_class.new(phone_verification_method: method).phone_verification_method).to eq(method) + end + end + + it 'raises an exception for an invalid phone_verification_method' do + expect do + described_class.new(phone_verification_method: :sms) + end.to raise_exception(Minfraud::NotEnumValueError) + end + end + + describe '#to_json' do + [true, false].each do |successful| + it "sends phone_was_verification_successful #{successful} as a JSON boolean" do + billing = described_class.new( + phone_verification_method: 'network', + phone_verification_time: '2026-10-01T14:30:00Z', + phone_was_verification_successful: successful, + ) + expect(billing.to_json).to eq( + 'phone_verification_method' => 'network', + 'phone_verification_time' => '2026-10-01T14:30:00Z', + 'phone_was_verification_successful' => successful, + ) + end + end + end + describe 'validation' do before do Minfraud.configure { |c| c.enable_validation = 1 } @@ -32,11 +63,30 @@ end.to raise_exception(Minfraud::InvalidInputError) end + it 'raises an exception for an invalid phone_was_verification_successful' do + expect do + described_class.new( + phone_was_verification_successful: 'true', + ) + end.to raise_exception(Minfraud::InvalidInputError) + end + + it 'raises an exception for an invalid phone_verification_time' do + expect do + described_class.new( + phone_verification_time: '2026-10-01 14:30:00', + ) + end.to raise_exception(Minfraud::InvalidInputError) + end + it 'does not raise an exception for valid values' do described_class.new( - region: 'BC', - country: 'CA', - phone_country_code: '1', + region: 'BC', + country: 'CA', + phone_country_code: '1', + phone_verification_method: :delivered_code, + phone_verification_time: '2026-10-01T14:30:00Z', + phone_was_verification_successful: false, ) end end