Skip to content

Auto-fill org type/website/address when linking from a registration, and keep a record of it - #2140

Open
maebeale wants to merge 15 commits into
mainfrom
maebeale/autofill-org-type-website
Open

Auto-fill org type/website/address when linking from a registration, and keep a record of it#2140
maebeale wants to merge 15 commits into
mainfrom
maebeale/autofill-org-type-website

Conversation

@maebeale

@maebeale maebeale commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 four POROs, shared controller helpers, a new column, one XSS fix, and address-matching semantics that decide when two saved addresses are the same place

Why

When an admin links an org to a registration, the submitted org profile answers (type, website, address) weren't applied — and there was no signal about what happened or whether the form's answers matched the org. Admins couldn't tell that a form said "School" while the org is saved as "501c3", or that the submitted address differs from the one on file.

What

Both link paths — Create and link and Link existing ({create,select}_organization):

  • Fill blanks only for agency_type, website_url, and the work address (street/city/state/ZIP/country) from the submission — never clobbering curated values.
  • Anchor the affiliation to the org's sole address when the submission carried no address.
  • Flash notice naming only what was really written: "Acme linked. Saved from the form: type, website, and work address in Austin.", or "ZIP and country on the Austin work address" when an existing one was filled, or nothing at all when the submission changed nothing.
  • Flash warning when a submitted answer differs from a value already on the org (kept-vs-form values named), so the admin can reconcile.
  • Two persistent per-org notes on the linking page, since the flash is gone by the next page load: what a form submission filled on an org that already existed (new event_registration_organizations.form_filled_fields, written by the public flow too — that's where most fills happen), and the type/website/address answers that differ from the org's saved profile.

Semantics

  • Whose answers apply to which org (submission_entry_for): the submission whose typed org name matches the org, else — only when there's exactly one submission and one linked org — that sole entry, which covers a registrant who named no org and an admin resolving a typo'd "Acme Inc" to the saved "Acme Corporation". An extra org an admin links by hand matches nothing, so another org's answers are never written onto it or reported against it.
  • Type/website/address are fill-blanks in the admin flow (public registration keeps its latest-wins overwrite via the new overwrite: flags on SyncProfile/UpsertAddress).
  • An org created from the submission records no "filled" note — everything on it came from the form, so there's no change to flag.
  • Address matching (AddressMatcher) tries same city + state, then same city + street, then — across cities — same street + same ZIP. The last one reunites an address whose city was respelled between submissions ("St. Louis" / "Saint Louis") without merging two real offices that share a street name in different towns. City and state on file are never flipped: the saved value stays and the difference is flagged for an admin.
  • An address the matcher doesn't recognize isn't a discrepancy — it's added as an additional work address.
  • Address fields are trimmed on write and compared trimmed, so a padded answer (browsers don't trim text inputs) isn't stored verbatim and then read back as a discrepancy against itself.

Security

Flash messages render with html_safe, so registrant-typed form answers folded into the linking warning reached admins as raw HTML. All dynamic values in the notice and warning are now escaped.

New / changed

  • event_registration_organizations.form_filled_fields (json) + EventRegistrationOrganization#form_filled_labels / #record_form_fills — persistent provenance for what a submission wrote onto an org.
  • OrganizationServices::SyncProfile — fill-blanks type/website sync; returns filled labels. Extracted from PublicRegistration.
  • OrganizationServices::ProfileDiff — read-only comparison → Discrepancy list (type + website + address).
  • OrganizationServices::AddressMatcher — shared street/ZIP-aware address matcher.
  • OrganizationServices::UpsertAddress — gains overwrite:, matcher-based lookup, and a Result (address, created, filled, saved_label) so callers report what was actually saved.

The linking actions memoize the form-submission entries they read repeatedly: 66 → 41 queries per select_organization.

Tests

  • Service specs for SyncProfile, ProfileDiff (incl. address + city), AddressMatcher (cross-city ZIP fallback, and that a differing ZIP does not merge), and UpsertAddress (overwrite: false, dedup, what filled reports, skipped street/ZIP/state).
  • Request specs: linking survives an address with no ZIP; both link paths fill blanks / preserve curated values / emit notice & warning; the fills are recorded and shown on the page afterwards; an org created from the submission records none; an extra linked org is neither seeded nor reported against; sole-address anchoring; a script payload in a form answer never renders unescaped; the notice doesn't claim an address save that didn't happen.
  • PublicRegistration spec: a registrant's answers are recorded on the registration's org link.
  • EventRegistrationOrganization model spec: form_filled_labels / record_form_fills — the merge-and-de-dup contract, and that a resubmission of what's already recorded issues no write.

Anything else to add?

addresses.street_address and zip_code are NOT NULL with no default, and city/state also have presence validations — but the registration form's address answers are individually optional. A registrant who named an org and gave a city but skipped the ZIP took down the whole public registration with a NotNullViolation (and the admin linking action with it). This predated the branch; fixed here since the flow is being reworked anyway:

  • Skipped street/ZIP are stored blank.
  • State can't be papered over the same way, so a submission without one saves no new address rather than failing the registration. A matching address is still updated, the answers stay on the form submission, and the linking page still shows them for an admin to enter.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 9, 2026 21:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 9, 2026 21:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale maebeale changed the title Auto-fill org type & website when creating and linking an org from a registration Auto-fill org type/website/address & flag discrepancies when linking an org from a registration Aug 9, 2026
@maebeale
maebeale marked this pull request as ready for review August 9, 2026 21:26
Copilot AI review requested due to automatic review settings August 9, 2026 21:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 9, 2026 21:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 9, 2026 23:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

notice = "#{flash_safe(organization.name)} #{verb}."
notice += " Saved from the form: #{saved.to_sentence}." if saved.any?
notice
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Flash messages render with html_safe, so anything registrant-typed that lands in one has to be escaped here — a form answer containing a script tag would otherwise run in the admin browser.

zip = zip_code&.strip
return if zip.blank?
addresses.find do |address|
address.street_address.to_s.strip.casecmp?(street) && address.zip_code.to_s.strip.casecmp?(zip)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: This last-resort match crosses city boundaries, so it needs the ZIP as corroboration — street alone would merge two real offices on "1 Main St" in different towns. Requiring both means it only fires for a genuinely respelled city.

Copilot AI review requested due to automatic review settings August 9, 2026 23:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

updates[:city] = @city if existing.city.blank?
updates[:state] = @state if @state.present? && existing.state.blank?
updates[:street_address] = @street_address if @street_address.present? && (@overwrite || existing.street_address.blank?)
updates[:zip_code] = @zip_code if @zip_code.present? && (@overwrite || existing.zip_code.blank?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: State is the one address field that can't be defaulted — Address validates its presence — so a submission without one saves no new address instead of failing the whole registration. A matching address is still updated, and the raw answers stay on the form submission for an admin to enter by hand.

Copilot AI review requested due to automatic review settings August 10, 2026 00:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale maebeale changed the title Auto-fill org type/website/address & flag discrepancies when linking an org from a registration Flag unapplied org answers when linking a registration; seed a profile only for an org we create Aug 10, 2026
Copilot AI review requested due to automatic review settings August 10, 2026 00:29
@maebeale
maebeale force-pushed the maebeale/autofill-org-type-website branch from 87c871b to 6ae1d1c Compare August 10, 2026 00:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale maebeale changed the title Flag unapplied org answers when linking a registration; seed a profile only for an org we create Auto-fill org type/website/address when linking from a registration, and keep a record of it Aug 10, 2026
@maebeale
maebeale force-pushed the maebeale/autofill-org-type-website branch from 6ae1d1c to 139d20c Compare August 10, 2026 00:37
Copilot AI review requested due to automatic review settings August 10, 2026 00:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 01:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 02:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 02:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale requested a review from jmilljr24 August 10, 2026 02:45
@maebeale

Copy link
Copy Markdown
Collaborator Author

@jmilljr24 this was on the spreadsheet: "Can the organization type and website url be automatically populated when we create and link the org?"

@jmilljr24 jmilljr24 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one thought

Comment on lines +12 to +13
def form_filled_labels
Array(form_filled_fields)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The column name form_filled_fields was a bit confusing on first read (I assumed it was saving the filled out answers). Disregard this comment if I'm wrong but it appears the column is storing the labels and this method just ensures an empty array is returned if the column is null.

Could the column be named form_filled_labels to be more clear its purpose? Then this method can still be called to return the empty array vs nil.

Suggested change
def form_filled_labels
Array(form_filled_fields)
def form_filled_labels
Array(super)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: You read it right, and the rename is in (7663a6cb9) — the column stored the names of what changed ("website", "ZIP on the Austin work address"), never the submitted answers, which stay on the form submission. It is now form_filled_labels, and the method survives as an override of the generated reader (Array(super)), so a link the form never filled still answers [] while the column itself stays NULL.

One thing worth a look: I did it as a separate rename_column migration rather than editing the original add_column, since that one has already run in dev DBs on this branch. If you would rather this ship as a single clean migration, say the word and I will fold it into the original.

S Mae Beale and others added 14 commits August 11, 2026 02:59
…a registration

The admin "create and link org" flow created the org with only name + Active
status, so orgs made this way started with blank type/website even though the
registration submission carried those answers — unlike the self-service
registration path, which syncs them.

Extract the type/website population out of PublicRegistration into a shared
OrganizationServices::SyncProfile and call it from create_organization with
overwrite: false, so new orgs get seeded and existing orgs only have blanks
filled (never clobbering curated values). Address + the affiliation-to-address
link were already handled by UpsertAddress + CreateFromRegistration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the fill-blanks profile sync to the select-existing org path (not just
create-and-link), and make both surfaces report what happened so an admin can
reconcile by hand:

- SyncProfile now returns a Result (filled columns + conflicts) and delegates
  conflict detection to a new read-only OrganizationServices::ProfileDiff.
- Both link actions set a flash notice ("Saved from the form: type, website,
  work address") and, when a submitted answer differs from a value already on
  the org, a flash warning naming the kept-vs-form values.
- The linking page shows a persistent per-org note flagging type/website
  answers that differ from the org's saved profile (survives the transient
  flash), so the discrepancy stays visible until resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the linking-flow discrepancy reporting to the work address and make the
address fill-blanks (like type/website) so a conflicting saved address is kept
and surfaced rather than silently overwritten:

- ProfileDiff now also compares the submitted street/ZIP/country against the
  org's same-city/state address; ProfileDiff is the single source for the flash
  warning and the persistent per-org note (SyncProfile no longer computes
  conflicts, just reports what it filled).
- UpsertAddress gains overwrite: (admin flow passes false → fills only blank
  fields; public flow keeps the default overwrite).
- When the submission carried no address, link the affiliation to the org's
  sole address so it's still anchored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…duplicating

When a submission reuses an existing address's street (in the same city) but
reformats the state or adds a country we didn't have, update that address
instead of spawning a duplicate.

- Add OrganizationServices::AddressMatcher: same city, prefer a state match,
  fall back to a same-street match. Used by both UpsertAddress and ProfileDiff
  so they target the same record.
- UpsertAddress fills a blank country/zip/street per the overwrite flag and
  treats state as fill-only (never flips a state already on file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flash messages render with html_safe, so the form answers folded into the
org-linking warning reached admins as raw HTML — a registrant could put a
script tag in their agency website and have it run in an admin's session.

The linking notice also claimed "work address" from the mere presence of a
submitted city, so it could credit an address save while the warning beside it
said those same fields were not applied. UpsertAddress now reports what it
actually wrote (created / which fields changed) and the notice names the city.

Matching gains a last-resort street + ZIP fallback across cities so a respelled
city ("St. Louis" / "Saint Louis") updates the address on file instead of
duplicating it; city joins state as fill-only, so the saved value is kept and
the difference is surfaced for an admin to reconcile.

Memoize the submission entries the linking actions read half a dozen times per
request: 66 -> 41 queries for one select_organization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
addresses.street_address and zip_code are NOT NULL with no default, so a
registrant who named an org and gave a city but skipped the street or ZIP took
down the whole public registration with a NotNullViolation — and the admin
linking action with it. Store those blank instead.

State can't be papered over the same way (Address validates it), so a
submission without one now saves no new address rather than failing the
registration over an optional answer. A matching address is still updated, the
answers remain on the form submission, and the linking page still shows them
for an admin to enter by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linking an org that already exists can't be assumed to be the org the
registrant wrote about — an admin also links extra orgs by hand — so
filling its blank type/website from the form wrote one org's answers onto
another. Seed only an org we create from the submitted name, and report
what an existing org doesn't carry (blank as well as conflicting) so the
admin enters it deliberately.

Scope the flash warning and the per-card note the same way: to the
submission that names the org, falling back to the sole submission only
when there's exactly one linked org, which still covers a typo'd name
resolved to a differently-spelled record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Filling an org's blanks from the form is the useful behavior, but the
flash saying so is gone by the next page load, leaving no trace that the
org's website/type/address came from a registrant rather than from
curation. Persist the labels on the registration-org link so the linking
page keeps showing them, and do it from the public flow too, which is
where most of these fills actually happen.

An org created from the submission records nothing: everything on it came
from the form, so there is no change to flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linking an extra organization by hand no longer stamps it with the title
the registrant typed about a different org, matching how the profile and
address answers are already attributed per org.

Also pick the same-street address when an org has several in one city
(state alone was rewriting the wrong office into a duplicate), and report
a profile column as filled only when the write actually changed it, so a
re-registration with unchanged answers stops leaving a permanent
"filled from the form" note on the linking page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Browsers don't trim text inputs, so a registrant's padded street/ZIP was
stored verbatim. ProfileDiff stripped only the submitted side, so the very
next comparison flagged the address as differing from the answer it came
from — the linking page showed a persistent "not applied" note, and the
link flashed the same warning, for an address that was applied.

Trim on write in UpsertAddress, and strip both sides in ProfileDiff so
legacy padded rows compare cleanly too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
form_filled_labels and record_form_fills were only exercised through the
request and public-registration specs, so the merge/de-dup contract and the
skip-the-write guard weren't pinned anywhere directly.

Each example is mutation-checked: dropping the guard, replacing instead of
merging, dropping uniq, or returning the raw column each fail at least one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The linking page re-derived which submission described which org on every
request, so an org linked under a name the registrant didn't type lost its
discrepancy note as soon as a second org was linked — the sole-submission
fallback that paired them stops applying at two. Recording the pairing when
the link is made keeps the note for as long as the link exists, and also
survives an admin renaming the org afterwards.
Pinning is what keeps the linking page's discrepancy note alive, so it can't
be conditioned on the submission having written something: an org whose every
answer conflicts is written nothing, and it's the one whose note matters most.
Same for an org the submission created — it skips the 'filled from the form'
note (no prior value to change) but is pinned like any other.
Copilot AI review requested due to automatic review settings August 11, 2026 07:10
@maebeale
maebeale force-pushed the maebeale/autofill-org-type-website branch from 0c868f0 to cfaf100 Compare August 11, 2026 07:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

form_filled_fields read as though it stored the registrant's submitted values;
it stores the names of what changed. Renaming it to form_filled_labels lets the
reader keep its Array() nil-guard by overriding the generated one, so callers
still get [] from a link the form never filled.
Copilot AI review requested due to automatic review settings August 11, 2026 07:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants