Readiness Batch Discovery Org Filter Fix - Deployment Proof
Summary
Fixed readiness endpoint returningtotal_businesses=0 when x-org-id header was present.
Root Cause: get_batch_discovered_business_ids was called with org_id=effective_org_id, but batch businesses are platform-scoped (org_id IS NULL in ingestion_batch_businesses).
Fix: Pass org_id=None to get_batch_discovered_business_ids to retrieve all platform-scoped batch businesses, then filter by org at the config level via get_business_readiness(org_id=effective_org_id).
Commit SHA
- Commit:
c8fff16 - Message:
fix(readiness): pass org_id=None to batch discovery for platform-scoped businesses
Security Validation
Access Control Model (Validated)
- Tenant Isolation:
get_ingestion_batch(batch_id, tenant_id)enforces tenant_id match - Batch IDs: UUIDs (unguessable)
- RBAC: Requires admin/ceo role (enforced by
get_ingestion_principal) - Org Filtering: Happens at config level (
get_business_readinessfilters byeffective_org_id) - Batch org_id: Always NULL (Phase 8D constraint)
org_id=None to batch discovery is safe because:
- Batch access is already protected by tenant_id + RBAC
- Org filtering happens at the correct layer (config readiness)
- No cross-org data leakage risk
BigQuery Validation
Batch Businesses Are Platform-Scoped (Confirmed)
org_id = null (platform-scoped), validating the fix.
Tests Added
Regression Tests (api/tests/test_readiness_batch_discovery_org_filter.py)
-
test_readiness_calls_batch_discovery_with_org_id_none: Verifies readiness handler calls
get_batch_discovered_business_idswithorg_id=Noneeven whenx-org-idheader is present. -
test_get_batch_discovered_business_ids_org_filter_clause: Tests SQL clause builder:
- When
org_idis not None => uses"ib.org_id = @org_id" - When
org_idis None => uses"ib.org_id IS NULL"
- When
- test_readiness_returns_businesses_when_batch_is_platform_scoped: Tests exact bug scenario (batch businesses platform-scoped, configs org-scoped).
- test_readiness_returns_zero_if_batch_discovery_fails_with_wrong_org: Documents bug behavior to prevent regression.
Code Changes
api/routes/intake.py
Change: Added security comment documenting access control model.
Location: Lines 1580-1588
org_id=None) was already applied in a previous commit. This commit adds documentation and regression tests.
Deployment Status
- GitHub Actions: Triggered by push to
main - Cloud Run Service:
payroll-backend-prod - Deployment Status: Pending verification
Post-Deployment Verification Steps
1. Verify Deployment SHA
c8fff16
2. Network-Level Verification
Using Chrome DevTools → Network tab:- Navigate to ingestion wizard preflight step
- Set period to
2025-12-01(or current period) - Observe readiness GET request:
- URL:
/api/v1/intake/ingestion-wizard/readiness?batch_id=...&period_label=... - Headers: Must include
x-org-id: cbs-main(or appropriate org) - Response: Must have
total_businesses > 0(not 0)
- URL:
total_businesses should match the number of businesses in the batch (e.g., 113 for the test batch).
3. UI Verification
- Open business drawer for a configured business (e.g., “AIC Inc”)
- Verify saved PEPM/assignments are visible
- Save a new configuration
- Close and reopen drawer
- Verify saved configuration persists
4. BigQuery Spot Check
Verify config rows have org_id set correctly:owning_org_id='cbs-main' (or appropriate org) and scope='ORG_SCOPED'.
Success Criteria
✅ All criteria must pass:- Readiness endpoint returns
total_businesses > 0whenx-org-idheader is present - Saved business configurations persist and are visible in UI
- No console errors in browser
- No 500 errors from readiness endpoint
- BigQuery confirms batch businesses are platform-scoped (
org_id IS NULL) - BigQuery confirms config rows are org-scoped (
owning_org_idset)
Rollback Plan
If deployment causes issues:- Revert commit
c8fff16 - Push to
mainto trigger rollback deployment - Investigate root cause before re-applying fix
Related Issues
- Original Issue: UI shows successful saves but readiness returns
0/113businesses - Root Cause:
get_batch_discovered_business_idscalled withorg_id=effective_org_idinstead oforg_id=None - Fix: Pass
org_id=Noneto batch discovery, filter by org at config level
Notes
- This fix does not change the security model - it corrects a bug that prevented correct data retrieval
- Org filtering still happens at the correct layer (config readiness)
- Batch access is still protected by tenant_id + RBAC
- No schema changes required