diff --git a/app/controllers/reimbursements_controller.rb b/app/controllers/reimbursements_controller.rb
index f78b0ed3d5..7ec4f9b1a2 100644
--- a/app/controllers/reimbursements_controller.rb
+++ b/app/controllers/reimbursements_controller.rb
@@ -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
diff --git a/app/mailers/volunteer_mailer.rb b/app/mailers/volunteer_mailer.rb
index e56d145160..2dc3a89ed6 100644
--- a/app/mailers/volunteer_mailer.rb
+++ b/app/mailers/volunteer_mailer.rb
@@ -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
diff --git a/app/notifications/reimbursement_complete_notifier.rb b/app/notifications/reimbursement_complete_notifier.rb
index ce94258e66..b390dda8c5 100644
--- a/app/notifications/reimbursement_complete_notifier.rb
+++ b/app/notifications/reimbursement_complete_notifier.rb
@@ -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
diff --git a/app/views/volunteer_mailer/reimbursement_complete_email.html.erb b/app/views/volunteer_mailer/reimbursement_complete_email.html.erb
new file mode 100644
index 0000000000..bcff38089b
--- /dev/null
+++ b/app/views/volunteer_mailer/reimbursement_complete_email.html.erb
@@ -0,0 +1,29 @@
+
+
+
+
+ |
+ Hello <%= @user.display_name %>,
+ |
+
+
+ |
+ 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.
+ |
+
+
+ |
+ You can view your case contacts and their reimbursement status by visiting
+ " target="_blank">this link.
+ |
+
+
+ |
+ If you have any questions, please contact your most recent CASA supervisor for assistance.
+ |
+
+
diff --git a/lib/mailers/debug_preview_mailer.rb b/lib/mailers/debug_preview_mailer.rb
index aa3948d63d..ffe59edb1e 100644
--- a/lib/mailers/debug_preview_mailer.rb
+++ b/lib/mailers/debug_preview_mailer.rb
@@ -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
diff --git a/lib/mailers/previews/volunteer_mailer_preview.rb b/lib/mailers/previews/volunteer_mailer_preview.rb
index 2bbe95639a..dbad416122 100644
--- a/lib/mailers/previews/volunteer_mailer_preview.rb
+++ b/lib/mailers/previews/volunteer_mailer_preview.rb
@@ -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:
diff --git a/spec/mailers/previews/volunteer_mailer_preview_spec.rb b/spec/mailers/previews/volunteer_mailer_preview_spec.rb
index 7b9c950987..3ae79f25bc 100644
--- a/spec/mailers/previews/volunteer_mailer_preview_spec.rb
+++ b/spec/mailers/previews/volunteer_mailer_preview_spec.rb
@@ -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
diff --git a/spec/mailers/volunteer_mailer_spec.rb b/spec/mailers/volunteer_mailer_spec.rb
index 0ae5571326..987b9c3f6a 100644
--- a/spec/mailers/volunteer_mailer_spec.rb
+++ b/spec/mailers/volunteer_mailer_spec.rb
@@ -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) }
diff --git a/spec/notifications/reimbursement_complete_notifier_spec.rb b/spec/notifications/reimbursement_complete_notifier_spec.rb
index 5417760470..91d2935abb 100644
--- a/spec/notifications/reimbursement_complete_notifier_spec.rb
+++ b/spec/notifications/reimbursement_complete_notifier_spec.rb
@@ -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)
@@ -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)
diff --git a/spec/requests/reimbursements_spec.rb b/spec/requests/reimbursements_spec.rb
index cc932f37ad..150cceaa8e 100644
--- a/spec/requests/reimbursements_spec.rb
+++ b/spec/requests/reimbursements_spec.rb
@@ -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
@@ -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