# 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](./mainPlan.md#2-region-management) 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](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)). --- ## 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](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)). Stand-in: `PUT /api/routes/{id}/region` assigns/reassigns an existing route directly to a region (see [11_route_sales_management.md](./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](../../sharedDocs/BACKEND_API_REFERENCE.md#65-regions) 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.