fix(team-members): keep full-name round-trip stable on update

Storing the submitted full name in FirstName while leaving a seeded
LastName intact made an unchanged save project a duplicated name
(e.g. "Legacy Manager Manager"). Leave FirstName/LastName untouched when
the submitted name already matches the stored first+last projection, and
otherwise split the submitted name across FirstName/LastName so renames
round-trip cleanly.
This commit is contained in:
Alexandre Brandizzi 2026-09-17 04:22:02 -03:00
parent bdfe666b8d
commit ba8907fad0
2 changed files with 70 additions and 1 deletions

View file

@ -216,6 +216,57 @@ public sealed class TeamMemberServiceTests
overrides.Verify(data => data.SetOverridesAsync(user.Id, It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["createSites"] == UserPermissionState.Deny), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Update_SavesUnchangedNameWithoutDuplicatingRoleLikeLastName()
{
var service = NewService(
out var userManager,
out var roleManager,
out var areas,
out _,
out var userData,
out var permissions);
var user = new ApplicationUser
{
Id = "admin",
FirstName = "Legacy",
LastName = "Manager",
Email = "legacy@example.com",
Color = "#F59E0B",
UniqueName = "Active"
};
userManager.Setup(manager => manager.FindByIdAsync(user.Id)).ReturnsAsync(user);
userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(new List<string> { "Admin" });
userManager.Setup(manager => manager.FindByEmailAsync(user.Email!)).ReturnsAsync(user);
roleManager.Setup(manager => manager.RoleExistsAsync("Admin")).ReturnsAsync(true);
areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(Array.Empty<string>());
permissions.Setup(data => data.GetProfileAsync(user.Id, It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(TeamPermissionResult<TeamPermissionProfileDTO>.Success(new TeamPermissionProfileDTO
{
UserId = user.Id,
Permissions = Array.Empty<TeamPermissionValueDTO>()
}));
var result = await service.UpdateAsync(
user.Id,
new UpdateTeamMemberRequestDTO
{
Name = "Legacy Manager",
Role = "Admin",
Color = "#F59E0B",
Email = user.Email
},
Admin(),
CancellationToken.None);
result.Success.Should().BeTrue();
user.FirstName.Should().Be("Legacy");
user.LastName.Should().Be("Manager");
result.Member!.Name.Should().Be("Legacy Manager");
userData.Verify(data => data.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Update_LeavesOmittedOverridesUnchanged()
{

View file

@ -167,7 +167,7 @@ public sealed class TeamMemberService : ITeamMemberService
if (roleError is not null)
return OperationFailure(roleError);
user.FirstName = candidate.Name!.Trim();
ApplyName(user, candidate.Name!.Trim());
user.Contact = candidate.Phone?.Trim();
user.PhoneNumber = candidate.Phone?.Trim();
user.Color = color;
@ -215,6 +215,24 @@ public sealed class TeamMemberService : ITeamMemberService
return await OperationSuccessAsync(user, caller, cancellationToken);
}
private static void ApplyName(ApplicationUser user, string fullName)
{
var current = $"{user.FirstName ?? ""} {user.LastName ?? ""}".Trim();
if (string.Equals(fullName, current, StringComparison.Ordinal))
return;
var separator = fullName.IndexOf(' ');
if (separator < 0)
{
user.FirstName = fullName;
user.LastName = null;
return;
}
user.FirstName = fullName[..separator].Trim();
user.LastName = fullName[(separator + 1)..].Trim();
}
private async Task<string?> EnsureRoleAsync(string role)
{
if (await _roleManager.RoleExistsAsync(role))