feat(team-members): report whether the re-invite after an email change was emailed

This commit is contained in:
Alexandre Brandizzi 2026-09-25 12:54:27 -03:00
parent f491d4c721
commit b50cd5f5df
3 changed files with 26 additions and 4 deletions

View file

@ -25,7 +25,10 @@ public class TeamMemberCreatedDTO
public required IReadOnlyList<string> ServiceAreas { get; init; }
public bool PendingRegistration { get; init; }
/// <summary>Set only on create: false means the member exists but the invite email failed; re-invite from their details.</summary>
/// <summary>
/// Set only when an invite was emailed: on create, and on an update that changed a pending member's email.
/// False means the invite email failed; re-invite from their details.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? InviteEmailSent { get; init; }
}

View file

@ -239,10 +239,11 @@ public sealed class TeamMemberService : ITeamMemberService
}
// An invite sent to the old address, and any code confirmed through it, must not register the new one.
bool? inviteEmailSent = null;
if (emailChanged && user.PendingRegistration == true)
await _inviteService.ReissueAsync(user, cancellationToken);
inviteEmailSent = await _inviteService.ReissueAsync(user, cancellationToken);
return await OperationSuccessAsync(user, caller, cancellationToken);
return await OperationSuccessAsync(user, caller, cancellationToken, inviteEmailSent);
}
private static void ApplyName(ApplicationUser user, string fullName)
@ -277,7 +278,8 @@ public sealed class TeamMemberService : ITeamMemberService
private async Task<TeamMemberOperationOutcomeDTO> OperationSuccessAsync(
ApplicationUser user,
ClaimsPrincipal caller,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
bool? inviteEmailSent = null)
{
var roles = await _userManager.GetRolesAsync(user);
var profile = await _permissionService.GetProfileAsync(user.Id, caller, cancellationToken);
@ -295,6 +297,7 @@ public sealed class TeamMemberService : ITeamMemberService
Phone = user.Contact ?? user.PhoneNumber,
ServiceAreas = areas,
PendingRegistration = user.PendingRegistration == true,
InviteEmailSent = inviteEmailSent,
Status = user.UniqueName ?? "Active",
IsAccountOwner = user.IsAccountOwner,
Permissions = profile?.Value?.Permissions ?? Array.Empty<TeamPermissionValueDTO>()

View file

@ -40,6 +40,7 @@ public sealed class TeamMemberInviteLifecycleTests
var updated = await UpdateAsync(host, userId, CorrectedEmail, role: "admin");
Assert.True(updated.Success, updated.Error);
Assert.True(updated.Member!.InviteEmailSent);
Assert.Equal(TeamMemberRegistrationStatus.InvalidInvite,
(await host.RegistrationAsync(service => service.ResolveAsync(oldToken, CancellationToken.None))).Status);
@ -59,6 +60,20 @@ public sealed class TeamMemberInviteLifecycleTests
Assert.Equal(TeamMemberRegistrationStatus.Ok, (await CompleteAsync(host, newToken)).Status);
}
[Fact]
public async Task ChangingAPendingMembersEmail_ReportsWhenTheNewInviteCouldNotBeEmailed()
{
await using var host = await TeamMemberInviteTestHost.CreateAsync();
var (userId, _) = await host.AddPendingMemberAsync(Email);
host.Sent.Succeeds = false;
var updated = await UpdateAsync(host, userId, CorrectedEmail, role: "dispatcher");
Assert.True(updated.Success, updated.Error);
Assert.Equal(CorrectedEmail, updated.Member!.Email);
Assert.False(updated.Member.InviteEmailSent);
}
[Fact]
public async Task EditingAPendingMemberWithoutChangingTheirEmail_KeepsTheirInvite()
{
@ -69,6 +84,7 @@ public sealed class TeamMemberInviteLifecycleTests
var updated = await UpdateAsync(host, userId, Email.ToUpperInvariant(), role: "dispatcher");
Assert.True(updated.Success, updated.Error);
Assert.Null(updated.Member!.InviteEmailSent);
Assert.Equal(emailsBefore, host.Sent.Messages.Count);
var invite = Assert.Single(await host.InvitesAsync(userId));
Assert.Null(invite.RevokedAt);