Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cf122a2
Allow setting code_sent_at on GpoConfirmationCode factories
matthinz Oct 20, 2023
876cbd6
Add migration to add expiration sent notice date
matthinz Oct 20, 2023
eb85115
Add query to find codes to send expiry notices for
matthinz Oct 24, 2023
478d993
Basic mail sending + analytics with placeholder content
matthinz Oct 24, 2023
4270527
Fill in gpo_code_expired email template
matthinz Oct 24, 2023
60f2dfd
Don't hardcode app_name in translations
matthinz Oct 25, 2023
0c5a449
Add mailer spec
matthinz Oct 25, 2023
1c80225
Cron configuration for expired gpo code notifications
matthinz Oct 25, 2023
44a6488
Refine query a little bit
matthinz Oct 25, 2023
fb84072
Update sliding expiration window logic
matthinz Oct 27, 2023
a8dcfa4
changelog: User-Facing Improvements, Identity verification, Notify us…
matthinz Oct 27, 2023
ac51ba2
Use code_sent_at for date in email
matthinz Oct 27, 2023
9d6c19a
Fix UserMailerPreview
matthinz Oct 27, 2023
6848b16
Use a range to track sending window bounds
matthinz Oct 30, 2023
b992712
Refactor big gross query into methods
matthinz Oct 30, 2023
13f5bdd
Don't hardcode name of GpoConfirmationCode table
matthinz Oct 30, 2023
8f1a68b
Update config/initializers/job_configurations.rb
matthinz Oct 30, 2023
8fa8a7d
Merge branch 'main' into matthinz/9564-verify-by-mail-expiring
matthinz Nov 1, 2023
a60f2b2
Use `and` and `where` for consistency in first criteria
matthinz Nov 1, 2023
99af297
Shorten criteria queries that reference profile
matthinz Nov 1, 2023
53c773b
Add a couple of clarifying comments
matthinz Nov 1, 2023
995f9a6
Use contain_exactly for array-related assertions
matthinz Nov 1, 2023
5d89724
Don't duplicate deactivation_reasons enum values
matthinz Nov 1, 2023
6c61149
Clarify some method names
matthinz Nov 1, 2023
54eb141
Add code_sent_at to GPO expiration email analytics event
matthinz Nov 1, 2023
9fb8790
Don't enable cron job yet.
matthinz Nov 1, 2023
e556d6b
Merge branch 'main' into matthinz/9564-verify-by-mail-expiring
matthinz Nov 1, 2023
0e58319
Propagate code_sent_at into user event created_at
matthinz Nov 1, 2023
a6a3cb0
Appease linter
matthinz Nov 1, 2023
e8c25c2
Whoops, fix created_at in user factory
matthinz Nov 1, 2023
f58f177
Fix variables that weren't renamed in test
matthinz Nov 1, 2023
072f4f4
Don't email users who are currently gpo rate limited
matthinz Nov 1, 2023
ab6c876
Actually check event_type when doing rate limit check
matthinz Nov 1, 2023
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
77 changes: 77 additions & 0 deletions app/jobs/send_gpo_code_expiration_notices_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
class SendGpoCodeExpirationNoticesJob < ApplicationJob
queue_as :low

def initialize(analytics: nil)
@analytics = analytics
end

def perform
codes_to_send_notifications_for.find_each do |code|
user = code.profile.user

user.send_email_to_all_addresses(:gpo_code_expired, code_sent_at: code.code_sent_at)

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.

As I commented on the ticket, we are assuming here that the config value max_mail_events_window_in_days, currently set to 30, will always be less than the code expiration, currently set to 31. I still think that we should check that assumption somewhere so we're not accidentally sending users emails inviting them to restart IdV if they're rate limited in the future when someone changes the config value.

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.

I added an additional condition, user_is_not_rate_limited_for_gpo in 072f4f4 (and fixed a bug in it in ab6c876). The idea is that we just don't email folks who are currently rate limited for GPO.

code.update(expiration_notice_sent_at: Time.zone.now)

analytics.idv_gpo_expiration_email_sent(user_id: user.uuid)
end
end

def codes_to_send_notifications_for
# We are looking at a 48 hr window in which all codes will _definitely_ be expired.
from, to = calculate_notification_window_bounds
expired_codes_needing_notification_sent_between(
from: from,
to: to,
)
Comment thread
matthinz marked this conversation as resolved.
Outdated
end

def calculate_notification_window_bounds(as_of: Time.zone.now)
to = as_of.beginning_of_day - IdentityConfig.store.usps_confirmation_max_days.days
from = to - 2.days
[from, to]
Comment thread
matthinz marked this conversation as resolved.
Outdated
end

private

