Files
ERP_Distribution/Distribution/docs/10_region_management.md
T

6.0 KiB

10 — Region Management (Beyond CRUD)

Status: Implemented (except assignAreaToRegion, blocked on the Area entity) Depends On: 00 — Existing Gap Fixes


Goal

Region CRUD already exists (Features/Region). This step covers everything in mainPlan.md §2 that isn't plain CRUD: targets, budgets, agency mapping, tax rules, hierarchy, and dashboarding.


Background / Context

Region today has only a Name. Nothing points to it except Route.FK_RegionId. There is no Area level (see Open Question in 00_existing_gaps.md).


Data Model Changes

Regional Target / Budget

Implemented as two separate entities, RegionalTarget and RegionalBudget (Models/RegionalTarget.cs, Models/RegionalBudget.cs), each with FK_RegionId, Period (string, yyyy-MM), and TargetValue/BudgetAmount respectively. A unique index on (FK_RegionId, Period) means each region has at most one target and one budget row per period; setting the same period again updates it in place rather than inserting a duplicate.

Agency-Region Map

Implemented as RegionAgencyMap (Models/RegionAgencyMap.cs): FK_RegionId, FK_AgencyUserId, unique on the pair. "Regional Agency" stayed a Role, not a distinct entity (see Open Questions, resolved) — FK_AgencyUserId points at an existing User who holds the RegionalAgency role, the same convention already used by Claim.FK_AgencyUserId (§18).

Regional Tax Rule

Implemented as RegionalTaxRule (Models/RegionalTaxRule.cs): FK_RegionId (unique — one rule per region), RuleConfig (raw JSON string). Nothing in pricing/tax calculation reads this yet; it's stored for a future pricing-rule integration.


API Endpoints

Function reference Endpoint Status
assignAreaToRegion(areaId, regionId) — Blocked — no Area entity exists (see 00_existing_gaps.md). Stand-in: PUT /api/routes/{id}/region assigns/reassigns an existing route directly to a region (see 11_route_sales_management.md).
setRegionalTarget(regionId, period, targetValue) PUT /api/regions/{id}/manage (consolidated, see below) Implemented
setRegionalBudget(regionId, period, budgetAmount) PUT /api/regions/{id}/manage (consolidated, see below) Implemented
mapAgencyToRegion(agencyId, regionId) PUT /api/regions/{id}/manage (consolidated, see below) Implemented
setRegionalTaxRule(regionId, ruleConfig) PUT /api/regions/{id}/manage (consolidated, see below) Implemented
getRegionHierarchy(regionId) GET /api/regions/{id}/hierarchy Implemented (region → routes → store ids; no Area level)
getRegionDashboard(regionId, period) GET /api/regions/{id}/dashboard?period=... Implemented (real-time aggregation; see Open Questions, resolved)

All request/response DTOs live in Features/Region/RegionDTOs.cs; routing and access checks are in RegionController.cs and RegionService.cs.

The four set*/mapAgencyToRegion actions above were later consolidated into a single PUT /api/regions/{id}/manage endpoint (RegionService.ManageAsync) — every field on the request is optional, and only the sub-action(s) whose fields are present get applied, so one call can update just one thing (as the frontend's region-manage-sheet.tsx does, one button per field) or several at once. See BACKEND_API_REFERENCE.md §6.5 for the exact request/response shape.


Business Rules

  • ⚠️ Temporarily disabled: RegionService.CanManageRegionAsync (the gate on ManageAsync) was stubbed to always return true — role-based restriction is deliberately deferred pending a decision on the role model for this endpoint (tracked as a TODO in RegionService.cs). The design below is what it enforced before being stubbed out, and what it should return to:
    • Only RegionalManager and above (per role level in Data/Seed/RoleAndPermissionSeed.cs) can set targets/budgets/tax-rule for their own region; only RegionalAgency can set them across regions. Role level ≥ 60 (RegionalAgency) allowed unconditionally, role level ≥ 50 (RegionalManager) requires a matching UserRegionScope row for that region, anything lower rejected.
  • agencyUserId (when supplied to manage) still rejects the target user if their role level is below the RegionalAgency threshold (60) — you can only map a user who actually holds (or outranks) that role. This check was not part of the disabled gate and remains active.

Open Questions

  • Is "Regional Agency" a business entity distinct from the RegionalAgency role...? Resolved: kept as a Role on User; no new agency entity, consistent with Claim.FK_AgencyUserId.
  • Does getRegionDashboard need real-time aggregation, or a scheduled/cached rollup? Resolved: real-time aggregation from Order/Store/Route for now; revisit if/when region-wide dashboard queries become a performance concern at scale.

Implementation Notes

  • Backend: Models/RegionalTarget.cs, Models/RegionalBudget.cs, Models/RegionAgencyMap.cs, Models/RegionalTaxRule.cs, Data/AppDBContext.cs (DbSets + unique indexes), Features/Region/{IRegionRepository,RegionRepository,RegionService,RegionController,RegionDTOs}.cs.
  • Frontend: distribution_fe/types/api.ts (response types), distribution_fe/lib/api/master.ts (regions.setTarget/setBudget/mapAgency/setTaxRule/hierarchy/dashboard), and a new RegionManageSheet dialog (distribution_fe/app/(dashboard)/network/regions/_components/region-manage-sheet.tsx) wired into the regions table's row actions.
  • The EF Core migration 20260831050805_AddRegionManagement has been generated and applied to the shared database — RegionalTargets, RegionalBudgets, RegionAgencyMaps, and RegionalTaxRules all exist.