Skip to content
Merged
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
10 changes: 0 additions & 10 deletions app/controllers/events/callouts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,6 @@ class CalloutsController < ApplicationController
policy.object_src :self
end

# Hidden Resource (by title) backing the handout links, in display order.
# Missing ones (e.g. not seeded in an environment) are silently skipped.
HANDOUT_RESOURCE_TITLES = [
"2-Day AWBW Facilitator Training Worksheets & Handouts",
"AWBW Training Workshop Worksheets",
"Aha Moments",
"Inviting and Responding to Participants' Sharing",
"Letter to Supervisors"
].freeze

# Payment page: the full allocation ledger with the running balance due, plus
# the linked documents (the W-9 from the payment callout's resources, and the
# dynamic invoice/receipt) for paid events.
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/events/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ def create_person_for_current_user
end

def set_event_registration
@event_registration = EventRegistration.find_by!(slug: params[:slug])
@event_registration = EventRegistration
.includes(event: { registration_ticket_callouts: :resources })
.find_by!(slug: params[:slug])
end

def requires_payment?(registration)
Expand Down
4 changes: 0 additions & 4 deletions app/models/event_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,6 @@ def deletable?
!allocations.exists? && !attendance_recorded? && !transferred_out?
end

def checked_in?
# checked_in_at.present?
end

# True when the registrant should be granted access to ticket materials
# (training links, etc.) even though they haven't paid in full yet. Admins
# flip the `intends_to_pay` flag when someone commits to paying after the
Expand Down
9 changes: 7 additions & 2 deletions app/services/builtin_callout_cards.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,21 @@ def config_gap?(builtin_key)

# A built-in card the event has materialized into an editable row renders from
# that row, not from #cards, so we skip it here to avoid double-rendering.
# Reads the (possibly preloaded) association in Ruby rather than a `.pluck`,
# so a preloaded ticket render doesn't issue a second query for the same rows.
def materialized?(builtin_key)
@materialized_keys ||= event.registration_ticket_callouts.builtin.pluck(:builtin_key).to_set
@materialized_keys ||= event.registration_ticket_callouts.filter_map(&:builtin_key).to_set
@materialized_keys.include?(builtin_key)
end

# Top card: an action card while a balance is due, a reference card once paid
# in full. Its page lists every allocation with the running balance, plus the
# linked documents (the W-9, and the invoice/receipt) for paid events.
# Unlike scholarship/CE/videoconference, payment never bypasses its gap in
# preview β€” a free event truly has no balance to preview, on a real ticket or
# a sample one, so there's nothing to click through.
def payment_card
return if config_gap?("payment")
return if self.class.config_gap(event, "payment")
due = registration.remaining_cost.to_i.positive?
Card.new(icon_class: "fa-solid fa-credit-card", color: due ? "orange" : "blue",
title: due ? "Make your payment" : "Payment",
Expand Down
19 changes: 12 additions & 7 deletions app/services/builtin_callouts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ class BuiltinCallouts
"The membership fee covers all facilitators connected to the same program or organization for the calendar year, regardless of the number of facilitators participating. Only one membership fee payment is required per program annually." ] }
].freeze

# Hidden Resource (by title) backing the handout links, in display order.
# Missing ones (e.g. not seeded in an environment) are silently skipped.
HANDOUT_RESOURCE_TITLES = [
"2-Day AWBW Facilitator Training Worksheets & Handouts",
"AWBW Training Workshop Worksheets",
"Aha Moments",
"Inviting and Responding to Participants' Sharing",
"Letter to Supervisors"
].freeze

# Default per-resource copy for the built-in handout links, keyed by resource
# title. `subtitle` is the short line on the handouts card; `page_content` is
# the longer copy shown under the title on the resource's own page. Materialized
Expand Down Expand Up @@ -82,11 +92,6 @@ class BuiltinCallouts
}
}.freeze

# builtin_keys this service knows how to materialize.
def self.seedable_keys
new(nil).send(:definitions).map { |definition| definition[:builtin_key] }
end

def self.seed(event)
new(event).seed
end
Expand Down Expand Up @@ -348,8 +353,8 @@ def resource_content_customized?(callout, definition)
# The training worksheet resources, by title, in the display order the code
# card used. Missing ones (not seeded in an environment) are simply skipped.
def handout_resources
by_title = Resource.where(title: Events::CalloutsController::HANDOUT_RESOURCE_TITLES).index_by(&:title)
Events::CalloutsController::HANDOUT_RESOURCE_TITLES.filter_map { |title| by_title[title] }
by_title = Resource.where(title: HANDOUT_RESOURCE_TITLES).index_by(&:title)
HANDOUT_RESOURCE_TITLES.filter_map { |title| by_title[title] }
end

# Renders FAQS to standard <details>/<summary> disclosures so each question is a
Expand Down
8 changes: 4 additions & 4 deletions app/views/event_registrations/_ticket.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@
default; "Show all options" additionally reveals unpublished rows so admins
can preview every card. -->
<% payment_access = event_registration.payment_access_granted? || @checkout_payment_status == "paid" %>
<% callouts = preview && show_all ? event_registration.event.registration_ticket_callouts : event_registration.event.registration_ticket_callouts.visible %>
<%# Reads the loaded association (not the `.visible` scope) so a preloaded
real-ticket render doesn't issue a second query for the same rows. %>
<% callouts = preview && show_all ? event_registration.event.registration_ticket_callouts : event_registration.event.registration_ticket_callouts.select(&:published?) %>
<% callouts.each do |callout| %>
<% next if callout.payment_access_gated && !payment_access && !(preview && show_all) %>
<% if callout.behavioral_builtin? %>
Expand Down Expand Up @@ -158,9 +160,7 @@

