From b50cd5f5df316b0c5d7e6f3bf46fae99434ff0aa Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 12:54:27 -0300 Subject: [PATCH] feat(team-members): report whether the re-invite after an email change was emailed --- SeaHaven.Services/DTOs/TeamMemberDTOs.cs | 5 ++++- .../Implementation/TeamMemberService.cs | 9 ++++++--- .../TeamMemberInviteLifecycleTests.cs | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/SeaHaven.Services/DTOs/TeamMemberDTOs.cs b/SeaHaven.Services/DTOs/TeamMemberDTOs.cs index 3a830fe..e0b8e31 100644 --- a/SeaHaven.Services/DTOs/TeamMemberDTOs.cs +++ b/SeaHaven.Services/DTOs/TeamMemberDTOs.cs @@ -25,7 +25,10 @@ public class TeamMemberCreatedDTO public required IReadOnlyList ServiceAreas { get; init; } public bool PendingRegistration { get; init; } - /// Set only on create: false means the member exists but the invite email failed; re-invite from their details. + /// + /// 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. + /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? InviteEmailSent { get; init; } } diff --git a/SeaHaven.Services/Implementation/TeamMemberService.cs b/SeaHaven.Services/Implementation/TeamMemberService.cs index d15a347..6d78a7b 100644 --- a/SeaHaven.Services/Implementation/TeamMemberService.cs +++ b/SeaHaven.Services/Implementation/TeamMemberService.cs @@ -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 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() diff --git a/SeaHavenIndustries.Tests/TeamMemberInviteLifecycleTests.cs b/SeaHavenIndustries.Tests/TeamMemberInviteLifecycleTests.cs index 521d7a4..c963a62 100644 --- a/SeaHavenIndustries.Tests/TeamMemberInviteLifecycleTests.cs +++ b/SeaHavenIndustries.Tests/TeamMemberInviteLifecycleTests.cs @@ -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);