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))
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 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);
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.
if (user.IsDeleted == true)
return true;
return null;
var name = $"{user.FirstName ?? ""} {user.LastName ?? ""}".Trim();
return await SendAsync(issued, user.Email, name, cancellationToken);

View file

@ -19,9 +19,9 @@ public interface ITeamMemberInviteService
/// <summary>
/// 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.
/// Returns false when that email failed.
/// Returns whether that email went out, or null when the member is deactivated and nothing was emailed.
/// </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>
Task<TeamMemberInviteResendOutcomeDTO> ResendAsync(

View file

@ -74,6 +74,21 @@ public sealed class TeamMemberInviteLifecycleTests
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]
public async Task EditingAPendingMemberWithoutChangingTheirEmail_KeepsTheirInvite()
{
@ -96,7 +111,8 @@ public sealed class TeamMemberInviteLifecycleTests
TeamMemberInviteTestHost host,
string userId,
string email,
string role) =>
string role,
bool isActive = true) =>
host.InScopeAsync(provider => provider.GetRequiredService<ITeamMemberService>().UpdateAsync(
userId,
new UpdateTeamMemberRequestDTO
@ -106,7 +122,8 @@ public sealed class TeamMemberInviteLifecycleTests
Color = "#0D9488",
Email = email,
Phone = "555-0100",
ServiceAreas = role == "dispatcher" ? new[] { "East" } : Array.Empty<string>()
ServiceAreas = role == "dispatcher" ? new[] { "East" } : Array.Empty<string>(),
IsActive = isActive
},
TeamMemberInviteTestHost.Admin(),
CancellationToken.None));