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 app/models/in_person_enrollment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class InPersonEnrollment < ApplicationRecord
primary_key: 'issuer',
inverse_of: :in_person_enrollments,
optional: true

has_one :notification_phone_configuration, dependent: :destroy, inverse_of: :in_person_enrollment

enum status: {
establishing: 0,
pending: 1,
Expand Down
20 changes: 20 additions & 0 deletions app/models/notification_phone_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class NotificationPhoneConfiguration < ApplicationRecord
include EncryptableAttribute

belongs_to :in_person_enrollment, inverse_of: :notification_phone_configuration
validates :encrypted_phone, presence: true

encrypted_attribute(name: :phone)

def formatted_phone
PhoneFormatter.format(phone)
end

def masked_phone
PhoneFormatter.mask(phone)
end

def friendly_name
:phone
end
end
7 changes: 2 additions & 5 deletions app/models/phone_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ class PhoneConfiguration < ApplicationRecord
enum delivery_preference: { sms: 0, voice: 1 }

def formatted_phone
Phonelib.parse(phone).international
PhoneFormatter.format(phone)
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.

It took me some digging, but I'm confident that these two are equivalent; PhoneFormatter.format sends 'US' as the country_code, which the code you're replacing does not, but we set Phonelib's default country to 'US' in config/initializers/phonelib.rb.

end

def masked_phone
return '' if phone.blank?

formatted = Phonelib.parse(phone).national
formatted[0..-5].gsub(/\d/, '*') + formatted[-4..-1]
PhoneFormatter.mask(phone)
end

def selection_presenters
Expand Down
7 changes: 7 additions & 0 deletions app/services/phone_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@ def self.format(phone, country_code: nil)
country_code = DEFAULT_COUNTRY if country_code.nil? && !phone&.start_with?('+')
Phonelib.parse(phone, country_code)&.international
end

def self.mask(phone)
return '' if phone.blank?

formatted = Phonelib.parse(phone).national
formatted[0..-5].gsub(/\d/, '*') + formatted[-4..-1]
Comment on lines +10 to +13
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This could use some fresh eyes -- for now I've just copied the implementation over from PhoneConfiguration. A separate PR/ticket will refresh this

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.

it looks good to me. What will the purpose of the refresh be?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It has a different interface from the above format method -- this one returns an empty string for invalid phone numbers and format returns nil. And this one is focused on national numbers whereas format has explicit support for international. That's all I have in mind

end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddNotificationPhoneConfigurationsTable < ActiveRecord::Migration[7.0]
def change
create_table :notification_phone_configurations do |t|
t.references :in_person_enrollment, null: false, index: { name: 'index_notification_phone_configurations_on_enrollment_id', unique: true }
t.text :encrypted_phone, null: false, comment: 'Encrypted phone number to send notifications to'
t.timestamps
end
end
end
10 changes: 9 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_06_22_142018) do
ActiveRecord::Schema[7.0].define(version: 2023_06_27_213457) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pgcrypto"
Expand Down Expand Up @@ -371,6 +371,14 @@
t.index ["issuer", "year_month", "user_id"], name: "index_monthly_auth_counts_on_issuer_and_year_month_and_user_id", unique: true
end

create_table "notification_phone_configurations", force: :cascade do |t|
t.bigint "in_person_enrollment_id", null: false
t.text "encrypted_phone", null: false, comment: "Encrypted phone number to send notifications to"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["in_person_enrollment_id"], name: "index_notification_phone_configurations_on_enrollment_id", unique: true
end

create_table "partner_account_statuses", force: :cascade do |t|
t.string "name", null: false
t.integer "order", null: false
Expand Down
8 changes: 8 additions & 0 deletions spec/factories/notification_phone_configurations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FactoryBot.define do
Faker::Config.locale = :en

factory :notification_phone_configuration do
phone { '+1 202-555-1212' }
in_person_enrollment { association :in_person_enrollment }
end
end
62 changes: 62 additions & 0 deletions spec/models/notification_phone_configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'rails_helper'

RSpec.describe NotificationPhoneConfiguration do
describe 'Associations' do
it { is_expected.to belong_to(:in_person_enrollment) }
it { is_expected.to validate_presence_of(:encrypted_phone) }
end

let(:phone) { '+1 703 555 1212' }

let(:notification_phone_configuration) { create(:notification_phone_configuration, phone: phone) }

describe 'creation' do
it 'stores an encrypted form of the phone number' do
expect(notification_phone_configuration.encrypted_phone).to_not be_blank
end
end

describe 'encrypted attributes' do
it 'decrypts phone' do
expect(notification_phone_configuration.phone).to eq phone
end

context 'with unnormalized phone' do
let(:phone) { ' 555 555 5555 ' }
let(:normalized_phone) { '555 555 5555' }

it 'normalizes phone' do
expect(notification_phone_configuration.phone).to eq normalized_phone
end
end
end

describe '#masked_phone' do
let(:notification_phone_configuration) do
build(:notification_phone_configuration, phone: phone)
end
let(:phone) { '+1 703 555 1212' }

subject(:masked_phone) { notification_phone_configuration.masked_phone }

it 'masks the phone number, leaving the last 4 digits' do
expect(masked_phone).to eq('(***) ***-1212')
end

context 'with a blank phone number' do
let(:phone) { ' ' }

it 'is the empty string' do
expect(masked_phone).to eq('')
end
end

context 'with an international number' do
let(:phone) { '+212 636-023853' }

it 'keeps the groupings and leaves the last 4 digits' do
expect(masked_phone).to eq('****-**3853')
end
end
end
end
20 changes: 20 additions & 0 deletions spec/services/phone_formatter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,24 @@
expect(formatted_phone).to be_nil
end
end

describe '#mask' do
it 'masks all but the last four digits' do
phone = '+1 703 555 1212'
masked_phone = PhoneFormatter.mask(phone)
expect(masked_phone).to eq('(***) ***-1212')
end

it 'masks all but the last four digits of formatted international numbers' do
phone = '+212 636-023853'
masked_phone = PhoneFormatter.mask(phone)
expect(masked_phone).to eq('****-**3853')
end

it 'returns an empty string for a blank phone number' do
phone = ' '
masked_phone = PhoneFormatter.mask(phone)
expect(masked_phone).to eq('')
end
end
end