Skip to content
Draft
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
2 changes: 2 additions & 0 deletions app/policies/event_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion app/services/magic_ticket_callouts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: The deadline suffix is suppressed once paid_in_full?. CE payment isn't tracked separately from the event fee, so this reuses the same signal callouts/ce already uses to show CE "Paid".

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?
Expand Down
21 changes: 21 additions & 0 deletions app/views/event_mailer/_ce_deadlines.html.erb
Original file line number Diff line number Diff line change
@@ -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) %>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: Gated on ce_credit_requested? so only CE registrants see deadlines, and on at least one deadline being set so events without them are unchanged. Same partial backs both confirmation and reminder (html + text) to keep the four templates in sync.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: Gated on ce_credit_requested? so only CE registrants see deadlines, and on at least one deadline being set so events without them are unchanged. One partial backs both confirmation and reminder (html + text) to keep the four templates in sync.

<div style="text-align: left; background-color: #f0fdfa; border: 1px solid #99f6e4; border-radius: 6px; padding: 16px; margin: 16px 0;">
<p style="font-size: 14px; font-weight: 600; color: #115e59; margin: 0 0 8px;">
<%= event.ce_hours_details_label %>
</p>
<% if event.ce_hours_request_deadline %>
<p style="font-size: 14px; color: #374151; margin: 0 0 4px;">
Request your CE hours by <strong><%= event.ce_hours_request_deadline.strftime("%B %-d, %Y") %></strong>
</p>
<% end %>
<% if event.ce_payment_due_deadline %>
<p style="font-size: 14px; color: #374151; margin: 0;">
CE payment due by <strong><%= event.ce_payment_due_deadline.strftime("%B %-d, %Y") %></strong>
</p>
<% end %>
</div>
<% end %>
5 changes: 5 additions & 0 deletions app/views/event_mailer/_ce_deadlines.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% 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 %><% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
<% end %>
</div>

<%= render "ce_deadlines", event: @event, registration: @event_registration %>

<% if @event.rhino_description.present? %>
<span style="padding-bottom: 12px"><strong>Details</strong></span><br>
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/views/event_mailer/event_registration_reminder.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<% end %>

<%= render "event_details_card", event: @event, time_zone: @time_zone %>

<%= render "ce_deadlines", event: @event, registration: @event_registration %>
</div>

<% if @event_registration.persisted? && @event_registration.slug.present? %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) %>
Expand Down
16 changes: 16 additions & 0 deletions app/views/events/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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. <p>AWBW is approved by CAMFT…</p><h3>Before the training</h3><ul><li>Email your license number</li></ul>" %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-2 pl-9">
<div>
<%= f.label :ce_hours_request_deadline, "Request hours by", class: "block text-xs font-medium text-gray-600 mb-0.5" %>
<p class="text-xs text-gray-500 mb-1">Deadline for registrants to request their CE hours. Shown on the CE callout. Leave blank to hide.</p>
<%= f.text_field :ce_hours_request_deadline,
type: "date",
class: "w-full rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %>
</div>
<div>
<%= f.label :ce_payment_due_deadline, "Payment due by", class: "block text-xs font-medium text-gray-600 mb-0.5" %>
<p class="text-xs text-gray-500 mb-1">Deadline for CE payment. Shown on the CE callout. Leave blank to hide.</p>
<%= f.text_field :ce_payment_due_deadline,
type: "date",
class: "w-full rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %>
</div>
</div>
<% when :event_details %>
<%= render "events/builtin_callout_card", f: f, card: card,
label_field: :event_details_label, content_field: :event_details,
Expand Down
7 changes: 6 additions & 1 deletion app/views/events/callouts/ce.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@
<% if @event_registration.ce_hours_requested.present? %>
<dd class="mt-1 text-lg font-semibold text-gray-900 tabular-nums"><%= @event_registration.ce_hours_requested %></dd>
<% else %>
<dd class="mt-1 text-sm text-amber-700">We don't have your requested hours yet.</dd>
<dd class="mt-1 text-sm text-amber-700">
We don't have your requested hours yet.<% if @event.ce_hours_request_deadline %> Please request by <%= @event.ce_hours_request_deadline.strftime("%B %-d, %Y") %>.<% end %>
</dd>
<% end %>
</div>
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-400">Cost<% if @event_registration.ce_hours_requested.present? %> (<%= @event_registration.ce_hours_requested %> &times; $<%= EventRegistration::CE_HOURLY_RATE_DOLLARS %>)<% end %></dt>
<dd class="mt-1 text-lg font-semibold text-gray-900 tabular-nums"><%= @event_registration.ce_hours_requested.present? ? dollars_from_cents(@event_registration.ce_amount_owed_cents) : "β€”" %></dd>
<% if @event.ce_payment_due_deadline && !@event_registration.paid_in_full? %>
<p class="mt-0.5 text-xs text-amber-700">Due by <%= @event.ce_payment_due_deadline.strftime("%B %-d, %Y") %></p>
<% end %>
</div>
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-400">License number</dt>
Expand Down
16 changes: 16 additions & 0 deletions app/views/events/ce_hours.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@
</div>

