From a24f5a3e7ac8077b2a8214137fa44c239949adc0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 12 Aug 2026 15:28:49 +0530 Subject: [PATCH] fix: anchor contact phone number validation (#15415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contact phone numbers with stray text in front of them, like `abc+12312312321`, were saving successfully instead of being rejected as invalid. Agents could end up with unusable numbers on a contact, and the same values were persisted rather than discarded when captured through the live chat widget. ## How to reproduce 1. Open a contact and edit its details. 2. Set the phone number to `abc+12312312321` via the API (`PATCH /api/v1/accounts/:id/contacts/:id`). 3. Before this change the update succeeds. Now it fails validation. ## What changed The E.164 format check was missing a leading `\A` anchor, so Rails matched it anywhere in the string and accepted any prefix ahead of a valid number. Both the validation and the `phone_number_format` fallback used by `discard_invalid_attrs` are now anchored, so the widget path discards these values instead of storing them. Contacts already holding a prefixed number will now fail validation on their next save. Worth a count on production first: ```sql SELECT count(*) FROM contacts WHERE phone_number !~ '^\+[1-9][0-9]{1,14}$' AND phone_number <> ''; ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- app/models/contact.rb | 4 ++-- spec/actions/contact_identify_action_spec.rb | 7 +++++++ spec/models/contact_spec.rb | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/models/contact.rb b/app/models/contact.rb index 3b04e6bc1..a63b164bb 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -53,7 +53,7 @@ class Contact < ApplicationRecord validates :identifier, allow_blank: true, uniqueness: { scope: [:account_id] } validates :phone_number, allow_blank: true, uniqueness: { scope: [:account_id] }, - format: { with: /\+[1-9]\d{1,14}\z/, message: I18n.t('errors.contacts.phone_number.invalid') } + format: { with: /\A\+[1-9]\d{1,14}\z/, message: I18n.t('errors.contacts.phone_number.invalid') } belongs_to :account has_many :conversations, dependent: :destroy_async @@ -206,7 +206,7 @@ class Contact < ApplicationRecord def phone_number_format return if phone_number.blank? - self.phone_number = phone_number_was unless phone_number.match?(/\+[1-9]\d{1,14}\z/) + self.phone_number = phone_number_was unless phone_number.match?(/\A\+[1-9]\d{1,14}\z/) end def email_format diff --git a/spec/actions/contact_identify_action_spec.rb b/spec/actions/contact_identify_action_spec.rb index 76b370f35..8e91ce0a9 100644 --- a/spec/actions/contact_identify_action_spec.rb +++ b/spec/actions/contact_identify_action_spec.rb @@ -144,6 +144,13 @@ describe ContactIdentifyAction do expect(contact.reload.name).to eq 'new name' expect(contact.phone_number).to be_nil end + + it 'discards a phone number that has text prefixed to a valid number' do + params = { phone_number: 'abc+12312312321', name: 'new name' } + described_class.new(contact: contact, params: params, discard_invalid_attrs: true).perform + expect(contact.reload.name).to eq 'new name' + expect(contact.phone_number).to be_nil + end end context 'when params have not changed' do diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb index 159454eec..fcd362be8 100644 --- a/spec/models/contact_spec.rb +++ b/spec/models/contact_spec.rb @@ -65,6 +65,11 @@ RSpec.describe Contact do expect { contact.update!(phone_number: '123456789') }.to raise_error(ActiveRecord::RecordInvalid) end + it 'will throw error when text is prefixed to a valid phone number' do + contact = create(:contact) + expect { contact.update!(phone_number: 'abc+12312312321') }.to raise_error(ActiveRecord::RecordInvalid) + end + it 'updates phone number when adding valid phone number' do contact = create(:contact) expect(contact.update!(phone_number: '+12312312321')).to be true