Add region code
This commit is contained in:
@@ -4,6 +4,8 @@ public interface IRegionRepository
|
||||
{
|
||||
Task<Models.Region?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.Region>> GetAllAsync();
|
||||
Task<List<string>> GetAllCodesAsync();
|
||||
Task<bool> CodeExistsAsync(string code, Guid? excludeId = null);
|
||||
Task AddAsync(Models.Region entity);
|
||||
void Update(Models.Region entity);
|
||||
void Remove(Models.Region entity);
|
||||
|
||||
@@ -10,6 +10,11 @@ public class RegionController(RegionService service) : ControllerBase
|
||||
public async Task<IActionResult> GetAll() =>
|
||||
Ok(await service.GetAllAsync());
|
||||
|
||||
// Suggested code for the Add Region form; the user may override it.
|
||||
[HttpGet("next-code")]
|
||||
public async Task<IActionResult> GetNextCode() =>
|
||||
Ok(await service.GetNextCodeAsync());
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
namespace Distribution.Features.Region;
|
||||
|
||||
public record CreateRegionRequest(string Name);
|
||||
// Code is optional: blank/null auto-generates the next REG-#### code; otherwise stored as entered (trimmed).
|
||||
public record CreateRegionRequest(string Name, string? Code = null);
|
||||
|
||||
public record UpdateRegionRequest(string Name);
|
||||
public record NextRegionCodeResponse(string Code);
|
||||
|
||||
public record RegionResponse(Guid Id, string Name, bool IsActive, DateTime CreatedAt);
|
||||
// Code is auto-generated on create; on update it is optional (null/blank keeps the current code).
|
||||
public record UpdateRegionRequest(string Name, string? Code = null);
|
||||
|
||||
public record RegionResponse(Guid Id, string Code, string Name, bool IsActive, DateTime CreatedAt);
|
||||
|
||||
public record RegionalTargetResponse(Guid RegionId, string Period, decimal TargetValue);
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@ public class RegionRepository(AppDBContext context) : IRegionRepository
|
||||
public async Task<List<Models.Region>> GetAllAsync() =>
|
||||
await context.Regions.ToListAsync();
|
||||
|
||||
// Includes soft-deleted regions: the unique index on Code doesn't know about IsActive.
|
||||
public async Task<List<string>> GetAllCodesAsync() =>
|
||||
await context.Regions.Select(r => r.Code).ToListAsync();
|
||||
|
||||
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 AddAsync(Models.Region entity) =>
|
||||
await context.Regions.AddAsync(entity);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Distribution.Features.Region;
|
||||
public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess)
|
||||
{
|
||||
private const int RegionalAgencyRoleLevel = 60;
|
||||
private const string CodePrefix = "REG-";
|
||||
|
||||
// TODO: re-enable the RegionalManager+scope / RegionalAgency role gate on ManageAsync once
|
||||
// the role model for this endpoint is finalized. Left open (allow-all) for now on purpose.
|
||||
@@ -14,19 +15,42 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
public async Task<List<RegionResponse>> GetAllAsync() =>
|
||||
(await repo.GetAllAsync())
|
||||
.Where(r => r.IsActive)
|
||||
.Select(r => new RegionResponse(r.Id, r.Name, r.IsActive, r.CreatedAt))
|
||||
.Select(r => new RegionResponse(r.Id, r.Code, r.Name, r.IsActive, r.CreatedAt))
|
||||
.ToList();
|
||||
|
||||
public async Task<RegionResponse?> GetByIdAsync(Guid id)
|
||||
{
|
||||
var region = await repo.GetByIdAsync(id);
|
||||
if (region is null || !region.IsActive) return null;
|
||||
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
|
||||
return new RegionResponse(region.Id, region.Code, region.Name, region.IsActive, region.CreatedAt);
|
||||
}
|
||||
|
||||
// Next code = highest existing REG-#### number + 1 (soft-deleted regions included so codes are never reused).
|
||||
public async Task<NextRegionCodeResponse> GetNextCodeAsync() => new(await GenerateCodeAsync());
|
||||
|
||||
private async Task<string> GenerateCodeAsync()
|
||||
{
|
||||
var max = (await repo.GetAllCodesAsync())
|
||||
.Where(c => c.StartsWith(CodePrefix, StringComparison.OrdinalIgnoreCase)
|
||||
&& int.TryParse(c[CodePrefix.Length..], out _))
|
||||
.Select(c => int.Parse(c[CodePrefix.Length..]))
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
return $"{CodePrefix}{max + 1:D4}";
|
||||
}
|
||||
|
||||
public async Task<RegionResponse> CreateAsync(CreateRegionRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var region = new Models.Region { Name = request.Name };
|
||||
string code;
|
||||
if (string.IsNullOrWhiteSpace(request.Code))
|
||||
code = await GenerateCodeAsync();
|
||||
else
|
||||
{
|
||||
code = request.Code.Trim();
|
||||
if (await repo.CodeExistsAsync(code))
|
||||
throw new InvalidOperationException($"Region code '{code}' is already in use.");
|
||||
}
|
||||
var region = new Models.Region { Name = request.Name, Code = code };
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
@@ -38,7 +62,7 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
|
||||
return new RegionResponse(region.Id, region.Code, region.Name, region.IsActive, region.CreatedAt);
|
||||
}
|
||||
|
||||
public async Task<RegionResponse?> UpdateAsync(Guid id, UpdateRegionRequest request, CancellationToken ct = default)
|
||||
@@ -46,6 +70,13 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
var region = await repo.GetByIdAsync(id);
|
||||
if (region is null || !region.IsActive) return null;
|
||||
region.Name = request.Name;
|
||||
if (!string.IsNullOrWhiteSpace(request.Code))
|
||||
{
|
||||
var code = request.Code.Trim();
|
||||
if (code != region.Code && await repo.CodeExistsAsync(code, id))
|
||||
throw new InvalidOperationException($"Region code '{code}' is already in use.");
|
||||
region.Code = code;
|
||||
}
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
@@ -57,7 +88,7 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
|
||||
return new RegionResponse(region.Id, region.Code, region.Name, region.IsActive, region.CreatedAt);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
||||
|
||||
Reference in New Issue
Block a user