diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index c043b3f52..e4a9bff91 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -2,7 +2,7 @@ class EventsController < ApplicationController include AhoyTracking, TagAssignable skip_before_action :authenticate_user!, only: [ :index, :show ] skip_before_action :verify_authenticity_token, only: [ :preview ] - before_action :set_event, only: %i[ show edit update destroy preview manage copy_registration_form ] + before_action :set_event, only: %i[ show edit update destroy preview manage preview_reminder send_reminder copy_registration_form ] def index authorize! @@ -59,6 +59,43 @@ def manage end end + def preview_reminder + authorize! @event, to: :preview_reminder? + @event = @event.decorate + @event_registrations = @event.event_registrations + .includes(:payments, registrant: [ :user, :contact_methods ]) + .joins(:registrant) + .select { |r| r.registrant.preferred_email.present? } + @sample_registration = @event_registrations.first + @days_until_event = @event.start_date.present? ? (@event.start_date.to_date - Date.current).to_i : nil + + if @sample_registration + mail = EventMailer.event_registration_reminder(@sample_registration, days_until_event: @days_until_event) + @reminder_preview_html = mail.html_part&.body&.decoded + end + end + + def send_reminder + authorize! @event, to: :send_reminder? + allowed_ids = Array(params[:registration_ids]).map(&:to_i).reject(&:zero?) + registrations = @event.event_registrations + .where(id: allowed_ids) + .includes(registrant: [ :user, :contact_methods ]) + .select { |r| r.registrant.preferred_email.present? } + days_until = @event.start_date.present? ? (@event.start_date.to_date - Date.current).to_i : nil + + if registrations.empty? + redirect_to remind_event_path(@event), alert: "Please select at least one recipient." + return + end + + registrations.each do |event_registration| + EventMailer.event_registration_reminder(event_registration, days_until_event: days_until).deliver_later + end + + redirect_to manage_event_path(@event), notice: "Reminder emails are being sent to #{registrations.size} registrant#{'s' if registrations.size != 1}." + end + def create authorize! @event = Event.new(event_params) diff --git a/app/mailers/event_mailer.rb b/app/mailers/event_mailer.rb index dd839a5f6..63e2b17b7 100644 --- a/app/mailers/event_mailer.rb +++ b/app/mailers/event_mailer.rb @@ -19,6 +19,28 @@ def event_registration_confirmation(event_registration) ) end + def event_registration_reminder(event_registration, days_until_event: nil) + @event_registration = event_registration + @event = event_registration.event.decorate + @person = event_registration.registrant + @days_until_event = days_until_event + + @notification_type = "Event registration reminder" + + @time_zone = @person.user&.time_zone || Time.zone.name + @event_url = @event_registration.slug.present? ? event_url(@event, reg: @event_registration.slug) : event_url(@event) + @organization_name = ENV.fetch("ORGANIZATION_NAME", "AWBW") + @organization_website = ENV.fetch("ORGANIZATION_WEBSITE", root_url) + + subject = "Reminder: #{@event.title} – #{@event.start_date.in_time_zone(@time_zone).strftime('%B %-d, %Y')}" + mail( + to: @person.preferred_email, + from: ENV.fetch("REPLY_TO_EMAIL", "no-reply@awbw.org"), + reply_to: ENV.fetch("REPLY_TO_EMAIL", "programs@awbw.org"), + subject: "AWBW Portal: #{subject}" + ) + end + def event_registration_cancelled(event_registration) @event_registration = event_registration @event = event_registration.event.decorate diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb index 7b1f5d2a7..8b76136db 100644 --- a/app/policies/event_policy.rb +++ b/app/policies/event_policy.rb @@ -33,6 +33,14 @@ def manage? admin? || owner? end + def preview_reminder? + manage? + end + + def send_reminder? + manage? + end + alias_rule :preview?, to: :edit? private diff --git a/app/views/event_mailer/event_registration_reminder.html.erb b/app/views/event_mailer/event_registration_reminder.html.erb new file mode 100644 index 000000000..1670c8f3c --- /dev/null +++ b/app/views/event_mailer/event_registration_reminder.html.erb @@ -0,0 +1,60 @@ +

Event reminder

+ +
+

+ Hello <%= @person.full_name %>, +

