Dev #55

Merged
DeepnaPooja merged 17 commits from dev into production 2026-10-07 10:47:55 +05:30
5 changed files with 120 additions and 0 deletions
Showing only changes of commit 92a311e1cc - Show all commits
@@ -6,6 +6,12 @@ public interface IRegionRepository
Task<List<Models.Region>> GetAllAsync();
Task<List<string>> GetAllCodesAsync();
Task<bool> CodeExistsAsync(string code, Guid? excludeId = null);
Task<Models.User?> GetUserWithRoleAsync(Guid userId);
Task<List<Models.UserRegionScope>> GetManagerScopesAsync(Guid regionId);
Task<Models.UserRegionScope?> GetRegionScopeAsync(Guid userId, Guid regionId);
Task AddRegionScopeAsync(Models.UserRegionScope scope);
void RemoveRegionScopes(IEnumerable<Models.UserRegionScope> scopes);
Task AddAsync(Models.Region entity);
void Update(Models.Region entity);
void Remove(Models.Region entity);
@@ -43,6 +43,34 @@ public class RegionController(RegionService service) : ControllerBase
return deleted ? NoContent() : NotFound();
}
[HttpGet("{id:guid}/manager")]
public async Task<IActionResult> GetManager(Guid id)
{
var result = await service.GetManagerAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPut("{id:guid}/manager")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Manage(Guid id, ManageRegionRequest request, CancellationToken ct)
@@ -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);
@@ -18,6 +18,25 @@ public class RegionRepository(AppDBContext context) : IRegionRepository
public async Task<bool> CodeExistsAsync(string code, Guid? excludeId = null) =>
await context.Regions.AnyAsync(r => r.Code == code && (excludeId == null || r.Id != excludeId));
public async Task<Models.User?> GetUserWithRoleAsync(Guid userId) =>
await context.Users.Include(u => u.Role).FirstOrDefaultAsync(u => u.Id == userId);
public async Task<List<Models.UserRegionScope>> 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<Models.UserRegionScope?> 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<Models.UserRegionScope> scopes) =>
context.UserRegionScopes.RemoveRange(scopes);
public async Task AddAsync(Models.Region entity) =>
await context.Regions.AddAsync(entity);
@@ -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<RegionManagerResponse?> 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<RegionManagerResponse?> 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<bool> 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<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var region = await repo.GetByIdAsync(id);