Skip to content
Open
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
80 changes: 78 additions & 2 deletions app/controllers/event_registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class EventRegistrationsController < ApplicationController
require "csv"

# show redirects to slug URL; kept for backwards compatibility
before_action :set_event_registration, only: [ :show, :edit, :update, :destroy, :update_onboarding, :toggle_certificate_issued, :update_attendance ]
before_action :set_event_registration, only: [ :show, :edit, :update, :destroy, :update_onboarding, :toggle_certificate_issued, :update_attendance, :transfer, :process_transfer ]

def index
authorize!
Expand Down Expand Up @@ -94,6 +94,16 @@ def update
@event_registration.notifications.select(&:new_record?).each { |n| n.recipient_email = recipient_email }

if @event_registration.save
# Marking transferred out β€” from the edit-form save OR the inline roster/
# onboarding status chip (Turbo) β€” with no destination yet sends the admin
# to the transfer screen to create/link the incoming registration. Handled
# before respond_to so both the HTML and Turbo paths redirect (issue #1944).
if @event_registration.saved_change_to_status? &&
@event_registration.transfer_destination_pending? &&
allowed_to?(:transfer?, @event_registration)
return redirect_to transfer_event_registration_path(@event_registration, return_to: params[:return_to]), status: :see_other
end

notice = "Registration was successfully updated."
respond_to do |format|
format.turbo_stream
Expand Down Expand Up @@ -195,6 +205,62 @@ def update_attendance
redirect_to attendance_report_path(date, reopen: true), status: :see_other
end

# Follow-up screen shown after a registration is marked "transferred out":
# pick the destination event so the incoming registration is created/linked
# and the transfer trail is preserved (issue #1944).
def transfer
authorize! @event_registration, to: :transfer?
@return_to = params[:return_to]
@events = transfer_destination_events
end

def process_transfer
authorize! @event_registration, to: :transfer?
destination_event = Event.find(params[:destination_event_id])

# The registrant may already be registered for the destination event, which
# would collide with the (registrant, event) uniqueness rule β€” link that
# record as the transfer target instead of creating a duplicate.
destination = EventRegistration.find_or_initialize_by(
registrant_id: @event_registration.registrant_id,
event_id: destination_event.id
)
# Collapse a double transfer (A→B→C) to two live regs: when the reg being
# transferred out is itself a transfer-in, its predecessor is the real origin,
# so the new reg points straight there and the middle stop is dropped. (#1944)
source = @event_registration.transferred_from_registration || @event_registration

if destination == source
# Transferring back to the origin event undoes the whole chain: restore the
# origin to the status it held before it was transferred out, instead of
# linking it to itself.
destination.status = destination.status_before_transfer.presence || "registered"
destination.status_before_transfer = nil
else
destination.transferred_from_registration = source
end

saved = ActiveRecord::Base.transaction do
next false unless destination.save
@event_registration.destroy! if @event_registration.transferred_in?
true
end

if saved
redirect_to edit_event_registration_path(destination, return_to: params[:return_to].presence),
notice: "Transfer recorded β€” #{source.registrant.full_name} is now registered for #{destination_event.title}.",
status: :see_other
else
@return_to = params[:return_to]
@events = transfer_destination_events
flash.now[:alert] = destination.errors.full_messages.to_sentence
render :transfer, status: :unprocessable_content
end
rescue ActiveRecord::RecordNotFound
redirect_to transfer_event_registration_path(@event_registration, return_to: params[:return_to].presence),
alert: "Select a destination event to transfer to.", status: :see_other
end

def confirm
@event_registration = EventRegistration.includes(registrant: :user, event: :location).find(params[:id])
authorize! @event_registration, to: :confirm?
Expand Down Expand Up @@ -382,6 +448,16 @@ def attendance_report_path(date, reopen: false)
edit: (cell if reopen), anchor: cell)
end

# Events a registrant can be transferred into: published events of the same
# format as the one they're leaving β€” an on-demand event only transfers to
# another on-demand event, and a scheduled (non-on-demand) event only to
# another scheduled event β€” excluding the source event, most recent first.
def transfer_destination_events
Event.where(published: true, on_demand: @event_registration.event.on_demand)
.where.not(id: @event_registration.event_id)
.order(start_date: :desc)
end

# Creates the audited completion row for a checklist step (recording who/when),
# or removes it β€” so an unchecked step leaves no trace.
def toggle_checklist_step(step, completed)
Expand Down Expand Up @@ -433,7 +509,7 @@ def csv_export(registrations)
r&.preferred_email.to_s,
r&.phone_number.to_s,
e&.title.to_s,
er.attendance_status_label,
er.attendance_status_report_label,
er.scholarships.any? ? "Yes" : "No",
er.scholarships.any?(&:tasks_completed?) ? "Yes" : "No",
cost_required ? er.payment_status_label : "",
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/events/bulk_payments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def index
authorize! @event
track_view("events.bulk_payments", { event_id: @event.id })

@event_registrations = @event.event_registrations.active.includes(:registrant)
@event_registrations = @event.event_registrations.active.not_transferred_in.includes(:registrant)
@submissions = @event.form_submissions
.where(role: "bulk_payment")
.includes(:person, form_answers: :form_field, payment: :allocations)
Expand All @@ -18,7 +18,7 @@ def index

