Phase 8M: Wizard “Not Ready” Configure-in-Place Implementation Plan
Goal: Implement minimal frontend-only changes to enable:- Configure Now opens BusinessDetailDrawer in wizardMode
- Drafts are frontend-only (no BQ writes before commit)
- Commit All saves drafted businesses exactly once for the CURRENT scope
- Drafts do not leak across periods OR orgs
1. REPO FINDINGS
1.1 Wizard Drawer Component
File:dashboard/src/components/admin/onboarding/BusinessDetailDrawer.tsx
- Component:
BusinessDetailDrawer(DetailsTab function) - Wizard Mode:
wizardMode={true}prop enables Save Draft / Commit Now flow - Usage in Wizard:
dashboard/src/components/intake/PreflightStep.tsx(line 2003-2015) - State Store:
useDraftStorefromdashboard/src/store/useDraftStore.ts - Draft Key:
business_idonly (NOT scoped byperiod_label— RED FLAG)
1.2 Legacy Admin Onboarding Drawer
File: Same component (BusinessDetailDrawer.tsx)
- Usage:
dashboard/src/pages/admin/onboarding/businesses.tsx(line 1055-1065) - Difference:
wizardMode={false}(default) — no Save Draft, only immediate save - Why Not Used: Wizard needs draft staging → commit all workflow, legacy is immediate save only
1.3 Draft Store Implementation
File:dashboard/src/store/useDraftStore.ts
- Key Structure:
Map<business_id, DraftEntry>— NOT scoped by tenant/org/period (RED FLAG) - Current Actions:
saveDraft(businessId, payload),discardDraft(businessId),discardAll() - Persistence: Uses Zustand
persistmiddleware (localStorage) - Issues:
- Drafts can conflict across periods if same business_id used in different periods
- Drafts can leak across tenants/orgs (no tenant/org scoping)
- Migration needed: clear old drafts on rehydrate (safest approach)
1.4 Endpoints Inventory
Discovery Read/Write Paths
-
Preflight Discovery Check (
POST /api/v1/ingestion-wizard/preflight)- Reads:
get_discovered_businesses()fromingestion_batch_businessestable - Writes:
discover_batch_businesses()if empty (writes toingestion_batch_businesses)
- Reads:
-
Discover Businesses Endpoint (
POST /api/v1/ingestion/{batch_id}/discover-businesses)- Writes:
discover_batch_businesses()→ingestion_batch_businessestable
- Writes:
-
List Businesses with Discovery (
GET /api/v1/admin/onboarding/businesses?include_discovered=true)- Reads:
list_businesses_for_onboarding()withinclude_discovered=True - Source:
transaction_events_raw+dim_business_mapping(NOTingestion_batch_businesses) - RED FLAG: Different source than readiness endpoint
- Reads:
Onboarding Endpoints (Phase 8K/8L)
-
List Businesses:
GET /api/v1/admin/onboarding/businesses- Function:
list_businesses_for_onboarding()inapi/bigquery/business_onboarding_queries.py - Tables:
config_business_onboarding+ optionallytransaction_events_raw(ifinclude_discovered=True)
- Function:
-
Get Business Config:
GET /api/v1/admin/onboarding/businesses/{business_id}- Function:
get_business_config_endpoint()inapi/routes/business_onboarding.py - Tables:
config_business_onboarding(authoritative)
- Function:
-
Save Business Config:
PUT /api/v1/admin/onboarding/businesses/{business_id}/save- Function:
save_business_config_endpoint()inapi/routes/business_onboarding.py - Tables:
config_business_onboarding(writes)
- Function:
Readiness Endpoints
-
Batch Readiness (
GET /api/v1/ingestion/{batch_id}/ingestion-wizard/readiness)- Function:
get_batch_readiness_endpoint()inapi/routes/intake.py(line 1539) - Source:
get_batch_discovered_business_ids()→ingestion_batch_businessestable - Then:
get_business_readiness()filters to batch businesses - Tables:
ingestion_batch_businesses→config_business_onboarding(readiness check)
- Function:
-
Business Readiness (
GET /api/v1/admin/onboarding/businesses/readiness)- Function:
get_business_readiness_endpoint()inapi/routes/business_onboarding.py - Tables:
config_business_onboardingonly
- Function:
1.5 Materialization Guarantee Analysis
Question: Are “not ready / discovered” businesses always present inconfig_business_onboarding for the wizard’s period_label?
Answer: NO — Line 1701 in api/routes/intake.py shows:
ingestion_batch_businesses are NOT automatically in config_business_onboarding. They must be:
- Adopted via
POST /api/v1/admin/onboarding/businesses/adopt(bulk adopt) - OR configured via
saveBusinessConfig(which callsadoptBusinessinternally)
saveBusinessConfig already calls auto_adopt_discovered_business internally (line 4359 in api/bigquery/business_onboarding_queries.py). The wizard can configure discovered businesses directly.
STOP CONDITION: If saveBusinessConfig cannot adopt/materialize a discovered-only business, STOP and report exact failing case + logs before adding endpoints.
2. RED FLAGS / ALIGNMENT GAPS
2.1 Discovery Source Drift
Issue: Readiness endpoint reads fromingestion_batch_businesses, but include_discovered reads from transaction_events_raw + dim_business_mapping.
Risk: Businesses shown in wizard readiness may not match businesses shown in admin onboarding list with include_discovered=True.
Mitigation: Wizard should use get_batch_discovered_business_ids() to get business list (already used by readiness), NOT list_businesses_for_onboarding(include_discovered=True).
2.2 Draft Store Scoping (P0 BLOCKER)
Issue: Draft store keys bybusiness_id only, not scoped by tenant/org/period.
Risks:
- If user drafts business A for period 2025-01-01, then switches to period 2025-02-01, draft from previous period may leak
- Drafts can leak across tenants (multi-tenant support)
- Drafts can leak across orgs (platform admin vs org-scoped)
${tenant_id}:${effective_org_id || 'platform'}:${period_label}:${business_id}. Clear old drafts on rehydrate (migration strategy).
2.3 Org/Tenant Scoping Inconsistencies
Finding:- Readiness endpoint: Uses
effective_org_id(resolved viaresolve_effective_org_scope) - Discovery write: Always writes with
org_id = NULLfor Phase 8D batches - Discovery read: Reads with
org_id = NULLfor Phase 8D batches saveBusinessConfig: UseseffectiveOrgIdfromresolveOrgDeterministically()
2.4 Race/Eventual Consistency Risks
Issue: BigQuery write → read lag (documented inhandleDrawerSuccess retry logic, line 499-536 in PreflightStep.tsx).
Risk: verifySave() may fail if config not immediately queryable after save.
Mitigation: Already handled with retry logic in verifySave() (300ms, 700ms, 1500ms, 2500ms, 4000ms delays).
2.5 UI State Risks
Issues:- Draft store persists across page refreshes (localStorage) — may show stale drafts
- Period label changes — drafts from old period may be invalid
- Business switching — drawer state may not reset properly
- Clear drafts when
period_labelchanges - Reset drawer state when business changes
- Validate draft
period_labelmatches current wizardperiod_labelbefore showing
2.6 Security/RBAC Risks
Finding:- Platform admin without org header → requires
X-Org-Idheader (400 error) - Org-scoped businesses → requires matching
org_idin header - Phase 8D batches → always
org_id = NULL(platform-scope)
useOrgScope hook (already used in PreflightStep.tsx line 28) to ensure correct org context.
3. PROPOSED MINIMAL DIFFS
3.1 Frontend Changes
3.1.1 Update Draft Store Key Structure (P0 BLOCKER)
File:dashboard/src/store/useDraftStore.ts
- Change key from
business_idto${tenant_id}:${effective_org_id || 'platform'}:${period_label}:${business_id} - Update method signatures to require:
tenantId,orgId(nullable),periodLabel,businessId - Update
saveDraft()→saveDraft(tenantId, orgId, periodLabel, businessId, payload) - Update
hasDraft(),getDraft(),discardDraft()→ require same scope params - Update
getAllDraftBusinessIds()→getAllDraftBusinessIds(tenantId, orgId, periodLabel)(filtered by current scope) - Migration: In
onRehydrateStorage, clear all old drafts (safest approach - no key migration) - Update all call sites in
BusinessDetailDrawer.tsxandPreflightStep.tsx
3.1.2 Add Draft Badge to Readiness List
File:dashboard/src/components/intake/PreflightStep.tsx
- In “Businesses Not Ready” section (line 1806), add badge check:
- 🟢 “Ready” if
readiness.is_ready === true(server readiness true) - 🟡 “Draft Saved” if draft exists AND
readiness.is_ready === false(draft exists, server not ready) - 🔴 “Not Ready” if no draft AND
readiness.is_ready === false(no draft, server not ready)
- 🟢 “Ready” if
- Important: Drafts are NOT ready - they are local-only until committed
3.1.3 Add Bulk “Select Remaining → Owner Rollup Draft” Action (P0 BLOCKER)
File:dashboard/src/components/intake/PreflightStep.tsx
- MUST draft-only in wizard - Remove/avoid
BulkAssignModalfor wizard bulk action - Implement “Select Remaining → Owner Rollup (Draft)” directly in
PreflightStep.tsx - Add button: “Select Remaining Non-Ready → Owner Rollup (Draft)”
- On click:
- Filter
pending_review_itemsto exclude businesses that already have drafts - For each selected business, create draft directly using
draftStore.saveDraft()withOWNER_ROLLUPmode - NO API calls - drafts are frontend-only until Commit All
- Filter
- Decimal Safety: All pepm/amount values remain strings in payloads (no JS number math)
3.1.4 Add “Commit All” Button (P0 BLOCKER)
File:dashboard/src/components/intake/PreflightStep.tsx
- Reuse existing
BulkActionBarcomponent (already exists indashboard/src/components/ui/BulkActionBar.tsx) - Wire to
draftStore.getAllDraftBusinessIds(tenantId, orgId, periodLabel)to get drafts for CURRENT scope only - Commit All filtering: Only commit drafts for current (tenant, org, period)
- Sequential commit: Concurrency=1 (one save at a time) with progress UI
- Progress UI: Show
{current}/{total}during commit - Partial failure handling:
- On partial failure: Discard only successful drafts; keep failed drafts
- Show failure list with business IDs and error messages
- Modal stays open until all succeed or user discards
- On success: Clear all committed drafts, refresh readiness exactly once at end (or debounced)
3.1.5 Update BusinessDetailDrawer for Discovered Businesses
File:dashboard/src/components/admin/onboarding/BusinessDetailDrawer.tsx
- Ensure
getBusinessConfighandles businesses not yet in onboarding (returns 404 or empty config) - Ensure
saveBusinessConfigcallsauto_adopt_discovered_businessinternally (already done in backend) - Pass scope params into draftStore calls: Update all
draftStore.saveDraft(),draftStore.hasDraft(), etc. to includetenantId,orgId,periodLabel - Access
tenantIdviaauth.getUser()?.tenantId - Access
orgIdviagetEffectiveOrgId()(can be null, use'platform'in key)
3.1.6 Wiring: Configure Now Opens Drawer
File:dashboard/src/components/intake/PreflightStep.tsx
- In readiness list: Configure button must open
BusinessDetailDrawer(wizardMode=true) - Create typed helper
convertReadinessItemToDrawerBusiness()with null-safe fields - Helper converts readiness item to
BusinessOnboardingListItemshape expected by drawer - Handle discovered-only businesses (may not have all fields populated)
3.2 Backend Changes
NONE REQUIRED — All endpoints already support:include_discoveredin list endpointsaveBusinessConfigwithauto_adopt_discovered_businessinternal call (line 4359 inbusiness_onboarding_queries.py)getBusinessConfigfor any business_id (will return 404 if not in onboarding, but save will adopt)
saveBusinessConfig cannot adopt/materialize a discovered-only business, STOP and report exact failing case + logs before adding endpoints.
4. STEP-BY-STEP EXECUTION CHECKLIST
Phase 1: Draft Store Scoping (P0 BLOCKER)
- Update
useDraftStore.tsto key by${tenant_id}:${effective_org_id || 'platform'}:${period_label}:${business_id} - Update
saveDraft()signature to require:tenantId,orgId(nullable),periodLabel,businessId,payload - Update
hasDraft(),getDraft(),discardDraft()to require same scope params - Update
getAllDraftBusinessIds()→getAllDraftBusinessIds(tenantId, orgId, periodLabel)(filtered by current scope) - Add migration: In
onRehydrateStorage, clear all old drafts (safest - no key migration) - Update all call sites in
BusinessDetailDrawer.tsx(passtenantId,orgId,periodLabel) - Update all call sites in
PreflightStep.tsx(passtenantId,orgId,periodLabel)
Phase 2: Draft Badge Display
- In
PreflightStep.tsxreadiness list, add draft check - Display 🟢 “Ready” badge if
readiness.is_ready === true(server readiness true) - Display 🟡 “Draft Saved” badge if draft exists AND
readiness.is_ready === false - Display 🔴 “Not Ready” badge if no draft AND
readiness.is_ready === false - Important: Drafts are NOT ready - they are local-only until committed
Phase 3: Bulk Owner Rollup Draft (P0 BLOCKER)
- Remove/avoid BulkAssignModal for wizard bulk action
- Implement “Select Remaining → Owner Rollup (Draft)” directly in
PreflightStep.tsx - Add button: “Select Remaining Non-Ready → Owner Rollup (Draft)”
- Filter
pending_review_itemsto exclude businesses that already have drafts - For each selected business, create draft directly using
draftStore.saveDraft()withOWNER_ROLLUPmode - NO API calls - drafts are frontend-only until Commit All
- Decimal Safety: Ensure all pepm/amount values remain strings (no JS number math)
Phase 4: Commit All Integration (P0 BLOCKER)
- Add
BulkActionBartoPreflightStep.tsx - Wire
draftCounttodraftStore.getAllDraftBusinessIds(tenantId, orgId, periodLabel).length - Implement
onCommitAllhandler:- Get all drafts for CURRENT scope only (
tenantId,orgId,periodLabel) - Sequential commit: Concurrency=1 (one save at a time)
- Progress UI: Show
{current}/{total}during commit - Track failures (business_id + error message)
- Partial failure handling: Discard only successful drafts; keep failed drafts
- Show failure list if any failures
- Refresh readiness exactly once at end (or debounced)
- Get all drafts for CURRENT scope only (
- Implement
onDiscardAllhandler:- Clear all drafts for current scope (
tenantId,orgId,periodLabel)
- Clear all drafts for current scope (
Phase 5: Drawer Integration for Discovered Businesses
- Create helper
convertReadinessItemToDrawerBusiness()with null-safe fields - In readiness list: Configure button opens
BusinessDetailDrawer(wizardMode=true) - Ensure
BusinessDetailDrawerhandles businesses not in onboarding - If
getBusinessConfigreturns 404, show empty form (no server config) - Ensure
saveBusinessConfigadopts business if not in onboarding (already done in backend) - STOP CONDITION: If
saveBusinessConfigcannot adopt/materialize discovered-only business, STOP and report exact failing case + logs
Phase 6: Testing
- Vitest test: Draft scoping (tenant/org/period) - drafts don’t leak across scopes
- Vitest test: Badge rendering - 🟢 Ready vs 🟡 Draft Saved vs 🔴 Not Ready
- Vitest test: Bulk owner rollup draft creates N drafts correctly (no saves)
- Vitest test: Commit All only commits current scope (tenant/org/period)
- Vitest test: Commit All handles partial failure retention (failed drafts kept)
- Vitest test: Sequential commit (concurrency=1) with progress UI
- Manual test: Configure discovered business in wizard
- Manual test: Draft multiple businesses, commit all
- Manual test: Verify readiness refresh after commit
- Manual test: Verify drafts don’t leak across periods/orgs
5. TEST PLAN
5.1 Unit Tests (Vitest)
File:dashboard/src/components/intake/__tests__/PreflightStep.test.tsx (create if not exists)
5.2 Integration Tests
Commands:5.3 Manual Test Scenarios
-
Configure Discovered Business:
- Upload batch → discover → see 🔴 “Not Ready” business
- Click “Configure” → opens
BusinessDetailDrawer(wizardMode=true) - Set mode to OWNER_ROLLUP, set date
- Click “Save Draft” → see 🟡 “Draft Saved” badge
- Click “Commit All” → see success, badge changes to 🟢 “Ready”
-
Bulk Owner Rollup Draft:
- See 5 🔴 “Not Ready” businesses
- Click “Select Remaining → Owner Rollup (Draft)”
- Creates 5 drafts directly (no modal, no API calls)
- See all 5 show 🟡 “Draft Saved” badge
- Click “Commit All” → see progress , , etc.
- All 5 save sequentially, badges change to 🟢 “Ready”
-
Partial Failure:
- Create 3 drafts
- Mock 1 save to fail
- Click “Commit All”
- See 2 successes, 1 failure in modal
- Modal stays open, failure list shown
- Only 2 drafts cleared (successful ones)
-
Scope Isolation:
- Create drafts for tenant A, org X, period 2025-01-01
- Switch to tenant B (or org Y, or period 2025-02-01)
- Verify old drafts not shown (scoped by tenant/org/period)
6. RISK MITIGATION CHECKLIST
- Draft store migration: Clear old drafts on rehydrate (safest - no key migration)
- Scope validation: Ensure drafts match current (tenant, org, period) before showing
- Org scoping: Use
getEffectiveOrgId()consistently (null → ‘platform’ in key) - Tenant scoping: Use
auth.getUser()?.tenantIdconsistently - Readiness refresh: Debounce or coalesce multiple refresh calls (refresh exactly once)
- Error handling: Show clear error messages for save failures (business_id + error)
- Loading states: Show progress UI during commit all (/)
- Success feedback: Show toast + update badges immediately
- Decimal safety: All pepm/amount values remain strings (no JS number math)
- STOP CONDITION: Verify
saveBusinessConfigcan adopt discovered-only businesses before proceeding
7. FILES TO TOUCH
Frontend
dashboard/src/store/useDraftStore.ts— Period scopingdashboard/src/components/intake/PreflightStep.tsx— Badge, bulk action, commit alldashboard/src/components/admin/onboarding/BusinessDetailDrawer.tsx— Period scoping in draft callsdashboard/src/components/ui/BulkActionBar.tsx— Already exists, reusedashboard/src/components/intake/__tests__/PreflightStep.test.tsx— New test file
Backend
NONE — All endpoints already support the required functionality.8. REUSED VS REFACTORED
Reused (No Changes)
BusinessDetailDrawercomponent (same component, wizardMode prop)saveBusinessConfigendpoint (already adopts businesses)getBusinessConfigendpoint (already handles any business_id)BulkActionBarcomponent (already exists)useDraftStorestructure (only key format changes)
Refactored (Minimal Changes)
- Draft store key format:
business_id→${tenant_id}:${effective_org_id || 'platform'}:${period_label}:${business_id} - Draft store method signatures: Require
tenantId,orgId,periodLabel,businessId PreflightStepreadiness list: Add draft badge logic (🟢 Ready / 🟡 Draft Saved / 🔴 Not Ready)PreflightStepbulk actions: Implement “Select Remaining → Owner Rollup (Draft)” directly (no BulkAssignModal)PreflightStepcommit all: WireBulkActionBarto draft store with scope filteringBusinessDetailDrawer: Pass scope params into all draftStore calls
New (Minimal)
- Helper function:
convertReadinessItemToDrawerBusiness()(null-safe field conversion) - Draft badge display logic in
PreflightStep(three states: Ready / Draft Saved / Not Ready) - Bulk owner rollup draft handler (direct draft creation, no API calls)
- Commit all handler with sequential save (concurrency=1) + progress UI + partial failure retention
- Draft store migration: Clear old drafts on rehydrate
SUMMARY
Status: ✅ GOOD — Single source of truth for drawer (BusinessDetailDrawer), endpoints already support discovered businesses, minimal frontend-only changes needed.
P0 BLOCKERS:
- Draft store scoping:
${tenant_id}:${effective_org_id || 'platform'}:${period_label}:${business_id} - Commit All filtering: Only current scope, sequential (concurrency=1), partial failure retention
- Bulk Owner Rollup: Draft-only, implemented directly in PreflightStep (no BulkAssignModal)
- Draft store scoping (tenant/org/period key format + migration)
- Draft badge display (🟢 Ready / 🟡 Draft Saved / 🔴 Not Ready)
- Bulk owner rollup draft action (direct draft creation, no API calls)
- Commit all integration (sequential, progress UI, failure handling)
- Configure Now wiring (opens BusinessDetailDrawer in wizardMode)
- Helper function:
convertReadinessItemToDrawerBusiness()
saveBusinessConfig calls auto_adopt_discovered_business internally.
STOP CONDITION: If saveBusinessConfig cannot adopt/materialize discovered-only businesses, STOP and report exact failing case + logs before adding endpoints.