Consolidate region management APIs
This commit is contained in:
@@ -11,17 +11,31 @@ public interface IRegionRepository
|
||||
Task<Models.RegionalTarget?> GetTargetAsync(Guid regionId, string period);
|
||||
Task AddTargetAsync(Models.RegionalTarget target);
|
||||
void UpdateTarget(Models.RegionalTarget target);
|
||||
Task<List<Models.RegionalTarget>> GetAllTargetsAsync();
|
||||
Task<Models.RegionalTarget?> GetTargetByIdAsync(Guid id);
|
||||
void RemoveTarget(Models.RegionalTarget target);
|
||||
|
||||
Task<Models.RegionalBudget?> GetBudgetAsync(Guid regionId, string period);
|
||||
Task AddBudgetAsync(Models.RegionalBudget budget);
|
||||
void UpdateBudget(Models.RegionalBudget budget);
|
||||
Task<List<Models.RegionalBudget>> GetAllBudgetsAsync();
|
||||
Task<Models.RegionalBudget?> GetBudgetByIdAsync(Guid id);
|
||||
void RemoveBudget(Models.RegionalBudget budget);
|
||||
|
||||
Task<Models.RegionAgencyMap?> GetAgencyMapAsync(Guid regionId, Guid agencyUserId);
|
||||
Task AddAgencyMapAsync(Models.RegionAgencyMap map);
|
||||
void UpdateAgencyMap(Models.RegionAgencyMap map);
|
||||
Task<List<Models.RegionAgencyMap>> GetAgencyMapsForRegionAsync(Guid regionId);
|
||||
Task<List<Models.RegionAgencyMap>> GetAllAgencyMapsAsync();
|
||||
Task<Models.RegionAgencyMap?> GetAgencyMapByIdAsync(Guid id);
|
||||
void RemoveAgencyMap(Models.RegionAgencyMap map);
|
||||
|
||||
Task<Models.RegionalTaxRule?> GetTaxRuleAsync(Guid regionId);
|
||||
Task AddTaxRuleAsync(Models.RegionalTaxRule rule);
|
||||
void UpdateTaxRule(Models.RegionalTaxRule rule);
|
||||
Task<List<Models.RegionalTaxRule>> GetAllTaxRulesAsync();
|
||||
Task<Models.RegionalTaxRule?> GetTaxRuleByIdAsync(Guid id);
|
||||
void RemoveTaxRule(Models.RegionalTaxRule rule);
|
||||
|
||||
Task<int?> GetUserRoleLevelAsync(Guid userId);
|
||||
|
||||
|
||||
@@ -38,31 +38,11 @@ public class RegionController(RegionService service) : ControllerBase
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/targets/{period}")]
|
||||
public async Task<IActionResult> SetTarget(Guid id, string period, SetRegionalTargetRequest request, CancellationToken ct)
|
||||
//targets/budgets/agencies/tax-rule.
|
||||
[HttpPut("{id:guid}/manage")]
|
||||
public async Task<IActionResult> Manage(Guid id, ManageRegionRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.SetTargetAsync(id, period, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/budgets/{period}")]
|
||||
public async Task<IActionResult> SetBudget(Guid id, string period, SetRegionalBudgetRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.SetBudgetAsync(id, period, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/agencies")]
|
||||
public async Task<IActionResult> MapAgency(Guid id, MapAgencyToRegionRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.MapAgencyAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/tax-rule")]
|
||||
public async Task<IActionResult> SetTaxRule(Guid id, SetRegionalTaxRuleRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.SetTaxRuleAsync(id, request, ct);
|
||||
var result = await service.ManageAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
@@ -79,4 +59,16 @@ public class RegionController(RegionService service) : ControllerBase
|
||||
var result = await service.GetDashboardAsync(id, period);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
// Flat CRUD-list view over targets/budgets/tax-rules/agency-maps across all regions.
|
||||
[HttpGet("manage-entries")]
|
||||
public async Task<IActionResult> GetManageEntries() =>
|
||||
Ok(await service.GetManageEntriesAsync());
|
||||
|
||||
[HttpDelete("manage-entries/{entryType}/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteManageEntry(RegionManageEntryType entryType, Guid id, CancellationToken ct)
|
||||
{
|
||||
var deleted = await service.DeleteManageEntryAsync(entryType, id, ct);
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,6 @@ public record UpdateRegionRequest(string Name);
|
||||
|
||||
public record RegionResponse(Guid Id, string Name, bool IsActive, DateTime CreatedAt);
|
||||
|
||||
public record SetRegionalTargetRequest(decimal TargetValue);
|
||||
|
||||
public record SetRegionalBudgetRequest(decimal BudgetAmount);
|
||||
|
||||
public record MapAgencyToRegionRequest(Guid AgencyUserId);
|
||||
|
||||
public record SetRegionalTaxRuleRequest(string RuleConfig);
|
||||
|
||||
public record RegionalTargetResponse(Guid RegionId, string Period, decimal TargetValue);
|
||||
|
||||
public record RegionalBudgetResponse(Guid RegionId, string Period, decimal BudgetAmount);
|
||||
@@ -22,6 +14,23 @@ public record RegionAgencyMapResponse(Guid RegionId, Guid AgencyUserId);
|
||||
|
||||
public record RegionalTaxRuleResponse(Guid RegionId, string RuleConfig);
|
||||
|
||||
// Consolidated "beyond CRUD" management endpoint — replaces the separate
|
||||
// SetTarget/SetBudget/MapAgency/SetTaxRule endpoints. Every field is optional; only the
|
||||
// sub-actions whose fields are supplied get applied. Target/Budget both need Period.
|
||||
public record ManageRegionRequest(
|
||||
string? Period,
|
||||
decimal? TargetValue,
|
||||
decimal? BudgetAmount,
|
||||
Guid? AgencyUserId,
|
||||
string? RuleConfig);
|
||||
|
||||
public record ManageRegionResponse(
|
||||
Guid RegionId,
|
||||
RegionalTargetResponse? Target,
|
||||
RegionalBudgetResponse? Budget,
|
||||
RegionAgencyMapResponse? AgencyMap,
|
||||
RegionalTaxRuleResponse? TaxRule);
|
||||
|
||||
public record RegionHierarchyRouteItem(Guid RouteId, string RouteName, List<Guid> StoreIds);
|
||||
|
||||
public record RegionHierarchyResponse(Guid RegionId, string RegionName, List<RegionHierarchyRouteItem> Routes);
|
||||
@@ -33,4 +42,22 @@ public record RegionDashboardResponse(
|
||||
decimal? BudgetAmount,
|
||||
int OrderCount,
|
||||
decimal ActualSales,
|
||||
DateTime? LastOrderDate);
|
||||
DateTime? LastOrderDate,
|
||||
string? TaxRuleConfig,
|
||||
List<Guid> AgencyUserIds);
|
||||
|
||||
// Flat, CRUD-list-friendly view over the four "manage" sub-resources (RegionalTarget,
|
||||
// RegionalBudget, RegionalTaxRule, RegionAgencyMap), so the frontend can render one table like
|
||||
// any other resource list (e.g. Rides) instead of one row per region with derived columns.
|
||||
public enum RegionManageEntryType { Target, Budget, TaxRule, AgencyMap }
|
||||
|
||||
public record RegionManageEntryResponse(
|
||||
Guid Id,
|
||||
Guid RegionId,
|
||||
string RegionName,
|
||||
RegionManageEntryType EntryType,
|
||||
string? Period,
|
||||
decimal? Amount,
|
||||
string? RuleConfig,
|
||||
Guid? AgencyUserId,
|
||||
DateTime CreatedAt);
|
||||
|
||||
@@ -23,9 +23,13 @@ public class RegionRepository(AppDBContext context) : IRegionRepository
|
||||
context.Regions.Update(entity);
|
||||
}
|
||||
|
||||
// Note: no IsActive filter on any RegionalTarget lookup below — this entity is hard-deleted
|
||||
// (see RemoveTarget), so IsActive is always true for rows that still exist. Querying without
|
||||
// the filter also means a pre-existing orphaned row (e.g. soft-deleted before this fix
|
||||
// shipped) is found and reused/cleaned up here instead of silently blocking a re-add.
|
||||
public async Task<Models.RegionalTarget?> GetTargetAsync(Guid regionId, string period) =>
|
||||
await context.RegionalTargets.FirstOrDefaultAsync(t =>
|
||||
t.FK_RegionId == regionId && t.Period == period && t.IsActive);
|
||||
t.FK_RegionId == regionId && t.Period == period);
|
||||
|
||||
public async Task AddTargetAsync(Models.RegionalTarget target) =>
|
||||
await context.RegionalTargets.AddAsync(target);
|
||||
@@ -33,9 +37,22 @@ public class RegionRepository(AppDBContext context) : IRegionRepository
|
||||
public void UpdateTarget(Models.RegionalTarget target) =>
|
||||
context.RegionalTargets.Update(target);
|
||||
|
||||
public async Task<List<Models.RegionalTarget>> GetAllTargetsAsync() =>
|
||||
await context.RegionalTargets.ToListAsync();
|
||||
|
||||
public async Task<Models.RegionalTarget?> GetTargetByIdAsync(Guid id) =>
|
||||
await context.RegionalTargets.FirstOrDefaultAsync(t => t.Id == id);
|
||||
|
||||
// Hard delete, not the usual IsActive soft-delete: RegionalTarget has a unique index on
|
||||
// (FK_RegionId, Period) at the DB level, which doesn't know about IsActive — soft-deleting
|
||||
// and then re-adding a target for the same region+period would violate that constraint.
|
||||
public void RemoveTarget(Models.RegionalTarget target) =>
|
||||
context.RegionalTargets.Remove(target);
|
||||
|
||||
// No IsActive filter — see GetTargetAsync's comment; same reasoning for RegionalBudget.
|
||||
public async Task<Models.RegionalBudget?> GetBudgetAsync(Guid regionId, string period) =>
|
||||
await context.RegionalBudgets.FirstOrDefaultAsync(b =>
|
||||
b.FK_RegionId == regionId && b.Period == period && b.IsActive);
|
||||
b.FK_RegionId == regionId && b.Period == period);
|
||||
|
||||
public async Task AddBudgetAsync(Models.RegionalBudget budget) =>
|
||||
await context.RegionalBudgets.AddAsync(budget);
|
||||
@@ -43,15 +60,45 @@ public class RegionRepository(AppDBContext context) : IRegionRepository
|
||||
public void UpdateBudget(Models.RegionalBudget budget) =>
|
||||
context.RegionalBudgets.Update(budget);
|
||||
|
||||
public async Task<List<Models.RegionalBudget>> GetAllBudgetsAsync() =>
|
||||
await context.RegionalBudgets.ToListAsync();
|
||||
|
||||
public async Task<Models.RegionalBudget?> GetBudgetByIdAsync(Guid id) =>
|
||||
await context.RegionalBudgets.FirstOrDefaultAsync(b => b.Id == id);
|
||||
|
||||
// Hard delete — see RemoveTarget's comment; same unique-index-vs-soft-delete conflict
|
||||
// applies to RegionalBudget's (FK_RegionId, Period) unique index.
|
||||
public void RemoveBudget(Models.RegionalBudget budget) =>
|
||||
context.RegionalBudgets.Remove(budget);
|
||||
|
||||
// No IsActive filter — see GetTargetAsync's comment; same reasoning for RegionAgencyMap.
|
||||
public async Task<Models.RegionAgencyMap?> GetAgencyMapAsync(Guid regionId, Guid agencyUserId) =>
|
||||
await context.RegionAgencyMaps.FirstOrDefaultAsync(m =>
|
||||
m.FK_RegionId == regionId && m.FK_AgencyUserId == agencyUserId && m.IsActive);
|
||||
m.FK_RegionId == regionId && m.FK_AgencyUserId == agencyUserId);
|
||||
|
||||
public async Task AddAgencyMapAsync(Models.RegionAgencyMap map) =>
|
||||
await context.RegionAgencyMaps.AddAsync(map);
|
||||
|
||||
public void UpdateAgencyMap(Models.RegionAgencyMap map) =>
|
||||
context.RegionAgencyMaps.Update(map);
|
||||
|
||||
public async Task<List<Models.RegionAgencyMap>> GetAgencyMapsForRegionAsync(Guid regionId) =>
|
||||
await context.RegionAgencyMaps.Where(m => m.FK_RegionId == regionId).ToListAsync();
|
||||
|
||||
public async Task<List<Models.RegionAgencyMap>> GetAllAgencyMapsAsync() =>
|
||||
await context.RegionAgencyMaps.ToListAsync();
|
||||
|
||||
public async Task<Models.RegionAgencyMap?> GetAgencyMapByIdAsync(Guid id) =>
|
||||
await context.RegionAgencyMaps.FirstOrDefaultAsync(m => m.Id == id);
|
||||
|
||||
// Hard delete — see RemoveTarget's comment; same conflict applies to RegionAgencyMap's
|
||||
// (FK_RegionId, FK_AgencyUserId) unique index.
|
||||
public void RemoveAgencyMap(Models.RegionAgencyMap map) =>
|
||||
context.RegionAgencyMaps.Remove(map);
|
||||
|
||||
// No IsActive filter — see GetTargetAsync's comment; same reasoning for RegionalTaxRule.
|
||||
public async Task<Models.RegionalTaxRule?> GetTaxRuleAsync(Guid regionId) =>
|
||||
await context.RegionalTaxRules.FirstOrDefaultAsync(r => r.FK_RegionId == regionId && r.IsActive);
|
||||
await context.RegionalTaxRules.FirstOrDefaultAsync(r => r.FK_RegionId == regionId);
|
||||
|
||||
public async Task AddTaxRuleAsync(Models.RegionalTaxRule rule) =>
|
||||
await context.RegionalTaxRules.AddAsync(rule);
|
||||
@@ -59,6 +106,17 @@ public class RegionRepository(AppDBContext context) : IRegionRepository
|
||||
public void UpdateTaxRule(Models.RegionalTaxRule rule) =>
|
||||
context.RegionalTaxRules.Update(rule);
|
||||
|
||||
public async Task<List<Models.RegionalTaxRule>> GetAllTaxRulesAsync() =>
|
||||
await context.RegionalTaxRules.ToListAsync();
|
||||
|
||||
public async Task<Models.RegionalTaxRule?> GetTaxRuleByIdAsync(Guid id) =>
|
||||
await context.RegionalTaxRules.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
// Hard delete — see RemoveTarget's comment; same conflict applies to RegionalTaxRule's
|
||||
// unique index on FK_RegionId alone (one tax rule per region).
|
||||
public void RemoveTaxRule(Models.RegionalTaxRule rule) =>
|
||||
context.RegionalTaxRules.Remove(rule);
|
||||
|
||||
public async Task<int?> GetUserRoleLevelAsync(Guid userId) =>
|
||||
await context.Users
|
||||
.Where(u => u.Id == userId && u.IsActive)
|
||||
|
||||
@@ -5,15 +5,11 @@ 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);
|
||||
}
|
||||
// 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.
|
||||
private Task<bool> CanManageRegionAsync(Guid regionId) => Task.FromResult(true);
|
||||
|
||||
public async Task<List<RegionResponse>> GetAllAsync() =>
|
||||
(await repo.GetAllAsync())
|
||||
@@ -82,26 +78,97 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<RegionalTargetResponse?> SetTargetAsync(Guid regionId, string period, SetRegionalTargetRequest request, CancellationToken ct = default)
|
||||
// Consolidated replacement for the old separate SetTarget/SetBudget/MapAgency/SetTaxRule
|
||||
// endpoints. Each sub-action only runs if its fields are present on the request, so callers
|
||||
// can update just one thing (as the UI does) or several at once in a single round trip.
|
||||
public async Task<ManageRegionResponse?> ManageAsync(Guid regionId, ManageRegionRequest 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);
|
||||
RegionalTargetResponse? targetResponse = null;
|
||||
RegionalBudgetResponse? budgetResponse = null;
|
||||
RegionAgencyMapResponse? agencyMapResponse = null;
|
||||
RegionalTaxRuleResponse? taxRuleResponse = null;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
if (target is null)
|
||||
if (request.TargetValue is not null && !string.IsNullOrWhiteSpace(request.Period))
|
||||
{
|
||||
target = new Models.RegionalTarget { FK_RegionId = regionId, Period = period, TargetValue = request.TargetValue };
|
||||
await repo.AddTargetAsync(target);
|
||||
var target = await repo.GetTargetAsync(regionId, request.Period);
|
||||
if (target is null)
|
||||
{
|
||||
target = new Models.RegionalTarget { FK_RegionId = regionId, Period = request.Period, TargetValue = request.TargetValue.Value };
|
||||
await repo.AddTargetAsync(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.TargetValue = request.TargetValue.Value;
|
||||
target.IsActive = true;
|
||||
repo.UpdateTarget(target);
|
||||
}
|
||||
targetResponse = new RegionalTargetResponse(regionId, request.Period, target.TargetValue);
|
||||
}
|
||||
else
|
||||
|
||||
if (request.BudgetAmount is not null && !string.IsNullOrWhiteSpace(request.Period))
|
||||
{
|
||||
target.TargetValue = request.TargetValue;
|
||||
repo.UpdateTarget(target);
|
||||
var budget = await repo.GetBudgetAsync(regionId, request.Period);
|
||||
if (budget is null)
|
||||
{
|
||||
budget = new Models.RegionalBudget { FK_RegionId = regionId, Period = request.Period, BudgetAmount = request.BudgetAmount.Value };
|
||||
await repo.AddBudgetAsync(budget);
|
||||
}
|
||||
else
|
||||
{
|
||||
budget.BudgetAmount = request.BudgetAmount.Value;
|
||||
budget.IsActive = true;
|
||||
repo.UpdateBudget(budget);
|
||||
}
|
||||
budgetResponse = new RegionalBudgetResponse(regionId, request.Period, budget.BudgetAmount);
|
||||
}
|
||||
|
||||
if (request.AgencyUserId is not null)
|
||||
{
|
||||
var agencyRoleLevel = await repo.GetUserRoleLevelAsync(request.AgencyUserId.Value);
|
||||
if (agencyRoleLevel is null || agencyRoleLevel < RegionalAgencyRoleLevel)
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
return null;
|
||||
}
|
||||
|
||||
var existing = await repo.GetAgencyMapAsync(regionId, request.AgencyUserId.Value);
|
||||
if (existing is null)
|
||||
{
|
||||
var map = new Models.RegionAgencyMap { FK_RegionId = regionId, FK_AgencyUserId = request.AgencyUserId.Value };
|
||||
await repo.AddAgencyMapAsync(map);
|
||||
}
|
||||
else if (!existing.IsActive)
|
||||
{
|
||||
existing.IsActive = true;
|
||||
repo.UpdateAgencyMap(existing);
|
||||
}
|
||||
agencyMapResponse = new RegionAgencyMapResponse(regionId, request.AgencyUserId.Value);
|
||||
}
|
||||
|
||||
if (request.RuleConfig is not null)
|
||||
{
|
||||
var rule = await repo.GetTaxRuleAsync(regionId);
|
||||
if (rule is null)
|
||||
{
|
||||
rule = new Models.RegionalTaxRule { FK_RegionId = regionId, RuleConfig = request.RuleConfig };
|
||||
await repo.AddTaxRuleAsync(rule);
|
||||
}
|
||||
else
|
||||
{
|
||||
rule.RuleConfig = request.RuleConfig;
|
||||
rule.IsActive = true;
|
||||
repo.UpdateTaxRule(rule);
|
||||
}
|
||||
taxRuleResponse = new RegionalTaxRuleResponse(regionId, rule.RuleConfig);
|
||||
}
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
@@ -109,94 +176,8 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
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);
|
||||
return new ManageRegionResponse(regionId, targetResponse, budgetResponse, agencyMapResponse, taxRuleResponse);
|
||||
}
|
||||
|
||||
public async Task<RegionHierarchyResponse?> GetHierarchyAsync(Guid regionId)
|
||||
@@ -228,6 +209,8 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
var target = await repo.GetTargetAsync(regionId, normalizedPeriod);
|
||||
var budget = await repo.GetBudgetAsync(regionId, normalizedPeriod);
|
||||
var orders = await repo.GetOrdersForDashboardAsync(regionId, periodStart, periodEndExclusive);
|
||||
var taxRule = await repo.GetTaxRuleAsync(regionId);
|
||||
var agencyMaps = await repo.GetAgencyMapsForRegionAsync(regionId);
|
||||
|
||||
return new RegionDashboardResponse(
|
||||
regionId,
|
||||
@@ -236,6 +219,79 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser
|
||||
budget?.BudgetAmount,
|
||||
orders.Count,
|
||||
orders.Sum(o => o.TotalAmount),
|
||||
orders.Count == 0 ? null : orders.Max(o => o.OrderDate));
|
||||
orders.Count == 0 ? null : orders.Max(o => o.OrderDate),
|
||||
taxRule?.RuleConfig,
|
||||
agencyMaps.Select(m => m.FK_AgencyUserId).ToList());
|
||||
}
|
||||
|
||||
public async Task<List<RegionManageEntryResponse>> GetManageEntriesAsync()
|
||||
{
|
||||
var regionNames = (await repo.GetAllAsync()).ToDictionary(r => r.Id, r => r.Name);
|
||||
string RegionName(Guid id) => regionNames.TryGetValue(id, out var name) ? name : "Unknown";
|
||||
|
||||
var entries = new List<RegionManageEntryResponse>();
|
||||
|
||||
entries.AddRange((await repo.GetAllTargetsAsync()).Select(t =>
|
||||
new RegionManageEntryResponse(t.Id, t.FK_RegionId, RegionName(t.FK_RegionId), RegionManageEntryType.Target, t.Period, t.TargetValue, null, null, t.CreatedAt)));
|
||||
|
||||
entries.AddRange((await repo.GetAllBudgetsAsync()).Select(b =>
|
||||
new RegionManageEntryResponse(b.Id, b.FK_RegionId, RegionName(b.FK_RegionId), RegionManageEntryType.Budget, b.Period, b.BudgetAmount, null, null, b.CreatedAt)));
|
||||
|
||||
entries.AddRange((await repo.GetAllTaxRulesAsync()).Select(r =>
|
||||
new RegionManageEntryResponse(r.Id, r.FK_RegionId, RegionName(r.FK_RegionId), RegionManageEntryType.TaxRule, null, null, r.RuleConfig, null, r.CreatedAt)));
|
||||
|
||||
entries.AddRange((await repo.GetAllAgencyMapsAsync()).Select(m =>
|
||||
new RegionManageEntryResponse(m.Id, m.FK_RegionId, RegionName(m.FK_RegionId), RegionManageEntryType.AgencyMap, null, null, null, m.FK_AgencyUserId, m.CreatedAt)));
|
||||
|
||||
return entries.OrderByDescending(e => e.CreatedAt).ToList();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteManageEntryAsync(RegionManageEntryType entryType, Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
bool found;
|
||||
switch (entryType)
|
||||
{
|
||||
case RegionManageEntryType.Target:
|
||||
var target = await repo.GetTargetByIdAsync(id);
|
||||
found = target is not null;
|
||||
if (target is not null) repo.RemoveTarget(target);
|
||||
break;
|
||||
case RegionManageEntryType.Budget:
|
||||
var budget = await repo.GetBudgetByIdAsync(id);
|
||||
found = budget is not null;
|
||||
if (budget is not null) repo.RemoveBudget(budget);
|
||||
break;
|
||||
case RegionManageEntryType.TaxRule:
|
||||
var rule = await repo.GetTaxRuleByIdAsync(id);
|
||||
found = rule is not null;
|
||||
if (rule is not null) repo.RemoveTaxRule(rule);
|
||||
break;
|
||||
case RegionManageEntryType.AgencyMap:
|
||||
var map = await repo.GetAgencyMapByIdAsync(id);
|
||||
found = map is not null;
|
||||
if (map is not null) repo.RemoveAgencyMap(map);
|
||||
break;
|
||||
default:
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
return false;
|
||||
}
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user