fix(captain): treat missing checkbox attributes as false in audience matcher

An unset checkbox custom attribute now matches equal_to false and
not_equal_to true instead of never matching.
This commit is contained in:
Pranav
2026-07-14 18:38:27 -07:00
parent 3cd3d653a5
commit fef3190be5
2 changed files with 19 additions and 1 deletions
@@ -72,11 +72,17 @@ class Captain::AudienceMatcher
# ignore the "+" prefix, and text compares case-insensitively.
def value_equal?(key, actual, expected)
return Array(actual).include?(expected) if key == 'labels'
return ActiveModel::Type::Boolean.new.cast(expected) == actual if [true, false].include?(actual)
return ActiveModel::Type::Boolean.new.cast(expected) == (actual == true) if boolean_condition?(actual, expected)
normalize(key, actual) == normalize(key, expected)
end
# An unset checkbox attribute counts as false; the expected value identifies
# the condition as boolean when the attribute is missing.
def boolean_condition?(actual, expected)
[true, false].include?(actual) || (actual.nil? && %w[true false].include?(expected.to_s))
end
def normalize(key, value)
return value if value.nil?
return "+#{value.to_s.delete('+')}" if key == 'phone_number'
@@ -37,6 +37,18 @@ RSpec.describe Captain::AudienceMatcher do
expect(matches?(leaf('plan_tier', 'not_equal_to', 'free'))).to be(true)
end
it 'matches checkbox custom attributes' do
contact.update!(custom_attributes: contact.custom_attributes.merge('newsletter_opt_in' => true))
expect(matches?(leaf('newsletter_opt_in', 'equal_to', 'true'))).to be(true)
expect(matches?(leaf('newsletter_opt_in', 'equal_to', 'false'))).to be(false)
end
it 'treats a missing checkbox attribute as false' do
expect(matches?(leaf('newsletter_opt_in', 'equal_to', 'false'))).to be(true)
expect(matches?(leaf('newsletter_opt_in', 'equal_to', 'true'))).to be(false)
expect(matches?(leaf('newsletter_opt_in', 'not_equal_to', 'true'))).to be(true)
end
it 'supports contains / starts_with on text' do
expect(matches?(leaf('email', 'contains', contact.email[2..5]))).to be(true)
expect(matches?(leaf('city', 'starts_with', 'Bos'))).to be(true)