Design: Path Routing Rules (G2) #785
ChakshuGautam
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Path Routing Rules
Architecture discussion for phase G2 of the CRS Configurator. Implementation tracked in (draft) PR #783. Stacked on the escalation foundation in PR #770.
G2: Path Routing Rules
Why
CRS today routes complaints implicitly: the citizen picks a category and the workflow runs in whichever path that category's metadata says it belongs to. There is no editable, auditable rules engine — and when a complaint arrives with a category the system doesn't recognise, the escalation scheduler logs an
UNMAPPED_CATEGORYskip and the case quietly falls through to the per-tenantCRS.StateSLAdefaults.This phase replaces the silent-fallback path with an explicit Path Routing Rules engine. The engine answers a single question: given a complaint's
(category, subcategoryL1?), which path should it land on? Where path is whatever opaque key the tenant has chosen — the BRD example usesIGEvsIGSAE(BRD §5.2 "Routing Logic"), but for the Bomet / Nairobi tenants today there is no path concept at all, so this phase lets each tenant define its own.The engine's output (the resolved path) feeds the same
(path, category, subcategoryL1)key thatCRS.CategorySLA(PR #770) is already shaped around — so once G2 is in place, the scheduler'sUNMAPPED_CATEGORYskip becomes vanishingly rare and is reserved for genuine misconfiguration (a complaint category the operator forgot to add a rule for, surfaced as an explicit "no rule matched" decision in the trace-back tool).Reference: BRD §5.2 ("Routing Logic"), the Strategy B wiring discussion in docs/categorysla-wiring-strategies.md, and the
CRS.CategorySLAkeying convention in docs/escalation-feature-design.md.Scope
In:
CRS.PathRoutingRule,CRS.PathRoutingDefault) — stubs reserved inutilities/default-data-handler/src/main/resources/schema/CRS.G2.jsonby the same PR that introduces this doc; full schema body filled in by the implementation PR./manage/crs-routing/...with a rule-list editor, a per-rule edit form, a per-tenant default editor, and a "preview" tool (paste a category, see the routing decision).pgr-services(~30-50 LOC) that runs the rule engine on complaint creation and writes the resolved path into the complaint record so downstream consumers (scheduler, dashboards) can key on it without re-running the engine.CRS.SLAAuditLogentry per rule save / per default save — reusing the audit primitive already shipped in PR feat(pgr+configurator): escalation feature — OTEL-instrumented scheduler, /escalation/_trigger admin endpoint, configurator editor, embedded workflow designer, integration tests #770. (When Phase G4'sCRS.ConfigAuditLoglands, the routing-rules editor migrates to that.)Out (deferred to later phases or upstream):
categoryandsubcategoryL1fields on a rule are typed as free-text-with-autocomplete (autocomplete sourced from whateverCRS.CategorySLArecords exist on the tenant, same pattern as the SLA Matrix in feat(pgr+configurator): escalation feature — OTEL-instrumented scheduler, /escalation/_trigger admin endpoint, configurator editor, embedded workflow designer, integration tests #770). G1 will swap these for a strict picker.pgr-services; it does NOT get its own microservice.CRS.CategorySLA.MDMS schemas
Two new schema codes, reserved as stubs in this PR (
utilities/default-data-handler/src/main/resources/schema/CRS.G2.json) so that future references in the configurator UI, thepgr-servicesrouting hook, and the open-questions list stay stable. The stubs ship with"isActive": falseand an emptydefinition.properties— the implementation PR will setisActive: trueand fill in the shape sketched below.CRS.PathRoutingRuleOne row per evaluable rule. Sketch shape (final shape will mirror
CRS.CategorySLA's conventions:objecttype,x-unique,x-ref-schema=[],additionalProperties=false):{ "code": "string (operator-defined or UUID; uniqueIdentifier)", "category": "string (free-text until G1; references CRS.CategoryTaxonomy.category after G1)", "subcategoryL1": "string? (optional — if absent, matches any L1 under the category)", "path": "string (the routing path key; consumed by CRS.CategorySLA — same token vocabulary)", "requiresManualTriage": "boolean (BRD §5.2: IGE path requires manual triage; IGSAE goes direct)", "priority": "integer (lower wins; ties broken by created-at)", "isActive": "boolean (soft-delete = false)" }x-unique:[code](a deterministic identifier so re-imports are idempotent).(category, subcategoryL1)is intentionally NOT unique — operators may want overlapping rules with different priorities (e.g. a tenant-wide rule plus a more-specific override).CRS.PathRoutingDefaultSingleton record per tenant. Sketch shape (mirrors
CRS.StateSLAandCRS.WorkflowStateMappingsingleton convention):{ "singletonKey": "default", "defaultPath": "string (the path to use when no rule matches)", "defaultRequiresManualTriage": "boolean (typically true so the case lands in a triage queue rather than being routed blindly)" }x-unique:[singletonKey], value always"default". Mirrors the singleton convention already used byCRS.StateSLAandCRS.WorkflowStateMapping(see PR #770 escalation foundation).Configurator routes + UI sketch
New routes added under
/manage/crs-routing/...indigit-configurator. Sidebar nav entry sits under the existing CRS group (alongside/manage/crs-sla-matrixand/manage/crs-sla-traceshipped by PR #770)./manage/crs-routing/manage/crs-routing/new/manage/crs-routing/:code/edit/manage/crs-routing/default/manage/crs-routing/preview(category, subcategoryL1?)→ see resolved path + rule that matchedPage anatomy — list view (
/manage/crs-routing)Page anatomy — preview tool (
/manage/crs-routing/preview)The preview tool is the primary debugging surface for operators when a complaint lands on an unexpected path in production.
API endpoints touched
/mdms-v2/v2/_create,/mdms-v2/v2/_update,/mdms-v2/v2/_search— the default read/write path for rules and the singleton default. Same call shape the configurator already uses forCRS.CategorySLA(PR feat(pgr+configurator): escalation feature — OTEL-instrumented scheduler, /escalation/_trigger admin endpoint, configurator editor, embedded workflow designer, integration tests #770).CRS.PathRoutingRuleis installed on the target tenant (used by the configurator to decide whether to show the route at all).pgr-services— one new internal helper that runs the rule engine on complaint create, and one new optional admin endpointPOST /pgr-services/routing/_previewthat the configurator's preview tool calls server-side (so the preview matches production resolution exactly, including the cache, rather than re-implementing the lookup in the SPA). The helper is ~30-50 LOC; the endpoint is another ~30 LOC plus tests.crs.routing.rules.<tenant>— populated on first lookup, invalidated on rule save. Same pattern as thevalidationRulescache for user-validation (referenced in~/CLAUDE.md"Mobile Number Validation" section) and thecrs.permission.matrix.<tenant>pattern planned for Phase G4.No new microservice. No schema changes to the existing PGR service contract — the resolved path is written into the existing
additionalDetailblob on the complaint (or into a new column oneg_pgr_serviceif the implementation PR decides that's cleaner — open question).Dependencies on prior phases
Must ship first:
CRS.CategorySLA,CRS.StateSLA,CRS.WorkflowStateMapping,CRS.SLAAuditLog, scheduler patch. The whole point of routing rules is to feed the path key thatCRS.CategorySLAalready keys on.refactor/scheduler-state-name-mdms) —CRS.WorkflowStateMappingso the scheduler can resolve state names. Routing doesn't directly depend on state-name mapping, but the implementation PR for G2 will land on top of #A so the test fixtures share a tenant baseline.docs/categorysla-wiring-strategies) — establishes the Strategy A (rich intake) vs Strategy B (ServiceDefs extension) framing that the routing engine plugs into. G2's engine is essentially a third option: instead of trusting the complaint payload (A) or the ServiceDefs row (B), the routing engine derives the path from a rule applied to the category. The wiring-strategies doc references G2 as the "future replacement for the silent UNMAPPED_CATEGORY fallback".Recommended (but not strictly blocking):
categoryandsubcategoryL1fields on a rule become a strict picker rather than free-text-with-autocomplete. G2 ships first against free-text (matching the SLA Matrix in feat(pgr+configurator): escalation feature — OTEL-instrumented scheduler, /escalation/_trigger admin endpoint, configurator editor, embedded workflow designer, integration tests #770), then migrates when G1 is ready.This phase blocks:
UNMAPPED_CATEGORYskip can become a hard error in strict mode (gated by a per-tenantstrictRoutingflag)._trigger/_close/_assign— without routing rules, the audit can't tell whether a given action was correct for the resolved path.Acceptance criteria
An operator can confirm Phase G2 is fully shipped by running:
mdms-v2 /v2/schema/_searchreturns active definitions for bothCRS.PathRoutingRuleandCRS.PathRoutingDefaulton the target tenant.mdms-v2 /v2/_searchforCRS.PathRoutingDefaultwithuniqueIdentifiers=["default"]returns a single record with adefaultPathset.POST /pgr-services/routing/_previewwith the operator's most common(category, subcategoryL1)returns apaththat matchesCRS.CategorySLA's expected key./manage/crs-routingloads without errors, lists installed rules, and the[+ New rule]button creates a rule that's visible immediately on save.eg_pgr_service.additionaldetailor the new column for that complaint).crs.routing.rules.<tenant>invalidation hook is wired.CRS.SLAAuditLog(G4:CRS.ConfigAuditLog) entry with the actor's userUuid, the before/after JSON, and a non-emptyrecordIdentifier. Visible in/manage/crs-sla-matrix's audit drawer.Estimated effort
M (~2-3 days) — schema + UI (4 routes including preview) + 1 backend hook in
pgr-services+ cache wiring. Comparable size to Phase G1 (Category Taxonomy) on the roadmap. The schema is small (2 codes), the UI is mostly a CRUD pattern already in use in PR #770, and the backend hook is well-trodden: the engine is a simple priority-ordered scan over the cached rule list.Open questions
additionalDetail.routing.pathblob (zero schema change, matches Strategy A in categorysla-wiring-strategies.md); (b) newpathcolumn oneg_pgr_service(requires Flyway migration, but makes downstream queries faster and avoids the OTEL serialisation cost on every escalation cycle). The wiring-strategies doc leans (a); a fresh look from the implementation engineer is welcome.requiresManualTriageconsumer. Which workflow state corresponds to "manual triage" today? Bomet / Nairobi do not have a distinct triage state — the BRD'sIN_SCREENINGmaps totriageinCRS.WorkflowStateMapping. Does settingrequiresManualTriage=truesimply force the workflow's first transition to the state mapped totriage, or is there a separate triage-queue dashboard widget that the IGE path requires?UNMAPPED_CATEGORYskip become a hard error when at least one routing rule exists, or stay soft-fallback forever and require an explicit per-tenantstrictRouting=trueflag to upgrade?codelexical order, or (c) log a warning and pick the first-created? Equivalent question for "no rule matches" — does the default fire silently, or does the audit log record a "no rule matched, default applied" entry?complaint.additionalDetail.pathset by the client) or Strategy B (ServiceDefs.pathfield) will, after G2 lands, have THREE possible sources of truth for the path. What's the precedence order? Proposal: G2 routing engine takes precedence; Strategy A / B values are used as a fallback when no rule matches AND noCRS.PathRoutingDefaultis configured. This needs explicit confirmation before the implementation PR locks it in.Cross-references
refactor/scheduler-state-name-mdms, stacked under PR #B)Beta Was this translation helpful? Give feedback.
All reactions