Reporting Hierarchy MVP Implementation Plan
Overview
Build audit-grade Reporting Hierarchy system (separate from commissions) to seed hierarchy fromagent2_pepm.csv safely, even though CSV only contains names. System uses agent_entity_id (UUID) for all FK references, implements SCD2 pattern, enforces cycle detection, maintains org-scoping, and supports ops-based changesets for future drag/drop UI. CRITICAL: Hierarchy is ONLY for reporting/visibility and dashboard scoping; must NOT be used by commission engine. Do not add hierarchy joins to payout paths.
Architecture
Data Flow
Key Tables
config_agent_hierarchy: SCD2 table usingagent_entity_id(UUID) for all FK references- Schema:
hierarchy_id,tenant_id,org_id,agent_entity_id,parent_agent_entity_id,effective_start_date,effective_end_date,change_status,is_active,created_at,created_by,record_hash - Located at:
integration/bigquery/sql/tables/config_agent_hierarchy.sql - Existing queries:
api/bigquery/phase4_hierarchy_queries.py
- Schema:
Implementation Tasks
A) Agent Resolver Export Endpoint
File:api/routes/admin.py (add new endpoint) or api/routes/admin_hierarchy.py
Endpoint: GET /admin/agents/resolver
Requirements:
- Org-scoped (use
get_org_id_from_requestpattern fromapi/utils/org_scoping.py) - RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Header: Must accept
X-Org-Idheader (usex_org_id: Optional[str] = Header(None, alias="X-Org-Id")) - Returns:
agent_entity_id,first_name,last_name,full_name_normalized,agent_id(optional),email(optional) - Normalization: MUST be identical in SQL and Python:
- Uppercase, trim, remove punctuation:
. , ' -(include hyphen removal) - Collapse whitespace
- SQL:
UPPER(TRIM(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(CONCAT(c.first_name, ' ', c.last_name), '.', ''), ',', ''), '''', ''), '-', ''), ' ', ' '))) - Python:
full.replace('.', '').replace(',', '').replace("'", '').replace('-', '').strip().upper()then collapse spaces - Deterministic: same normalization used in seed generator
- Uppercase, trim, remove punctuation:
- Query joins
config_agent_profile_identity(foragent_entity_id,agent_id) withconfig_agent_profile_contact(forfirst_name,last_name,email) - Filter:
is_active=TRUE,effective_end_date IS NULL, tenant/org scope - Response format: List of
AgentResolverResponseobjects (200 OK) - Error handling: 403 if not admin/CEO, 400 if org_id missing for platform admin
api/bigquery/phase4_profile_queries.py:export_bq_agents):
api/schemas/admin_hierarchy.py:
B) Hierarchy Bootstrap Endpoint
File:api/routes/admin.py (add new endpoint) or create api/routes/admin_hierarchy.py
Endpoint: POST /admin/hierarchy/bootstrap
Requirements:
- Request format: JSON body with
rowsarray (NOT multipart CSV upload - use JSON for consistency with other admin endpoints) - Accepts JSON rows with ONLY existing schema fields:
child_agent_entity_id,parent_agent_entity_id,effective_start_date,effective_end_date(optional),change_status,active - IMPORTANT: Schema does NOT have
relationship_typeornotesfields - MVP must not include these - Header: Must accept
X-Org-Idheader (usex_org_id: Optional[str] = Header(None, alias="X-Org-Id")) - UUID normalization: Normalize all UUIDs to lowercase for consistency (accept both cases, store lowercase)
- Validations:
- Tenant/org isolation: Child and parent must belong to same org/tenant (reject cross-org refs)
- IDs exist: Verify
child_agent_entity_idandparent_agent_entity_id(if not NULL) exist inconfig_agent_profile_identity - Date normalized:
YYYY-MM-DDformat, validate date is valid (no timezone, no time component) - Single-parent policy (MVP): At most one active parent per child as-of date
- Cycle prevention: Use
detect_circular_hierarchy()fromapi/bigquery/phase4_hierarchy_queries.pyto reject loops - Idempotency: Same
(child_agent_entity_id, parent_agent_entity_id, effective_start_date)=> reused/no-op (query existing row before insert)
- Error response format: FastAPI
HTTPExceptionwithdetailfield (string or list of strings for validation errors)- 400: Validation errors (invalid IDs, cycles, cross-org refs)
- 403: RBAC failure
- 500: Internal server error
- Response format:
HierarchyBootstrapResponse(200 OK with created/skipped counts)
- Writes SCD2 rows: Insert only (no in-place edits), use
create_agent_hierarchy()fromapi/bigquery/phase4_hierarchy_queries.py - Set
change_status='APPROVED',is_active=TRUE(fromactivefield)
api/schemas/admin_hierarchy.py:
C) Reparent Endpoint
File:api/routes/admin.py (add new endpoint) or api/routes/admin_hierarchy.py
Endpoint: POST /admin/hierarchy/reparent
Requirements:
-
Input:
child_agent_entity_id,new_parent_agent_entity_id(nullable),effective_start_date,change_status(optional, default “APPROVED”),active(optional, default True) -
IMPORTANT: Schema does NOT have
notesfield - MVP must not include it -
Header: Must accept
X-Org-Idheader (usex_org_id: Optional[str] = Header(None, alias="X-Org-Id")) - UUID normalization: Normalize all UUIDs to lowercase (accept both cases, store lowercase)
-
Algorithm (SCD2):
- Scope + existence checks: Verify child and parent (if not null) exist in tenant/org scope
- NOOP check: If current parent (as-of
effective_start_date) equalsnew_parent_agent_entity_id, return early (idempotent) - Cycle check (single-parent optimized): Walk up from
new_parent_agent_entity_idvia parent links (as-ofeffective_start_date); if hitchild_agent_entity_id=> reject with 400 - Close existing: Find any overlapping existing relationship for child (active row with
effective_end_date IS NULL), seteffective_end_date = effective_start_date - 1 day(useclose_agent_hierarchy()) - Insert new: Create new relationship row with
effective_start_date,effective_end_date=NULL(usecreate_agent_hierarchy())
-
Error response format: FastAPI
HTTPExceptionwithdetailfield- 400: Validation errors (invalid IDs, cycles, cross-org refs, NOOP returns 200 with status=“noop”)
- 403: RBAC failure
- 500: Internal server error
-
Response format:
HierarchyReparentResponse(200 OK for created, 200 OK for noop)
- Use
get_agent_hierarchy_rows()to find current parent - Use
detect_circular_hierarchy()for cycle check (walk up from new_parent) - Use
close_agent_hierarchy()andcreate_agent_hierarchy()for SCD2 operations - Server-side hashing: Compute
op_hash(SHA-256 of canonical JSON) and return in response (hashes NOT stored in DB) - Audit logging: Log
request_id,actor(user email),tenant_id,org_id,op_hash,status
D) Batch Reparent Endpoint
File:api/routes/admin.py or api/routes/admin_hierarchy.py
Endpoint: POST /admin/hierarchy/batch_reparent
Requirements:
- Accepts list of reparent operations (same fields as
/reparent) - Header: Must accept
X-Org-Idheader (usex_org_id: Optional[str] = Header(None, alias="X-Org-Id")) - UUID normalization: Normalize all UUIDs to lowercase
- Applies ops in deterministic order:
- Validate all first (including cross-op cycles/conflicts) OR fail the whole batch (preferred)
- Then apply SCD2 close+insert per op
- Returns per-op results:
created/noop/errorstatus for each operation - Must be idempotent per op (same op can be replayed safely)
- Error response format:
- 400: Validation errors (fail whole batch, return all errors in response)
- 403: RBAC failure
- 500: Internal server error
- Response format:
HierarchyBatchReparentResponse(200 OK even if some ops fail - errors in per-op results)
- Scope: Compute
op_hashandbatch_hashONLY on backend (Python). UI treats hashes as opaque metadata returned by API. - Non-goals: No client-side hashing, no preview hashes, no cross-language parity tests.
-
op_hash computation (per reparent operation):
- Formula:
op_hash = sha256(UTF8(canonical_json(op_core))) - op_core fields (ONLY these, exclude notes/comments/UI metadata/timestamps):
op: “reparent” (string)tenant_id: stringorg_id: string (or null)child_agent_entity_id: lowercase UUID stringnew_parent_agent_entity_id: lowercase UUID string or nulleffective_start_date: “YYYY-MM-DD” stringchange_status: string (default “APPROVED”)active: boolean (default True, maps to “Yes”/“No” in canonical JSON)
- Canonical JSON rules (Python):
- UTF-8 encoding, no BOM
- Keys sorted lexicographically
- No whitespace:
json.dumps(..., sort_keys=True, separators=(',', ':')) - Arrays preserve order
- Null values:
null(JSON null, not string “null”)
- Implementation:
api/utils/hierarchy_hashing.pywithcompute_op_hash(op_core: Dict) -> str - Example:
json.dumps(op_core, sort_keys=True, separators=(',', ':')).encode('utf-8')thenhashlib.sha256(json_bytes).hexdigest()
- Formula:
-
batch_hash computation:
-
Compute
op_hashfor each operation first -
Enforce deterministic server order for application (recommended: sort by
child_agent_entity_idtheneffective_start_date) -
Formula:
batch_hash = sha256(UTF8(canonical_json({tenant_id, org_id, effective_start_date, operations: [{op_hash}, ...]}))) -
Batch structure:
-
Implementation:
api/utils/hierarchy_hashing.pywithcompute_batch_hash(tenant_id: str, org_id: str, effective_start_date: str, op_hashes: List[str]) -> str
-
Compute
-
API behavior:
/admin/hierarchy/reparentreturnsop_hashin response (hash NOT stored in DB, computed on-the-fly)/admin/hierarchy/batch_reparentreturns per-opop_hash+batch_hash(hashes NOT stored in DB, computed on-the-fly)- Hashes are computed server-side only and returned as opaque metadata
- UI treats hashes as opaque strings (no client-side computation, no preview hashes)
-
Audit logging:
- Log fields:
request_id(UUID),actor(user identity/email from JWT),tenant_id,org_id,op_hash(es),batch_hash(if applicable),status(success/failure) - Log format: Structured JSON log entry for audit trail
- Log location: Application logs (structured JSON format for parsing)
- Log fields:
-
Tests (backend only, Python):
- Same
op_core=> sameop_hash(deterministic) - Different parent or date => different
op_hash - Batch ordering deterministic => stable
batch_hash - Hashing does not affect DB writes or reads (hashes computed but not stored)
- Explicit non-goals: No client-side hash computation, no parity tests with frontend, no impact to ingestion wizard/commission engine/dashboards
- Same
- Check all operations for:
- Scope/existence (all IDs exist in tenant/org)
- Cross-op cycles (op A’s child becomes parent of op B’s child, creating cycle)
- Conflicts (multiple ops try to reparent same child to different parents)
- If any validation fails, return 400 with all errors (do not apply any ops)
- If all pass, apply each op sequentially using same logic as single reparent
- op_hash: SHA-256 of canonical JSON representation of operation (server-side Python)
- Canonical JSON: Sort keys alphabetically, no whitespace, deterministic encoding
- Fields included:
child_agent_entity_id,new_parent_agent_entity_id(null as “null” string),effective_start_date,change_status,active - Implementation:
api/utils/hierarchy_hashing.pywithcompute_op_hash(op: Dict) -> str
- batch_hash: SHA-256 of deterministic order (server sorts by
child_agent_entity_idtheneffective_start_date)- Sort operations:
sorted(operations, key=lambda op: (op['child_agent_entity_id'], op['effective_start_date'])) - Concatenate all
op_hashvalues in sorted order, then hash:SHA256(''.join(sorted_op_hashes))
- Sort operations:
- Audit logging: Log
request_id(UUID),actor(user email from JWT),tenant_id,org_id,op_hashes(list),batch_hash,status(success/partial/failed)- Log format: Structured JSON log entry for audit trail
E) Validate Move Endpoint (Optional but Recommended)
File:api/routes/admin.py or api/routes/admin_hierarchy.py
Endpoint: POST /admin/hierarchy/validate_move
Requirements:
- Input:
child_agent_entity_id,new_parent_agent_entity_id(nullable),effective_start_date - Output:
ok/errors(no writes, validation only) - Used later by drag/drop UI for pre-flight validation
- Checks: scope, existence, cycle detection (same as reparent validation)
- Header: Must accept
X-Org-Idheader - UUID normalization: Normalize all UUIDs to lowercase
- Error response format: 400 for validation errors, 403 for RBAC failure
F) Tree and History Endpoints
File:api/routes/admin.py or api/routes/admin_hierarchy.py
Endpoints:
-
GET /admin/hierarchy/tree?as_of=YYYY-MM-DD- Returns tree structure (parent-child relationships) as-of date
-
Filter:
effective_start_date <= as_of AND (effective_end_date IS NULL OR effective_end_date > as_of) -
Format: Nested JSON structure (recommended for dashboard consumption):
-
Header: Must accept
X-Org-Idheader - Error response: 400 for invalid date format, 403 for RBAC failure
-
GET /admin/hierarchy/history?agent_entity_id=...- Returns all SCD2 rows for given
agent_entity_id - Ordered by
effective_start_date DESC - Header: Must accept
X-Org-Idheader - Error response: 400 for invalid UUID, 403 for RBAC failure, 404 if agent not found
- Returns all SCD2 rows for given
api/bigquery/phase4_hierarchy_queries.py:
get_hierarchy_tree(tenant_id, org_id, as_of_date)get_hierarchy_history(tenant_id, org_id, agent_entity_id)
G) Seed Generator Script
File:tools/generate_paid_today_hierarchy_seed.py
Requirements:
-
Inputs:
/mnt/data/agent2_pepm.csv(ordata/reference/agent2_pepm.csv)- Resolver export (API call to
GET /admin/agents/resolveror CSV file) - Optional:
hierarchy_changeset.json(for overrides) - Optional:
hierarchy_overrides.csv(for CSV-format overrides)
-
Logic:
-
Load
agent2_pepm.csv, dedupe by normalized name:UPPER(TRIM(CONCAT(Agent_2_First_Name, ' ', Agent_2_Last_Name))) -
Join to resolver by EXACT normalized name only (fail-closed):
- 0 matches => exception row
UNKNOWN_AGENT(write to exceptions CSV) -
1 matches => exception row
AMBIGUOUS_AGENT_NAME(write to exceptions CSV)
- 0 matches => exception row
-
Hierarchy construction (Option 1 default):
- Robin Bundy is ROOT:
parent_agent_entity_id = null - Default parent for every resolved paid agent = Robin Bundy (direct child)
- Apply overrides from config (see H) Changeset/Override Support
- Robin Bundy is ROOT:
-
Output:
hierarchy_paid_today_seed.json: JSON array ofHierarchyBootstrapRowobjects (for API consumption)hierarchy_paid_today_seed.csv: CSV format (for manual review)- Schema-aligned fields only:
child_agent_entity_id,parent_agent_entity_id,effective_start_date,effective_end_date(optional),change_status,active hierarchy_paid_today_exceptions.csv:Agent_2_First_Name,Agent_2_Last_Name,Agent_2_Business_Name,Root_Owner,error_type,error_message- Optional:
hierarchy_changeset.json(export format for ops-based changeset)
-
Load
H) Changeset/Override Support
Files:docs/config/hierarchy_changeset.sample.json(new)
-
Changeset JSON Format (preferred, ops-based, schema-aligned):
-
File:
hierarchy_changeset.json -
Schema: Maps directly to
/reparentor/batch_reparentWITHOUT non-schema fields -
IMPORTANT: No
relationship_typeornotesfields (not in schema) - Generator can read this file and apply overrides by generating final seed CSV
- No full-tree snapshot imports. Ops-based only (replayable, auditable, SCD2-safe)
- Sample must include Tommy Dang -> Richard Ballard reparent op (no notes field)
-
File:
- Generator script reads changeset JSON and applies overrides
- Changeset JSON is structured for future drag/drop UI (replayable, auditable)
- Operations map directly to
/reparentor/batch_reparentendpoints - All fields must match actual schema (no
relationship_typeornotes)
I) UI/UX Changes (MVP Now, Future Drag/Drop Ready)
File:dashboard/src/pages/admin/hierarchy.tsx (new page) or extend existing hierarchy page
Components:
-
Hierarchy Bootstrap Upload:
- Separate from old Bulk Import changeset flow
- File upload for
hierarchy_paid_today_seed.json(JSON array) OR CSV (for backward compatibility) - Parse JSON/CSV and call
POST /admin/hierarchy/bootstrapwith JSON body - Display results table (created/reused/errors) + download exceptions
- Fix “Go to Bulk Import” button to route to Hierarchy Bootstrap
-
Reparent Modal (conventional):
- Child search (shows entity id)
- Parent search or “Set as root” checkbox
- Effective start date picker
- Change status (default: APPROVED)
- Active toggle (default: Yes)
- Call
POST /admin/hierarchy/reparent
-
Bulk Moves Panel:
- Upload
hierarchy_changeset.json-> parse and callPOST /admin/hierarchy/batch_reparent - Shows per-op results + export results JSON/CSV
- Upload
-
Tree View:
- Call
GET /admin/hierarchy/tree?as_of=YYYY-MM-DD(org-scoped) - Display hierarchical tree (collapsible nodes)
- Call
dashboard/src/lib/apiClient.ts (add methods):
bootstrapHierarchy(rows)- returns response with created/skipped countsreparentHierarchy(request)- returns response withop_hash(opaque metadata, no client-side computation)batchReparentHierarchy(operations)- returns response with per-opop_hash+batch_hash(opaque metadata)validateMoveHierarchy(request)- returns validation result (no writes)getHierarchyTree(asOfDate)- returns tree structuregetHierarchyHistory(agentEntityId)- returns SCD2 historygetAgentResolver(orgId)- returns resolver export
op_hash, batch_hash) as opaque metadata returned by API. No client-side hash computation, no preview hashes, no cross-language parity tests.
API Client Pattern (follow existing AgentMappingClient pattern):
J) Tests (pytest)
File:api/tests/test_admin_hierarchy.py (new)
Test Cases:
-
Resolver endpoint:
- Returns normalized names deterministically (verify
full_name_normalizedformat matches seed generator) - Org/tenant scoping enforced (cross-org agents not returned)
- RBAC (non-admin gets 403)
- X-Org-Id header handling (platform admin requires header, org-scoped user uses JWT org_id)
- Returns normalized names deterministically (verify
-
Bootstrap endpoint:
- Org isolation rejects cross-org refs (child and parent must be same org/tenant)
- Idempotency (duplicate row => reused/no-op)
- Invalid IDs => error (400)
- Cycle reject (rejects cycles)
- Single-parent overlap handling consistent with policy (at most one active parent per child/date)
- UUID normalization (accepts both cases, stores lowercase)
- Error response format (FastAPI HTTPException with detail field)
-
Reparent endpoint:
- Reparent closes old row + inserts new (verify SCD2 pattern)
- Set-root works (parent=NULL)
- NOOP works (same parent => no changes, returns 200 with status=“noop”)
- Cycle reject (400 if cycle created)
- UUID normalization
-
Batch Reparent endpoint:
- Batch validates all then applies (preferred: fail whole batch on validation errors)
- Returns per-op results (created/noop/error)
- Cross-op cycle detection (op A’s child becomes parent of op B’s child)
- Conflicts detection (multiple ops try to reparent same child)
- Idempotent per op (same op can be replayed safely)
- UUID normalization
-
Tree endpoint:
- Returns correct parent after reparent with effective dating
as_ofdate filtering works correctly- Response format (nested JSON structure)
-
Server-side hashing (backend only):
- op_hash is deterministic (same op_core produces same hash)
- Different parent or date => different op_hash
- batch_hash is deterministic (same operations in deterministic order produce same hash)
- Hash computation uses canonical JSON (UTF-8, sorted keys, no whitespace, arrays preserve order)
- Hashes NOT stored in DB (computed on-the-fly, returned as metadata)
- UI treats hashes as opaque metadata (no client-side computation)
-
Regression guard (prevent commission engine contamination):
- Test/grep rule: commission engine modules must NOT import hierarchy query module
- Verify no hierarchy joins in payout paths
- Verify dashboards use hierarchy only for visibility, not payouts
kill_switch_bigquery fixture from api/tests/test_onboarding.py to mock BigQuery calls.
File Changes Summary
New Files
api/routes/admin_hierarchy.py(recommended: separate router for hierarchy endpoints)api/schemas/admin_hierarchy.pyapi/bigquery/admin_hierarchy_queries.py(or extendphase4_hierarchy_queries.py)api/utils/hierarchy_hashing.py(deterministic hashing utilities)tools/generate_paid_today_hierarchy_seed.pyapi/tests/test_admin_hierarchy.pyapi/tests/test_hierarchy_regression_guards.py(regression guard tests)dashboard/src/pages/admin/hierarchy.tsx(if new page)
Modified Files
api/routes/admin.py(add resolver endpoint, or create separate router)api/bigquery/phase4_hierarchy_queries.py(add tree/history query functions)dashboard/src/lib/apiClient.ts(add hierarchy API methods)api/main.py(register new router:app.include_router(admin_hierarchy_router, prefix="/api/v1/admin", tags=["Admin Hierarchy"]))
Critical Constraints
- NO commission engine changes: Do not touch commission logic, assignments, or payout paths. Hierarchy is ONLY for reporting/visibility and dashboard scoping. Do not add hierarchy joins to payout paths.
-
Guarantee no downstream breakage:
- No schema changes to ingestion/commission tables
- Hierarchy endpoints only write
config_agent_hierarchy(no other tables) - Add regression guard test/grep rule: commission engine modules must NOT import hierarchy query module
- Dashboards may use hierarchy only for visibility scoping and rollups, never payouts
- Decimal-only rules: Do not introduce floats anywhere in shared libs. Hierarchy has no money but must not regress standards.
-
Strict tenant + org isolation: Primary org for Phase 2 prod validation:
org_id = "creative_benefit_strategies". All operations enforce tenant/org scoping. - Deterministic, auditable behavior: No destructive edits. Use SCD2 (close prior row + insert new row). Ops-based changesets are replayable and auditable. Implement deterministic hashing (op_hash, batch_hash) for audit trail.
-
agent_entity_id only: All FK references use
agent_entity_id(UUID), neveragent_id - Fail-closed: Name matching must reject ambiguous cases (0 or >1 matches)
-
Normalization consistency: Normalization MUST be identical in SQL and Python (uppercase, trim, remove punctuation:
. , ' -, collapse spaces). Include hyphen removal in SQL. -
Schema alignment:
config_agent_hierarchytable DOES NOT haverelationship_typeornotesfields. MVP must not include these in CSV/JSON payloads or inserts. - UUID normalization: All UUIDs normalized to lowercase (accept both cases in API, store lowercase in DB)
-
Date format: All dates use
YYYY-MM-DDformat, no timezone, no time component -
Error response format: FastAPI
HTTPExceptionwithdetailfield (string or list of strings) -
Router registration: Must register router in
api/main.pywith prefix/api/v1/admin -
X-Org-Id header: All endpoints must accept
X-Org-Idheader for org-scoping
Dependencies
- Existing:
config_agent_hierarchytable (already exists) - Existing:
phase4_hierarchy_queries.py(cycle detection, create/close functions) - Existing:
config_agent_profile_identityandconfig_agent_profile_contacttables - Existing: Org scoping utilities (
api/utils/org_scoping.py)
Regression Guards
File:api/tests/test_hierarchy_regression_guards.py (new)
Purpose: Guarantee no downstream breakage - prevent commission engine contamination
Test Cases:
-
Commission Engine Isolation:
- Grep test: Commission engine modules must NOT import hierarchy query module
- Verify:
api/bigquery/commission_assignments_queries.pydoes not importphase4_hierarchy_queriesoradmin_hierarchy_queries - Verify:
api/routes/agents.py(commission endpoints) does not import hierarchy modules - Verify: No hierarchy joins in payout/commission calculation queries
- Test pattern: Scan Python files for
from api.bigquery.phase4_hierarchy_queries importorfrom api.bigquery.admin_hierarchy_queries importin commission-related modules
-
Schema Isolation:
- Verify: Hierarchy endpoints only write to
config_agent_hierarchytable - Verify: No schema changes to ingestion/commission tables
- Verify: No writes to
agent_report_v5,stage3_snapshots, or other commission tables - Test pattern: Mock BigQuery and verify only
config_agent_hierarchyINSERT/UPDATE calls
- Verify: Hierarchy endpoints only write to
-
Dashboard Usage:
- Verify: Dashboards may use hierarchy only for visibility scoping and rollups
- Verify: Dashboard queries do not use hierarchy for payout calculations
- Document: Hierarchy is reporting-only, not used in commission engine
- Add grep-based test that scans commission engine modules for hierarchy imports
- Add integration test that verifies hierarchy writes only affect
config_agent_hierarchy - Add documentation comment in hierarchy modules:
# REPORTING ONLY - DO NOT USE IN COMMISSION ENGINE - Add CI check: Fail if commission modules import hierarchy modules
Deliverables
- Endpoints + tests passing
- Generator script + sample outputs
- Sample config files:
docs/config/hierarchy_changeset.sample.json(include Tommy Dang -> Richard Ballard reparent op, no notes field)
- Confirm commission/assignment/payout paths unchanged (no hierarchy joins)
Implementation Order
- Resolver endpoint (
GET /admin/agents/resolver) - Hierarchy model + reparent + tree read (
POST /admin/hierarchy/reparent,GET /admin/hierarchy/tree,GET /admin/hierarchy/history) - Batch reparent + validate move (
POST /admin/hierarchy/batch_reparent,POST /admin/hierarchy/validate_moveoptional) - Bootstrap import (
POST /admin/hierarchy/bootstrap) - Seed generator script (
tools/generate_paid_today_hierarchy_seed.py) - UI wiring for bootstrap + reparent + batch moves
- Tests + regression coverage
Validation Checklist
- Resolver returns normalized names deterministically matching seed generator normalization
- Bootstrap rejects cycles, invalid IDs, multiple parents, cross-org refs
- Reparent implements SCD2 correctly (close old + insert new)
- Batch reparent validates all then applies (fail whole batch on validation errors)
- Tree endpoint respects effective dating
- Seed generator fails-closed on ambiguous names
- All endpoints enforce org-scoping
- Changeset JSON format supported (read/export, schema-aligned, no relationship_type/notes)
- Tests pass with mocked BigQuery
- Commission/assignment/payout paths confirmed unchanged (no hierarchy joins)
- Server-side hashing implemented (op_hash, batch_hash) - backend only, hashes NOT stored in DB
- Audit logging implemented (request_id, actor, tenant_id, org_id, op_hashes, batch_hash, status)
- Hashing tests verify deterministic behavior (same op_core => same hash, different => different hash)
- Regression guard tests pass (commission engine does not import hierarchy modules)
- Router registered in
api/main.pywith prefix/api/v1/admin - All endpoints accept
X-Org-Idheader - UUID normalization (accepts both cases, stores lowercase)
- Error response format consistent (FastAPI HTTPException with detail field)
- Tree response format (nested JSON structure)
- Bootstrap accepts JSON body (not multipart CSV)
Red Flags & Blockers Review
✅ RESOLVED
- Schema alignment: Removed all references to
relationship_typeandnotesfields - Normalization consistency: SQL normalization includes hyphen removal to match Python
- UUID normalization: Added explicit requirement to normalize UUIDs to lowercase
- Date format: Specified
YYYY-MM-DDformat, no timezone, no time component - Error response format: Specified FastAPI
HTTPExceptionwithdetailfield - Router registration: Added requirement to register router in
api/main.py - X-Org-Id header: Added requirement for all endpoints to accept header
- Bootstrap request format: Changed from CSV upload to JSON body (consistent with other admin endpoints)
- Tree response format: Specified nested JSON structure for dashboard consumption
- Batch reparent error handling: Specified 200 OK with errors in per-op results (not 400 for partial failures)
⚠️ REMAINING CONSIDERATIONS
- Idempotency detection: Implementation must query existing row before insert (not specified in plan, but implied)
- CSV parsing for seed generator: Script must handle both JSON and CSV input (specified)
- Dashboard error handling: Dashboard must handle FastAPI error format (
{"detail": "..."}or{"detail": [...]}) - API client method signatures: Must follow existing
AgentMappingClientpattern withrequestWithHeadersforX-Org-Id
Notes
- All endpoints follow existing FastAPI patterns (Pydantic schemas, dependency injection, HTTPException)
- Dashboard API client methods should use
requestWithHeaderspattern forX-Org-Idheader - Error responses follow FastAPI standard format (no custom error schemas needed)
- Tree endpoint returns nested JSON structure (dashboard-friendly)
- Bootstrap endpoint accepts JSON body (not multipart CSV) for consistency