9.0 KiB
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:
- The Supplier API returns valid suppliers with real names (e.g., "Lácteos del Valle S.A.")
- The alerts contain reasoning data with supplier names
- 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:
# 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):
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):
# 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:
# 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
# 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
# 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:
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_namecontains 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:
# 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:
- Base template tenant (e.g.,
a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6) is cloned - Virtual tenant is created (e.g.,
f8e7d6c5-b4a3-2918-1726-354443526178) - Suppliers are cloned using XOR pattern:
# In services/suppliers/app/api/internal_demo.py new_supplier_id = uuid.UUID(int=virtual_tenant_int ^ base_supplier_int) - Purchase orders are cloned with matching supplier IDs
- 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:
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:
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):
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):
// 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:
# 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:
- ✅ POs are created with
reasoning_datacontainingsupplier_nameparameter - ✅ Frontend extracts
supplier_namefromreasoning_data.parameters.supplier_name - ✅ Frontend prioritizes this value over ID lookup
- ✅ 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:
# 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.