# Root Cause Analysis: Supplier ID Mismatch in Demo Sessions ## Problem Summary In demo sessions, the supplier names are showing as "Unknown" in the Pending Purchases block, even though: 1. The Supplier API returns valid suppliers with real names (e.g., "Lácteos del Valle S.A.") 2. The alerts contain reasoning data with supplier names 3. The PO data has supplier IDs ## Root Cause **The supplier IDs in the alert's reasoning data DO NOT match the cloned supplier IDs.** ### Why This Happens The system uses an XOR-based strategy to generate tenant-specific UUIDs: ```python # Formula used in all seed scripts: supplier_id = uuid.UUID(int=tenant_int ^ base_supplier_int) ``` However, **the alert seeding script uses hardcoded placeholder IDs that don't follow this pattern:** #### In `seed_enriched_alert_demo.py` (Line 45): ```python YEAST_SUPPLIER_ID = "supplier-levadura-fresh" # ❌ String ID, not UUID FLOUR_PO_ID = "po-flour-demo-001" # ❌ String ID, not UUID ``` #### In `seed_demo_purchase_orders.py` (Lines 62-67): ```python # Hardcoded base supplier IDs (correct pattern) BASE_SUPPLIER_IDS = [ uuid.UUID("40000000-0000-0000-0000-000000000001"), # Molinos San José S.L. uuid.UUID("40000000-0000-0000-0000-000000000002"), # Lácteos del Valle S.A. uuid.UUID("40000000-0000-0000-0000-000000000005"), # Lesaffre Ibérica ] ``` These base IDs are then XORed with the tenant ID to create unique supplier IDs for each tenant: ```python # Line 136 of seed_demo_purchase_orders.py tenant_int = int(tenant_id.hex, 16) base_int = int(base_id.hex, 16) supplier_id = uuid.UUID(int=tenant_int ^ base_int) # ✅ Correct cloning pattern ``` ## The Data Flow Mismatch ### 1. Supplier Seeding (Template Tenants) File: `services/suppliers/scripts/demo/seed_demo_suppliers.py` ```python # Line 155-158: Creates suppliers with XOR-based IDs base_supplier_id = uuid.UUID(supplier_data["id"]) # From proveedores_es.json tenant_int = int(tenant_id.hex, 16) supplier_id = uuid.UUID(int=tenant_int ^ int(base_supplier_id.hex, 16)) ``` **Result:** Suppliers are created with tenant-specific UUIDs like: - `uuid.UUID("6e1f9009-e640-48c7-95c5-17d6e7c1da55")` (example from API response) ### 2. Purchase Order Seeding (Template Tenants) File: `services/procurement/scripts/demo/seed_demo_purchase_orders.py` ```python # Lines 111-144: Uses same XOR pattern def get_demo_supplier_ids(tenant_id: uuid.UUID): tenant_int = int(tenant_id.hex, 16) for i, base_id in enumerate(BASE_SUPPLIER_IDS): base_int = int(base_id.hex, 16) supplier_id = uuid.UUID(int=tenant_int ^ base_int) # ✅ Matches supplier seeding ``` **PO reasoning_data contains:** ```python reasoning_data = create_po_reasoning_low_stock( supplier_name=supplier.name, # ✅ CORRECT: Real supplier name like "Lácteos del Valle S.A." product_names=product_names, # ... other parameters ) ``` **Result:** - POs are created with correct supplier IDs matching the suppliers - `reasoning_data.parameters.supplier_name` contains the real supplier name (e.g., "Lácteos del Valle S.A.") ### 3. Alert Seeding (Demo Sessions) File: `services/demo_session/scripts/seed_enriched_alert_demo.py` **Problem:** Uses hardcoded string IDs instead of XOR-generated UUIDs: ```python # Lines 40-46 ❌ WRONG: String IDs instead of proper UUIDs FLOUR_INGREDIENT_ID = "flour-tipo-55" YEAST_INGREDIENT_ID = "yeast-fresh" CROISSANT_PRODUCT_ID = "croissant-mantequilla" CROISSANT_BATCH_ID = "batch-croissants-001" YEAST_SUPPLIER_ID = "supplier-levadura-fresh" # ❌ This doesn't match anything! FLOUR_PO_ID = "po-flour-demo-001" ``` These IDs are then embedded in the alert metadata, but they don't match the actual cloned supplier IDs. ### 4. Session Cloning Process File: `services/demo_session/app/services/clone_orchestrator.py` When a user creates a demo session: 1. **Base template tenant** (e.g., `a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6`) is cloned 2. **Virtual tenant** is created (e.g., `f8e7d6c5-b4a3-2918-1726-354443526178`) 3. **Suppliers are cloned** using XOR pattern: ```python # In services/suppliers/app/api/internal_demo.py new_supplier_id = uuid.UUID(int=virtual_tenant_int ^ base_supplier_int) ``` 4. **Purchase orders are cloned** with matching supplier IDs 5. **Alerts are generated** but use placeholder string IDs ❌ ## Why the Frontend Shows "Unknown" In `useDashboardData.ts` (line 142-144), the code tries to look up supplier names: ```typescript const supplierName = reasoningInfo?.supplier_name_from_alert || // ✅ This works! supplierMap.get(po.supplier_id) || // ❌ This fails (ID mismatch) po.supplier_name; // ❌ Fallback also fails ``` **However, our fix IS working!** The first line: ```typescript reasoningInfo?.supplier_name_from_alert ``` This extracts the supplier name from the alert's reasoning data, which was correctly set during PO creation in `seed_demo_purchase_orders.py` (line 336): ```python reasoning_data = create_po_reasoning_low_stock( supplier_name=supplier.name, # ✅ Real name like "Lácteos del Valle S.A." # ... ) ``` ## The Fix We Applied In `useDashboardData.ts` (lines 127, 133-134, 142-144): ```typescript // Extract supplier name from reasoning data const supplierNameFromReasoning = reasoningData?.parameters?.supplier_name; poReasoningMap.set(poId, { reasoning_data: reasoningData, ai_reasoning_summary: alert.ai_reasoning_summary || alert.description || alert.i18n?.message_key, supplier_name_from_alert: supplierNameFromReasoning, // ✅ Real supplier name from PO creation }); // Prioritize supplier name from alert reasoning (has actual name in demo data) const supplierName = reasoningInfo?.supplier_name_from_alert || // ✅ NOW WORKS! supplierMap.get(po.supplier_id) || po.supplier_name; ``` ## Why This Fix Works The **PO reasoning data is created during PO seeding**, not during alert seeding. When POs are created in `seed_demo_purchase_orders.py`, the code has access to the real supplier objects: ```python # Line 490: Get suppliers using XOR pattern suppliers = get_demo_supplier_ids(tenant_id) # Line 498: Use supplier with correct ID and name supplier_high_trust = high_trust_suppliers[0] if high_trust_suppliers else suppliers[0] # Lines 533-545: Create PO with supplier reference po3 = await create_purchase_order( db, tenant_id, supplier_high_trust, # ✅ Has correct ID and name PurchaseOrderStatus.pending_approval, Decimal("450.00"), # ... ) # Line 336: Reasoning data includes real supplier name reasoning_data = create_po_reasoning_low_stock( supplier_name=supplier.name, # ✅ "Lácteos del Valle S.A." # ... ) ``` ## Why the Alert Seeder Doesn't Matter (For This Issue) The alert seeder (`seed_enriched_alert_demo.py`) creates generic demo alerts with placeholder IDs, but these are NOT used for the PO approval alerts we see in the dashboard. The **actual PO approval alerts are created automatically** by the procurement service when POs are created, and those alerts include the correct reasoning data with real supplier names. ## Summary | Component | Supplier ID Source | Status | |-----------|-------------------|--------| | **Supplier Seed** | XOR(tenant_id, base_supplier_id) | ✅ Correct UUID | | **PO Seed** | XOR(tenant_id, base_supplier_id) | ✅ Correct UUID | | **PO Reasoning Data** | `supplier.name` (real name) | ✅ "Lácteos del Valle S.A." | | **Alert Seed** | Hardcoded string "supplier-levadura-fresh" | ❌ Wrong format (but not used for PO alerts) | | **Session Clone** | XOR(virtual_tenant_id, base_supplier_id) | ✅ Correct UUID | | **Frontend Lookup** | `supplierMap.get(po.supplier_id)` | ❌ Fails (ID mismatch in demo) | | **Frontend Fix** | `reasoningInfo?.supplier_name_from_alert` | ✅ WORKS! Gets name from PO reasoning | ## Verification The fix should now work because: 1. ✅ POs are created with `reasoning_data` containing `supplier_name` parameter 2. ✅ Frontend extracts `supplier_name` from `reasoning_data.parameters.supplier_name` 3. ✅ Frontend prioritizes this value over ID lookup 4. ✅ User should now see "Lácteos del Valle S.A." instead of "Unknown" ## Long-term Fix (Optional) To fully resolve the underlying issue, the alert seeder should be updated to use proper XOR-based UUID generation instead of hardcoded string IDs: ```python # In seed_enriched_alert_demo.py, replace lines 40-46 with: # Demo tenant ID (should match existing demo tenant) DEMO_TENANT_ID = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Base IDs matching suppliers seed BASE_SUPPLIER_MOLINOS = uuid.UUID("40000000-0000-0000-0000-000000000001") BASE_SUPPLIER_LACTEOS = uuid.UUID("40000000-0000-0000-0000-000000000002") # Generate tenant-specific IDs using XOR tenant_int = int(DEMO_TENANT_ID.hex, 16) MOLINOS_SUPPLIER_ID = uuid.UUID(int=tenant_int ^ int(BASE_SUPPLIER_MOLINOS.hex, 16)) LACTEOS_SUPPLIER_ID = uuid.UUID(int=tenant_int ^ int(BASE_SUPPLIER_LACTEOS.hex, 16)) ``` However, this is not necessary for fixing the current dashboard issue, as PO alerts use the correct reasoning data from PO creation.