<div class="px-6 sm:px-8 py-7">
<% if @event.ce_hours_request_deadline || @event.ce_payment_due_deadline %>
<dl class="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2">
<% if @event.ce_hours_request_deadline %>
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4">
<dt class="text-xs font-medium uppercase tracking-wide text-gray-400">Request hours by</dt>
<dd class="mt-1 text-base font-semibold text-gray-900"><%= @event.ce_hours_request_deadline.strftime("%B %-d, %Y") %></dd>
</div>
<% end %>
<% if @event.ce_payment_due_deadline %>
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4">
<dt class="text-xs font-medium uppercase tracking-wide text-gray-400">Payment due by</dt>
<dd class="mt-1 text-base font-semibold text-gray-900"><%= @event.ce_payment_due_deadline.strftime("%B %-d, %Y") %></dd>
</div>
<% end %>
</dl>
<% end %>
<div class="rich-label prose max-w-none text-gray-700 leading-relaxed prose-headings:text-gray-800 prose-a:text-blue-700">
<%= form_label_html(@event.ce_hours_details) %>
</div>
Expand Down
11 changes: 11 additions & 0 deletions db/migrate/20260622135205_add_ce_deadlines_to_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class AddCeDeadlinesToEvents < ActiveRecord::Migration[8.1]
def up
add_column :events, :ce_hours_request_deadline, :date unless column_exists?(:events, :ce_hours_request_deadline)
add_column :events, :ce_payment_due_deadline, :date unless column_exists?(:events, :ce_payment_due_deadline)
end

def down
remove_column :events, :ce_hours_request_deadline, if_exists: true
remove_column :events, :ce_payment_due_deadline, if_exists: true
end
end
4 changes: 3 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[8.1].define(version: 2026_06_21_213947) do
ActiveRecord::Schema[8.1].define(version: 2026_06_22_135205) do
create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "action_text_rich_text_id", null: false
t.datetime "created_at", null: false
Expand Down Expand Up @@ -519,6 +519,8 @@
t.boolean "autoshow_videoconference_link", default: true, null: false
t.text "ce_hours_details"
t.string "ce_hours_details_label", default: "CE hours", null: false
t.date "ce_hours_request_deadline"
t.date "ce_payment_due_deadline"
t.integer "cost_cents"
t.datetime "created_at", null: false
t.integer "created_by_id"
Expand Down
39 changes: 39 additions & 0 deletions spec/mailers/event_mailer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@
expect(body).not_to include("Meeting ID")
end
end

context "when the registrant requested CE credit and the event has deadlines" do
let(:event) do
create(:event, ce_hours_request_deadline: Date.new(2026, 7, 1),
ce_payment_due_deadline: Date.new(2026, 8, 15))
end
let(:event_registration) { create(:event_registration, event: event, ce_credit_requested: true) }

it "surfaces both CE deadlines in the body" do
expect(mail.body.encoded).to include("Request your CE hours by")
expect(mail.body.encoded).to include("July 1, 2026")
expect(mail.body.encoded).to include("CE payment due by")
expect(mail.body.encoded).to include("August 15, 2026")
end
end

context "when the registrant did not request CE credit" do
let(:event) { create(:event, ce_hours_request_deadline: Date.new(2026, 7, 1)) }
let(:event_registration) { create(:event_registration, event: event, ce_credit_requested: false) }

it "omits the CE deadlines" do
expect(mail.body.encoded).not_to include("Request your CE hours by")
end
end
end

describe "#bulk_payment_confirmation" do
Expand Down Expand Up @@ -156,6 +180,21 @@
expect(mail.body.encoded).to include(event_registration.registrant.full_name)
end

context "when the registrant requested CE credit and the event has deadlines" do
let(:event) do
create(:event, ce_hours_request_deadline: Date.new(2026, 7, 1),
ce_payment_due_deadline: Date.new(2026, 8, 15))
end
let(:event_registration) { create(:event_registration, event: event, ce_credit_requested: true) }

it "surfaces both CE deadlines in the body" do
expect(mail.body.encoded).to include("Request your CE hours by")
expect(mail.body.encoded).to include("July 1, 2026")
expect(mail.body.encoded).to include("CE payment due by")
expect(mail.body.encoded).to include("August 15, 2026")
end
end

context "with a custom subject" do
let(:mail) { described_class.event_registration_reminder(event_registration, custom_subject: "Don't forget us tomorrow!") }

Expand Down
20 changes: 20 additions & 0 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@
expect(response.body).to include("Email your license number")
end

it "surfaces the CE deadlines when set" do
event.update!(ce_hours_details: "<p>CE info</p>",
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))
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions spec/services/magic_ticket_callouts_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion test/mailers/previews/event_mailer_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down