From 92a311e1cc171297b8433ab7d1acc0c21fbefa51 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Wed, 7 Oct 2026 10:39:17 +0530 Subject: [PATCH] Add region manager assignment endpoints --- .../Features/Region/IRegionRepository.cs | 6 ++ .../Features/Region/RegionController.cs | 28 +++++++++ Distribution/Features/Region/RegionDTOs.cs | 6 ++ .../Features/Region/RegionRepository.cs | 19 ++++++ Distribution/Features/Region/RegionService.cs | 61 +++++++++++++++++++ 5 files changed, 120 insertions(+) diff --git a/Distribution/Features/Region/IRegionRepository.cs b/Distribution/Features/Region/IRegionRepository.cs index 33dbfb9..1575063 100644 --- a/Distribution/Features/Region/IRegionRepository.cs +++ b/Distribution/Features/Region/IRegionRepository.cs @@ -6,6 +6,12 @@ public interface IRegionRepository Task> GetAllAsync(); Task> GetAllCodesAsync(); Task CodeExistsAsync(string code, Guid? excludeId = null); + + Task GetUserWithRoleAsync(Guid userId); + Task> GetManagerScopesAsync(Guid regionId); + Task GetRegionScopeAsync(Guid userId, Guid regionId); + Task AddRegionScopeAsync(Models.UserRegionScope scope); + void RemoveRegionScopes(IEnumerable scopes); Task AddAsync(Models.Region entity); void Update(Models.Region entity); void Remove(Models.Region entity); diff --git a/Distribution/Features/Region/RegionController.cs b/Distribution/Features/Region/RegionController.cs index 755a745..c7a658d 100644 --- a/Distribution/Features/Region/RegionController.cs +++ b/Distribution/Features/Region/RegionController.cs @@ -43,6 +43,34 @@ public class RegionController(RegionService service) : ControllerBase return deleted ? NoContent() : NotFound(); } + [HttpGet("{id:guid}/manager")] + public async Task GetManager(Guid id) + { + var result = await service.GetManagerAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPut("{id:guid}/manager")] + public async Task SetManager(Guid id, SetRegionManagerRequest request, CancellationToken ct) + { + try + { + var result = await service.SetManagerAsync(id, request, ct); + return result is null ? NotFound() : Ok(result); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpDelete("{id:guid}/manager")] + public async Task RemoveManager(Guid id, CancellationToken ct) + { + var removed = await service.RemoveManagerAsync(id, ct); + return removed ? NoContent() : NotFound(); + } + //targets/budgets/agencies/tax-rule. [HttpPut("{id:guid}/manage")] public async Task Manage(Guid id, ManageRegionRequest request, CancellationToken ct) diff --git a/Distribution/Features/Region/RegionDTOs.cs b/Distribution/Features/Region/RegionDTOs.cs index c21a594..fe7efb5 100644 --- a/Distribution/Features/Region/RegionDTOs.cs +++ b/Distribution/Features/Region/RegionDTOs.cs @@ -65,3 +65,9 @@ public record RegionManageEntryResponse( string? RuleConfig, Guid? AgencyUserId, DateTime CreatedAt); + +// Region manager = the single active user with the RegionalManager role scoped to the region +// (UserRegionScope). Users themselves are owned by the auth service; this only links one to a region. +public record SetRegionManagerRequest(Guid UserId); + +public record RegionManagerResponse(Guid RegionId, Guid UserId, string Username, string FullName); diff --git a/Distribution/Features/Region/RegionRepository.cs b/Distribution/Features/Region/RegionRepository.cs index b06b6e8..73551e8 100644 --- a/Distribution/Features/Region/RegionRepository.cs +++ b/Distribution/Features/Region/RegionRepository.cs @@ -18,6 +18,25 @@ public class RegionRepository(AppDBContext context) : IRegionRepository public async Task CodeExistsAsync(string code, Guid? excludeId = null) => await context.Regions.AnyAsync(r => r.Code == code && (excludeId == null || r.Id != excludeId)); + public async Task GetUserWithRoleAsync(Guid userId) => + await context.Users.Include(u => u.Role).FirstOrDefaultAsync(u => u.Id == userId); + + public async Task> GetManagerScopesAsync(Guid regionId) => + await context.UserRegionScopes + .Include(s => s.User).ThenInclude(u => u.Role) + .Where(s => s.FK_RegionId == regionId && s.User.IsActive && s.User.Role.Name == "RegionalManager") + .ToListAsync(); + + public async Task GetRegionScopeAsync(Guid userId, Guid regionId) => + await context.UserRegionScopes.FirstOrDefaultAsync(s => s.FK_UserId == userId && s.FK_RegionId == regionId); + + public async Task AddRegionScopeAsync(Models.UserRegionScope scope) => + await context.UserRegionScopes.AddAsync(scope); + + // Scope rows are hard-deleted (composite key user+region), same as UserRepository.ReplaceRegionScopesAsync. + public void RemoveRegionScopes(IEnumerable scopes) => + context.UserRegionScopes.RemoveRange(scopes); + public async Task AddAsync(Models.Region entity) => await context.Regions.AddAsync(entity); diff --git a/Distribution/Features/Region/RegionService.cs b/Distribution/Features/Region/RegionService.cs index 6c8bdc7..37b5c3c 100644 --- a/Distribution/Features/Region/RegionService.cs +++ b/Distribution/Features/Region/RegionService.cs @@ -91,6 +91,67 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser return new RegionResponse(region.Id, region.Code, region.Name, region.IsActive, region.CreatedAt); } + private const string RegionalManagerRole = "RegionalManager"; + + public async Task GetManagerAsync(Guid regionId) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + var scope = (await repo.GetManagerScopesAsync(regionId)).FirstOrDefault(); + return scope is null ? null : new RegionManagerResponse(regionId, scope.FK_UserId, scope.User.Username, scope.User.FullName); + } + + // A region has exactly one manager: assigning replaces any existing RegionalManager scoped to it. + // Returns null when the region does not exist; throws InvalidOperationException for an invalid user. + public async Task SetManagerAsync(Guid regionId, SetRegionManagerRequest request, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + + var user = await repo.GetUserWithRoleAsync(request.UserId); + if (user is null || !user.IsActive) + throw new InvalidOperationException("User not found."); + if (user.Role?.Name != RegionalManagerRole) + throw new InvalidOperationException($"User must have the {RegionalManagerRole} role to manage a region."); + + await uow.BeginAsync(ct); + try + { + var existing = await repo.GetManagerScopesAsync(regionId); + repo.RemoveRegionScopes(existing.Where(s => s.FK_UserId != user.Id)); + if (await repo.GetRegionScopeAsync(user.Id, regionId) is null) + await repo.AddRegionScopeAsync(new Models.UserRegionScope { FK_UserId = user.Id, FK_RegionId = regionId }); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return new RegionManagerResponse(regionId, user.Id, user.Username, user.FullName); + } + + public async Task RemoveManagerAsync(Guid regionId, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return false; + var existing = await repo.GetManagerScopesAsync(regionId); + if (existing.Count == 0) return false; + + await uow.BeginAsync(ct); + try + { + repo.RemoveRegionScopes(existing); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return true; + } + public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var region = await repo.GetByIdAsync(id);