Files
bakery-ia/SUPPLIER_ID_MISMATCH_ROOT_CAUSE.md

239 lines
9.0 KiB
Markdown
Raw Normal View History

2025-12-10 11:23:53 +01:00
# 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.