From 7e53403c8ba71e24dc09b8bd00908d53d5ef7ad8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 21 Jun 2026 23:16:58 -0400 Subject: [PATCH] Match interstitial duplicates on legal first name too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new-person duplicate interstitial matched the typed first name (and its nickname variants) only against the stored first_name column. A returning registrant whose legal name lives in legal_first_name (with a nickname in first_name) slipped past the name check when they typed their legal name — the exact duplicate this surfaces is meant to prevent. Mirror the registration matcher (PublicRegistration#find_matching_person): match the first-name variants against either first_name or legal_first_name, and count a legal-name hit as an exact name match for badge/sort purposes. Co-Authored-By: Claude Opus 4.8 --- app/controllers/people_controller.rb | 14 ++++++-- spec/requests/people_check_duplicates_spec.rb | 33 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index e698aa54eb..b8df6f904e 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -293,16 +293,26 @@ def find_duplicate_people(first_name, last_name, email) normalized_last = NicknameMap.normalize(last_name) normalized_first = NicknameMap.normalize(first_name) + # Match the typed first name (and its nickname variants) against either the + # stored first_name or legal_first_name, so a returning registrant whose legal + # name lives in legal_first_name (nickname in first_name) is still surfaced when + # they type their legal name. Mirrors the registration matcher in + # EventRegistrationServices::PublicRegistration#find_matching_person. name_matches = Person.includes(:user) .where("REPLACE(REPLACE(LOWER(last_name), '.', ''), ' ', '') = ?", normalized_last) - .where("REPLACE(REPLACE(LOWER(first_name), '.', ''), ' ', '') IN (?)", first_variants) + .where( + "REPLACE(REPLACE(LOWER(first_name), '.', ''), ' ', '') IN (:names) " \ + "OR REPLACE(REPLACE(LOWER(COALESCE(legal_first_name, '')), '.', ''), ' ', '') IN (:names)", + names: first_variants + ) .limit(10) name_matches.each do |person| next if duplicate_ids.include?(person.id) duplicate_ids.add(person.id) - exact_name = NicknameMap.normalize(person.first_name) == normalized_first + exact_name = NicknameMap.normalize(person.first_name) == normalized_first || + NicknameMap.normalize(person.legal_first_name) == normalized_first duplicates << format_duplicate(person, exact: exact_name, entered_email: email) end end diff --git a/spec/requests/people_check_duplicates_spec.rb b/spec/requests/people_check_duplicates_spec.rb index 4651b63d24..5d920b2ba2 100644 --- a/spec/requests/people_check_duplicates_spec.rb +++ b/spec/requests/people_check_duplicates_spec.rb @@ -63,6 +63,39 @@ end end + # --- Legal first name matching --- + + context "when the entered first name matches a stored legal first name" do + before do + create(:person, first_name: "Sunny", legal_first_name: "Yuki", last_name: "Tanaka") + end + + it "matches a returning registrant who types their legal name" do + get check_duplicates_people_path, params: { first_name: "Yuki", last_name: "Tanaka", email: "" } + + expect(response).to have_http_status(:ok) + expect(response.body).to include("Sunny Tanaka") + expect(response.body).to include("name match") + end + + it "matches the legal name through period normalization" do + create(:person, first_name: "Beanie", legal_first_name: "J.J.", last_name: "Park") + + get check_duplicates_people_path, params: { first_name: "JJ", last_name: "Park", email: "" } + + expect(response).to have_http_status(:ok) + expect(response.body).to include("Beanie Park") + expect(response.body).to include("name match") + end + + it "does not match when neither first nor legal name matches" do + get check_duplicates_people_path, params: { first_name: "Kenji", last_name: "Tanaka", email: "" } + + expect(response).to have_http_status(:ok) + expect(response.body).not_to include("Sunny Tanaka") + end + end + # --- Period and space normalization --- context "when names contain periods or extra spaces" do