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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def create
ActiveRecord::Base.transaction do
apply_ce_params(@ce_registration)
@ce_registration.save!
@event_registration.update_column(:ce_requested, true)
end
redirect_to edit_event_registration_path(@ce_registration.event_registration), notice: "CE registration created.", status: :see_other
rescue ActiveRecord::RecordInvalid
Expand Down Expand Up @@ -55,7 +54,6 @@ def destroy

registration = @ce_registration.event_registration
@ce_registration.destroy!
registration.update_column(:ce_requested, false)
redirect_to edit_event_registration_path(registration), notice: "CE registration removed.", status: :see_other
end

Expand Down
51 changes: 1 addition & 50 deletions app/controllers/event_registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ def create
authorize! @event_registration

if @event_registration.save
reconcile_ce_registration if @event_registration.event&.ce_eligible?
respond_to do |format|
format.html {
redirect_to confirm_event_registration_path(@event_registration, return_to: params[:return_to])
Expand Down Expand Up @@ -88,10 +87,7 @@ def update
@event_registration.notifications.select(&:new_record?).each { |n| n.recipient_email = recipient_email }

if @event_registration.save
reconcile_ce_registration if @event_registration.event&.ce_eligible?
# Prefer the CE flash ("CE registration created/removed") when reconcile set
# one; a blocked toggle-off sets flash[:alert], which survives independently.
notice = flash[:notice].presence || "Registration was successfully updated."
notice = "Registration was successfully updated."
respond_to do |format|
format.turbo_stream
format.html {
Expand Down Expand Up @@ -327,56 +323,11 @@ def toggle_checklist_step(step, completed)
end
end

# Keep the registration's CE record in step with the `ce_requested` flag (set on
# the edit form / at intake), mirroring how a scholarship is awarded from its
# "Requested" toggle. Requested + none yet → create a stub against the chosen
# license (or the registrant's only license, else a placeholder); license/hours/
# cost/certificate are then edited on the CE edit page. Un-requested → remove it,
# admins only, and never one that carries payments (the flag is restored and the
# admin is told to revert the payment first). Sets a flash describing what changed.
def reconcile_ce_registration
if @event_registration.ce_requested?
create_ce_registration_stub
else
remove_ce_registration
end
end

def create_ce_registration_stub
return if @event_registration.continuing_education_registrations.exists?

@event_registration.continuing_education_registrations.create!(professional_license: ce_license_for_create)
flash[:notice] = "CE registration created."
end

def remove_ce_registration
registrations = @event_registration.continuing_education_registrations
return if registrations.none? || !allowed_to?(:manage?, with: EventRegistrationPolicy)

if registrations.any? { |registration| registration.allocations.exists? }
@event_registration.update_column(:ce_requested, true)
flash[:alert] = "Can't remove CE — it has payments. Revert the payment first."
return
end

registrations.destroy_all
flash[:notice] = "CE registration removed."
end

# License a brand-new CE registration attaches to: the registrant's existing
# license, else an empty placeholder (number pending). Which license is used
# (and its number) is then changeable on the CE registration's edit page.
def ce_license_for_create
@event_registration.registrant.professional_licenses.first ||
ProfessionalLicense.find_or_create_for(person: @event_registration.registrant)
end

# Strong parameters
def event_registration_params
params.require(:event_registration).permit(
:event_id, :registrant_id, :status,
:scholarship_requested,
:ce_requested,
:shoutout,
:intends_to_pay,
:expected_payment_method,
Expand Down
63 changes: 57 additions & 6 deletions app/controllers/events/callouts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ def scholarship

# CE hours status: hours, amount owed, and license number.
def ce
case params[:checkout]
when "success"
flash.now[:notice] = "Your CE payment was successful."
when "cancelled"
flash.now[:alert] = "CE payment was cancelled. You can try again whenever you're ready."
end
end

# Pay the CE balance via Stripe Checkout.
def pay_ce
ce_registration = @event_registration.continuing_education_registrations.first
unless ce_registration&.remaining_cost&.positive?
redirect_to registration_ce_path(@event_registration.slug), alert: "No CE payment is due."
return
end

redirect_to_ce_stripe_checkout(ce_registration)
end

# Public license entry from the CE callout (type, number, issuing state, and
Expand Down Expand Up @@ -96,7 +113,6 @@ def update_ce_license
def request_ce
return redirect_to(registration_ce_path(@event_registration.slug)) unless @event.ce_eligible?

@event_registration.update!(ce_requested: true)
unless @event_registration.continuing_education_registrations.exists?
license = ProfessionalLicense.find_or_create_for(person: @event_registration.registrant)
@event_registration.continuing_education_registrations.create!(professional_license: license)
Expand Down Expand Up @@ -161,13 +177,11 @@ def set_event
@event = @event_registration.event
end

# Keep the registrant's form submission in step with a license number entered
# on the callout, so the registration record shows the same value. A no-op
# when the form, field, or submission isn't present.
# Update form submission if ce record is updated via callout
def record_ce_license_answer(number)
form = @event.registration_form
form = @event.continuing_education_form
field = form&.form_fields&.find_by(field_identifier: "ce_license_number")
submission = form&.form_submissions&.find_by(person: @event_registration.registrant)
submission = form&.form_submissions&.find_by(person: @event_registration.registrant, role: "continuing_education")
return unless field && submission

answer = submission.form_answers.find_or_initialize_by(form_field: field)
Expand Down Expand Up @@ -205,5 +219,42 @@ def resource_card(icon:, title:, subtitle:, href:, target: "_blank", trailing_ic
MagicTicketCallouts::Card.new(icon_class: icon, color: "blue", title: title, subtitle: subtitle,
href: href, target: target, trailing_icon: trailing_icon)
end

def redirect_to_ce_stripe_checkout(ce_registration)
person = @event_registration.registrant
amount = ce_registration.remaining_cost

person.set_payment_processor :stripe

checkout_session = person.payment_processor.checkout(
mode: "payment",
metadata: {
ce_registration_id: ce_registration.id,
event_registration_id: @event_registration.id,
event_id: @event.id
},
payment_intent_data: {
metadata: {
ce_registration_id: ce_registration.id,
event_registration_id: @event_registration.id,
event_id: @event.id
},
description: "CE Hours: #{@event.title}"
},
line_items: [ {
price_data: {
currency: "usd",
product_data: { name: "CE Hours: #{@event.title}" },
unit_amount: amount
},
quantity: 1
} ],
success_url: registration_ce_url(@event_registration.slug, checkout: "success"),
cancel_url: registration_ce_url(@event_registration.slug, checkout: "cancelled")
)

@event_registration.update!(checkout_session_id: checkout_session.id)
redirect_to checkout_session.url, allow_other_host: true, status: :see_other
end
end
end
36 changes: 33 additions & 3 deletions app/controllers/events/public_registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def new
@form_fields = visible_form_fields
@scholarship = scholarship_mode?
@scholarship_form = @event.scholarship_form if @scholarship
@continuing_education_form = @event.continuing_education_form
@event = @event.decorate
end

Expand All @@ -35,17 +36,23 @@ def create
@registration_form = registration_form
@scholarship = scholarship_mode?
@scholarship_form = @event.scholarship_form if @scholarship
@continuing_education_form = @event.continuing_education_form

all_params = params.dig(:public_registration, :form_fields)&.to_unsafe_h || {}
registration_params, scholarship_params = split_form_params(all_params)
registration_params, scholarship_params, continuing_education_params = split_form_params(all_params)

@field_errors = validate_required_fields(registration_params)
if @scholarship_form
scholarship_fields = @scholarship_form.form_fields.where.not(answer_type: :group_header).to_a
@field_errors = @field_errors.merge(FormAnswerValidator.call(scholarship_fields, scholarship_params))
end
if @continuing_education_form
ce_fields = @continuing_education_form.form_fields.where.not(answer_type: :group_header).to_a
@field_errors = @field_errors.merge(FormAnswerValidator.call(ce_fields, continuing_education_params))
end
if @field_errors.any?
@form_fields = visible_form_fields
@continuing_education_form = @event.continuing_education_form
@event = @event.decorate
flash.now[:alert] = "Your registration is not complete yet. Scroll down to check for any errors or missing information."
render :new, status: :unprocessable_content
Expand All @@ -61,7 +68,9 @@ def create
scholarship_requested: @scholarship,
person: current_user&.person,
scholarship_form: @scholarship_form,
scholarship_params: scholarship_params
scholarship_params: scholarship_params,
continuing_education_form: @continuing_education_form,
continuing_education_params: continuing_education_params
)

if result.success?
Expand All @@ -76,6 +85,7 @@ def create
end
else
@form_fields = visible_form_fields
@continuing_education_form = @event.continuing_education_form
@event = @event.decorate
flash.now[:error] = result.errors.join(", ")
flash.now[:alert] = "Your registration is not complete yet. Scroll down to check for any errors or missing information."
Expand Down Expand Up @@ -120,6 +130,7 @@ def show
@responses = @form_submission.form_answers.index_by(&:form_field_id)

load_scholarship_submission(person)
load_continuing_education_submission(person)

@event = @event.decorate
end
Expand Down Expand Up @@ -190,6 +201,19 @@ def load_scholarship_submission(person)
@scholarship_responses = application.answers_by_field_id
end

def load_continuing_education_submission(person)
ce_form = @event.continuing_education_form
return unless ce_form

submission = ce_form.form_submissions.find_by(person: person, event: @event, role: "continuing_education")
return unless submission

@continuing_education_submission = submission
@continuing_education_form = ce_form
@continuing_education_fields = ce_form.form_fields.reorder(position: :asc)
@continuing_education_responses = submission.form_answers.index_by(&:form_field_id)
end

def scholarship_mode?
params[:scholarship_requested] == "true"
end
Expand All @@ -204,7 +228,13 @@ def split_form_params(all_params)
scholarship = all_params.slice(*scholarship_field_ids)
end

[ registration, scholarship ]
continuing_education = {}
if @continuing_education_form
ce_field_ids = @continuing_education_form.form_fields.pluck(:id).map(&:to_s)
continuing_education = all_params.slice(*ce_field_ids)
end

[ registration, scholarship, continuing_education ]
end

def visible_form_fields
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ def onboarding_csv_row(registration, cost_required, day_count, include_ce = fals
row << onboarding_scholarship_tasks_csv(registration)
if include_ce
ce_hours = registration.ce_hours_total
row << (registration.ce_requested? ? "Yes" : "No")
row << (registration.ce_registered? ? "Yes" : "No")
row << (ce_hours.positive? ? helpers.plain_number(ce_hours) : "")
row << (registration.ce_amount_owed_cents.positive? ? helpers.dollars_from_cents(registration.ce_amount_owed_cents) : "")
row << registration.ce_license_numbers.join("; ")
Expand Down Expand Up @@ -700,6 +700,7 @@ def assign_event_forms(event)
assign_event_form(event, :registration, params.dig(:event, :registration_form_id))
assign_event_form(event, :scholarship, params.dig(:event, :scholarship_form_id))
assign_event_form(event, :bulk_payment, params.dig(:event, :bulk_payment_form_id))
assign_event_form(event, :continuing_education, params.dig(:event, :continuing_education_form_id))
end

def assign_event_form(event, role, form_id)
Expand All @@ -719,6 +720,7 @@ def set_form_variables
@registration_forms = Form.standalone.where(role: "registration").order(:name)
@scholarship_forms = Form.standalone.where(role: "scholarship").order(:name)
@bulk_payment_forms = Form.standalone.where(role: "bulk_payment").order(:name)
@continuing_education_forms = Form.standalone.where(role: "continuing_education").order(:name)
@categories_grouped =
Category
.includes(:category_type)
Expand Down
3 changes: 1 addition & 2 deletions app/decorators/event_registration_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ class EventRegistrationDecorator < ApplicationDecorator
# `simulate_paid:` lets the CE callout's ?admin=true preview the post-payment state
# without recording a payment.
def ce_status_badge(simulate_paid: false)
return unless ce_requested? || ce_registered?
return ce_badge("Requested", "fa-solid fa-clock", :amber) unless ce_registered?
return unless ce_registered?
return ce_badge("Issued", "fa-solid fa-circle-check", :green) if ce_certificate_issued?
return ce_badge("License # needed", "fa-solid fa-id-card", :amber) unless ce_license_provided?
return ce_badge("Pending", "fa-solid fa-hourglass-half", :blue) if ce_paid_in_full? || simulate_paid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export default class extends Controller {
el.classList.toggle("hidden", sectionRole !== "scholarship")
} else if (role === "bulk_payment") {
el.classList.toggle("hidden", sectionRole !== "bulk_payment")
} else if (role === "continuing_education") {
el.classList.toggle("hidden", sectionRole !== "continuing_education")
} else {
el.classList.toggle("hidden", sectionRole === "scholarship" || sectionRole === "bulk_payment")
el.classList.toggle("hidden", sectionRole === "scholarship" || sectionRole === "bulk_payment" || sectionRole === "continuing_education")
}
})
}
Expand Down
1 change: 0 additions & 1 deletion app/helpers/events_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def onboarding_columns(event)
columns << { key: "funder", label: "Scholarship grant", kind: :funder, sortable: true, align: "left", toggle: "funder" }
columns << { key: "scholarship_tasks_completed", label: "Scholarship tasks done", kind: :scholarship_tasks, sortable: true, align: "center", toggle: "scholarship_tasks_completed" }
if event.ce_eligible?
columns << { key: "ce_requested", label: "CE requested", kind: :ce_requested, sortable: true, align: "center", toggle: "ce_requested" }
columns << { key: "ce_hours", label: "CE hours", kind: :ce_hours, sortable: true, align: "center", toggle: "ce_hours" }
columns << { key: "ce_amount", label: "CE amount", kind: :ce_amount, sortable: true, align: "center", toggle: "ce_amount" }
columns << { key: "ce_license", label: "License #", kind: :ce_license, sortable: true, align: "center", toggle: "ce_license" }
Expand Down
34 changes: 33 additions & 1 deletion app/models/concerns/pay_charge_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ def create_external_processor_payment
return if ExternalProcessorPayment.exists?(pay_charge_id: id)
return unless object["paid"] == true

if (event_registration_id = metadata["event_registration_id"])
if (ce_registration_id = metadata["ce_registration_id"])
create_ce_payment(ce_registration_id)
elsif (event_registration_id = metadata["event_registration_id"])
create_event_registration_payment(event_registration_id)
elsif (form_submission_id = metadata["form_submission_id"])
create_bulk_payment(form_submission_id)
Expand Down Expand Up @@ -54,6 +56,36 @@ def create_event_registration_payment(event_registration_id)
registration.update!(payment_unresolved: false)
end

def create_ce_payment(ce_registration_id)
ce_registration = ContinuingEducationRegistration.find_by(id: ce_registration_id)
return unless ce_registration

person = ce_registration.event_registration&.registrant
return unless person

payment = ExternalProcessorPayment.create!(
stripe_charge_id: processor_id,
external_origin: false,
person: person,
amount_cents: amount,
amount_cents_remaining: amount,
currency: currency,
pay_charge_id: id,
metadata: metadata.merge(stripe_charge: object)
)

remaining_needed = ce_registration.remaining_cost
allocation_amount = [ amount, remaining_needed ].min

if allocation_amount > 0
Allocation.create!(
source: payment,
allocatable: ce_registration,
amount: allocation_amount
)
end
end

def create_bulk_payment(form_submission_id)
submission = FormSubmission.find_by(id: form_submission_id)
return unless submission&.role == "bulk_payment"
Expand Down
4 changes: 4 additions & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ def bulk_payment_form
forms.find_by(event_forms: { role: "bulk_payment" })
end

def continuing_education_form
forms.find_by(event_forms: { role: "continuing_education" })
end

def active_registration_for(person)
return nil unless person
event_registrations.active.find_by(registrant_id: person.id)
Expand Down
Loading