-
Notifications
You must be signed in to change notification settings - Fork 24
CE cutover: reroute intake/display, drop ce_* columns (PR 2) #1917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: maebeale/continuing-education-model
Are you sure you want to change the base?
Changes from all commits
d91d07d
222e4e9
56c611e
6eb578a
c1117e6
2b1f4d4
bc77080
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,10 +44,30 @@ def scholarship | |
| @form_responses_available = @event.registration_form&.form_submissions&.exists?(person: @event_registration.registrant) | ||
| end | ||
|
|
||
| # CE hours status: requested hours, amount owed, and license number. | ||
| # CE hours status: hours, amount owed, and license number. | ||
| def ce | ||
| end | ||
|
|
||
| # Public license-number entry from the CE callout. Sets the number on the | ||
| # registrant's (first) CE registration via a found-or-created license, mirrors | ||
| # it onto the registration's form answer, then returns to the callout. Plain | ||
| # full-page POST β no Turbo. | ||
| def update_ce_license | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ From Claude: Public license entry, authorized by the registration slug (the same bearer-token model as the other callouts). Plain full-page POST (no Turbo) β it sets the number on the first CE registration via find-or-create and mirrors it onto the form answer. |
||
| ce_registration = @event_registration.continuing_education_registrations.first | ||
| return redirect_to(registration_ce_path(@event_registration.slug)) unless ce_registration | ||
|
|
||
| number = params[:license_number].to_s.strip.presence | ||
| ce_registration.professional_license = ProfessionalLicense.find_or_create_for( | ||
| person: @event_registration.registrant, number: number | ||
| ) | ||
| ce_registration.save! | ||
| record_ce_license_answer(number) | ||
|
|
||
| redirect_to registration_ce_path(@event_registration.slug), notice: "License number saved." | ||
| rescue ActiveRecord::RecordInvalid | ||
| redirect_to registration_ce_path(@event_registration.slug), alert: "We couldn't save that license number." | ||
| end | ||
|
|
||
| # Forms page: callout-card links to the W-9 and letter-to-supervisors | ||
| # resource pages (when seeded) and the invoice, each returning to forms. | ||
| def forms | ||
|
|
@@ -103,6 +123,19 @@ 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. | ||
| def record_ce_license_answer(number) | ||
| form = @event.registration_form | ||
| field = form&.form_fields&.find_by(field_identifier: "ce_license_number") | ||
| submission = form&.form_submissions&.find_by(person: @event_registration.registrant) | ||
| return unless field && submission | ||
|
|
||
| answer = submission.form_answers.find_or_initialize_by(form_field: field) | ||
| answer.update!(submitted_answer: number.to_s, question_name_when_answered: field.name) | ||
| end | ||
|
|
||
| # Builds the callout-card links shown on the forms page. The W-9 and the | ||
| # letter to supervisors open in their own resource page (preview + download) | ||
| # when seeded; the invoice is always available. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,16 +39,11 @@ class EventRegistration < ApplicationRecord | |
| # only the first event.day_count of them are shown on the Onboarding tab. | ||
| DAY_FIELDS = (1..5).map { |day| "completed_day_#{day}" }.freeze | ||
|
|
||
| # Default price the registrant owes per requested continuing-education hour. | ||
| # The CE summary on the registration form multiplies it by ce_hours_requested. | ||
| CE_HOURLY_RATE_DOLLARS = 25 | ||
|
|
||
| # Validations | ||
| validates :registrant_id, uniqueness: { scope: :event_id } | ||
| validates :event_id, presence: true | ||
| validates :status, inclusion: { in: ATTENDANCE_STATUSES }, allow_nil: false | ||
| validates :slug, uniqueness: true, allow_nil: true | ||
| validates :ce_hours_requested, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true | ||
|
|
||
| # Scopes | ||
| scope :registrant_name, ->(registrant_name) { joins(:registrant).where( | ||
|
|
@@ -146,6 +141,30 @@ class EventRegistration < ApplicationRecord | |
| else all | ||
| end | ||
| } | ||
| # Filter by CE state. "needs_license" is derived (a CE registration sits on a | ||
| # placeholder license); the rest match a ContinuingEducationRegistration#status. | ||
| scope :ce_status, ->(value) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ From Claude: |
||
| case value | ||
| when "needs_license" | ||
| where(<<~SQL.squish) | ||
| EXISTS ( | ||
| SELECT 1 FROM continuing_education_registrations cer | ||
| JOIN professional_licenses pl ON pl.id = cer.professional_license_id | ||
| WHERE cer.event_registration_id = event_registrations.id | ||
| AND pl.number IS NULL | ||
| ) | ||
| SQL | ||
| when *ContinuingEducationRegistration::STATUSES | ||
| where(<<~SQL.squish, value) | ||
| EXISTS ( | ||
| SELECT 1 FROM continuing_education_registrations cer | ||
| WHERE cer.event_registration_id = event_registrations.id | ||
| AND cer.status = ? | ||
| ) | ||
| SQL | ||
| else all | ||
| end | ||
| } | ||
| scope :keyword, ->(term) { | ||
| return none if term.blank? | ||
|
|
||
|
|
@@ -184,6 +203,9 @@ def self.search_by_params(params) | |
| .where(event_registration_organizations: { organization_id: params[:organization_id] }) | ||
| .distinct | ||
| end | ||
| if params[:ce_status].present? | ||
| registrations = registrations.ce_status(params[:ce_status]) | ||
| end | ||
| registrations | ||
| end | ||
|
|
||
|
|
@@ -299,15 +321,54 @@ def discount_sum | |
| allocations.where(source_type: "Discount").sum(:amount) | ||
| end | ||
|
|
||
| # True when the registrant has supplied a CE license number. | ||
| def ce_license_provided? | ||
| ce_license_number.present? | ||
| # CE is now tracked as one or more ContinuingEducationRegistration records, | ||
| # each against a professional license. These aggregate across them so callers | ||
| # (callouts, onboarding, CSV) read a single registration-level figure. | ||
| def ce_requested? | ||
| if ce_registrations_in_memory? | ||
| return continuing_education_registrations.any? | ||
| end | ||
| continuing_education_registrations.exists? | ||
| end | ||
|
|
||
| def ce_hours_total | ||
| if ce_registrations_in_memory? | ||
| return continuing_education_registrations.sum { |c| c.hours.to_d } | ||
| end | ||
| continuing_education_registrations.sum(:hours) | ||
| end | ||
|
|
||
| # What the registrant owes for their requested CE hours, in cents, at the | ||
| # default hourly rate. Zero when no hours were requested. | ||
| def ce_amount_owed_cents | ||
| ce_hours_requested.to_i * CE_HOURLY_RATE_DOLLARS * 100 | ||
| if ce_registrations_in_memory? | ||
| return continuing_education_registrations.sum { |c| c.amount_cents.to_i } | ||
| end | ||
| continuing_education_registrations.sum(:amount_cents) | ||
| end | ||
|
|
||
| # True only when every CE registration has a known license number on file. | ||
| def ce_license_provided? | ||
| return false unless ce_requested? | ||
|
|
||
| continuing_education_registrations.all? { |c| c.professional_license&.number_known? } | ||
| end | ||
|
|
||
| # True when CE is requested and every CE registration is fully paid. | ||
| def ce_paid_in_full? | ||
| return false unless ce_requested? | ||
|
|
||
| continuing_education_registrations.all?(&:paid_in_full?) | ||
| end | ||
|
|
||
| # License numbers on file across this registration's CE registrations. | ||
| def ce_license_numbers | ||
| continuing_education_registrations.filter_map { |c| c.professional_license&.number } | ||
| end | ||
|
|
||
| # Read CE registrations from the in-memory collection rather than the DB when | ||
| # it's already loaded or this registration isn't persisted (e.g. the unsaved | ||
| # sample-ticket preview builds CE registrations without saving). | ||
| def ce_registrations_in_memory? | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ From Claude: The aggregators read the in-memory collection when the registration is a |
||
| continuing_education_registrations.loaded? || new_record? | ||
| end | ||
|
|
||
| def joinable? | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π€ From Claude: The admin CE section posts under a separate
cenamespace and is reconciled here rather than viaaccepts_nested_attributes_for. Editing a license number through the CE registration means touching a separateProfessionalLicense(find-or-create), which nested attributes handle fragilely; this keeps it explicit and covers the common single-license case (multi-license is managed elsewhere).