# 11 — Route & Sales Visit Management **Status**: Partially Implemented **Depends On**: 00 — Existing Gap Fixes --- ## Goal Route CRUD already exists (`Features/Route`). This step covers the sales-visit half of [mainPlan.md §3](./mainPlan.md#3-route-management) — the part that is about a SalesRep's daily visit schedule, not the ride/dispatch route from [03_ride_lifecycle.md](./03_ride_lifecycle.md)'s `planDeliveryRoute`. --- ## Background / Context Today a `Store` has a single `FK_RouteId` (bulk-assign not yet supported via API — see gap #2 in [00_existing_gaps.md](./00_existing_gaps.md)), and route access is only granted via `UserRouteScope` (a many-to-many grant), not single ownership. There is no visit check-in/out data anywhere. --- ## Data Model Changes ### Route Ownership `RouteId`, `OwnerUserId` — single-owner assignment, distinct from the broader `UserRouteScope` access grant which may cover multiple users (e.g. a Supervisor overseeing several reps' routes). ### Route Visit Log `RouteId`, `StoreId`, `UserId`, `CheckInAt`, `CheckInGps`, `CheckOutAt` — one row per planned visit. --- ## API Endpoints | Function reference | Endpoint intent | |---|---| | `assignOutletsToRoute(routeId, outletIds[])` | `POST /api/routes/{id}/stores` — bulk version of the existing single `Store.RouteId` field | | `assignRouteToSalesRep(routeId, salesRepId)` | `PUT /api/routes/{id}/owner` | | `optimizeRouteSequence(routeId)` | `POST /api/routes/{id}/optimize` | | `recordRouteCheckIn(routeId, outletId, gpsCoords, timestamp)` | `POST /api/routes/{id}/visits/check-in` | | `recordRouteCheckOut(routeId, outletId, timestamp)` | `POST /api/routes/{id}/visits/check-out` | | `getRouteAdherence(routeId, date)` | `GET /api/routes/{id}/adherence?date=...` | | `reassignRoute(routeId, newSalesRepId)` | `PUT /api/routes/{id}/owner` (same as assign, different actor) | | `getRoutePerformance(routeId, period)` | `GET /api/routes/{id}/performance?period=...` | --- ## Business Rules - Check-in `gpsCoords` feeds `sendRouteDeviationAlert` in [09_fraud_controls.md](./09_fraud_controls.md) — a check-in materially outside the store's registered location should flag, not silently accept. - `getStoreVisitHistory` (§4) reads directly from `Route Visit Log` — no separate storage. --- ## Open Questions - Is route sequence optimization (`optimizeRouteSequence`) a manual reorder endpoint, or does it need actual distance/priority-based computation (external mapping API)? — **Deferred.** The endpoint is stubbed (`501 Not Implemented`) until this is decided. --- ## Implementation Notes Implemented: `assignOutletsToRoute`, `assignRouteToSalesRep`/`reassignRoute` (same endpoint), `recordRouteCheckIn`/`recordRouteCheckOut`, and `getStoreVisitHistory` (§4, now reads from `RouteVisitLog`). See [BACKEND_API_REFERENCE.md §6.6](../../sharedDocs/BACKEND_API_REFERENCE.md#66-routes) for full request/response shapes. Deliberately deferred, not oversights: - **`optimizeRouteSequence`** — stubbed as `501 Not Implemented`; no distance/priority computation exists (see Open Questions above). - **`getRouteAdherence`** / **`getRoutePerformance`** — not built; the underlying metrics (adherence %, strike rate, missed-visit count) were never defined, so there was nothing concrete to implement against. - **`sendRouteDeviationAlert`** — check-in computes and stores a basic `IsDeviationFlagged` boolean (Haversine distance > 500m from the store's registered `Latitude`/`Longitude`) but does not call any alert/notification pipeline, since [09_fraud_controls.md](./09_fraud_controls.md) itself is not implemented yet. Data model differs slightly from the original sketch above: `RouteId`/`OwnerUserId` became a nullable `FK_OwnerUserId` directly on `Route` (no separate ownership table needed for a single-owner field), and `Route Visit Log`'s `CheckInGps` became two decimal columns (`CheckInLatitude`/`CheckInLongitude`) to match the existing `Store.Latitude`/`Longitude` shape and make the distance calculation straightforward. Authorization for the new endpoints follows the `RegionService.CanManageRegionAsync` pattern (scope check via `IScopeAccessService`, no dedicated `routes.*` permission code): bulk store assignment and owner assignment require `IScopeAccessService.HasRouteScopeAsync` plus role level ≥ 20 (Supervisor+); check-in/check-out require route scope only, no role floor, since any rep with access to the route should be able to log their own visits. Frontend: implemented in `distribution_fe` — an admin "Manage" dialog on the Routes page (owner + bulk store assignment), a read-only visit-history dialog on the Stores page, and a new rep-facing `/sales-visits` page for GPS check-in/check-out (see `distribution_fe/app/(dashboard)/sales-visits/`).