def analytics
@analytics ||= Analytics.new(user: AnonymousUser.new, request: nil, session: {}, sp: nil)
end

def expired_codes_needing_notification_sent_between(
from:,
to:
)
GpoConfirmationCode.joins(:profile).
# 1. Exclude codes that we've already sent an expiration notice for
where(expiration_notice_sent_at: nil).

# 2. Exclude codes not sent in the window we're looking at
Comment thread
matthinz marked this conversation as resolved.
Outdated
where(code_sent_at: from...to).
Comment thread
matthinz marked this conversation as resolved.
Outdated

# 3. Exclude codes where the associated profile does not have the GPO pending timestamp set
# (meaning they either completed GPO or reset their password).
where.not(profile: { gpo_verification_pending_at: nil }).

# 4. Exclude codes where the associated profile has been deactivated for some reason
where(profile: { deactivation_reason: nil }).

# 5. Exclude codes where the user has since gotten an active profile (no point in notifying)
where.not(
profile: {
user_id: User.joins(:profiles).where(
profiles: {
active: true,
},
),
},
).

# 6. Only include codes that are the most recent code sent for the profile
where(
code_sent_at: GpoConfirmationCode.
select('max(code_sent_at)').
from('usps_confirmation_codes child').
where('child.profile_id = usps_confirmation_codes.profile_id'),
)
Comment thread
matthinz marked this conversation as resolved.
Outdated
end
end
10 changes: 10 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,16 @@ def gpo_reminder
end
end

def gpo_code_expired(code_sent_at:)
with_user_locale(user) do
@code_sent_at = I18n.l(
code_sent_at,
format: :event_date,
)
mail(to: email_address.email, subject: t('user_mailer.gpo_code_expired.subject'))
Comment thread
matthinz marked this conversation as resolved.
end
end

private

def email_should_receive_nonessential_notifications?(email)
Expand Down
4 changes: 2 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,12 @@ def analytics
@analytics ||= Analytics.new(user: self, request: nil, session: {}, sp: nil)
end

def send_email_to_all_addresses(user_mailer_template)
def send_email_to_all_addresses(user_mailer_template, **args)

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.

Minor change here to support forwarding args to UserMailer

confirmed_email_addresses.each do |email_address|
UserMailer.with(
user: self,
email_address: email_address,
).send(user_mailer_template).
).send(user_mailer_template, **args).
deliver_now_or_later
end
end
Expand Down
6 changes: 6 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,12 @@ def idv_gpo_confirm_start_over_visited(**extra)
track_event('IdV: gpo confirm start over visited', **extra) # rubocop:disable IdentityIdp/AnalyticsEventNameLinter
end

# A GPO expiration email was sent to the user
# @param [String] user_id UUID of user we sent an email to
def idv_gpo_expiration_email_sent(user_id:, **extra)
track_event(:idv_gpo_expiration_email_sent, user_id: user_id, **extra)
Comment thread
matthinz marked this conversation as resolved.
Outdated
end

# A GPO reminder email was sent to the user
# @param [String] user_id UUID of user who we sent a reminder to
def idv_gpo_reminder_email_sent(user_id:, **extra)
Expand Down
41 changes: 41 additions & 0 deletions app/views/user_mailer/gpo_code_expired.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

<%= t(
'user_mailer.gpo_code_expired.body.intro',
code_sent_at: @code_sent_at,
app_name: APP_NAME,
) %>

<%= link_to(
t('user_mailer.gpo_code_expired.body.learn_more_link'),
help_center_redirect_url(
category: 'verify-your-identity',
article: 'verify-your-address-by-mail',
flow: :idv,
step: :gpo_send_letter,
),
{ style: "text-decoration: 'underline'" },
) %>

