From da977031a3cbb85d21665273d0171e86cf7478a3 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 21 Jun 2026 23:20:26 -0400 Subject: [PATCH 1/3] Apply registrant's org type & website to linked organizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registration captured Organization Type and Website only as FormAnswer rows; they were never written to the Organization's agency_type / website_url columns. Both org-linking actions now apply the registrant's answers to the org — for new orgs (create_organization) and existing ones (select_organization) alike — overriding any curated value so the registrant's answer wins. A blank answer is skipped so it can't wipe a curated value, and every change is captured as an Ahoy lifecycle event. Co-Authored-By: Claude Opus 4.8 --- .../event_registrations_controller.rb | 53 +++++++++++++--- spec/requests/event_registrations_spec.rb | 63 +++++++++++++++++++ 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb index 8bba54a91c..972ce1ce0d 100644 --- a/app/controllers/event_registrations_controller.rb +++ b/app/controllers/event_registrations_controller.rb @@ -219,6 +219,8 @@ def select_organization training_date: @event_registration.event.start_date ) + apply_submitted_organization_attributes(organization, @event_registration) + @event_registration.event_registration_organizations .find_or_create_by!(organization: organization) @@ -251,6 +253,9 @@ def create_organization job_title: submitted_position(@event_registration), training_date: @event_registration.event.start_date ) + + apply_submitted_organization_attributes(organization, @event_registration) + @event_registration.event_registration_organizations.find_or_create_by!(organization: organization) notice = existing ? "#{organization.name} linked." : "#{organization.name} created and linked." @@ -375,17 +380,20 @@ def csv_export(registrations) end end - # Each registration-form submission with the org name and position the registrant - # entered on it: [{ submission:, org_name:, position: }], oldest first. + # Each registration-form submission with the org details the registrant entered on + # it: [{ submission:, org_name:, position:, agency_type:, agency_website: }], oldest + # first. def registration_submission_entries(registration) form = registration.event.registration_form return [] unless form field_ids = form.form_fields - .where(field_identifier: %w[agency_name agency_position]) + .where(field_identifier: %w[agency_name agency_position agency_type agency_website]) .pluck(:field_identifier, :id).to_h name_field_id = field_ids["agency_name"] position_field_id = field_ids["agency_position"] + type_field_id = field_ids["agency_type"] + website_field_id = field_ids["agency_website"] entries = registration.registrant.form_submissions .where(form: form) @@ -396,7 +404,9 @@ def registration_submission_entries(registration) { submission: submission, org_name: name_field_id && answers[name_field_id]&.submitted_answer, - position: position_field_id && answers[position_field_id]&.submitted_answer + position: position_field_id && answers[position_field_id]&.submitted_answer, + agency_type: type_field_id && answers[type_field_id]&.submitted_answer, + agency_website: website_field_id && answers[website_field_id]&.submitted_answer } end @@ -418,13 +428,36 @@ def submitted_agency_names(registration) .uniq { |name| name.downcase } end + # The "primary" registration submission used whenever we apply what the registrant + # typed to a linked org: the first submission that named an org, else the first. + # link_organization shows this same submission, so what we apply matches the editor. + def primary_submission_entry(registration) + entries = registration_submission_entries(registration) + entries.find { |entry| entry[:org_name].present? } || entries.first + end + # The job title/position the registrant typed for their organization on the - # registration form. Uses the same "primary" submission as link_organization - # (the first submission that named an org, else the first), so the title applied - # when linking matches what the editor shows. + # registration form, from the primary submission. def submitted_position(registration) - entries = registration_submission_entries(registration) - primary = entries.find { |entry| entry[:org_name].present? } || entries.first - primary && primary[:position] + primary_submission_entry(registration)&.dig(:position) + end + + # The Organization Type and Website the registrant typed on the registration form, + # as Organization attributes ({ agency_type:, website_url: }) — only the values the + # registrant actually provided, so a blank answer never clobbers a curated value. + def submitted_organization_attributes(registration) + entry = primary_submission_entry(registration) + return {} unless entry + + { agency_type: entry[:agency_type], website_url: entry[:agency_website] } + .select { |_, value| value.present? } + end + + # Overwrite the org's Organization Type and Website with what the registrant typed. + # We intentionally override curated values here (admins asked for the registrant's + # answer to win); every change is captured as an Ahoy lifecycle event. + def apply_submitted_organization_attributes(organization, registration) + attributes = submitted_organization_attributes(registration) + organization.update!(attributes) if attributes.any? end end diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb index cbbf84ab23..353e584e96 100644 --- a/spec/requests/event_registrations_spec.rb +++ b/spec/requests/event_registrations_spec.rb @@ -634,6 +634,51 @@ def details_open?(body, heading) expect(regular_user.person.affiliations.where(organization: organization).pluck(:title)) .to contain_exactly("Facilitator") end + + it "applies the submitted organization type and website to the linked org" do + reg_form = create(:form, name: "Reg form") + type_field = create(:form_field, form: reg_form, field_identifier: "agency_type") + website_field = create(:form_field, form: reg_form, field_identifier: "agency_website") + create(:event_form, :registration, event: event, form: reg_form) + submission = create(:form_submission, person: regular_user.person, form: reg_form) + create(:form_answer, form_submission: submission, form_field: type_field, submitted_answer: "For-profit") + create(:form_answer, form_submission: submission, form_field: website_field, submitted_answer: "example.org") + + post select_organization_event_registration_path(existing_registration), + params: { organization_id: organization.id } + + expect(organization.reload).to have_attributes(agency_type: "For-profit", website_url: "example.org") + end + + it "overrides the existing org's curated type and website with the submitted values" do + organization.update!(agency_type: "Government agency", website_url: "curated.org") + reg_form = create(:form, name: "Reg form") + type_field = create(:form_field, form: reg_form, field_identifier: "agency_type") + website_field = create(:form_field, form: reg_form, field_identifier: "agency_website") + create(:event_form, :registration, event: event, form: reg_form) + submission = create(:form_submission, person: regular_user.person, form: reg_form) + create(:form_answer, form_submission: submission, form_field: type_field, submitted_answer: "For-profit") + create(:form_answer, form_submission: submission, form_field: website_field, submitted_answer: "new.org") + + post select_organization_event_registration_path(existing_registration), + params: { organization_id: organization.id } + + expect(organization.reload).to have_attributes(agency_type: "For-profit", website_url: "new.org") + end + + it "leaves a curated value untouched when the registrant left that answer blank" do + organization.update!(agency_type: "Government agency", website_url: "curated.org") + reg_form = create(:form, name: "Reg form") + website_field = create(:form_field, form: reg_form, field_identifier: "agency_website") + create(:event_form, :registration, event: event, form: reg_form) + submission = create(:form_submission, person: regular_user.person, form: reg_form) + create(:form_answer, form_submission: submission, form_field: website_field, submitted_answer: "new.org") + + post select_organization_event_registration_path(existing_registration), + params: { organization_id: organization.id } + + expect(organization.reload).to have_attributes(agency_type: "Government agency", website_url: "new.org") + end end describe "POST /event_registrations/:id/create_organization" do @@ -711,6 +756,24 @@ def details_open?(body, heading) expect(existing_registration.organizations.pluck(:name)).not_to include("Alpha Agency") end + it "applies the submitted organization type and website to the new org" do + create(:organization_status, name: "Active") + reg_form = create(:form, name: "Reg form") + name_field = create(:form_field, form: reg_form, field_identifier: "agency_name") + type_field = create(:form_field, form: reg_form, field_identifier: "agency_type") + website_field = create(:form_field, form: reg_form, field_identifier: "agency_website") + create(:event_form, :registration, event: event, form: reg_form) + submission = create(:form_submission, person: regular_user.person, form: reg_form) + create(:form_answer, form_submission: submission, form_field: name_field, submitted_answer: "Brand New Org") + create(:form_answer, form_submission: submission, form_field: type_field, submitted_answer: "501c3/nonprofit") + create(:form_answer, form_submission: submission, form_field: website_field, submitted_answer: "brandnew.org") + + post create_organization_event_registration_path(existing_registration) + + expect(Organization.find_by(name: "Brand New Org")) + .to have_attributes(agency_type: "501c3/nonprofit", website_url: "brandnew.org") + end + it "rejects creating an org name the registrant didn't submit" do create(:organization_status, name: "Active") reg_form = create(:form, name: "Reg form") From 25a73d4e159d6d08ac880a43f964081c0a9966b2 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 21 Jun 2026 23:28:57 -0400 Subject: [PATCH 2/3] Use the newest submission as the registrant's primary org details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A registrant can have multiple registration-form submissions; the latest one reflects their current org details. Treat the newest submission (that named an org) as primary instead of the oldest, so the type/website/position we apply — and what the editor surfaces — come from their latest answer. Consolidates the duplicate "primary submission" logic the editor had into the shared helper. Co-Authored-By: Claude Opus 4.8 --- .../event_registrations_controller.rb | 24 ++++++++++--------- spec/requests/event_registrations_spec.rb | 22 +++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb index 972ce1ce0d..183e4fc74f 100644 --- a/app/controllers/event_registrations_controller.rb +++ b/app/controllers/event_registrations_controller.rb @@ -187,9 +187,10 @@ def link_organization # duplicate data), so we surface them all rather than silently picking one. @submitted_entries = registration_submission_entries(@event_registration) @form_submission = @submitted_entries.first&.fetch(:submission) - # The "primary" submitted org/title — the first submission that named an org - # (else the first submission) — drives the suggested-match and comparison logic. - primary = @submitted_entries.find { |entry| entry[:org_name].present? } || @submitted_entries.first + # The "primary" submitted org/title — the newest submission that named an org + # (else the newest submission) — drives the suggested-match and comparison logic, + # and is the same submission whose details linking applies to the org. + primary = primary_submission_entry(@submitted_entries) @submitted_org_name = primary && primary[:org_name] @submitted_position = primary && primary[:position] # Each distinct submitted org name that isn't already in the database gets its @@ -428,25 +429,26 @@ def submitted_agency_names(registration) .uniq { |name| name.downcase } end - # The "primary" registration submission used whenever we apply what the registrant - # typed to a linked org: the first submission that named an org, else the first. - # link_organization shows this same submission, so what we apply matches the editor. - def primary_submission_entry(registration) - entries = registration_submission_entries(registration) - entries.find { |entry| entry[:org_name].present? } || entries.first + # The "primary" entry among a registrant's submission entries — the one whose + # details we apply to a linked org and surface in the editor: the NEWEST submission + # that named an org, else the newest submission. Entries come oldest-first, so we + # scan from the end. link_organization picks the same one, so what we apply matches + # the editor. + def primary_submission_entry(entries) + entries.reverse.find { |entry| entry[:org_name].present? } || entries.last end # The job title/position the registrant typed for their organization on the # registration form, from the primary submission. def submitted_position(registration) - primary_submission_entry(registration)&.dig(:position) + primary_submission_entry(registration_submission_entries(registration))&.dig(:position) end # The Organization Type and Website the registrant typed on the registration form, # as Organization attributes ({ agency_type:, website_url: }) — only the values the # registrant actually provided, so a blank answer never clobbers a curated value. def submitted_organization_attributes(registration) - entry = primary_submission_entry(registration) + entry = primary_submission_entry(registration_submission_entries(registration)) return {} unless entry { agency_type: entry[:agency_type], website_url: entry[:agency_website] } diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb index 353e584e96..993b688aea 100644 --- a/spec/requests/event_registrations_spec.rb +++ b/spec/requests/event_registrations_spec.rb @@ -666,6 +666,28 @@ def details_open?(body, heading) expect(organization.reload).to have_attributes(agency_type: "For-profit", website_url: "new.org") end + it "applies the newest submission's type and website when there are several" do + reg_form = create(:form, name: "Reg form") + name_field = create(:form_field, form: reg_form, field_identifier: "agency_name") + type_field = create(:form_field, form: reg_form, field_identifier: "agency_type") + website_field = create(:form_field, form: reg_form, field_identifier: "agency_website") + create(:event_form, :registration, event: event, form: reg_form) + + older = create(:form_submission, person: regular_user.person, form: reg_form, created_at: 2.days.ago) + create(:form_answer, form_submission: older, form_field: name_field, submitted_answer: "Helping Hands") + create(:form_answer, form_submission: older, form_field: type_field, submitted_answer: "Government agency") + create(:form_answer, form_submission: older, form_field: website_field, submitted_answer: "old.org") + newer = create(:form_submission, person: regular_user.person, form: reg_form, created_at: 1.day.ago) + create(:form_answer, form_submission: newer, form_field: name_field, submitted_answer: "Helping Hands") + create(:form_answer, form_submission: newer, form_field: type_field, submitted_answer: "For-profit") + create(:form_answer, form_submission: newer, form_field: website_field, submitted_answer: "new.org") + + post select_organization_event_registration_path(existing_registration), + params: { organization_id: organization.id } + + expect(organization.reload).to have_attributes(agency_type: "For-profit", website_url: "new.org") + end + it "leaves a curated value untouched when the registrant left that answer blank" do organization.update!(agency_type: "Government agency", website_url: "curated.org") reg_form = create(:form, name: "Reg form") From e0d8e85a8e6cbd003da6a38f3e711e1ef421bce8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 21 Jun 2026 23:35:23 -0400 Subject: [PATCH 3/3] Pick the newest submission that collected an org address as primary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not every registration form asks for organization info, so the newest submission overall may carry none. Select the newest submission whose org address was filled in (agency_city present — the same signal public_registration uses to build the address), falling back to the newest that named an org, then the newest overall. Keeps the applied org details from latching onto a later, unrelated submission. Co-Authored-By: Claude Opus 4.8 --- .../event_registrations_controller.rb | 20 +++++++++++----- spec/requests/event_registrations_spec.rb | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb index 183e4fc74f..754f8fe6d3 100644 --- a/app/controllers/event_registrations_controller.rb +++ b/app/controllers/event_registrations_controller.rb @@ -389,10 +389,11 @@ def registration_submission_entries(registration) return [] unless form field_ids = form.form_fields - .where(field_identifier: %w[agency_name agency_position agency_type agency_website]) + .where(field_identifier: %w[agency_name agency_position agency_city agency_type agency_website]) .pluck(:field_identifier, :id).to_h name_field_id = field_ids["agency_name"] position_field_id = field_ids["agency_position"] + city_field_id = field_ids["agency_city"] type_field_id = field_ids["agency_type"] website_field_id = field_ids["agency_website"] @@ -406,6 +407,7 @@ def registration_submission_entries(registration) submission: submission, org_name: name_field_id && answers[name_field_id]&.submitted_answer, position: position_field_id && answers[position_field_id]&.submitted_answer, + agency_city: city_field_id && answers[city_field_id]&.submitted_answer, agency_type: type_field_id && answers[type_field_id]&.submitted_answer, agency_website: website_field_id && answers[website_field_id]&.submitted_answer } @@ -430,12 +432,18 @@ def submitted_agency_names(registration) end # The "primary" entry among a registrant's submission entries — the one whose - # details we apply to a linked org and surface in the editor: the NEWEST submission - # that named an org, else the newest submission. Entries come oldest-first, so we - # scan from the end. link_organization picks the same one, so what we apply matches - # the editor. + # details we apply to a linked org and surface in the editor. Not every form asks + # for organization info, so the newest submission overall may carry none; we pick + # the NEWEST submission that actually collected an org address (agency_city present + # — the same signal public_registration uses to build the address), falling back to + # the newest that named an org, then the newest overall. Entries come oldest-first, + # so we scan from the end. link_organization picks the same one, so what we apply + # matches the editor. def primary_submission_entry(entries) - entries.reverse.find { |entry| entry[:org_name].present? } || entries.last + newest_first = entries.reverse + newest_first.find { |entry| entry[:agency_city].present? } || + newest_first.find { |entry| entry[:org_name].present? } || + newest_first.first end # The job title/position the registrant typed for their organization on the diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb index 993b688aea..e3a62e8704 100644 --- a/spec/requests/event_registrations_spec.rb +++ b/spec/requests/event_registrations_spec.rb @@ -688,6 +688,29 @@ def details_open?(body, heading) expect(organization.reload).to have_attributes(agency_type: "For-profit", website_url: "new.org") end + it "skips a newer submission that collected no org address, using the newest one that did" do + reg_form = create(:form, name: "Reg form") + city_field = create(:form_field, form: reg_form, field_identifier: "agency_city") + type_field = create(:form_field, form: reg_form, field_identifier: "agency_type") + website_field = create(:form_field, form: reg_form, field_identifier: "agency_website") + create(:event_form, :registration, event: event, form: reg_form) + + # Older submission collected an org address (and its type/website). + with_address = create(:form_submission, person: regular_user.person, form: reg_form, created_at: 2.days.ago) + create(:form_answer, form_submission: with_address, form_field: city_field, submitted_answer: "Seattle") + create(:form_answer, form_submission: with_address, form_field: type_field, submitted_answer: "For-profit") + create(:form_answer, form_submission: with_address, form_field: website_field, submitted_answer: "right.org") + # Newer submission from a form that didn't ask for org info. + no_address = create(:form_submission, person: regular_user.person, form: reg_form, created_at: 1.day.ago) + create(:form_answer, form_submission: no_address, form_field: type_field, submitted_answer: "Government agency") + create(:form_answer, form_submission: no_address, form_field: website_field, submitted_answer: "wrong.org") + + post select_organization_event_registration_path(existing_registration), + params: { organization_id: organization.id } + + expect(organization.reload).to have_attributes(agency_type: "For-profit", website_url: "right.org") + end + it "leaves a curated value untouched when the registrant left that answer blank" do organization.update!(agency_type: "Government agency", website_url: "curated.org") reg_form = create(:form, name: "Reg form")