Reporting Hierarchy MVP Implementation Plan (FINAL)
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.
Non-Negotiable Decisions (FINAL)
1) Hashing Strategy (FINAL)
Option A (CHOSEN):op_hash and batch_hash are computed SERVER-SIDE ONLY, returned in API responses, and written to structured audit logs.
- Do NOT implement any client-side hashing, preview hashes, or cross-language parity tests.
- Do NOT “choose later.” This is final.
- Schema shows:
record_hash STRING NOT NULL(no default) inintegration/bigquery/sql/tables/config_agent_hierarchy.sql - REQUIRED: Populate
record_hashwith deterministic ROW-CORE hash to satisfy schema (insert would fail without it). - Document clearly:
record_hashis a compatibility field, not relied upon for audit integrity. - Row-core fields for
record_hash(canonical JSON + sha256):tenant_id,org_id,agent_entity_id,parent_agent_entity_id,effective_start_date,effective_end_date,change_status,is_active
- Formula:
record_hash = sha256(UTF8(canonical_json(row_core))) - Implementation:
api/utils/hierarchy_hashing.pywithcompute_record_hash(row_core: Dict) -> str
2) Batch Semantics (FINAL)
/admin/hierarchy/batch_reparent is ATOMIC for MVP:
- Validate ALL ops first; if ANY invalid -> return 400 and apply NONE.
- Response must include ALL validation errors with
operation_index. - Partial-success mode is NOT MVP. If desired later, it must be an explicit flag (not now).
3) Canonical Hashing Rules (SERVER ONLY)
op_hash:- Formula:
op_hash = sha256(UTF8(canonical_json(op_core))) - op_core fields ONLY (exclude notes/comments/UI metadata/timestamps):
op: “reparent” (string)tenant_id: stringorg_id: string (or JSON null)child_agent_entity_id: lowercase UUID stringnew_parent_agent_entity_id: lowercase UUID string or JSON null (NOT string “null”)effective_start_date: “YYYY-MM-DD” stringchange_status: string (default “APPROVED”)active: boolean true/false (NOT “Yes”/“No” string)
- Canonical JSON (Python):
json.dumps(op_core, sort_keys=True, separators=(',', ':'))(no whitespace)- UTF-8, no BOM, arrays preserve order
- JSON null is
null(never string “null”)
- Compute
op_hashfor each operation first - Enforce deterministic server order for batch application: sort by
(child_agent_entity_id, effective_start_date) - MUST use canonical JSON (NOT string concatenation):
batch_hash = sha256(UTF8(canonical_json({tenant_id, org_id, effective_start_date, operations: [{op_hash}, ...]})))- Operations array sorted by deterministic order
4) SCD2 Overlap Logic (FINAL)
All overlap checks MUST handle bounded rows, not only “end_date IS NULL”: Overlap predicate:- reparent “close existing” selection
- bootstrap single-parent enforcement
- as_of tree queries
5) Normalization Consistency (FINAL)
Name matching is FAIL-CLOSED. No fuzzy matching in MVP. Normalization must be identical in SQL and Python:- uppercase, trim
- remove punctuation:
. , ' -(including hyphen) - collapse ALL whitespace (not single REPLACE)
Security & RBAC (FINAL)
Design Principle: Strict for safety but NEVER blocks the platform owner (super admin) from doing full admin operations across this tenant and future tenants.1) Platform Owner / Super Admin (FINAL)
Definition: Platform Super Admin is detected using_is_platform_admin() function from api/routes/admin.py:
- Requires ALL of:
role == "admin"org_idis None/missing (no org_id in JWT)- User email/sub is in
ADMIN_PLATFORM_ALLOWLIST_EMAILS(if allowlist is configured)
- If
ADMIN_PLATFORM_ALLOWLIST_EMAILSis not set, falls back to role+org_id check (backward compatible, but less secure)
- Treat email
aaronbundy681@gmail.comas platform_super_admin (allowlist) - Apply to all
/api/v1/admin/hierarchy/*endpoints and/admin/agents/resolver - Super Admin must provide X-Org-Id on WRITE endpoints; missing => 400 (safety)
- Super Admin write access must NOT depend on org membership rows existing (prevent bootstrapping deadlocks)
- Non-Super Admin users: require Admin/CEO for org
- Super Admin can perform ALL hierarchy operations (bootstrap, reparent, batch_reparent, validate_move, tree/history, resolver) across any tenant/org they are authorized to administer
- Super Admin must NOT be dependent on org membership rows existing ahead of time (avoid bootstrapping deadlocks)
- Super Admin access is NOT blocked by org membership table checks
- Super Admin must supply X-Org-Id for ALL hierarchy endpoints (writes AND reads including resolver) - missing X-Org-Id => 400 (safety check to prevent accidental cross-org operations)
- Use
_is_platform_admin(current_user)fromapi/routes/admin.pyto detect platform admin - Create helper function
is_platform_super_admin(current_user: dict) -> boolinapi/routes/admin_hierarchy.pythat wraps_is_platform_admin() - For MVP: Hardcode check for
aaronbundy681@gmail.comOR useADMIN_PLATFORM_ALLOWLIST_EMAILSenv var (if set, must include this email) - All hierarchy endpoints check
is_platform_super_admin()before enforcing org-scoped restrictions
2) RBAC for Non-Owner Users (FINAL)
Write Endpoints (bootstrap, reparent, batch_reparent, validate_move):- For standard org users: Require Admin/CEO role for that org
- Use
require_admin_or_ceodependency fromapi/routes/admin_identity.py - Non-admin/CEO users receive 403 Forbidden
- Admin/CEO can read full org tree/history
- If exposed to agents later, agent reads must be server-side subtree-scoped (filter to agent’s subtree only)
- MVP: Only Admin/CEO can read (agent access deferred)
- Write endpoints: Use
require_admin_or_ceodependency - Read endpoints: Use
require_admin_or_ceodependency (MVP) - Future agent access: Add
require_agent_or_admin_or_ceodependency with subtree filtering
3) Org Scoping Rules (FINAL - Do NOT Lock Out Super Admin)
Do NOT rely solely on X-Org-Id: Validate org scope against auth claims. If caller is Super Admin:- X-Org-Id is REQUIRED when performing org-scoped operations (to prevent accidental writes to wrong org)
- Request must be allowed even if the user is not explicitly “a member” of that org in a membership table
- Super Admin can operate on any org_id they specify via X-Org-Id header (no membership validation)
- X-Org-Id must match the org in their auth context / membership; otherwise 403
- Use
get_org_id_from_request()fromapi/utils/org_scoping.pywithallow_platform_level=False - Org-scoped users cannot override their JWT org_id via X-Org-Id header (prevent spoofing)
- Create
get_org_id_for_hierarchy_ops()function inapi/routes/admin_hierarchy.py:
4) Safe Default Behavior (FINAL)
For Super Admin:- Missing X-Org-Id on write endpoints -> 400 Bad Request with clear message: “X-Org-Id header required for platform admin org-scoped operations”
- This prevents accidental cross-tenant/org writes while still preserving full power
- Super Admin can still operate on any org (just must specify it explicitly)
- Missing X-Org-Id -> Use existing repo pattern (typically 400 or 403 per
get_org_id_from_request()) - Org-scoped users use JWT org_id (X-Org-Id optional but must match if provided)
- All write endpoints use
get_org_id_for_hierarchy_ops()dependency - All read endpoints use
get_org_id_for_hierarchy_ops()dependency (for consistency) - Error messages clearly distinguish between Super Admin (400) and non-Super Admin (403) failures
5) Tests (MUST BE ADDED)
File:api/tests/test_admin_hierarchy.py
Test Cases:
-
Super Admin Access:
- Super Admin can call all hierarchy endpoints with X-Org-Id set (works even if no prior org membership record exists)
- Super Admin missing X-Org-Id on writes gets 400 (not 403) with clear message
- Super Admin can operate on any org_id (no membership validation)
- Super Admin with
aaronbundy681@gmail.comemail is detected correctly
-
Non-Super Admin Access:
- Non-admin org user cannot call write endpoints (403)
- Org admin/CEO can call write endpoints for their org (200)
- Org admin/CEO cannot call write endpoints for different org (403)
- Org-scoped user with mismatched X-Org-Id gets 403 (prevent spoofing)
-
Read Endpoints:
- Admin/CEO can read full org tree/history (200)
- Non-admin/CEO cannot read (403)
- Super Admin can read any org with X-Org-Id (200)
-
Org Scoping:
- Super Admin with X-Org-Id can write to any org (200, no membership check)
- Org-scoped user can only write to their JWT org_id (200)
- Org-scoped user with X-Org-Id mismatched to JWT gets 403
- Mock
_is_platform_admin()to return True/False for Super Admin tests - Use
kill_switch_bigqueryfixture to mock BigQuery calls - Test both allowlist-configured and fallback scenarios
- Test
aaronbundy681@gmail.comspecifically
Security Note
This RBAC/scoping design prevents data leakage and sabotage while guaranteeing the platform owner retains god-mode across all tenants/orgs.Implementation Tasks
A) Agent Resolver Export Endpoint
File:api/routes/admin_hierarchy.py (recommended: separate router)
Endpoint: GET /admin/agents/resolver
Requirements:
- RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Org-scoped: Use
get_org_id_for_hierarchy_ops()dependency (see Security & RBAC section)- Super Admin: Requires X-Org-Id header (400 if missing) - applies to ALL hierarchy endpoints including resolver reads
- Non-Super Admin: Uses JWT org_id (X-Org-Id optional but must match if provided)
- Returns:
agent_entity_id,first_name,last_name,full_name_normalized,agent_id(optional),email(optional) - SQL normalization uses REGEXP_REPLACE whitespace collapse + hyphen removal (see Normalization Consistency above)
- 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 - Strict org scoping: Require
i.org_id = @org_id(do not use NULL org clause - MVP assumes all tenants have org_id) - Response format: List of
AgentResolverResponseobjects (200 OK) - Error handling: 403 if not admin/CEO, 400 if Super Admin missing X-Org-Id, 403 if org-scoped user with mismatched X-Org-Id
api/schemas/admin_hierarchy.py:
B) Hierarchy Bootstrap Endpoint
File:api/routes/admin_hierarchy.py
Endpoint: POST /admin/hierarchy/bootstrap
Requirements:
- Request format: JSON body with
rowsarray (NOT multipart CSV upload) - 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 - RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Org-scoped: Use
get_org_id_for_hierarchy_ops()dependency (see Security & RBAC section)- Super Admin: Requires X-Org-Id header (400 if missing)
- Non-Super Admin: Uses JWT org_id (X-Org-Id optional but must match if provided)
- UUID normalization: Normalize all UUIDs to lowercase (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 (use overlap predicate)
- 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)- 400: Validation errors (invalid IDs, cycles, cross-org refs), Super Admin missing X-Org-Id
- 403: RBAC failure (non-admin/CEO), org-scoped user with mismatched X-Org-Id
- 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) - Populate
record_hashwith deterministic row-core hash (schema compatibility shim)
api/schemas/admin_hierarchy.py:
C) Reparent Endpoint
File: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 - RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Org-scoped: Use
get_org_id_for_hierarchy_ops()dependency (see Security & RBAC section)- Super Admin: Requires X-Org-Id header (400 if missing)
- Non-Super Admin: Uses JWT org_id (X-Org-Id optional but must match if provided)
- 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 (use overlap predicate:
effective_start_date <= new_start_date AND (effective_end_date IS NULL OR effective_end_date >= new_start_date)), 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”), Super Admin missing X-Org-Id
- 403: RBAC failure (non-admin/CEO), org-scoped user with mismatched X-Org-Id
- 500: Internal server error
- Response format:
HierarchyReparentResponse(200 OK for created, 200 OK for noop) - Populate
record_hashwith deterministic row-core hash (schema compatibility shim) - Server-side hashing: Compute
op_hash(SHA-256 of canonical JSON) and return in response (hashes NOT stored in DB exceptrecord_hashshim) - Audit logging: Log
request_id,actor(user email),tenant_id,org_id,op_hash,status
D) Batch Reparent Endpoint
File:api/routes/admin_hierarchy.py
Endpoint: POST /admin/hierarchy/batch_reparent
Requirements:
- Accepts list of reparent operations (same fields as
/reparent) - RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Org-scoped: Use
get_org_id_for_hierarchy_ops()dependency (see Security & RBAC section)- Super Admin: Requires X-Org-Id header (400 if missing)
- Non-Super Admin: Uses JWT org_id (X-Org-Id optional but must match if provided)
- UUID normalization: Normalize all UUIDs to lowercase
- ATOMIC: Validate ALL ops first; if ANY invalid -> return 400 and apply NONE
- Response must include ALL validation errors with
operation_index - Deterministic order: Sort ops by
(child_agent_entity_id, effective_start_date)before validation and application - If all pass, apply each op sequentially using same logic as single reparent
- Must be idempotent per op (same op can be replayed safely)
- Error response format:
- 400: Validation errors (fail whole batch, return all errors in structured format), Super Admin missing X-Org-Id
- 403: RBAC failure (non-admin/CEO), org-scoped user with mismatched X-Org-Id
- 500: Internal server error
- 400 Error Response Schema (FastAPI-compatible structured detail):
- Response format:
HierarchyBatchReparentResponse(200 OK ONLY returned when full batch applies successfully - all ops validated and applied) - Populate
record_hashfor each inserted row (schema compatibility shim) - Server-side hashing: Compute per-op
op_hash+batch_hash(canonical JSON, NOT concatenation) - Audit logging: Log
request_id,actor,tenant_id,org_id,op_hashes(list),batch_hash,status
- 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 structured detail array containing all errors (do not apply any ops)
- If all pass, apply each op sequentially using same logic as single reparent
- 200 response is ONLY returned when the full batch applies successfully (all ops validated and applied)
-
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 JSON null, NOT string “null”)child_agent_entity_id: lowercase UUID stringnew_parent_agent_entity_id: lowercase UUID string or JSON null (NOT string “null”)effective_start_date: “YYYY-MM-DD” stringchange_status: string (default “APPROVED”)active: boolean true/false (NOT “Yes”/“No” string)
- 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
- Formula:
-
batch_hash computation:
- Compute
op_hashfor each operation first - Enforce deterministic server order for application: sort by
(child_agent_entity_id, effective_start_date) - MUST use canonical JSON (NOT string concatenation):
batch_hash = sha256(UTF8(canonical_json({tenant_id, org_id, effective_start_date, operations: [{op_hash}, ...]})))- Operations array sorted by deterministic order
- Implementation:
api/utils/hierarchy_hashing.pywithcompute_batch_hash(tenant_id: str, org_id: str, effective_start_date: str, op_hashes: List[str]) -> str
- Compute
-
record_hash computation (schema compatibility shim):
- Row-core fields:
tenant_id,org_id,agent_entity_id,parent_agent_entity_id,effective_start_date,effective_end_date,change_status,is_active - Formula:
record_hash = sha256(UTF8(canonical_json(row_core))) - Implementation:
api/utils/hierarchy_hashing.pywithcompute_record_hash(row_core: Dict) -> str - Document:
record_hashis a compatibility field, not relied upon for audit integrity
- Row-core fields:
-
API behavior:
/admin/hierarchy/reparentreturnsop_hashin response (hash NOT stored in DB exceptrecord_hashshim)/admin/hierarchy/batch_reparentreturns per-opop_hash+batch_hash(hashes NOT stored in DB exceptrecord_hashshim)- 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),errors(if validation fails) - Log format: Structured JSON log entry for audit trail
- Log location: Application logs (structured JSON format for parsing)
- Log fields:
E) Validate Move Endpoint (Optional but Recommended)
File: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)
- RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Org-scoped: Use
get_org_id_for_hierarchy_ops()dependency (see Security & RBAC section)- Super Admin: Requires X-Org-Id header (400 if missing)
- Non-Super Admin: Uses JWT org_id (X-Org-Id optional but must match if provided)
- UUID normalization: Normalize all UUIDs to lowercase
- Error response format: 400 for validation errors or Super Admin missing X-Org-Id, 403 for RBAC failure or org-scoped user with mismatched X-Org-Id
F) Tree and History Endpoints
File: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: Use overlap predicate:
effective_start_date <= as_of AND (effective_end_date IS NULL OR effective_end_date > as_of) - Format: Nested JSON structure (dashboard-friendly)
- RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Org-scoped: Use
get_org_id_for_hierarchy_ops()dependency (see Security & RBAC section)- Super Admin: Requires X-Org-Id header (400 if missing)
- Non-Super Admin: Uses JWT org_id (X-Org-Id optional but must match if provided)
- Error response: 400 for invalid date format or Super Admin missing X-Org-Id, 403 for RBAC failure or org-scoped user with mismatched X-Org-Id
-
GET /admin/hierarchy/history?agent_entity_id=...- Returns all SCD2 rows for given
agent_entity_id - Ordered by
effective_start_date DESC - RBAC: Admin/CEO only (use
require_admin_or_ceofromapi/routes/admin_identity.py) - Org-scoped: Use
get_org_id_for_hierarchy_ops()dependency (see Security & RBAC section)- Super Admin: Requires X-Org-Id header (400 if missing)
- Non-Super Admin: Uses JWT org_id (X-Org-Id optional but must match if provided)
- Error response: 400 for invalid UUID or Super Admin missing X-Org-Id, 403 for RBAC failure or org-scoped user with mismatched X-Org-Id, 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)(uses overlap predicate)get_hierarchy_history(tenant_id, org_id, agent_entity_id)
G) Seed Generator Script
File:tools/generate_paid_today_hierarchy_seed.py
Requirements:
- Inputs:
data/reference/agent2_pepm.csv- Resolver export (API call to
GET /admin/agents/resolveror CSV file) - Optional:
hierarchy_changeset.json(for overrides)
- Logic:
- Load
agent2_pepm.csv, dedupe by normalized 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, FINAL):
- Robin Bundy is ROOT:
parent_agent_entity_id = null - Default parent for every resolved paid agent = Robin Bundy (direct child)
- Apply overrides from ops-based changeset JSON (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 JSON
- 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:
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: File upload for
hierarchy_paid_today_seed.json(JSON array), parse and callPOST /admin/hierarchy/bootstrapwith JSON body - Reparent Modal: Child search, parent search or “Set as root”, 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 - Tree View: Call
GET /admin/hierarchy/tree?as_of=YYYY-MM-DD(org-scoped), display hierarchical tree (collapsible nodes)
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
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, RBAC (Super Admin vs non-Super Admin), X-Org-Id header handling (Super Admin requires header, non-Super Admin uses JWT), normalization handles hyphens and multiple spaces - Bootstrap endpoint: Org isolation rejects cross-org refs, idempotency, invalid IDs => error (400), cycle reject, single-parent overlap handling (uses overlap predicate), UUID normalization, error response format, Super Admin access (works without membership), Super Admin missing X-Org-Id => 400
- Reparent endpoint: Reparent closes old row + inserts new (verify SCD2 pattern with overlap predicate), set-root works, NOOP works, cycle reject, UUID normalization, Super Admin access
- Batch Reparent endpoint: Atomic validation (fail whole batch on validation errors, return 400), returns per-op results (created/noop), cross-op cycle detection, conflicts detection, idempotent per op, UUID normalization, batch_hash uses canonical JSON (NOT concatenation), Super Admin access
- Tree endpoint: Returns correct parent after reparent with effective dating,
as_ofdate filtering works correctly (uses overlap predicate), response format (nested JSON structure), Super Admin access - 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 (canonical JSON, NOT concatenation), hash computation uses canonical JSON (UTF-8, sorted keys, no whitespace, arrays preserve order, JSON null is null), active is boolean (NOT “Yes”/“No”), hashes NOT stored in DB (except record_hash shim), UI treats hashes as opaque metadata
- Security & RBAC (prevent unauthorized access):
- Super Admin can call all hierarchy endpoints with X-Org-Id set (works even if no prior org membership record exists)
- Super Admin missing X-Org-Id on writes gets 400 (not 403) with clear message
- Non-admin org user cannot call write endpoints (403)
- Org admin/CEO can call write endpoints for their org (200)
- Org admin/CEO cannot call write endpoints for different org (403)
- Org-scoped user with mismatched X-Org-Id gets 403 (prevent spoofing)
- Admin/CEO can read full org tree/history (200)
- Non-admin/CEO cannot read (403)
- Super Admin can read any org with X-Org-Id (200)
- Super Admin with
aaronbundy681@gmail.comemail is detected correctly
- 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(separate router for hierarchy endpoints)- Includes
is_platform_super_admin()helper function (wraps_is_platform_admin()) - Includes
get_org_id_for_hierarchy_ops()dependency (Super Admin-aware org scoping)
- Includes
api/schemas/admin_hierarchy.pyapi/utils/hierarchy_hashing.py(deterministic hashing utilities:compute_op_hash,compute_batch_hash,compute_record_hash)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)docs/config/hierarchy_changeset.sample.json(include Tommy Dang -> Richard Ballard reparent op, no notes field)
Modified Files
api/bigquery/phase4_hierarchy_queries.py(add tree/history query functions with overlap predicate)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, add regression guard test/grep rule, dashboards may use hierarchy only for visibility scoping and rollups, never payouts - Decimal-only rules: Do not introduce floats anywhere in shared libs
- 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 using REGEXP_REPLACE 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 - SCD2 overlap predicate: All overlap checks MUST handle bounded rows using:
effective_start_date <= new_start_date AND (effective_end_date IS NULL OR effective_end_date >= new_start_date) - Batch semantics: Atomic validation (fail whole batch on validation errors, return 400), no partial-success mode in MVP
- Canonical hashing: op_hash uses boolean true/false (NOT “Yes”/“No”), JSON null is null (NOT string “null”), batch_hash uses canonical JSON (NOT concatenation)
- record_hash compatibility: Populate
record_hashwith deterministic row-core hash to satisfy schema (NOT NULL constraint) - Super Admin support: MVP must include Super Admin bypass to prevent bootstrapping deadlocks. Email
aaronbundy681@gmail.comtreated as platform_super_admin (allowlist).
Implementation Order
- Resolver endpoint (
GET /admin/agents/resolver) with REGEXP_REPLACE normalization - Hierarchy hashing utilities (
api/utils/hierarchy_hashing.py:compute_op_hash,compute_batch_hash,compute_record_hash) - Hierarchy model + reparent + tree read (
POST /admin/hierarchy/reparent,GET /admin/hierarchy/tree,GET /admin/hierarchy/history) with overlap predicate - Batch reparent + validate move (
POST /admin/hierarchy/batch_reparentatomic,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 (REGEXP_REPLACE whitespace collapse)
- Bootstrap rejects cycles, invalid IDs, multiple parents (overlap predicate), cross-org refs
- Reparent implements SCD2 correctly (close old + insert new, overlap predicate)
- Batch reparent validates all then applies atomically (fail whole batch on validation errors, return 400)
- Tree endpoint respects effective dating (overlap predicate)
- 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, canonical JSON (NOT concatenation), boolean active (NOT “Yes”/“No”), JSON null (NOT string “null”)
- record_hash compatibility shim implemented (populate with row-core hash)
- Audit logging implemented (request_id, actor, tenant_id, org_id, op_hashes, batch_hash, status, errors)
- Hashing tests verify deterministic behavior (same op_core => same hash, different => different hash, batch_hash uses canonical JSON)
- 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)
- Normalization test validates hyphens and multiple spaces handling
- Super Admin support implemented (
aaronbundy681@gmail.comallowlist, works without membership records) - Super Admin missing X-Org-Id on writes => 400 (not 403)
- Non-admin users cannot write (403)