fix(team-members): do not report an invite email for a deactivated member

This commit is contained in:
Alexandre Brandizzi 2026-09-25 13:13:50 -03:00
parent afc2330184
commit cc328ce22a
3 changed files with 24 additions and 7 deletions

View file

@ -89,13 +89,13 @@ public sealed class TeamMemberInviteService : ITeamMemberInviteService
if (user.IsDeleted == true || user.PendingRegistration != true || string.IsNullOrWhiteSpace(user.Email)) if (user.IsDeleted == true || user.PendingRegistration != true || string.IsNullOrWhiteSpace(user.Email))
return ResendFailure("Only pending team members can be re-invited."); return ResendFailure("Only pending team members can be re-invited.");
if (!await ReissueAsync(user, cancellationToken)) if (await ReissueAsync(user, cancellationToken) != true)
return ResendFailure("The invite could not be emailed. Try again."); return ResendFailure("The invite could not be emailed. Try again.");
return new TeamMemberInviteResendOutcomeDTO { Success = true }; return new TeamMemberInviteResendOutcomeDTO { Success = true };
} }
public async Task<bool> ReissueAsync(ApplicationUser user, CancellationToken cancellationToken) public async Task<bool?> ReissueAsync(ApplicationUser user, CancellationToken cancellationToken)
{ {
ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(user);
ArgumentException.ThrowIfNullOrWhiteSpace(user.Email); ArgumentException.ThrowIfNullOrWhiteSpace(user.Email);
@ -105,7 +105,7 @@ public sealed class TeamMemberInviteService : ITeamMemberInviteService
// A deactivated member cannot register; the admin re-sends the invite after reactivating them. // A deactivated member cannot register; the admin re-sends the invite after reactivating them.
if (user.IsDeleted == true) if (user.IsDeleted == true)
return true; return null;
var name = $"{user.FirstName ?? ""} {user.LastName ?? ""}".Trim(); var name = $"{user.FirstName ?? ""} {user.LastName ?? ""}".Trim();
return await SendAsync(issued, user.Email, name, cancellationToken); return await SendAsync(issued, user.Email, name, cancellationToken);

View file

@ -19,9 +19,9 @@ public interface ITeamMemberInviteService
/// <summary> /// <summary>
/// Revokes the member's open invites, including any email confirmation made through them, /// Revokes the member's open invites, including any email confirmation made through them,
/// and emails a new invite to their current address unless they are deactivated. /// and emails a new invite to their current address unless they are deactivated.
/// Returns false when that email failed. /// Returns whether that email went out, or null when the member is deactivated and nothing was emailed.
/// </summary> /// </summary>
Task<bool> ReissueAsync(ApplicationUser user, CancellationToken cancellationToken); Task<bool?> ReissueAsync(ApplicationUser user, CancellationToken cancellationToken);
/// <summary>Admin-only: revokes a pending member's open invites and emails a new one.</summary> /// <summary>Admin-only: revokes a pending member's open invites and emails a new one.</summary>
Task<TeamMemberInviteResendOutcomeDTO> ResendAsync( Task<TeamMemberInviteResendOutcomeDTO> ResendAsync(

View file

@ -74,6 +74,21 @@ public sealed class TeamMemberInviteLifecycleTests
Assert.False(updated.Member.InviteEmailSent); Assert.False(updated.Member.InviteEmailSent);
} }
[Fact]
public async Task DeactivatingAPendingMemberWhileChangingTheirEmail_RevokesTheInviteWithoutClaimingAnEmail()
{
await using var host = await TeamMemberInviteTestHost.CreateAsync();
var (userId, oldToken) = await host.AddPendingMemberAsync(Email);
var updated = await UpdateAsync(host, userId, CorrectedEmail, role: "dispatcher", isActive: false);
Assert.True(updated.Success, updated.Error);
Assert.Null(updated.Member!.InviteEmailSent);
Assert.DoesNotContain(host.Sent.Messages, message => message.To == CorrectedEmail);
Assert.Equal(TeamMemberRegistrationStatus.InvalidInvite,
(await host.RegistrationAsync(service => service.ResolveAsync(oldToken, CancellationToken.None))).Status);
}
[Fact] [Fact]
public async Task EditingAPendingMemberWithoutChangingTheirEmail_KeepsTheirInvite() public async Task EditingAPendingMemberWithoutChangingTheirEmail_KeepsTheirInvite()
{ {
@ -96,7 +111,8 @@ public sealed class TeamMemberInviteLifecycleTests
TeamMemberInviteTestHost host, TeamMemberInviteTestHost host,
string userId, string userId,
string email, string email,
string role) => string role,
bool isActive = true) =>
host.InScopeAsync(provider => provider.GetRequiredService<ITeamMemberService>().UpdateAsync( host.InScopeAsync(provider => provider.GetRequiredService<ITeamMemberService>().UpdateAsync(
userId, userId,
new UpdateTeamMemberRequestDTO new UpdateTeamMemberRequestDTO
@ -106,7 +122,8 @@ public sealed class TeamMemberInviteLifecycleTests
Color = "#0D9488", Color = "#0D9488",
Email = email, Email = email,
Phone = "555-0100", Phone = "555-0100",
ServiceAreas = role == "dispatcher" ? new[] { "East" } : Array.Empty<string>() ServiceAreas = role == "dispatcher" ? new[] { "East" } : Array.Empty<string>(),
IsActive = isActive
}, },
TeamMemberInviteTestHost.Admin(), TeamMemberInviteTestHost.Admin(),
CancellationToken.None)); CancellationToken.None));