Skip to content

Commit 10dc95d

Browse files
committed
Add test for service
1 parent ce88d13 commit 10dc95d

File tree

2 files changed

+350
-12
lines changed

2 files changed

+350
-12
lines changed

app/services/upsert_source_into_campaign_contacts.rb

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ def self.call(**kwargs)
33
new(**kwargs).call
44
end
55

6-
def initialize(source:, source_id:, first_name:, last_name:, email:, phone:, email_opt_in:, sms_opt_in:, locale: "en", state_file_ref: [])
6+
def initialize(source:, source_id:, first_name:, last_name:, email:, phone:, email_opt_in:, sms_opt_in:, locale: "en", state_file_ref: nil)
77
@source = source
88
@source_id = source_id
99
@first_name = first_name
@@ -19,19 +19,19 @@ def initialize(source:, source_id:, first_name:, last_name:, email:, phone:, ema
1919
def call
2020
contact = find_contact || CampaignContact.new
2121

22-
contact.email_address = @email if @email.present?
23-
contact.sms_phone_number ||= @phone
22+
contact.email_address = @email unless @email.blank?
23+
contact.sms_phone_number = @phone unless @phone.blank?
2424
contact.first_name = choose_name(contact.first_name, @first_name, source: @source)
2525
contact.last_name = choose_name(contact.last_name, @last_name, source: @source)
2626
contact.email_notification_opt_in = contact.email_notification_opt_in || @email_opt_in
2727
contact.sms_notification_opt_in = contact.sms_notification_opt_in || @sms_opt_in
28-
contact.locale ||= @locale
28+
contact.locale = @locale unless @locale.blank?
2929

3030
case @source
3131
when :gyr
32-
contact.gyr_intake_ids = (contact.gyr_intake_ids + [@source_id]).uniq
32+
contact.gyr_intake_ids = ((contact.gyr_intake_ids || []) + [@source_id]).uniq
3333
when :signup
34-
contact.sign_up_ids = (contact.sign_up_ids + [@source_id]).uniq
34+
contact.sign_up_ids = ((contact.sign_up_ids || []) + [@source_id]).uniq
3535
end
3636

3737
if @state_file_ref.present?
@@ -40,17 +40,18 @@ def call
4040
contact.state_file_intake_refs = refs
4141
end
4242

43-
contact.save!
43+
contact.tap(&:save!)
4444
end
4545

4646
private
4747

4848
def find_contact
49-
if @email.present?
50-
CampaignContact.find_by(email_address: @email)
51-
elsif @phone.present? && (!@email_opt_in && @email.blank?)
52-
CampaignContact.find_by(sms_phone_number: @phone)
53-
end
49+
return CampaignContact.find_by(email_address: @email) if @email.present?
50+
51+
return unless @phone.present?
52+
return if @email_opt_in
53+
54+
CampaignContact.find_by(sms_phone_number: @phone, email_address: nil)
5455
end
5556

5657
def choose_name(existing, incoming, source:)
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
require "rails_helper"
2+
3+
RSpec.describe UpsertSourceIntoCampaignContacts do
4+
describe ".call" do
5+
subject(:call_service) do
6+
described_class.call(
7+
source: source,
8+
source_id: source_id,
9+
first_name: first_name,
10+
last_name: last_name,
11+
email: email,
12+
phone: phone,
13+
email_opt_in: email_opt_in,
14+
sms_opt_in: sms_opt_in,
15+
locale: locale,
16+
state_file_ref: state_file_ref
17+
)
18+
end
19+
20+
let(:source) { :gyr }
21+
let(:source_id) { 123 }
22+
let(:first_name) { "NewFirst" }
23+
let(:last_name) { "NewLast" }
24+
let(:email) { "[email protected]" }
25+
let(:phone) { "+15551234567" }
26+
let(:email_opt_in) { true }
27+
let(:sms_opt_in) { true }
28+
let(:locale) { "es" }
29+
let(:state_file_ref) { [] }
30+
31+
context "when no existing contact matches" do
32+
it "creates a new CampaignContact" do
33+
expect { call_service }.to change(CampaignContact, :count).by(1)
34+
end
35+
36+
it "sets email/phone when present" do
37+
call_service
38+
contact = CampaignContact.last
39+
expect(contact.email_address).to eq("[email protected]")
40+
expect(contact.sms_phone_number).to eq("+15551234567")
41+
end
42+
43+
it "updates the names" do
44+
contact = call_service
45+
expect(contact.first_name).to eq("NewFirst")
46+
expect(contact.last_name).to eq("NewLast")
47+
end
48+
49+
it "sets opt-ins from incoming when contact has false/nil" do
50+
contact = call_service
51+
expect(contact.email_notification_opt_in).to eq(true)
52+
expect(contact.sms_notification_opt_in).to eq(true)
53+
end
54+
55+
it "sets locale when provided" do
56+
contact = call_service
57+
expect(contact.locale).to eq("es")
58+
end
59+
60+
context "when source is :gyr" do
61+
let(:source) { :gyr }
62+
let(:source_id) { 111 }
63+
64+
it "adds source_id to gyr_intake_ids (unique)" do
65+
contact = call_service
66+
expect(contact.gyr_intake_ids).to match_array([111])
67+
expect(contact.sign_up_ids).to be_blank.or eq([])
68+
end
69+
end
70+
71+
context "when source is :signup" do
72+
let(:source) { :signup }
73+
let(:source_id) { 222 }
74+
75+
it "adds source_id to sign_up_ids (unique)" do
76+
contact = call_service
77+
expect(contact.sign_up_ids).to match_array([222])
78+
expect(contact.gyr_intake_ids).to be_blank.or eq([])
79+
end
80+
end
81+
end
82+
83+
context "when an existing contact matches by email" do
84+
let!(:existing) do
85+
create(
86+
:campaign_contact,
87+
email_address: "[email protected]",
88+
sms_phone_number: "+19998887777",
89+
first_name: "ExistingFirst",
90+
last_name: "ExistingLast",
91+
email_notification_opt_in: false,
92+
sms_notification_opt_in: true,
93+
locale: "en",
94+
gyr_intake_ids: [5],
95+
sign_up_ids: [9],
96+
state_file_intake_refs: []
97+
)
98+
end
99+
100+
it "does not create a new record" do
101+
expect { call_service }.not_to change(CampaignContact, :count)
102+
end
103+
104+
it "finds by email and overwrites email/phone only if incoming present" do
105+
contact = call_service
106+
expect(contact.id).to eq(existing.id)
107+
108+
expect(contact.email_address).to eq("[email protected]")
109+
expect(contact.sms_phone_number).to eq("+15551234567")
110+
end
111+
112+
context "when incoming email is blank" do
113+
let(:email) { "" }
114+
let(:phone) { "+19998887777" }
115+
let(:email_opt_in) { false }
116+
117+
before do
118+
existing.update!(email_address: nil)
119+
end
120+
121+
it "keeps the existing email address (nil) and does not overwrite" do
122+
contact = call_service
123+
expect(contact.email_address).to be_nil
124+
end
125+
end
126+
127+
context "when incoming phone is blank" do
128+
let(:phone) { nil }
129+
130+
it "keeps the existing phone number" do
131+
call_service
132+
contact = CampaignContact.last
133+
expect(contact.sms_phone_number).to eq("+19998887777")
134+
end
135+
end
136+
137+
context "name selection rules" do
138+
context "when source is :signup" do
139+
let(:source) { :signup }
140+
141+
it "keeps existing names when both existing and incoming are present" do
142+
call_service
143+
contact = CampaignContact.last
144+
expect(contact.first_name).to eq("ExistingFirst")
145+
expect(contact.last_name).to eq("ExistingLast")
146+
end
147+
end
148+
149+
context "when source is :gyr" do
150+
let(:source) { :gyr }
151+
152+
it "overwrites with incoming names when both existing and incoming are present" do
153+
call_service
154+
contact = CampaignContact.last
155+
expect(contact.first_name).to eq("NewFirst")
156+
expect(contact.last_name).to eq("NewLast")
157+
end
158+
end
159+
160+
context "when incoming name is blank" do
161+
let(:first_name) { "" }
162+
let(:last_name) { nil }
163+
164+
it "keeps existing names" do
165+
call_service
166+
contact = CampaignContact.last
167+
expect(contact.first_name).to eq("ExistingFirst")
168+
expect(contact.last_name).to eq("ExistingLast")
169+
end
170+
end
171+
172+
context "when existing name is blank" do
173+
before do
174+
existing.update!(first_name: nil, last_name: "")
175+
end
176+
177+
it "uses incoming names" do
178+
call_service
179+
contact = CampaignContact.last
180+
expect(contact.first_name).to eq("NewFirst")
181+
expect(contact.last_name).to eq("NewLast")
182+
end
183+
end
184+
end
185+
186+
it "opt-ins once true won't get overwritten" do
187+
contact = call_service
188+
expect(contact.email_notification_opt_in).to eq(true)
189+
expect(contact.sms_notification_opt_in).to eq(true)
190+
end
191+
192+
context "when incoming opt-ins are false" do
193+
let(:email_opt_in) { false }
194+
let(:sms_opt_in) { false }
195+
196+
it "does not turn off existing true opt-ins" do
197+
existing.update!(email_notification_opt_in: true, sms_notification_opt_in: true)
198+
contact = call_service
199+
expect(contact.email_notification_opt_in).to eq(true)
200+
expect(contact.sms_notification_opt_in).to eq(true)
201+
end
202+
end
203+
204+
context "when locale is nil" do
205+
let(:locale) { nil }
206+
207+
it "does not overwrite existing locale" do
208+
contact = call_service
209+
expect(contact.locale).to eq("en")
210+
end
211+
end
212+
213+
context "when source is :gyr" do
214+
let(:source) { :gyr }
215+
let(:source_id) { 5 }
216+
217+
it "adds source_id to gyr_intake_ids without duplicating" do
218+
call_service
219+
contact = CampaignContact.last
220+
expect(contact.gyr_intake_ids).to match_array([5])
221+
end
222+
end
223+
224+
context "when source is :signup" do
225+
let(:source) { :signup }
226+
let(:source_id) { 10 }
227+
228+
it "adds source_id to sign_up_ids without duplicating" do
229+
call_service
230+
contact = CampaignContact.last
231+
expect(contact.sign_up_ids).to match_array([9, 10])
232+
end
233+
end
234+
235+
context "state_file_ref behavior" do
236+
let(:state_file_ref) { { id: 1, type: "StateFileAzIntake" } }
237+
238+
it "appends the ref when not present yet" do
239+
call_service
240+
contact = CampaignContact.last
241+
expect(contact.state_file_intake_refs).to include(hash_including("id" => 1, "type" => "StateFileAzIntake"))
242+
end
243+
244+
it "does not add a duplicate ref with same id+type" do
245+
existing.update!(state_file_intake_refs: [{ "id" => 1, "type" => "StateFileAzIntake" }])
246+
247+
call_service
248+
contact = CampaignContact.last
249+
matches = contact.state_file_intake_refs.select { |r| r["id"] == 1 && r["type"] == "StateFileAzIntake" }
250+
expect(matches.length).to eq(1)
251+
end
252+
253+
it "allows another ref if id matches but type differs" do
254+
existing.update!(state_file_intake_refs: [{ "id" => 1, "type" => "StateFileNyIntake" }])
255+
256+
call_service
257+
contact = CampaignContact.last
258+
expect(contact.state_file_intake_refs).to include({ "id" => 1, "type" => "StateFileNyIntake" })
259+
expect(contact.state_file_intake_refs).to include(hash_including("id" => 1, "type" => "StateFileAzIntake"))
260+
end
261+
end
262+
end
263+
264+
context "when matching by phone" do
265+
let(:source) { :gyr }
266+
let(:source_id) { 123 }
267+
let(:first_name) { "NewFirst" }
268+
let(:last_name) { "NewLast" }
269+
let(:email) { nil }
270+
let(:phone) { "+15551234567" }
271+
let(:sms_opt_in) { true }
272+
let(:locale) { "en" }
273+
let(:state_file_ref) { nil }
274+
275+
let!(:existing) do
276+
create(
277+
:campaign_contact,
278+
email_address: nil,
279+
sms_phone_number: "+15551234567",
280+
first_name: "ExistingFirst",
281+
last_name: "ExistingLast",
282+
email_notification_opt_in: false,
283+
sms_notification_opt_in: false
284+
)
285+
end
286+
287+
context "and email_opt_in is false" do
288+
let(:email_opt_in) { false }
289+
290+
it "finds by sms_phone_number (only among email-less contacts) and does not create a new record" do
291+
expect { call_service }.not_to change(CampaignContact, :count)
292+
293+
contact = call_service
294+
expect(contact.id).to eq(existing.id)
295+
expect(contact.sms_phone_number).to eq("+15551234567")
296+
end
297+
end
298+
299+
context "and email_opt_in is true" do
300+
let(:email_opt_in) { true }
301+
302+
it "does not match by phone and creates a new contact" do
303+
expect { call_service }.to change(CampaignContact, :count).by(1)
304+
305+
contact = call_service
306+
expect(contact.id).not_to eq(existing.id)
307+
expect(contact.sms_phone_number).to eq("+15551234567")
308+
end
309+
end
310+
311+
context "and the only phone match has an email" do
312+
let(:email_opt_in) { false }
313+
314+
before do
315+
existing.update!(email_address: "[email protected]")
316+
end
317+
318+
it "does not match (because find_contact requires email_address nil) and creates a new contact" do
319+
expect { call_service }.to change(CampaignContact, :count).by(1)
320+
end
321+
end
322+
end
323+
324+
context "persistence" do
325+
it "saves the contact (bang) and returns it" do
326+
call_service
327+
contact = CampaignContact.last
328+
expect(contact).to be_persisted
329+
end
330+
331+
it "raises if save! fails" do
332+
allow_any_instance_of(CampaignContact).to receive(:save!).and_raise(ActiveRecord::RecordInvalid.new(CampaignContact.new))
333+
expect { call_service }.to raise_error(ActiveRecord::RecordInvalid)
334+
end
335+
end
336+
end
337+
end

0 commit comments

Comments
 (0)