Skip to content
Open
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
12 changes: 9 additions & 3 deletions app/controllers/reimbursements_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ def change_complete_status
@grouped_case_contacts = fetch_reimbursements
.where({occurred_at: @case_contact.occurred_at, creator_id: @case_contact.creator_id})
@grouped_case_contacts.update_all(reimbursement_params.to_h)
notification_recipients = [@case_contact.creator]
notification_recipients << @case_contact.supervisor if @case_contact.supervisor
ReimbursementCompleteNotifier.with(case_contact: @case_contact).deliver(notification_recipients)

# This action also backs mark_as_needs_review, so only notify when
# actually marking complete (cast, since the value's format varies).
if ActiveModel::Type::Boolean.new.cast(reimbursement_params[:reimbursement_complete])
notification_recipients = [@case_contact.creator]
notification_recipients << @case_contact.supervisor if @case_contact.supervisor
ReimbursementCompleteNotifier.with(case_contact: @case_contact).deliver(notification_recipients)
end

redirect_to reimbursements_path unless params[:ajax]
end

Expand Down
7 changes: 7 additions & 0 deletions app/mailers/volunteer_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ def case_contacts_reminder(user, cc_recipients)
@casa_organization = user.casa_org
mail(to: @user.email, cc: cc_recipients, subject: "Reminder to input case contacts")
end

def reimbursement_complete_email(user, case_contact)
@user = user
@case_contact = case_contact
@casa_organization = @user.casa_org
mail(to: @user.email, subject: "Your reimbursement request has been processed")
end
end
16 changes: 10 additions & 6 deletions app/notifications/reimbursement_complete_notifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
# ReimbursementCompleteNotifier.with(case_contact: @case_contact).deliver(current_user)
#
class ReimbursementCompleteNotifier < BaseNotifier
# deliver_by :email do |config|
# config.mailer = "UserMailer"
# ...
# end
# deliver_by :slack
# deliver_by :custom, class: "MyDeliveryMethod"
# This notifier is delivered to both the volunteer who submitted the
# reimbursement and their supervisor (see ReimbursementsController#change_complete_status).
# The email delivery is scoped to volunteer recipients only, since the
# supervisor already knows they just marked it complete.
deliver_by :email do |config|
config.mailer = "VolunteerMailer"
config.method = "reimbursement_complete_email"
config.args = -> { [recipient, params[:case_contact]] }
config.if = -> { recipient.volunteer? && recipient.receive_email_notifications? }
end

required_param :case_contact

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<meta itemprop="name" content="Reimbursement Processed" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<style>/* Email styles need to be inline */</style>
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<tr style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
Hello <%= @user.display_name %>,
</td>
</tr>
<tr style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
Your reimbursement request for <%= @case_contact.miles_driven %>mi
<% if @case_contact.reimbursement_amount %>
(<%= number_to_currency(@case_contact.reimbursement_amount) %>)
<% end %>
on <%= @case_contact.occurred_at_display %> has been processed and is en route.
</td>
</tr>
<tr style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
You can view your case contacts and their reimbursement status by visiting
<a href="<%= "#{case_contacts_url(casa_case_id: @case_contact.casa_case_id)}" %>" target="_blank">this link</a>.
</td>
</tr>
<tr style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
If you have any questions, please contact your most recent CASA supervisor for assistance.
</td>
</tr>
</table>
13 changes: 13 additions & 0 deletions lib/mailers/debug_preview_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,17 @@ def invalid_user(role)
body: "User does not exist or is not a #{role}"
)
end

# Use when the looked-up user is valid but has no related record to preview
# with, e.g. a volunteer with no case contact wanting reimbursement. Keeps
# the "missing_#{role}@example.com" convention so existing preview specs
# asserting on that address don't need to change.
def no_data(role)
mail(
from: "reply@example.com",
to: "missing_#{role}@example.com",
subject: "no_preview_data",
body: "No #{role} record was found to preview"
)
end
end
18 changes: 18 additions & 0 deletions lib/mailers/previews/volunteer_mailer_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,23 @@ def case_contacts_reminder
VolunteerMailer.court_report_reminder(volunteer, true)
end
end

# Unlike the other previews above, a valid volunteer here isn't enough on
# its own, they also need a case contact wanting reimbursement to render
# against. If they don't have one, DebugPreviewMailer.no_data renders that
# distinctly from invalid_user, which is reserved for a failed ID lookup.
def reimbursement_complete_email
volunteer = params.has_key?(:id) ? Volunteer.find_by(id: params[:id]) : Volunteer.last
if volunteer.nil?
DebugPreviewMailer.invalid_user("volunteer")
else
case_contact = CaseContact.where(creator: volunteer, want_driving_reimbursement: true).last
if case_contact.nil?
DebugPreviewMailer.no_data("case_contact")
else
VolunteerMailer.reimbursement_complete_email(volunteer, case_contact)
end
end
end
end
# :nocov:
35 changes: 35 additions & 0 deletions spec/mailers/previews/volunteer_mailer_preview_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,39 @@
it { expect(email.to).to eq ["missing_volunteer@example.com"] }
end
end

describe "#reimbursement_complete_email" do
let!(:case_contact) { create(:case_contact, :wants_reimbursement, creator: volunteer) }

context "When no ID is passed" do
let(:preview) { described_class.new }
let(:email) { preview.reimbursement_complete_email }

it { expect(email.to).to eq [volunteer.email] }
end

context "When passed ID is valid" do
let(:preview) { described_class.new(id: volunteer.id) }
let(:email) { preview.reimbursement_complete_email }

it { expect(email.to).to eq [volunteer.email] }
end

