Merge branch 'dev' into fix/ab/sh-287-backend-staging-lane

This commit is contained in:
Alexandre Brandizzi 2026-09-17 14:16:10 -03:00 • committed by GitHub
commit 5e70163941
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 497 additions and 25 deletions

View file

@ -10,6 +10,7 @@ using Moq;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Implementation;
using SeaHaven.Services.Interfaces;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
@ -25,7 +26,9 @@ public sealed class TeamMemberServiceTests
userManager.Object,
roleManager.Object,
Mock.Of<IUserServiceAreaDataService>(),
Mock.Of<ITeamPermissionOverrideDataService>());
Mock.Of<ITeamPermissionOverrideDataService>(),
Mock.Of<IUserDataService>(),
Mock.Of<ITeamPermissionService>());
var result = await service.CreateAsync(
ValidRequest() with { ServiceAreas = Array.Empty<string>() },
@ -40,7 +43,7 @@ public sealed class TeamMemberServiceTests
[Fact]
public async Task Create_RejectsUnknownColorAndPermission()
{
var service = NewService(out var userManager, out _, out _, out _);
var service = NewService(out var userManager, out _, out _, out _, out _, out _);
var badColor = await service.CreateAsync(
ValidRequest() with { Color = "#000000" },
@ -65,7 +68,7 @@ public sealed class TeamMemberServiceTests
[Fact]
public async Task Create_PersistsPendingUserWithoutPasswordAndStoresAreasAndOverrides()
{
var service = NewService(out var userManager, out var roleManager, out var areas, out var overrides);
var service = NewService(out var userManager, out var roleManager, out var areas, out var overrides, out _, out _);
ApplicationUser? created = null;
userManager
.Setup(manager => manager.CreateAsync(It.IsAny<ApplicationUser>()))
@ -96,10 +99,227 @@ public sealed class TeamMemberServiceTests
overrides.Verify(data => data.SetOverridesAsync("new-user", It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["deleteSites"] == UserPermissionState.Allow), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Update_ProtectsAccountOwnerFromRoleChangeAndDeactivation()
{
var service = NewService(
out var userManager,
out _,
out _,
out _,
out var userData,
out _);
var owner = new ApplicationUser
{
Id = "owner",
FirstName = "Primary",
Email = "owner@example.com",
IsAccountOwner = true
};
userManager.Setup(manager => manager.FindByIdAsync(owner.Id)).ReturnsAsync(owner);
userManager.Setup(manager => manager.GetRolesAsync(owner)).ReturnsAsync(new List<string> { "Admin" });
userData.Setup(data => data.IsAccountOwnerAsync(owner.Id, It.IsAny<CancellationToken>())).ReturnsAsync(true);
var result = await service.UpdateAsync(
owner.Id,
new UpdateTeamMemberRequestDTO
{
Name = "Primary",
Role = "Dispatcher",
Color = "#F59E0B",
Email = owner.Email,
ServiceAreas = new[] { "East" },
IsActive = false
},
Admin(),
CancellationToken.None);
result.Success.Should().BeFalse();
result.Error.Should().Be("Forbidden");
userData.Verify(data => data.UpdateUserAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Update_ReplacesAreasAndOverridesAndReturnsFullDetail()
{
var service = NewService(
out var userManager,
out var roleManager,
out var areas,
out var overrides,
out var userData,
out var permissions);
var user = new ApplicationUser
{
Id = "dispatcher",
FirstName = "Taylor",
Email = "old@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> { "Dispatcher" });
userManager.Setup(manager => manager.FindByEmailAsync("new@example.com")).ReturnsAsync((ApplicationUser?)null);
userManager.Setup(manager => manager.SetEmailAsync(user, "new@example.com"))
.Callback<ApplicationUser, string>((member, email) => member.Email = email)
.ReturnsAsync(IdentityResult.Success);
userManager.Setup(manager => manager.SetUserNameAsync(user, "new@example.com"))
.Callback<ApplicationUser, string>((member, email) => member.UserName = email)
.ReturnsAsync(IdentityResult.Success);
userManager.Setup(manager => manager.UpdateSecurityStampAsync(user))
.ReturnsAsync(IdentityResult.Success);
roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true);
areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(new[] { "Central" });
permissions.Setup(data => data.GetProfileAsync(user.Id, It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(TeamPermissionResult<TeamPermissionProfileDTO>.Success(new TeamPermissionProfileDTO
{
UserId = user.Id,
RoleName = "Dispatcher",
Permissions = new[]
{
new TeamPermissionValueDTO
{
PermissionKey = "createSites",
OverrideState = UserPermissionState.Deny,
IsGranted = false
}
}
}));
var result = await service.UpdateAsync(
user.Id,
new UpdateTeamMemberRequestDTO
{
Name = "Taylor Updated",
Role = "Dispatcher",
Color = "#0D9488",
Email = "new@example.com",
Phone = "555-0101",
ServiceAreas = new[] { "West" },
PermissionOverrides = new Dictionary<string, UserPermissionState>
{
["createSites"] = UserPermissionState.Deny
}
},
Admin(),
CancellationToken.None);
result.Success.Should().BeTrue();
result.Member!.Name.Should().Be("Taylor Updated");
result.Member.Email.Should().Be("new@example.com");
result.Member.ServiceAreas.Should().Equal("Central");
result.Member.Permissions.Should().ContainSingle(permission => permission.PermissionKey == "createSites");
userData.Verify(data => data.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
userManager.Verify(manager => manager.UpdateSecurityStampAsync(user), Times.Once);
areas.Verify(data => data.ReplaceAsync(user.Id, It.Is<IReadOnlyCollection<string>>(value => value.SequenceEqual(new[] { "West" })), It.IsAny<CancellationToken>()), Times.Once);
overrides.Verify(data => data.ClearOverridesAsync(user.Id, It.IsAny<CancellationToken>()), Times.Once);
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()
{
var service = NewService(
out var userManager,
out var roleManager,
out var areas,
out var overrides,
out var userData,
out var permissions);
var user = new ApplicationUser
{
Id = "dispatcher",
FirstName = "Taylor",
Email = "taylor@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> { "Dispatcher" });
roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).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 = "Taylor",
Role = "Dispatcher",
Color = "#F59E0B",
Email = user.Email,
ServiceAreas = new[] { "East" }
},
Admin(),
CancellationToken.None);
result.Success.Should().BeTrue();
overrides.Verify(data => data.ClearOverridesAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
overrides.Verify(data => data.SetOverridesAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, UserPermissionState>>(), It.IsAny<CancellationToken>()), Times.Never);
userData.Verify(data => data.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Create_ConcurrentDuplicateEmail_ReturnsAlreadyInUseInsteadOf500()
{
var service = NewService(out var userManager, out var roleManager, out _, out _);
var service = NewService(out var userManager, out var roleManager, out _, out _, out _, out _);
userManager
.Setup(manager => manager.FindByEmailAsync(It.IsAny<string>()))
.ReturnsAsync((ApplicationUser?)null);
@ -120,13 +340,23 @@ public sealed class TeamMemberServiceTests
out Mock<UserManager<ApplicationUser>> userManager,
out Mock<RoleManager<IdentityRole>> roleManager,
out Mock<IUserServiceAreaDataService> areas,
out Mock<ITeamPermissionOverrideDataService> overrides)
out Mock<ITeamPermissionOverrideDataService> overrides,
out Mock<IUserDataService> userData,
out Mock<ITeamPermissionService> permissions)
{
userManager = UserManager();
roleManager = RoleManager();
areas = new Mock<IUserServiceAreaDataService>();
overrides = new Mock<ITeamPermissionOverrideDataService>();
return new TeamMemberService(userManager.Object, roleManager.Object, areas.Object, overrides.Object);
userData = new Mock<IUserDataService>();
permissions = new Mock<ITeamPermissionService>();
return new TeamMemberService(
userManager.Object,
roleManager.Object,
areas.Object,
overrides.Object,
userData.Object,
permissions.Object);
}
private static Mock<UserManager<ApplicationUser>> UserManager()

View file

@ -33,4 +33,35 @@ public sealed class TeamMemberController : ControllerBase
return Ok(outcome.Member);
}
[HttpGet("{userId}")]
public async Task<IActionResult> Get(string userId, CancellationToken cancellationToken)
{
var outcome = await _teamMemberService.GetAsync(userId, User, cancellationToken);
return ToActionResult(outcome);
}
[HttpPut("{userId}")]
public async Task<IActionResult> Update(
string userId,
UpdateTeamMemberRequestDTO request,
CancellationToken cancellationToken)
{
var outcome = await _teamMemberService.UpdateAsync(userId, request, User, cancellationToken);
return ToActionResult(outcome);
}
private IActionResult ToActionResult(TeamMemberOperationOutcomeDTO outcome)
{
if (outcome.Success)
return Ok(outcome.Member);
if (string.Equals(outcome.Error, "Forbidden", StringComparison.Ordinal))
return Forbid();
if (string.Equals(outcome.Error, "Team member not found.", StringComparison.Ordinal))
return NotFound(new { message = outcome.Error });
return BadRequest(new { message = outcome.Error });
}
}

View file

@ -51,19 +51,13 @@ public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideD
};
}
public async Task SetOverrideAsync(
public Task SetOverrideAsync(
string userId,
string permissionKey,
UserPermissionState state,
CancellationToken cancellationToken)
{
await SetOverridesAsync(
userId,
new Dictionary<string, UserPermissionState>(StringComparer.OrdinalIgnoreCase)
{
[permissionKey] = state
},
cancellationToken);
return SetOverrideCoreAsync(userId, permissionKey, state, cancellationToken);
}
public async Task SetOverridesAsync(
@ -108,10 +102,9 @@ public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideD
}
catch (DbUpdateException)
{
// A concurrent write inserted the same composite key between our
// existence check and this save. Converge on the caller's intent.
// A concurrent write (create or PUT) inserted the same composite key
// between our existence check and this save. Converge on the caller's intent.
_context.Entry(addition).State = EntityState.Detached;
var winner = await _context.UserPermissionOverrides
.SingleAsync(
permission => permission.UserId == userId
@ -121,4 +114,11 @@ public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideD
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task ClearOverridesAsync(string userId, CancellationToken cancellationToken)
{
await _context.UserPermissionOverrides
.Where(item => item.UserId == userId)
.ExecuteDeleteAsync(cancellationToken);
}
}

View file

@ -16,4 +16,6 @@ public interface ITeamPermissionOverrideDataService
string userId,
IReadOnlyDictionary<string, UserPermissionState> overrides,
CancellationToken cancellationToken);
Task ClearOverridesAsync(string userId, CancellationToken cancellationToken);
}

View file

@ -140,5 +140,11 @@ public sealed class TeamPermissionServiceTests
return Task.CompletedTask;
}
public Task ClearOverridesAsync(string userId, CancellationToken cancellationToken)
{
_overrides.Clear();
return Task.CompletedTask;
}
}
}

View file

@ -13,7 +13,7 @@ public sealed record CreateTeamMemberRequestDTO
public IReadOnlyDictionary<string, UserPermissionState>? PermissionOverrides { get; init; }
}
public sealed class TeamMemberCreatedDTO
public class TeamMemberCreatedDTO
{
public required string Id { get; init; }
public required string Name { get; init; }
@ -25,6 +25,32 @@ public sealed class TeamMemberCreatedDTO
public bool PendingRegistration { get; init; }
}
public sealed class TeamMemberDetailDTO : TeamMemberCreatedDTO
{
public string Status { get; init; } = "Active";
public bool IsAccountOwner { get; init; }
public IReadOnlyList<TeamPermissionValueDTO> Permissions { get; init; } = Array.Empty<TeamPermissionValueDTO>();
}
public sealed record UpdateTeamMemberRequestDTO
{
public string? Name { get; init; }
public string? Role { get; init; }
public string? Color { get; init; }
public string? Email { get; init; }
public string? Phone { get; init; }
public IReadOnlyList<string>? ServiceAreas { get; init; }
public IReadOnlyDictionary<string, UserPermissionState>? PermissionOverrides { get; init; }
public bool IsActive { get; init; } = true;
}
public sealed class TeamMemberOperationOutcomeDTO
{
public bool Success { get; init; }
public string? Error { get; init; }
public TeamMemberDetailDTO? Member { get; init; }
}
public sealed class CreateTeamMemberOutcomeDTO
{
public bool Success { get; init; }

View file

@ -18,17 +18,23 @@ public sealed class TeamMemberService : ITeamMemberService
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IUserServiceAreaDataService _areaDataService;
private readonly ITeamPermissionOverrideDataService _permissionDataService;
private readonly IUserDataService _userDataService;
private readonly ITeamPermissionService _permissionService;
public TeamMemberService(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IUserServiceAreaDataService areaDataService,
ITeamPermissionOverrideDataService permissionDataService)
ITeamPermissionOverrideDataService permissionDataService,
IUserDataService userDataService,
ITeamPermissionService permissionService)
{
_userManager = userManager;
_roleManager = roleManager;
_areaDataService = areaDataService;
_permissionDataService = permissionDataService;
_userDataService = userDataService;
_permissionService = permissionService;
}
public async Task<CreateTeamMemberOutcomeDTO> CreateAsync(
@ -46,12 +52,9 @@ public sealed class TeamMemberService : ITeamMemberService
if (await _userManager.FindByEmailAsync(email!) is not null)
return Failure("Email is already in use.");
if (!await _roleManager.RoleExistsAsync(role!))
{
var roleResult = await _roleManager.CreateAsync(new IdentityRole(role!));
if (!roleResult.Succeeded)
return Failure(roleResult.Errors.FirstOrDefault()?.Description ?? "Unable to create role.");
}
var roleError = await EnsureRoleAsync(role!);
if (roleError is not null)
return Failure(roleError);
var now = DateTime.UtcNow;
var user = new ApplicationUser
@ -121,6 +124,168 @@ public sealed class TeamMemberService : ITeamMemberService
};
}
public async Task<TeamMemberOperationOutcomeDTO> GetAsync(
string userId,
ClaimsPrincipal caller,
CancellationToken cancellationToken)
{
if (caller?.IsInRole("Admin") != true)
return OperationFailure("Forbidden");
var user = await _userManager.FindByIdAsync(userId);
if (user is null)
return OperationFailure("Team member not found.");
return await OperationSuccessAsync(user, caller, cancellationToken);
}
public async Task<TeamMemberOperationOutcomeDTO> UpdateAsync(
string userId,
UpdateTeamMemberRequestDTO request,
ClaimsPrincipal caller,
CancellationToken cancellationToken)
{
if (caller?.IsInRole("Admin") != true)
return OperationFailure("Forbidden");
var user = await _userManager.FindByIdAsync(userId);
if (user is null)
return OperationFailure("Team member not found.");
var existingRoles = await _userManager.GetRolesAsync(user);
var existingRole = existingRoles.FirstOrDefault();
var candidate = new CreateTeamMemberRequestDTO
{
Name = request.Name,
Role = request.Role,
Color = request.Color,
Email = request.Email,
Phone = request.Phone,
ServiceAreas = request.ServiceAreas,
PermissionOverrides = request.PermissionOverrides
};
var validation = Validate(candidate, out var role, out var color, out var email, out var areas, out var overrides);
if (validation is not null)
return OperationFailure(validation);
var roleChanged = !string.Equals(existingRole, role, StringComparison.OrdinalIgnoreCase);
var isOwner = await _userDataService.IsAccountOwnerAsync(user.Id, cancellationToken);
if (isOwner && (roleChanged || !request.IsActive))
return OperationFailure("Forbidden");
var duplicate = await _userManager.FindByEmailAsync(email!);
if (duplicate is not null && !string.Equals(duplicate.Id, user.Id, StringComparison.Ordinal))
return OperationFailure("Email is already in use.");
var roleError = await EnsureRoleAsync(role!);
if (roleError is not null)
return OperationFailure(roleError);
ApplyName(user, candidate.Name!.Trim());
user.Contact = candidate.Phone?.Trim();
user.PhoneNumber = candidate.Phone?.Trim();
user.Color = color;
user.UniqueName = request.IsActive ? ActiveStatus : "Inactive";
user.IsDeleted = !request.IsActive;
if (!string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase))
{
var emailResult = await _userManager.SetEmailAsync(user, email!);
if (!emailResult.Succeeded)
return OperationFailure(emailResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email.");
var userNameResult = await _userManager.SetUserNameAsync(user, email!);
if (!userNameResult.Succeeded)
return OperationFailure(userNameResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email.");
var securityStampResult = await _userManager.UpdateSecurityStampAsync(user);
if (!securityStampResult.Succeeded)
return OperationFailure(securityStampResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email.");
}
if (roleChanged)
{
if (existingRoles.Count > 0)
{
var removeRoleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles);
if (!removeRoleResult.Succeeded)
return OperationFailure(removeRoleResult.Errors.FirstOrDefault()?.Description ?? "Unable to update role.");
}
var addRoleResult = await _userManager.AddToRoleAsync(user, role!);
if (!addRoleResult.Succeeded)
return OperationFailure(addRoleResult.Errors.FirstOrDefault()?.Description ?? "Unable to update role.");
}
await _userDataService.UpdateUserAsync(user, cancellationToken);
await _areaDataService.ReplaceAsync(user.Id, areas!, cancellationToken);
if (roleChanged || request.PermissionOverrides is not null)
{
await _permissionDataService.ClearOverridesAsync(user.Id, cancellationToken);
if (request.PermissionOverrides is not null)
await _permissionDataService.SetOverridesAsync(user.Id, overrides!, cancellationToken);
}
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))
return null;
var roleResult = await _roleManager.CreateAsync(new IdentityRole(role));
return roleResult.Succeeded
? null
: roleResult.Errors.FirstOrDefault()?.Description ?? "Unable to create role.";
}
private async Task<TeamMemberOperationOutcomeDTO> OperationSuccessAsync(
ApplicationUser user,
ClaimsPrincipal caller,
CancellationToken cancellationToken)
{
var roles = await _userManager.GetRolesAsync(user);
var profile = await _permissionService.GetProfileAsync(user.Id, caller, cancellationToken);
var areas = await _areaDataService.GetForUserAsync(user.Id, cancellationToken);
return new TeamMemberOperationOutcomeDTO
{
Success = true,
Member = new TeamMemberDetailDTO
{
Id = user.Id,
Name = $"{user.FirstName ?? ""} {user.LastName ?? ""}".Trim(),
Role = TeamMemberConstants.CanonicalRole(roles.FirstOrDefault()) ?? roles.FirstOrDefault() ?? "",
Color = user.Color ?? "",
Email = user.Email,
Phone = user.Contact ?? user.PhoneNumber,
ServiceAreas = areas,
PendingRegistration = user.PendingRegistration == true,
Status = user.UniqueName ?? "Active",
IsAccountOwner = user.IsAccountOwner,
Permissions = profile?.Value?.Permissions ?? Array.Empty<TeamPermissionValueDTO>()
}
};
}
private static string? Validate(
CreateTeamMemberRequestDTO request,
out string? role,
@ -225,4 +390,7 @@ public sealed class TeamMemberService : ITeamMemberService
private static CreateTeamMemberOutcomeDTO Failure(string error) =>
new() { Success = false, Error = error };
private static TeamMemberOperationOutcomeDTO OperationFailure(string error) =>
new() { Success = false, Error = error };
}

View file

@ -9,4 +9,13 @@ public interface ITeamMemberService
CreateTeamMemberRequestDTO request,
ClaimsPrincipal caller,
CancellationToken cancellationToken);
Task<TeamMemberOperationOutcomeDTO> GetAsync(
string userId,
ClaimsPrincipal caller,
CancellationToken cancellationToken);
Task<TeamMemberOperationOutcomeDTO> UpdateAsync(
string userId,
UpdateTeamMemberRequestDTO request,
ClaimsPrincipal caller,
CancellationToken cancellationToken);
}