Fix existing user names in partner invites - #5640
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves the partner user invite flow by detecting existing users when an email is entered, clarifying how the submitted name will be applied, and aligning user lookup/invitation behavior with case-insensitive email matching.
Changes:
- Add a partner-users
lookupendpoint plus a Stimulus controller to check for existing users on email-field blur and show guidance in the invite form. - Update
UserInviteServiceto find existing users case-insensitively and to apply the submitted name only when the existing user has no name. - Add request/service/system specs covering the lookup behavior, case-insensitive matching, and name handling for existing users.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
spec/system/partner_system_spec.rb |
Adds JS system coverage for the “existing user detected on blur” UX. |
spec/services/user_invite_service_spec.rb |
Adds coverage for case-insensitive lookup and name-setting behavior for existing users. |
spec/requests/partner_users_requests_spec.rb |
Adds request coverage for the new GET #lookup endpoint. |
config/routes.rb |
Adds GET /partners/:partner_id/users/lookup collection route. |
app/views/partner_users/_form.html.erb |
Wires up the Stimulus controller/targets and adds an inline message live region. |
app/services/user_invite_service.rb |
Implements case-insensitive lookup and conditional name update for existing users. |
app/javascript/controllers/existing_user_controller.js |
New Stimulus controller to call lookup endpoint and update the form message. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| class PartnerUsersController < ApplicationController | ||
| before_action :authorize_admin | ||
| before_action :set_partner, only: %i[index create destroy resend_invitation] | ||
| before_action :set_partner, only: %i[index lookup create destroy resend_invitation] |
| def lookup | ||
| user = User.find_by("LOWER(email) = ?", params[:email].to_s.downcase) | ||
| render json: {exists: user.present?} | ||
| end |
| action: "blur->existing-user#lookup" | ||
| } | ||
| } %> | ||
| <small class="form-text text-muted" data-existing-user-target="message" aria-live="polite"></small> |
| end | ||
|
|
||
| user = User.find_by(email: email) | ||
| user = User.find_by("LOWER(email) = ?", email.to_s.downcase) |
| it "should find an existing user case-insensitively" do | ||
| uppercase_email = user.email.upcase | ||
| target_partner = partner | ||
| expect(User.find_by("LOWER(email) = ?", uppercase_email.downcase)).to eq(user) | ||
|
|
||
| expect { | ||
| described_class.invite( | ||
| email: uppercase_email, | ||
| roles: [Role::PARTNER], | ||
| resource: target_partner | ||
| ) | ||
| }.not_to change(User, :count) | ||
|
|
||
| expect(user.reload).to have_role(Role::PARTNER, target_partner) | ||
| end |
| const response = await fetch(`${this.lookupUrlValue}?email=${encodeURIComponent(email)}`, { | ||
| headers: { "Accept": "application/json" } | ||
| }) | ||
| if (!response.ok || this.emailTarget.value.trim() !== email) return |
There was a problem hiding this comment.
This is for debouncing purposes? Wouldn't it be better to cancel the existing fetch if we detect that the name has changed when a fetch is in progress?
There was a problem hiding this comment.
Do we still need this check now that we're aborting?
| this.resetNameField() | ||
| } else { | ||
| this.nameTarget.disabled = false | ||
| this.messageTarget.textContent = "This user already exists. The submitted name will only be used if their profile has no name yet." |
There was a problem hiding this comment.
Probably need more information here... does this imply that we can't change the user's name once it's set? Or we have to change it somewhere else? (In which case, where?)
There was a problem hiding this comment.
Looks like the issue didn't specify what should happen in this case... @ruestitch do we want to prevent name changes, or allow name changes that actually work?
|
|
||
| resetNameField() { | ||
| this.nameTarget.disabled = false | ||
| this.messageTarget.textContent = "" |
There was a problem hiding this comment.
Do we want to clear out the name field when an error happens? Why?
| input_html: { | ||
| data: { | ||
| existing_user_target: "email", | ||
| action: "blur->existing-user#lookup" |
There was a problem hiding this comment.
Should this be on blur or on change?
| ) | ||
|
|
||
| expect(response).to have_http_status(:ok) | ||
| expect(response.parsed_body).to eq("exists" => true) |
There was a problem hiding this comment.
No test for the false path.
|
Thanks — addressed the review in
I kept lookup on blur so tabbing/clicking away performs one request after email entry rather than firing during each edit. |
dorner
left a comment
There was a problem hiding this comment.
The Copilot comments also still need to be addressed.
| } else { | ||
| this.nameTarget.disabled = false | ||
| this.messageTarget.textContent = "This user already exists. The submitted name will only be used if their profile has no name yet." | ||
| this.messageTarget.textContent = "This user already exists. Their current profile name will be kept; the submitted name is used only if their profile has no name yet." |
There was a problem hiding this comment.
Shouldn't we know whether the user has a name or not? Can we add this to the lookup endpoint?
| const response = await fetch(`${this.lookupUrlValue}?email=${encodeURIComponent(email)}`, { | ||
| headers: { "Accept": "application/json" } | ||
| }) | ||
| if (!response.ok || this.emailTarget.value.trim() !== email) return |
There was a problem hiding this comment.
Do we still need this check now that we're aborting?
|
Tests are failing as well. |
|
Thanks — addressed the new review in
I kept the email equality guard because changing the field after a blur does not start a new lookup until the next blur; the guard prevents that already-started response from updating the form with stale data. Validation: |
Resolves #5612
Description
When a bank user leaves the email field, the form now checks whether that account already exists and explains how the submitted name will be handled. Existing accounts keep their current name, while an account without a name receives the submitted one when the partner role is added.
The lookup and invitation paths also match email addresses case-insensitively, consistent with the user email uniqueness rule.
Type of change
How Has This Been Tested?
bundle exec rspec spec/services/user_invite_service_spec.rb spec/requests/partner_users_requests_spec.rb(30 examples)bin/lint(RuboCop and ERB lint)bundle exec brakeman --no-pager --quiet(0 warnings)node --check app/javascript/controllers/existing_user_controller.jsgit diff --check origin/main...HEADThe full RSpec suite is deferred to CI.