diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb index e345eb5352..3e7c7fbf61 100644 --- a/app/policies/event_policy.rb +++ b/app/policies/event_policy.rb @@ -115,6 +115,8 @@ def google_analytics? :event_details_label, :ce_hours_details, :ce_hours_details_label, + :ce_hours_request_deadline, + :ce_payment_due_deadline, :autoshow_cost, :autoshow_date, :autoshow_location, diff --git a/app/services/magic_ticket_callouts.rb b/app/services/magic_ticket_callouts.rb index 73ee8d6a88..bf51063053 100644 --- a/app/services/magic_ticket_callouts.rb +++ b/app/services/magic_ticket_callouts.rb @@ -135,6 +135,7 @@ def ce_hours_card def ce_hours_subtitle return "#{registration.ce_hours_requested} hours" if registration.ce_hours_requested.present? + return "Request your hours by #{ce_deadline_text(event.ce_hours_request_deadline)}" if event.ce_hours_request_deadline "Continuing education credit" end @@ -147,13 +148,19 @@ def ce_hours_badge(complete) if complete return unless amount_cents.positive? - return "#{amount} due" + return "#{amount} due" if registration.paid_in_full? || event.ce_payment_due_deadline.blank? + return "#{amount} due by #{ce_deadline_text(event.ce_payment_due_deadline)}" end needed = ce_missing_text amount_cents.positive? ? "#{amount} · #{needed}" : needed end + # Short month/day for a deadline shown inline on the CE card, e.g. "Jun 30". + def ce_deadline_text(deadline) + deadline.strftime("%b %-d") + end + def ce_missing_text missing_hours = registration.ce_hours_requested.blank? missing_license = !registration.ce_license_provided? diff --git a/app/views/event_mailer/_ce_deadlines.html.erb b/app/views/event_mailer/_ce_deadlines.html.erb new file mode 100644 index 0000000000..5406040cc4 --- /dev/null +++ b/app/views/event_mailer/_ce_deadlines.html.erb @@ -0,0 +1,21 @@ +<%# CE deadlines for a registrant who requested continuing-education credit, + shown in the confirmation and reminder emails. Mirrors the CE callout's + deadline copy. Locals: event (decorated), registration. The deadlines are + plain dates, so no time zone is applied. %> +<% if registration.ce_credit_requested? && (event.ce_hours_request_deadline || event.ce_payment_due_deadline) %> +
+ <%= event.ce_hours_details_label %> +
+ <% if event.ce_hours_request_deadline %> ++ Request your CE hours by <%= event.ce_hours_request_deadline.strftime("%B %-d, %Y") %> +
+ <% end %> + <% if event.ce_payment_due_deadline %> ++ CE payment due by <%= event.ce_payment_due_deadline.strftime("%B %-d, %Y") %> +
+ <% end %> +diff --git a/app/views/event_mailer/event_registration_confirmation.text.erb b/app/views/event_mailer/event_registration_confirmation.text.erb index 1b0c5bc8cc..82e98af58b 100644 --- a/app/views/event_mailer/event_registration_confirmation.text.erb +++ b/app/views/event_mailer/event_registration_confirmation.text.erb @@ -27,6 +27,7 @@ Videoconference: <%= event_platform_label(@event) %> View your ticket: <%= registration_ticket_url(@event_registration.slug) %> +<%= render "ce_deadlines", event: @event, registration: @event_registration %> <% if @event.registration_close_date %> Registration closes on diff --git a/app/views/event_mailer/event_registration_reminder.html.erb b/app/views/event_mailer/event_registration_reminder.html.erb index 121e44363e..04436b43ea 100644 --- a/app/views/event_mailer/event_registration_reminder.html.erb +++ b/app/views/event_mailer/event_registration_reminder.html.erb @@ -18,6 +18,8 @@ <% end %> <%= render "event_details_card", event: @event, time_zone: @time_zone %> + + <%= render "ce_deadlines", event: @event, registration: @event_registration %> <% if @event_registration.persisted? && @event_registration.slug.present? %> diff --git a/app/views/event_mailer/event_registration_reminder.text.erb b/app/views/event_mailer/event_registration_reminder.text.erb index 8aeb3b1302..41c016f952 100644 --- a/app/views/event_mailer/event_registration_reminder.text.erb +++ b/app/views/event_mailer/event_registration_reminder.text.erb @@ -22,7 +22,7 @@ Location: <%= event_location_label(@event.object) %> <% if @event.labelled_cost.present? %> <%= @event.labelled_cost %> <% end %> - +<%= render "ce_deadlines", event: @event, registration: @event_registration %> <% if @event_registration.slug.present? %> View your ticket for event details, directions, and calendar links: <%= registration_ticket_url(@event_registration.slug) %> diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index b5790dc9fe..574b289cf7 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -460,6 +460,22 @@ title_placeholder: "CE hours", content_help: "CE requirements, payment, sign-in rules, and the post-training evaluation — shown on its own page linked from the ticket. Accepts basic HTML — bold, italics, links, lists, headings, and line breaks.", content_placeholder: "e.g.
AWBW is approved by CAMFT…
Deadline for registrants to request their CE hours. Shown on the CE callout. Leave blank to hide.
+ <%= f.text_field :ce_hours_request_deadline, + type: "date", + class: "w-full rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %> +Deadline for CE payment. Shown on the CE callout. Leave blank to hide.
+ <%= f.text_field :ce_payment_due_deadline, + type: "date", + class: "w-full rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %> +Due by <%= @event.ce_payment_due_deadline.strftime("%B %-d, %Y") %>
+ <% end %>CE info
", + ce_hours_request_deadline: Date.new(2026, 7, 1), + ce_payment_due_deadline: Date.new(2026, 8, 15)) + get ce_hours_event_path(event) + expect(response.body).to include("Request hours by") + expect(response.body).to include("July 1, 2026") + expect(response.body).to include("Payment due by") + expect(response.body).to include("August 15, 2026") + end + it "redirects to the event when details are blank" do get ce_hours_event_path(event) expect(response).to redirect_to(event_path(event)) @@ -391,6 +402,15 @@ expect(event.reload.hint_dates).to eq("must attend both days") expect(event.hint_registration_cost).to eq("due within 3 weeks of registration") end + + it "persists the CE deadlines" do + patch event_path(event), params: { event: { + ce_hours_request_deadline: "2026-07-01", + ce_payment_due_deadline: "2026-08-15" + } } + expect(event.reload.ce_hours_request_deadline).to eq(Date.new(2026, 7, 1)) + expect(event.ce_payment_due_deadline).to eq(Date.new(2026, 8, 15)) + end end context "as non-admin" do diff --git a/spec/services/magic_ticket_callouts_spec.rb b/spec/services/magic_ticket_callouts_spec.rb index b7f3b77578..c2eaf2f409 100644 --- a/spec/services/magic_ticket_callouts_spec.rb +++ b/spec/services/magic_ticket_callouts_spec.rb @@ -98,6 +98,24 @@ def card(reg, title) expect(complete.badge_classes).to include("teal") end + it "prompts to request CE hours by the deadline until hours are on file" do + event.update!(ce_hours_request_deadline: Date.new(2026, 7, 1)) + registration.update!(ce_credit_requested: true, ce_hours_requested: nil) + expect(card(registration, event.ce_hours_details_label).subtitle).to eq("Request your hours by Jul 1") + + registration.update!(ce_hours_requested: 6) + expect(card(registration, event.ce_hours_details_label).subtitle).to eq("6 hours") + end + + it "appends the CE payment deadline to the amount-due badge until paid" do + event.update!(ce_payment_due_deadline: Date.new(2026, 8, 15)) + registration.update!(ce_credit_requested: true, ce_hours_requested: 6, ce_license_number: "LIC123") + expect(card(registration, event.ce_hours_details_label).badge).to eq("$150 due by Aug 15") + + create(:allocation, source: create(:payment), allocatable: registration, amount: event.cost_cents) + expect(card(registration, event.ce_hours_details_label).badge).to eq("$150 due") + end + it "shows the scholarship card only when requested, without an amount chip until awarded" do expect(card_titles(registration)).not_to include("Scholarship") registration.update!(scholarship_requested: true) diff --git a/test/mailers/previews/event_mailer_preview.rb b/test/mailers/previews/event_mailer_preview.rb index ebcda85e09..1e856c5310 100644 --- a/test/mailers/previews/event_mailer_preview.rb +++ b/test/mailers/previews/event_mailer_preview.rb @@ -47,7 +47,13 @@ def sample_event_registration event = Event.first || create_event person = Person.first || create_person - EventRegistration.find_or_create_by!(event: event, registrant: person) + registration = EventRegistration.find_or_create_by!(event: event, registrant: person) + # Showcase the CE deadlines block (set in memory only, not persisted). Set on + # registration.event — the object the mailer decorates and reads. + registration.event.ce_hours_request_deadline ||= 2.weeks.from_now.to_date + registration.event.ce_payment_due_deadline ||= 3.weeks.from_now.to_date + registration.ce_credit_requested = true + registration end def create_event