39eab2e3a2
Extends RegionService, RegionRepository, and RegionController with the endpoints from mainPlan.md sec 2 beyond plain CRUD: setRegionalTarget, setRegionalBudget, mapAgencyToRegion, setRegionalTaxRule, getRegionHierarchy, and getRegionDashboard. Target/budget/tax-rule mutation is gated to users with region scope at RegionalManager level or RegionalAgency level across all regions; mapAgencyToRegion links an existing User holding the RegionalAgency role rather than introducing a separate agency entity, matching the convention already used by Claim.FK_AgencyUserId elsewhere in the codebase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
242 lines
8.8 KiB
C#
242 lines
8.8 KiB
C#
using Distribution.Infra.Authorization;
|
|
using Distribution.Infra.UoW;
|
|
|
|
namespace Distribution.Features.Region;
|
|
|
|
public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess)
|
|
{
|
|
private const int RegionalManagerRoleLevel = 50;
|
|
private const int RegionalAgencyRoleLevel = 60;
|
|
|
|
private async Task<bool> CanManageRegionAsync(Guid regionId)
|
|
{
|
|
if (currentUser.RoleLevel >= RegionalAgencyRoleLevel) return true;
|
|
if (currentUser.RoleLevel < RegionalManagerRoleLevel || currentUser.UserId is null) return false;
|
|
return await scopeAccess.HasRegionScopeAsync(currentUser.UserId.Value, regionId);
|
|
}
|
|
|
|
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))
|
|
.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);
|
|
}
|
|
|
|
public async Task<RegionResponse> CreateAsync(CreateRegionRequest request, CancellationToken ct = default)
|
|
{
|
|
var region = new Models.Region { Name = request.Name };
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await repo.AddAsync(region);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
|
|
}
|
|
|
|
public async Task<RegionResponse?> UpdateAsync(Guid id, UpdateRegionRequest request, CancellationToken ct = default)
|
|
{
|
|
var region = await repo.GetByIdAsync(id);
|
|
if (region is null || !region.IsActive) return null;
|
|
region.Name = request.Name;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(region);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
var region = await repo.GetByIdAsync(id);
|
|
if (region is null || !region.IsActive) return false;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Remove(region);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public async Task<RegionalTargetResponse?> SetTargetAsync(Guid regionId, string period, SetRegionalTargetRequest request, CancellationToken ct = default)
|
|
{
|
|
var region = await repo.GetByIdAsync(regionId);
|
|
if (region is null || !region.IsActive) return null;
|
|
if (!await CanManageRegionAsync(regionId)) return null;
|
|
|
|
var target = await repo.GetTargetAsync(regionId, period);
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
if (target is null)
|
|
{
|
|
target = new Models.RegionalTarget { FK_RegionId = regionId, Period = period, TargetValue = request.TargetValue };
|
|
await repo.AddTargetAsync(target);
|
|
}
|
|
else
|
|
{
|
|
target.TargetValue = request.TargetValue;
|
|
repo.UpdateTarget(target);
|
|
}
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return new RegionalTargetResponse(regionId, period, target.TargetValue);
|
|
}
|
|
|
|
public async Task<RegionalBudgetResponse?> SetBudgetAsync(Guid regionId, string period, SetRegionalBudgetRequest request, CancellationToken ct = default)
|
|
{
|
|
var region = await repo.GetByIdAsync(regionId);
|
|
if (region is null || !region.IsActive) return null;
|
|
if (!await CanManageRegionAsync(regionId)) return null;
|
|
|
|
var budget = await repo.GetBudgetAsync(regionId, period);
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
if (budget is null)
|
|
{
|
|
budget = new Models.RegionalBudget { FK_RegionId = regionId, Period = period, BudgetAmount = request.BudgetAmount };
|
|
await repo.AddBudgetAsync(budget);
|
|
}
|
|
else
|
|
{
|
|
budget.BudgetAmount = request.BudgetAmount;
|
|
repo.UpdateBudget(budget);
|
|
}
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return new RegionalBudgetResponse(regionId, period, budget.BudgetAmount);
|
|
}
|
|
|
|
public async Task<RegionAgencyMapResponse?> MapAgencyAsync(Guid regionId, MapAgencyToRegionRequest request, CancellationToken ct = default)
|
|
{
|
|
var region = await repo.GetByIdAsync(regionId);
|
|
if (region is null || !region.IsActive) return null;
|
|
if (!await CanManageRegionAsync(regionId)) return null;
|
|
|
|
var agencyRoleLevel = await repo.GetUserRoleLevelAsync(request.AgencyUserId);
|
|
if (agencyRoleLevel is null || agencyRoleLevel < RegionalAgencyRoleLevel) return null;
|
|
|
|
var existing = await repo.GetAgencyMapAsync(regionId, request.AgencyUserId);
|
|
if (existing is not null) return new RegionAgencyMapResponse(regionId, request.AgencyUserId);
|
|
|
|
var map = new Models.RegionAgencyMap { FK_RegionId = regionId, FK_AgencyUserId = request.AgencyUserId };
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await repo.AddAgencyMapAsync(map);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return new RegionAgencyMapResponse(regionId, request.AgencyUserId);
|
|
}
|
|
|
|
public async Task<RegionalTaxRuleResponse?> SetTaxRuleAsync(Guid regionId, SetRegionalTaxRuleRequest request, CancellationToken ct = default)
|
|
{
|
|
var region = await repo.GetByIdAsync(regionId);
|
|
if (region is null || !region.IsActive) return null;
|
|
if (!await CanManageRegionAsync(regionId)) return null;
|
|
|
|
var rule = await repo.GetTaxRuleAsync(regionId);
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
if (rule is null)
|
|
{
|
|
rule = new Models.RegionalTaxRule { FK_RegionId = regionId, RuleConfig = request.RuleConfig };
|
|
await repo.AddTaxRuleAsync(rule);
|
|
}
|
|
else
|
|
{
|
|
rule.RuleConfig = request.RuleConfig;
|
|
repo.UpdateTaxRule(rule);
|
|
}
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return new RegionalTaxRuleResponse(regionId, rule.RuleConfig);
|
|
}
|
|
|
|
public async Task<RegionHierarchyResponse?> GetHierarchyAsync(Guid regionId)
|
|
{
|
|
var region = await repo.GetByIdAsync(regionId);
|
|
if (region is null || !region.IsActive) return null;
|
|
|
|
var routes = await repo.GetRoutesAsync(regionId);
|
|
var stores = await repo.GetStoresByRouteIdsAsync(routes.Select(r => r.Id).ToList());
|
|
var routeItems = routes
|
|
.Select(r => new RegionHierarchyRouteItem(
|
|
r.Id,
|
|
r.RouteName,
|
|
stores.Where(s => s.FK_RouteId == r.Id).Select(s => s.Id).ToList()))
|
|
.ToList();
|
|
return new RegionHierarchyResponse(region.Id, region.Name, routeItems);
|
|
}
|
|
|
|
public async Task<RegionDashboardResponse?> GetDashboardAsync(Guid regionId, string period)
|
|
{
|
|
var region = await repo.GetByIdAsync(regionId);
|
|
if (region is null || !region.IsActive) return null;
|
|
|
|
if (!DateTime.TryParseExact(period, "yyyy-MM", null, System.Globalization.DateTimeStyles.None, out var periodStart))
|
|
periodStart = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
|
|
var normalizedPeriod = periodStart.ToString("yyyy-MM");
|
|
var periodEndExclusive = periodStart.AddMonths(1);
|
|
|
|
var target = await repo.GetTargetAsync(regionId, normalizedPeriod);
|
|
var budget = await repo.GetBudgetAsync(regionId, normalizedPeriod);
|
|
var orders = await repo.GetOrdersForDashboardAsync(regionId, periodStart, periodEndExclusive);
|
|
|
|
return new RegionDashboardResponse(
|
|
regionId,
|
|
normalizedPeriod,
|
|
target?.TargetValue,
|
|
budget?.BudgetAmount,
|
|
orders.Count,
|
|
orders.Sum(o => o.TotalAmount),
|
|
orders.Count == 0 ? null : orders.Max(o => o.OrderDate));
|
|
}
|
|
}
|