<!-- Status -->
<div class="text-center space-y-2">
<% if event_registration.checked_in? %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800"> Checked in </span>
<% elsif event_registration.status == "cancelled" %>
<% if event_registration.status == "cancelled" %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800"> Registration cancelled </span>
<% if event_registration.event.registerable? %>
<div><%= button_to "Register again", registration_reactivate_path(event_registration.slug),
Expand Down
90 changes: 90 additions & 0 deletions spec/requests/events/callouts_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,94 @@
end
end
end

describe "GET /registration/:slug/certificate" do
let(:event) { create(:event, end_date: 2.days.ago) }
let(:registration) { create(:event_registration, event: event, status: "attended") }

it "renders once the certificate is unlocked" do
get registration_certificate_path(registration.slug)
expect(response).to have_http_status(:success)
end

it "redirects to the ticket when the certificate isn't unlocked yet" do
registration.update!(status: "registered")

get registration_certificate_path(registration.slug)

expect(response).to redirect_to(registration_ticket_path(registration.slug))
end
end

describe "POST /registration/:slug/ce/license" do
let(:event) { create(:event) }

context "with a CE registration on file" do
before do
license = create(:professional_license, person: registration.registrant, number: nil)
create(:continuing_education_registration, event_registration: registration, professional_license: license)
end

it "saves the license and mirrors it onto the CE form answer" do
post registration_ce_license_path(registration.slug),
params: { license_number: "LIC-999", license_kind: "LCSW", license_issuing_state: "TN" }

expect(response).to redirect_to(registration_ce_path(registration.slug))
expect(flash[:notice]).to eq("License saved.")
expect(registration.reload.continuing_education_registrations.first.professional_license.number).to eq("LIC-999")
end

it "refuses to change the license once the certificate has been issued" do
registration.continuing_education_registrations.first.update!(certificate_sent_at: Time.current)

post registration_ce_license_path(registration.slug), params: { license_number: "LIC-999" }

expect(response).to redirect_to(registration_ce_path(registration.slug))
expect(flash[:alert]).to include("can no longer be changed here")
end
end

context "when no CE registration exists" do
it "redirects to the CE page" do
post registration_ce_license_path(registration.slug), params: { license_number: "LIC-999" }

expect(response).to redirect_to(registration_ce_path(registration.slug))
end
end
end

describe "POST /registration/:slug/ce/request" do
context "when the event offers CE hours" do
let(:event) { create(:event, ce_hours_offered: 6, ce_hours_cost_cents: 15_000) }

it "creates a CE registration against a placeholder license and redirects" do
expect(registration.continuing_education_registrations).to be_empty

post registration_ce_request_path(registration.slug)

expect(response).to redirect_to(registration_ce_path(registration.slug))
expect(flash[:notice]).to eq("Continuing education credit requested.")
expect(registration.reload.continuing_education_registrations.count).to eq(1)
end

it "doesn't create a second CE registration if one already exists" do
license = create(:professional_license, person: registration.registrant)
create(:continuing_education_registration, event_registration: registration, professional_license: license)

expect { post registration_ce_request_path(registration.slug) }
.not_to change { registration.reload.continuing_education_registrations.count }
end
end

context "when the event offers no CE hours" do
let(:event) { create(:event) }

it "redirects without creating a CE registration" do
post registration_ce_request_path(registration.slug)

expect(response).to redirect_to(registration_ce_path(registration.slug))
expect(registration.reload.continuing_education_registrations).to be_empty
end
end
end
end
25 changes: 19 additions & 6 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,28 @@ def offer_ce!(target_event)
expect(response.body).to include(sample_payment_event_path(event))
end

it "previews a published behavioral card even when the event config is incomplete" do
it "previews a published scholarship/CE card even when the event config is incomplete" do
no_form = create(:event)
BuiltinCallouts.seed(no_form)
# An event with no scholarship form has a scholarship "config gap"
# (BuiltinCalloutCards.config_gap) that hides the card on a real ticket;
# the preview shows it anyway (via ?options=all, which also flags the
# sample registrant as having requested it) so the admin can see and
# click it while finishing setup. Only scholarship/CE bypass this way β€”
# payment and videoconference describe the event itself (its real cost,
# its real join link), so there's nothing truthful to preview when the
# event isn't actually configured for them.
no_form.registration_ticket_callouts.find_by(builtin_key: "scholarship").update!(hidden: false)
get sample_ticket_event_path(no_form, options: "all")
expect(response.body).to include(sample_scholarship_event_path(no_form))
end

it "still hides the payment card for a free event even with ?options=all" do
free = create(:event, cost_cents: 0)
BuiltinCallouts.seed(free)
# A free event has a payment "config gap" (BuiltinCalloutCards.config_gap)
# that hides the card on a real ticket; the preview shows it anyway so the
# admin can see and click it while finishing setup.
free.registration_ticket_callouts.find_by(builtin_key: "payment").update!(hidden: false)
get sample_ticket_event_path(free)
expect(response.body).to include(sample_payment_event_path(free))
get sample_ticket_event_path(free, options: "all")
expect(response.body).not_to include(sample_payment_event_path(free))
end

it "models a typical registrant by default, hiding scholarship and CE" do
Expand Down