+ +

+ This is a reminder that you're registered for the following <%= @organization_name %> event<%= raw(" " + (@days_until_event == 0 ? "today" : @days_until_event == 1 ? "tomorrow" : "in #{@days_until_event} days")) if @days_until_event.is_a?(Integer) %>: +

+ +
+ <% if @event.respond_to?(:pre_title) && @event.pre_title.present? %> +

+ <%= @event.pre_title %> +

+ <% end %> + +

+ <%= @event.title %> +

+ +

+ <% Time.use_zone(@time_zone) { %><%= @event.times(display_day: true, display_date: true) %><% } %> +

+ + <% if @event.labelled_cost.present? %> +

+ <%= @event.labelled_cost %> +

+ <% end %> + + <% if @event.location.present? %> +

+ <%= @event.location.name %> +

+ <% end %> + + <% if @event.videoconference_url.present? %> +

+ Join us on <%= @event.decorate.videoconference_domain %> +

+ <% end %> +
+
+ +

+ Visit the event page for updates, directions, or calendar links: +

+ +

+ <% if @event_registration.persisted? && @event_registration.slug.present? %> + View registration + <% end %> + View event +

+ +

+ This is an automated reminder from <%= @organization_name %>. +

diff --git a/app/views/event_mailer/event_registration_reminder.text.erb b/app/views/event_mailer/event_registration_reminder.text.erb new file mode 100644 index 000000000..8de17315c --- /dev/null +++ b/app/views/event_mailer/event_registration_reminder.text.erb @@ -0,0 +1,32 @@ +Event reminder + +Hello <%= @person.full_name %>, + +This is a reminder that you're registered for the following event<%= @days_until_event.is_a?(Integer) ? (@days_until_event == 0 ? " today" : @days_until_event == 1 ? " tomorrow" : " in #{@days_until_event} days") : "" %>: + +<% if @event.respond_to?(:pre_title) && @event.pre_title.present? %> +<%= @event.pre_title %> +<% end %><%= @event.title %> +<% Time.use_zone(@time_zone) { %><%= @event.times(display_day: true, display_date: true) %><% } %> + +<% if @event.location.present? %> +Location: <%= @event.location.name %> +<% end %> + +<% if @event.videoconference_url.present? %> +Videoconference URL: <%= @event.videoconference_url %> +<% end %> + +<% if @event.labelled_cost.present? %> +<%= @event.labelled_cost %> +<% end %> + +<% if @event_registration.slug.present? %> +View your registration: +<%= registration_ticket_url(@event_registration.slug) %> + +<% end %>View the event page: +<%= @event_url %> + +-- +This is an automated reminder from <%= @organization_name %>. diff --git a/app/views/events/manage.html.erb b/app/views/events/manage.html.erb index 1ecfe73ae..6fea00da1 100644 --- a/app/views/events/manage.html.erb +++ b/app/views/events/manage.html.erb @@ -11,6 +11,7 @@

+ <%= link_to "Send reminder", preview_reminder_event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %> <%= link_to "Download CSV", manage_event_path(@event, format: :csv), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1", data: { turbo_frame: "_top" } %> <%= link_to "View", event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %> <% if allowed_to?(:edit?, @event) %> diff --git a/app/views/events/preview_reminder.html.erb b/app/views/events/preview_reminder.html.erb new file mode 100644 index 000000000..624edc75c --- /dev/null +++ b/app/views/events/preview_reminder.html.erb @@ -0,0 +1,77 @@ +<% content_for(:page_bg_class, "admin-only bg-blue-100") %> +
+
+ <%= link_to "← Manage registrants", manage_event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 mb-2 inline-block" %> +

+ Send reminder email +

+

+ <%= @event.title %> +

+
+ + <% if @event_registrations.empty? %> +

There are no registrants with an email address to send a reminder to.

+ <%= link_to "Back to manage", manage_event_path(@event), class: "btn btn-secondary-outline" %> + <% else %> + <%= form_with url: send_reminder_event_path(@event), method: :post, local: true do |f| %> +

Recipients

+

+ Choose who will receive this reminder: +

