Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
attributes to `Minfraud::Components::Billing` and
`Minfraud::Components::Shipping`. They describe the most recent
verification of the billing or shipping phone number.
* Added the `verification_time` and `was_verification_successful`
attributes to `Minfraud::Components::Email`. They describe the most
recent verification of the email address.
* Boolean inputs are now always sent as JSON booleans, and `false` is no
longer dropped. Previously, every input set to `false` was omitted.
Now only `nil` inputs are omitted. In particular:
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,18 @@ assessment = Minfraud::Assessments.new(
party: :customer,
transaction_id: 'txn3134133',
shop_id: 's2123',
time: '2012-04-12T23:20:50+00:00',
time: '2026-10-05T23:20:50+00:00',
type: :purchase,
},
account: {
user_id: '3132',
username_md5: '4f9726678c438914fa04bdb8c1a24088',
},
email: {
address: 'test@maxmind.com',
domain: 'maxmind.com',
address: 'test@maxmind.com',
domain: 'maxmind.com',
verification_time: '2026-10-01T14:30:00Z',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: in this example, verification_time (2026-10-01) is 14 years after the example event time (2012-04-12, line 93). The "most recent verification" then comes after the transaction that it describes.

The phone_verification_time values (lines 119 and 136) have the same problem. They are not new in this PR.

Suggested fix: use a verification time before the event time, or move the event time to a recent date.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reply by Claude Opus 5.5 on Greg's behalf.

Fixed in 6a19605. The example event time is now 2026-10-05, after every verification time. This also fixes the phone examples.

was_verification_successful: true,
},
billing: {
first_name: 'First',
Expand Down
28 changes: 25 additions & 3 deletions lib/minfraud/components/email.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,32 @@ class Email < Base
# @return [Boolean, nil]
attr_accessor :hash_address

# The date and time of the most recent attempt to verify the email

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the email and phone time docs now use different wording. This says "the most recent attempt to verify the email address, if known". phone_verification_time in lib/minfraud/components/addressable.rb (line 96) says "the most recent verification of the phone number". The changelog bullets (lines 8-9 and 11-12) also say "most recent verification".

The two inputs have the same meaning: the time also applies when was_verification_successful is false. The "attempt" wording here is more accurate, and it matches the dev-site text. The phone attributes are also unreleased (v2.11.0), so the phone doc and the changelog can use the same wording now.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given it's about phone, we can leave it

# address, if known. The string must be in the RFC 3339 date-time
# format, e.g., "2012-04-12T23:20:50.52Z". The time must not be before
# 1970-01-01T00:00:00Z or more than two days in the future. The web
# service ignores a time outside this range and returns a warning.
#
# @see https://datatracker.ietf.org/doc/html/rfc3339
#
# @return [String, nil]
attr_accessor :verification_time

# Whether the most recent verification that the end user controls the
# email address succeeded, for example by confirming a link sent to the
# address. Do not include this field if no verification was attempted.
#
# @return [Boolean, nil]
attr_accessor :was_verification_successful

# @param params [Hash] Hash of parameters. Each key/value should
# correspond to one of the available attributes.
def initialize(params = {})
@address = params[:address]
@domain = params[:domain]
@hash_address = params[:hash_address]
@address = params[:address]
@domain = params[:domain]
@hash_address = params[:hash_address]
@verification_time = params[:verification_time]
@was_verification_successful = params[:was_verification_successful]

validate
end
Expand Down Expand Up @@ -72,6 +92,8 @@ def validate

validate_email('email', @address)
validate_string('domain', 255, @domain)
validate_rfc3339('verification_time', @verification_time)
validate_boolean('was_verification_successful', @was_verification_successful)
end

def hash_email_address(address)
Expand Down
27 changes: 26 additions & 1 deletion spec/components/email_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,34 @@
require 'spec_helper'

describe Minfraud::Components::Email do
it_behaves_like 'a component with verification inputs', ''

describe '#to_json' do
it 'sends the verification inputs with a hashed address' do
email = described_class.new(
address: 'test@maxmind.com',
domain: 'maxmind.com',
hash_address: true,
verification_time: '2026-10-01T14:30:00Z',
was_verification_successful: true,
)
expect(email.to_json).to eq(
'address' => '977577b140bfb7c516e4746204fbdb01',
'domain' => 'maxmind.com',
'verification_time' => '2026-10-01T14:30:00Z',
'was_verification_successful' => true,
)
end
end

describe 'validation' do
before do
around do |example|
previous = Minfraud.enable_validation

Minfraud.configure { |c| c.enable_validation = 1 }
example.run
ensure
Minfraud.configure { |c| c.enable_validation = previous }
end

it 'raises an exception for an invalid email address' do
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
require 'minfraud'
require_relative 'support/addressable_examples'
require_relative 'support/verification_examples'
52 changes: 5 additions & 47 deletions spec/support/addressable_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,53 +17,11 @@
end.to raise_exception(Minfraud::NotEnumValueError)
end

[true, false].each do |successful|
it "sends phone_was_verification_successful #{successful} as a JSON boolean" do
component = described_class.new(
phone_verification_method: 'network',
phone_verification_time: '2026-10-01T14:30:00Z',
phone_was_verification_successful: successful,
)
expect(component.to_json).to eq(
'phone_verification_method' => 'network',
'phone_verification_time' => '2026-10-01T14:30:00Z',
'phone_was_verification_successful' => successful,
)
end
it 'sends phone_verification_method as a string' do
expect(described_class.new(phone_verification_method: 'network').to_json).to eq(
'phone_verification_method' => 'network',
)
end

context 'with validation' do
around do |example|
previous = Minfraud.enable_validation

Minfraud.configure { |c| c.enable_validation = 1 }
example.run
ensure
Minfraud.configure { |c| c.enable_validation = previous }
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 phone verification values' do
described_class.new(
phone_verification_method: :delivered_code,
phone_verification_time: '2026-10-01T14:30:00Z',
phone_was_verification_successful: false,
)
end
end
it_behaves_like 'a component with verification inputs', 'phone_'
end
63 changes: 63 additions & 0 deletions spec/support/verification_examples.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

# The prefix is the text before verification_time and
# was_verification_successful in the field names, such as 'phone_'.
RSpec.shared_examples 'a component with verification inputs' do |prefix|
let(:time_field) { :"#{prefix}verification_time" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: time_field and successful_field are let values, so the example descriptions build the same names again with "#{prefix}verification_time" and "#{prefix}was_verification_successful". Each name is built in two places, so a change to one can make a description name a different field than the one the example tests.

Suggest block-local variables, such as time_field = :"#{prefix}verification_time". They work in both the descriptions and the example bodies.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reply by Claude Opus 5.5 on Greg's behalf.

We will keep the let values. RuboCop rejects block-local variables that the examples use (RSpec/LeakyLocalVariable).

let(:successful_field) { :"#{prefix}was_verification_successful" }

[true, false].each do |successful|
it "sends #{prefix}was_verification_successful #{successful} as a JSON boolean" do
component = described_class.new(
time_field => '2026-10-01T14:30:00Z',
successful_field => successful,
)
expect(component.to_json).to eq(
time_field.to_s => '2026-10-01T14:30:00Z',
successful_field.to_s => successful,
)
end
end

context 'with verification validation' do
around do |example|
previous = Minfraud.enable_validation

Minfraud.configure { |c| c.enable_validation = 1 }
example.run
ensure
Minfraud.configure { |c| c.enable_validation = previous }
end

it "raises an exception for an invalid #{prefix}verification_time" do
expect do
described_class.new(time_field => '2026-10-01 14:30:00')
end.to raise_exception(Minfraud::InvalidInputError)
end

it "raises an exception for a boolean #{prefix}verification_time" do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: These boolean checks repeat the tables in spec/components/base_spec.rb ('boolean inputs' and 'booleans with validation'), and they are weaker.

This example checks only for InvalidInputError, and only for true. If validate_rfc3339 stopped calling validate_not_boolean, the example would still pass, because "true" also fails the RFC 3339 regex. The base_spec table checks /must not be a boolean/ for both true and false.

Suggest: add Email#verification_time and Email#was_verification_successful to the base_spec tables, or match the error message here. The phone verification fields are also missing from those tables.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reply by Claude Opus 5.5 on Greg's behalf.

Fixed in 58d0b12. The example now matches /must not be a boolean/, so it fails if validate_rfc3339 stops calling validate_not_boolean.

expect do
described_class.new(time_field => true)
end.to raise_exception(Minfraud::InvalidInputError, /must not be a boolean/)
end

it "raises an exception for an invalid #{prefix}was_verification_successful" do
expect do
described_class.new(successful_field => 'true')
end.to raise_exception(Minfraud::InvalidInputError)
end

it "raises an exception for a non-boolean #{prefix}was_verification_successful" do
expect do
described_class.new(successful_field => 1)
end.to raise_exception(Minfraud::InvalidInputError)
end

it 'does not raise an exception for valid verification values' do
described_class.new(
time_field => '2026-10-01T14:30:00Z',
successful_field => false,
)
end
end
end
Loading