context "When passed ID is invalid" do
let(:preview) { described_class.new(id: -1) }
let(:email) { preview.reimbursement_complete_email }

it { expect(email.to).to eq ["missing_volunteer@example.com"] }
end

context "When the volunteer has no case contact wanting reimbursement" do
let(:other_volunteer) { create(:volunteer) }
let(:preview) { described_class.new(id: other_volunteer.id) }
let(:email) { preview.reimbursement_complete_email }

it "does not fall back to a different volunteer's case contact" do
expect(email.to).to eq ["missing_case_contact@example.com"]
end
end
end
end
35 changes: 35 additions & 0 deletions spec/mailers/volunteer_mailer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,41 @@
end
end

describe ".reimbursement_complete_email" do
let(:case_contact) { create(:case_contact, :wants_reimbursement, creator: volunteer) }
let(:mail) { VolunteerMailer.reimbursement_complete_email(volunteer, case_contact) }

it "sends an email confirming the reimbursement was processed" do
expect(mail.to).to eq([volunteer.email])
expect(mail.subject).to eq("Your reimbursement request has been processed")
expect(mail.body.encoded).to match("#{case_contact.miles_driven}mi")
expect(mail.body.encoded).to match(case_contact.occurred_at_display)
end

context "when the casa org has a mileage rate" do
let!(:mileage_rate) { create(:mileage_rate, casa_org: case_contact.casa_case.casa_org, amount: 6.50, effective_date: 3.days.ago) }

it "includes the reimbursement amount" do
# Bare "$" is a regex end-anchor via String#match, not a literal dollar sign.
expect(mail.body.encoded).to match(/\$[\d,]+\.\d{2}/)
end
end

context "when the casa org has no mileage rate" do
it "omits the reimbursement amount rather than rendering a blank or nil value" do
expect(mail.body.encoded).not_to match(/\$[\d,]+\.\d{2}/)
end
end

describe "the currency pattern itself" do
# An unescaped "." would still match a real decimal point, so neither
# test above would catch it regressing; guard the escape directly.
it "does not match if the decimal point is some other character" do
expect("$40X50").not_to match(/\$[\d,]+\.\d{2}/)
end
end
end

describe ".invitation_instructions for a volunteer" do
let(:mail) { volunteer.invite! }
let(:expiration_date) { I18n.l(volunteer.invitation_due_at, format: :full, default: nil) }
Expand Down
36 changes: 36 additions & 0 deletions spec/notifications/reimbursement_complete_notifier_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require "rails_helper"

RSpec.describe ReimbursementCompleteNotifier, type: :model do
include ActiveJob::TestHelper

describe "title" do
it "returns 'Reimbursement Approved'" do
case_contact = build(:case_contact, :wants_reimbursement)
Expand Down Expand Up @@ -31,6 +33,40 @@
end
end

describe "email delivery" do
let(:volunteer) { create(:volunteer, receive_email_notifications: true) }
let(:supervisor) { create(:supervisor, receive_email_notifications: true) }
let(:case_contact) { create(:case_contact, :wants_reimbursement, creator: volunteer) }

it "emails the volunteer recipient" do
perform_enqueued_jobs do
ReimbursementCompleteNotifier.with(case_contact: case_contact).deliver(volunteer)
end

expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(volunteer.email)
end

it "does not email a volunteer who has opted out of email notifications" do
# UserValidator requires at least one communication channel enabled, so
# disabling email requires enabling SMS, which in turn requires a phone number.
volunteer.update!(receive_email_notifications: false, receive_sms_notifications: true, phone_number: "5555555555")

perform_enqueued_jobs do
ReimbursementCompleteNotifier.with(case_contact: case_contact).deliver(volunteer)
end

expect(ActionMailer::Base.deliveries.map(&:to).flatten).not_to include(volunteer.email)
end

it "does not email a supervisor recipient" do
perform_enqueued_jobs do
ReimbursementCompleteNotifier.with(case_contact: case_contact).deliver(supervisor)
end

expect(ActionMailer::Base.deliveries.map(&:to).flatten).not_to include(supervisor.email)
end
end

describe "url" do
it "returns the case contacts URL path for the given case contact" do
case_contact = create(:case_contact, :wants_reimbursement)
Expand Down
24 changes: 24 additions & 0 deletions spec/requests/reimbursements_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@
expect(Noticed::Notification.last.recipient).to eq(case_contact.supervisor)
end
end

context "when reimbursement_complete arrives as the string \"1\" instead of a boolean" do
# Default Rails checkboxes submit "1"/"0"; the boolean cast must handle this too.
it "still sends a notification" do
expect do
patch reimbursement_mark_as_complete_url(case_contact, case_contact: {reimbursement_complete: "1"})
end.to change(Noticed::Notification, :count).by(1)
end
end
end

describe "PATCH /mark_as_needs_review" do
Expand All @@ -76,5 +85,20 @@
expect(response).to have_http_status(:redirect)
expect(case_contact.reload.reimbursement_complete).to be_falsey
end

it "does not send a notification" do
expect do
patch reimbursement_mark_as_needs_review_url(case_contact, case_contact: {reimbursement_complete: false})
end.not_to change(Noticed::Notification, :count)
end

context "when reimbursement_complete arrives as the string \"0\" instead of a boolean" do
# Mirror of the "1" case: "0" must cast to false, not a truthy string.
it "still withholds the notification" do
expect do
patch reimbursement_mark_as_needs_review_url(case_contact, case_contact: {reimbursement_complete: "0"})
end.not_to change(Noticed::Notification, :count)
end
end
end
end
Loading