+
+ + + + + + + + + + + <% @event_registrations.each do |reg| %> + <% person = reg.registrant %> + + + + + + + <% end %> + +
+ + NameEmailPayment status
+ <%= check_box_tag "registration_ids[]", reg.id, true, class: "recipient-checkbox rounded border-gray-300", id: "registration_ids_#{reg.id}" %> + <%= person.full_name %><%= person.preferred_email %> + <% if @event.object.cost_cents.to_i > 0 %> + <% if reg.scholarship_recipient? && reg.successful_payments_total_cents <= 0 %> + + <% elsif reg.paid_in_full? %> + Paid + <% else %> + <% due_cents = @event.object.cost_cents - reg.successful_payments_total_cents %> + $<%= "%.2f" % (due_cents / 100.0) %> due + <% end %> + <% else %> + + <% end %> +
+
+ + <% if @reminder_preview_html.present? %> +
+

Preview (sample registrant)

+
+ +
+
+ <% end %> + + <%= f.submit "Send reminder", class: "btn btn-primary", data: { turbo_confirm: "Send reminder emails to the selected registrant(s)?" } %> + <% end %> + <% end %> +
diff --git a/config/routes.rb b/config/routes.rb index 21a48374d..512f94212 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -99,8 +99,10 @@ resources :events do member do get :manage + get :preview_reminder patch :preview post :copy_registration_form + post :send_reminder end resource :registrations, only: %i[ create destroy ], module: :events, as: :registrant_registration resource :public_registration, only: [ :new, :create, :show ], module: :events diff --git a/spec/mailers/event_mailer_spec.rb b/spec/mailers/event_mailer_spec.rb index 5ed83c5d2..4d4d31726 100644 --- a/spec/mailers/event_mailer_spec.rb +++ b/spec/mailers/event_mailer_spec.rb @@ -46,4 +46,74 @@ end end end + + describe "#event_registration_reminder" do + let(:event_registration) { create(:event_registration) } + let(:mail) { described_class.event_registration_reminder(event_registration, days_until_event: days_until_event) } + let(:days_until_event) { 7 } + + it "renders without raising" do + expect { mail.deliver_now }.not_to raise_error + end + + it "sends to the registrant" do + expect(mail.to).to eq([ event_registration.registrant.preferred_email ]) + end + + it "includes the event title in the subject" do + expect(mail.subject).to include(event_registration.event.title) + end + + it "includes the event title in the body" do + expect(mail.body.encoded).to include(event_registration.event.title) + end + + it "includes the registrant name in the body" do + expect(mail.body.encoded).to include(event_registration.registrant.full_name) + end + + it "includes reminder wording in the body" do + expect(mail.body.encoded).to include("This is a reminder that you're registered for the following") + end + + context "when days_until_event is 0" do + let(:days_until_event) { 0 } + + it "includes today in the body" do + expect(mail.body.encoded).to include("today") + end + end + + context "when days_until_event is 1" do + let(:days_until_event) { 1 } + + it "includes tomorrow in the body" do + expect(mail.body.encoded).to include("tomorrow") + end + end + + context "when days_until_event is 7" do + let(:days_until_event) { 7 } + + it "includes the number of days in the body" do + expect(mail.body.encoded).to include("7 days") + end + end + + context "when days_until_event is nil" do + let(:days_until_event) { nil } + let(:mail) { described_class.event_registration_reminder(event_registration) } + + it "renders without raising" do + expect { mail.deliver_now }.not_to raise_error + end + + it "does not include today, tomorrow, or in N days in the body" do + body = mail.body.encoded + expect(body).not_to include("today") + expect(body).not_to include("tomorrow") + expect(body).not_to match(/\bin \d+ days\b/) + end + end + end end diff --git a/test/mailers/previews/event_mailer_preview.rb b/test/mailers/previews/event_mailer_preview.rb index 09b4ae2d8..297ad51f5 100644 --- a/test/mailers/previews/event_mailer_preview.rb +++ b/test/mailers/previews/event_mailer_preview.rb @@ -4,6 +4,11 @@ def event_registration_confirmation EventMailer.event_registration_confirmation(event_registration) end + def event_registration_reminder + event_registration = sample_event_registration + EventMailer.event_registration_reminder(event_registration, days_until_event: 1) + end + def event_registration_cancelled event_registration = sample_event_registration event_registration.status = "cancelled"