def create
authorize! @event
@event_registrations = @event.event_registrations.active.includes(:registrant)
@event_registrations = @event.event_registrations.active.not_transferred_in.includes(:registrant)
@allocated_by_registration = allocated_cents_by_registration(@event_registrations)

submission = @event.form_submissions.find(params[:submission_id])
Expand Down Expand Up @@ -149,13 +149,13 @@ def set_event
def assign_allocation_card_data(payment)
@payment = payment.reload
@submission = @payment.form_submission
@event_registrations = @event.event_registrations.active.includes(:registrant)
@event_registrations = @event.event_registrations.active.not_transferred_in.includes(:registrant)
@allocated_by_registration = allocated_cents_by_registration(@event_registrations)
end

def assign_bulk_payment_card_data(submission)
@submission = submission.reload.decorate
@event_registrations = @event.event_registrations.active.includes(:registrant)
@event_registrations = @event.event_registrations.active.not_transferred_in.includes(:registrant)
@allocated_by_registration = allocated_cents_by_registration(@event_registrations)
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ def onboarding_csv_row(registration, cost_required, day_count, include_ce = fals
(1..day_count).each do |day|
row << (registration.public_send("completed_day_#{day}") ? "Yes" : "No")
end
row << registration.attendance_status_label
row << registration.attendance_status_report_label
row << registration.comments.map { |comment| comment.body.to_s.strip }.reject(&:blank?).join(" ::: ")
row << (registration.comments.any?(&:flagged?) ? "Yes" : "No")
row
Expand Down
14 changes: 14 additions & 0 deletions app/controllers/scholarships_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def new
@scholarship = Scholarship.new(recipient: @allocatable.registrant)
@grants = Grant.selectable_for(@scholarship)
authorize! @scholarship
return if redirect_transferred_in_scholarship
load_scholarship_submission
end

Expand All @@ -51,6 +52,7 @@ def create
@scholarship = Scholarship.new(scholarship_params.merge(recipient: @allocatable.registrant))
@scholarship.build_allocation(allocatable: @allocatable, amount: @scholarship.amount_cents.to_i)
authorize! @scholarship
return if redirect_transferred_in_scholarship

if @scholarship.save
redirect_to scholarship_save_path, notice: "Scholarship created."
Expand Down Expand Up @@ -259,6 +261,18 @@ def locate_allocatable
GlobalID::Locator.locate_signed(sgid) if sgid
end

# A transferred-in reg carries no scholarship of its own β€” its recognition comes
# from the source it transferred from (see EventRegistration#effective_scholarship)
# and the dollars stay there. The UI hides the add link, but block the URL too and
# send the admin to the source, where the scholarship belongs. (#1944)
def redirect_transferred_in_scholarship
return false unless @allocatable.is_a?(EventRegistration) && @allocatable.transferred_in?

redirect_to edit_event_registration_path(@allocatable.transferred_from_registration),
alert: "This registrant transferred in from another event β€” add the scholarship on their original registration."
true
end

def scholarship_params
params.require(:scholarship).permit(
:amount_dollars, :amount_cents, :tasks_completed, :agreement_signed, :grant_id, :recipient_id,
Expand Down
24 changes: 21 additions & 3 deletions app/models/continuing_education_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,30 @@ def self.search_by_params(params)
# sign-ins/early sign-outs. You can't certify hours the sign-in sheet doesn't support.
ATTENDANCE_COVERAGE_THRESHOLD = 0.9

# The registration whose event the hours are completed/certified at β€” the home
# reg, or the destination it transferred to. Record + payment stay on the home
# reg; only certification follows the person. Derived from the transfer link, so
# there's nothing to keep in sync. (issue #1944)
def certified_at_registration
event_registration.transferred_to_registration || event_registration
end

# True when this record's hours are certified at a *different* event than the
# one it's billed to (i.e. the home reg transferred out).
def certified_elsewhere?
event_registration.transferred_to_registration.present?
end

# CE certificate eligibility β€” its own rule (not shared): the event grants CE,
# the registrant attended, the training has ended, the CE balance is paid, and
# (when attendance was tracked) the logged time approximately covers the hours.
# Attendance + the training-ended check run against the certified-at reg (the
# destination event after a transfer); payment stays this record's own balance.
def certificate_available?
event = event_registration&.event
reg = certified_at_registration
event = reg&.event
return false unless event&.ce_eligible?
return false unless event.end_date&.past? && event_registration.attended? && paid_in_full?
return false unless event.end_date&.past? && reg.attended? && paid_in_full?

attendance_time_sufficient?
end
Expand All @@ -75,8 +92,9 @@ def certificate_available?
# cover the awarded hours before the certificate unlocks. With nothing logged (the
# portal sign-in wasn't used for this event), day-level attendance alone governs,
# so this doesn't block β€” it never retroactively gates events that never tracked time.
# Reads the certified-at reg's log (the destination event after a transfer).
def attendance_time_sufficient?
logged = event_registration.attendance_minutes_total
logged = certified_at_registration.attendance_minutes_total
return true if logged.zero?

logged >= required_attendance_minutes
Expand Down
Loading