<table class="button expanded large radius">
<tbody>
<tr>
<td>
<table style="margin-bottom: 1em; margin-top: 1em">
<tbody>
<tr>
<td style="text-align: center">
<%= link_to t('user_mailer.gpo_code_expired.body.cta'),
idv_url,
target: '_blank',
class: 'float-center',
align: 'center',
rel: 'noopener' %>
</td>
</tr>
</tbody>
</table>
</td>
<td class="expander"></td>
</tr>
</tbody>
</table>
6 changes: 6 additions & 0 deletions config/initializers/job_configurations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@
cron: cron_24h,
args: -> { [14.days.ago] },
},
# Send emails when unused GPO verification codes have expired
send_gpo_code_expiration_notices: {
class: 'SendGpoCodeExpirationNoticesJob',
cron: cron_24h,
args: -> { [] },
Comment thread
matthinz marked this conversation as resolved.
Outdated
},
Comment thread
matthinz marked this conversation as resolved.
Outdated
# Monthly report checking in on key metrics
monthly_key_metrics_report: {
class: 'Reports::MonthlyKeyMetricsReport',
Expand Down
8 changes: 8 additions & 0 deletions config/locales/user_mailer/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ en:
help_html: If you did not want to delete this email address, please visit the
%{app_name_html} %{help_link_html} or %{contact_link_html}.
subject: Email address deleted
gpo_code_expired:
body:
cta: Start verifying your identity
intro: You requested a letter with a verification code on %{code_sent_at}. This
verification code has now expired. Sign back into %{app_name} to start
verifying your identity again.
learn_more_link: Learn more about verifying your address by mail.
subject: Your verification code has expired
help_link_text: Help Center
in_person_completion_survey:
body:
Expand Down
9 changes: 9 additions & 0 deletions config/locales/user_mailer/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ es:
help_html: Si no desea eliminar esta dirección de correo electrónico, visite el
%{app_name_html} %{help_link_html} o el %{contact_link_html}.
subject: Dirección de correo electrónico eliminada
gpo_code_expired:
body:
cta: Comenzar a verificar su identidad
intro: Solicitó una carta con un código de verificación el %{code_sent_at}. Este
código de verificación ya expiró. Vuelva a iniciar sesión en
%{app_name} para comenzar a verificar su identidad nuevamente.
learn_more_link: Obtenga más información sobre la verificación de su dirección
por correo.
subject: Su código de verificación ha caducado
help_link_text: Centro de Ayuda
in_person_completion_survey:
body:
Expand Down
8 changes: 8 additions & 0 deletions config/locales/user_mailer/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ fr:
veuillez visiter le %{help_link_html} de %{app_name_html} ou
%{contact_link_html}.
subject: Adresse email supprimée
gpo_code_expired:
body:
cta: Commencez à vérifier votre identité
intro: Vous avez demandé une lettre avec un code de vérification le
%{code_sent_at}. Ce code de vérification a expiré. Reconnectez-vous à
%{app_name} pour reprendre la vérification de votre identité.
learn_more_link: En savoir plus sur la vérification de votre adresse par courrier.
subject: Votre code de vérification a expiré
help_link_text: Centre d’aide
in_person_completion_survey:
body:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddExpirationNoticeSentAtToUspsConfirmationCodes < ActiveRecord::Migration[7.0]
disable_ddl_transaction!

def change
add_column :usps_confirmation_codes, :expiration_notice_sent_at, :datetime, precision: nil
add_index :usps_confirmation_codes, :expiration_notice_sent_at, algorithm: :concurrently
Comment on lines +5 to +6

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.

Migration to add an indexed column to usps_confirmation_codes to track the date/time we sent an expiration notice for a single code.

end
end
5 changes: 4 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_08_31_124437) do
ActiveRecord::Schema[7.0].define(version: 2023_10_18_230656) 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 @@ -621,6 +621,9 @@
t.datetime "updated_at", precision: nil, null: false
t.datetime "bounced_at", precision: nil
t.datetime "reminder_sent_at", precision: nil
t.datetime "expiration_notice_sent_at", precision: nil
t.index ["code_sent_at"], name: "ix_code_sent_at"
t.index ["expiration_notice_sent_at"], name: "index_usps_confirmation_codes_on_expiration_notice_sent_at"
t.index ["otp_fingerprint"], name: "index_usps_confirmation_codes_on_otp_fingerprint"
t.index ["profile_id"], name: "index_usps_confirmation_codes_on_profile_id"
t.index ["reminder_sent_at"], name: "index_usps_confirmation_codes_on_reminder_sent_at"
Expand Down
1 change: 1 addition & 0 deletions spec/factories/gpo_confirmation_codes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
factory :gpo_confirmation_code do
profile
otp_fingerprint { Pii::Fingerprinter.fingerprint('ABCDE12345') }
code_sent_at { 1.day.ago }
end
end
28 changes: 22 additions & 6 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,28 @@
end

trait :with_pending_gpo_profile do
after :build do |user|
profile = create(:profile, :with_pii, gpo_verification_pending_at: 1.day.ago, user: user)
gpo_code = create(:gpo_confirmation_code)
profile.gpo_confirmation_codes << gpo_code
device = create(:device, user: user)
create(:event, user: user, device: device, event_type: :gpo_mail_sent)
transient do
code_sent_at { 1.day.ago }
end

after :create do |user, context|
profile = create(
:profile,
:with_pii,
gpo_verification_pending_at: context.code_sent_at,
user: user,
)
create(
:gpo_confirmation_code,
profile: profile,
code_sent_at: context.code_sent_at,
)
create(
:event,
user: user,
device: create(:device, user: user),
event_type: :gpo_mail_sent,
)
end
end

Expand Down
Loading