demo seed change 5

This commit is contained in:
Urtzi Alfaro
2025-12-14 20:13:59 +01:00
parent 82f9622411
commit 56a1821256
4 changed files with 462 additions and 173 deletions

View File

@@ -58,6 +58,107 @@ def verify_internal_api_key(x_internal_api_key: str = Header(...)):
return True
async def load_fixture_data_for_tenant(
db: AsyncSession,
tenant_uuid: UUID,
demo_account_type: str,
reference_time: datetime
) -> int:
"""
Load orchestration run data from JSON fixture directly into the virtual tenant.
Returns the number of runs created.
"""
from shared.utils.seed_data_paths import get_seed_data_path
from shared.utils.demo_dates import resolve_time_marker, adjust_date_for_demo
# Load fixture data
try:
json_file = get_seed_data_path(demo_account_type, "11-orchestrator.json")
except ImportError:
# Fallback to original path
seed_data_dir = Path(__file__).parent.parent.parent.parent / "shared" / "demo" / "fixtures"
json_file = seed_data_dir / demo_account_type / "11-orchestrator.json"
if not json_file.exists():
logger.warning("Orchestrator fixture file not found", file=str(json_file))
return 0
with open(json_file, 'r', encoding='utf-8') as f:
fixture_data = json.load(f)
orchestration_run_data = fixture_data.get("orchestration_run")
if not orchestration_run_data:
logger.warning("No orchestration_run data in fixture")
return 0
# Parse and adjust dates from fixture to reference_time
base_started_at = resolve_time_marker(orchestration_run_data.get("started_at"))
base_completed_at = resolve_time_marker(orchestration_run_data.get("completed_at"))
# Adjust dates to make them appear recent relative to session creation
started_at = adjust_date_for_demo(base_started_at, reference_time) if base_started_at else reference_time - timedelta(hours=2)
completed_at = adjust_date_for_demo(base_completed_at, reference_time) if base_completed_at else started_at + timedelta(minutes=15)
# Generate unique run number with session context
current_year = reference_time.year
unique_suffix = str(uuid.uuid4())[:8].upper()
run_number = f"ORCH-DEMO-PROF-{current_year}-001-{unique_suffix}"
# Create orchestration run for virtual tenant
new_run = OrchestrationRun(
id=uuid.uuid4(), # Generate new UUID
tenant_id=tenant_uuid,
run_number=run_number,
status=OrchestrationStatus[orchestration_run_data["status"]],
run_type=orchestration_run_data.get("run_type", "daily"),
priority="normal",
started_at=started_at,
completed_at=completed_at,
duration_seconds=orchestration_run_data.get("duration_seconds", 900),
# Step statuses from orchestration_results
forecasting_status="success",
forecasting_started_at=started_at,
forecasting_completed_at=started_at + timedelta(minutes=2),
production_status="success",
production_started_at=started_at + timedelta(minutes=2),
production_completed_at=started_at + timedelta(minutes=5),
procurement_status="success",
procurement_started_at=started_at + timedelta(minutes=5),
procurement_completed_at=started_at + timedelta(minutes=8),
notification_status="success",
notification_started_at=started_at + timedelta(minutes=8),
notification_completed_at=completed_at,
# Results from orchestration_results
forecasts_generated=fixture_data.get("orchestration_results", {}).get("forecasts_generated", 10),
production_batches_created=fixture_data.get("orchestration_results", {}).get("production_batches_created", 18),
procurement_plans_created=0,
purchase_orders_created=fixture_data.get("orchestration_results", {}).get("purchase_orders_created", 6),
notifications_sent=fixture_data.get("orchestration_results", {}).get("notifications_sent", 8),
# Metadata
triggered_by="system",
created_at=started_at,
updated_at=completed_at
)
db.add(new_run)
await db.flush()
logger.info(
"Loaded orchestration run from fixture",
tenant_id=str(tenant_uuid),
run_number=new_run.run_number,
started_at=started_at.isoformat()
)
return 1
@router.post("/internal/demo/clone")
async def clone_demo_data(
base_tenant_id: str,
@@ -73,6 +174,8 @@ async def clone_demo_data(
This endpoint is called by the demo_session service during session initialization.
It clones orchestration runs with date adjustments to make them appear recent.
If the base tenant has no orchestration runs, it will first seed them from the fixture.
"""
start_time = datetime.now(timezone.utc)
@@ -96,150 +199,24 @@ async def clone_demo_data(
)
try:
base_uuid = uuid.UUID(base_tenant_id)
virtual_uuid = uuid.UUID(virtual_tenant_id)
# Fetch base tenant orchestration runs
# Get all completed and partial_success runs from the base tenant
result = await db.execute(
select(OrchestrationRun)
.where(OrchestrationRun.tenant_id == base_uuid)
.order_by(OrchestrationRun.started_at.desc())
.limit(10) # Clone last 10 runs for demo
# Load fixture data directly into virtual tenant (no base tenant cloning)
runs_created = await load_fixture_data_for_tenant(
db,
virtual_uuid,
demo_account_type,
reference_time
)
base_runs = list(result.scalars().all())
runs_cloned = 0
# Clone each orchestration run with date adjustment
for base_run in base_runs:
# Use the shared date adjustment utility to ensure dates are always in the past
# This calculates the offset from BASE_REFERENCE_DATE and applies it to session creation time
if base_run.started_at:
new_started_at = adjust_date_for_demo(
base_run.started_at, reference_time
)
else:
new_started_at = reference_time - timedelta(hours=2)
# Adjust completed_at using the same utility
if base_run.completed_at:
new_completed_at = adjust_date_for_demo(
base_run.completed_at, reference_time
)
# Ensure completion is after start (in case of edge cases)
if new_completed_at and new_started_at and new_completed_at < new_started_at:
# Preserve original duration
duration = base_run.completed_at - base_run.started_at
new_completed_at = new_started_at + duration
else:
new_completed_at = None
# Adjust all step timestamps using the shared utility
def adjust_timestamp(original_timestamp):
if not original_timestamp:
return None
return adjust_date_for_demo(original_timestamp, reference_time)
# Create new orchestration run for virtual tenant
# Update run_number to have current year instead of original year, and make it unique
current_year = reference_time.year
# Extract type from original run number and create new format
parts = base_run.run_number.split('-')
if len(parts) >= 4:
tenant_prefix = parts[1] if len(parts) > 1 else "DEMO"
type_code = parts[2] if len(parts) > 2 else "TST"
original_index = parts[3] if len(parts) > 3 else "001"
# Generate a more robust unique suffix to avoid collisions
# Use UUID instead of just session_id substring to ensure uniqueness
unique_suffix = str(uuid.uuid4())[:8].upper()
proposed_run_number = f"ORCH-{tenant_prefix}-{type_code}-{current_year}-{original_index}-{unique_suffix}"
else:
unique_suffix = str(uuid.uuid4())[:12].upper()
proposed_run_number = f"{base_run.run_number}-{unique_suffix}"
# Ensure the run number is truly unique by checking against existing entries
# This prevents collisions especially in high-concurrency scenarios
run_number = await ensure_unique_run_number(db, proposed_run_number)
new_run = OrchestrationRun(
id=uuid.uuid4(),
tenant_id=virtual_uuid,
run_number=run_number,
status=base_run.status,
run_type=base_run.run_type,
priority=base_run.priority,
started_at=new_started_at,
completed_at=new_completed_at,
duration_seconds=base_run.duration_seconds,
# Forecasting step
forecasting_started_at=adjust_timestamp(base_run.forecasting_started_at),
forecasting_completed_at=adjust_timestamp(base_run.forecasting_completed_at),
forecasting_status=base_run.forecasting_status,
forecasting_error=base_run.forecasting_error,
# Production step
production_started_at=adjust_timestamp(base_run.production_started_at),
production_completed_at=adjust_timestamp(base_run.production_completed_at),
production_status=base_run.production_status,
production_error=base_run.production_error,
# Procurement step
procurement_started_at=adjust_timestamp(base_run.procurement_started_at),
procurement_completed_at=adjust_timestamp(base_run.procurement_completed_at),
procurement_status=base_run.procurement_status,
procurement_error=base_run.procurement_error,
# Notification step
notification_started_at=adjust_timestamp(base_run.notification_started_at),
notification_completed_at=adjust_timestamp(base_run.notification_completed_at),
notification_status=base_run.notification_status,
notification_error=base_run.notification_error,
# AI Insights (if exists)
ai_insights_started_at=adjust_timestamp(base_run.ai_insights_started_at) if hasattr(base_run, 'ai_insights_started_at') else None,
ai_insights_completed_at=adjust_timestamp(base_run.ai_insights_completed_at) if hasattr(base_run, 'ai_insights_completed_at') else None,
ai_insights_status=base_run.ai_insights_status if hasattr(base_run, 'ai_insights_status') else None,
ai_insights_generated=base_run.ai_insights_generated if hasattr(base_run, 'ai_insights_generated') else None,
ai_insights_posted=base_run.ai_insights_posted if hasattr(base_run, 'ai_insights_posted') else None,
# Results summary
forecasts_generated=base_run.forecasts_generated,
production_batches_created=base_run.production_batches_created,
procurement_plans_created=base_run.procurement_plans_created,
purchase_orders_created=base_run.purchase_orders_created,
notifications_sent=base_run.notifications_sent,
# Performance metrics
fulfillment_rate=base_run.fulfillment_rate,
on_time_delivery_rate=base_run.on_time_delivery_rate,
cost_accuracy=base_run.cost_accuracy,
quality_score=base_run.quality_score,
# Data
forecast_data=base_run.forecast_data,
run_metadata=base_run.run_metadata,
# Metadata
triggered_by=base_run.triggered_by,
created_at=reference_time,
updated_at=reference_time
)
db.add(new_run)
await db.flush()
runs_cloned += 1
await db.commit()
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
logger.info(
"Orchestration runs cloned successfully",
"Orchestration runs loaded from fixture successfully",
virtual_tenant_id=str(virtual_tenant_id),
runs_cloned=runs_cloned,
runs_created=runs_created,
duration_ms=duration_ms
)
@@ -247,8 +224,8 @@ async def clone_demo_data(
"service": "orchestrator",
"status": "completed",
"success": True,
"records_cloned": runs_cloned,
"runs_cloned": runs_cloned,
"records_cloned": runs_created,
"runs_cloned": runs_created,
"duration_ms": duration_ms
}

View File

@@ -187,7 +187,7 @@ async def clone_demo_data(
logger.info(
"Loaded orders seed data",
customers=len(seed_data.get('customers', [])),
orders=len(seed_data.get('orders', []))
orders=len(seed_data.get('customer_orders', []))
)
# Load Customers from seed data
@@ -249,7 +249,7 @@ async def clone_demo_data(
# Load Customer Orders from seed data
order_id_map = {}
for order_data in seed_data.get('orders', []):
for order_data in seed_data.get('customer_orders', []):
# Transform IDs using XOR
from shared.utils.demo_id_transformer import transform_id
try:

View File

@@ -246,47 +246,60 @@ async def clone_demo_data(
# Flush to get equipment IDs
await db.flush()
# Clone Quality Check Templates
# Note: Quality check templates are not included in seed data
# They would need to be added to the production seed data if needed
# Clone Quality Check Templates from seed data
template_id_map = {}
base_templates = []
logger.info(
"No quality check templates to clone (not in seed data)",
count=len(base_templates)
)
for template_data in seed_data.get('quality_check_templates', []):
# Transform template ID using XOR
from shared.utils.demo_id_transformer import transform_id
try:
template_uuid = UUID(template_data['id'])
transformed_id = transform_id(template_data['id'], virtual_uuid)
except ValueError as e:
logger.error("Failed to parse template UUID",
template_id=template_data['id'],
error=str(e))
continue
# Only create templates if they exist in base templates
for template in base_templates:
new_template_id = uuid.uuid4()
template_id_map[template.id] = new_template_id
template_id_map[UUID(template_data['id'])] = transformed_id
# Parse date fields (supports BASE_TS markers and ISO timestamps)
adjusted_created_at = parse_date_field(
template_data.get('created_at'),
session_time,
"created_at"
) or session_time
adjusted_updated_at = parse_date_field(
template_data.get('updated_at'),
session_time,
"updated_at"
) or adjusted_created_at
new_template = QualityCheckTemplate(
id=new_template_id,
id=str(transformed_id),
tenant_id=virtual_uuid,
name=template.name,
template_code=template.template_code,
check_type=template.check_type,
category=template.category,
description=template.description,
instructions=template.instructions,
parameters=template.parameters,
thresholds=template.thresholds,
scoring_criteria=template.scoring_criteria,
is_active=template.is_active,
is_required=template.is_required,
is_critical=template.is_critical,
weight=template.weight,
min_value=template.min_value,
max_value=template.max_value,
target_value=template.target_value,
unit=template.unit,
tolerance_percentage=template.tolerance_percentage,
applicable_stages=template.applicable_stages,
created_by=template.created_by,
created_at=session_time,
updated_at=session_time
name=template_data.get('name'),
template_code=template_data.get('template_code'),
check_type=template_data.get('check_type'),
category=template_data.get('category'),
description=template_data.get('description'),
instructions=template_data.get('instructions'),
parameters=template_data.get('parameters'),
thresholds=template_data.get('thresholds'),
scoring_criteria=template_data.get('scoring_criteria'),
is_active=template_data.get('is_active', True),
is_required=template_data.get('is_required', False),
is_critical=template_data.get('is_critical', False),
weight=template_data.get('weight', 1.0),
min_value=template_data.get('min_value'),
max_value=template_data.get('max_value'),
target_value=template_data.get('target_value'),
unit=template_data.get('unit'),
tolerance_percentage=template_data.get('tolerance_percentage'),
applicable_stages=template_data.get('applicable_stages'),
created_by=template_data.get('created_by'),
created_at=adjusted_created_at,
updated_at=adjusted_updated_at
)
db.add(new_template)
stats["quality_check_templates"] += 1

View File

@@ -1731,5 +1731,304 @@
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
}
],
"quality_check_templates": [
{
"id": "80000000-0000-0000-0000-000000000001",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Inspección Visual de Cocción",
"template_code": "QC-VISUAL-001",
"check_type": "visual",
"category": "Apariencia",
"description": "Inspección visual del color, dorado y apariencia general del producto horneado",
"instructions": "Verificar que el producto tenga un color uniforme, dorado apropiado y sin quemaduras. Revisar grietas, corteza y estructura general.",
"parameters": {
"color_uniformity": true,
"golden_brown": true,
"no_burns": true,
"proper_crust": true
},
"thresholds": {
"min_score": 7.0,
"critical_defects": ["burnt", "raw", "collapsed"]
},
"scoring_criteria": {
"color": 3.0,
"texture": 3.0,
"appearance": 2.0,
"structure": 2.0
},
"is_active": true,
"is_required": true,
"is_critical": false,
"weight": 1.0,
"min_value": null,
"max_value": null,
"target_value": null,
"unit": null,
"tolerance_percentage": null,
"applicable_stages": ["baking", "cooling", "packaging"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
},
{
"id": "80000000-0000-0000-0000-000000000002",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Control de Peso Unitario",
"template_code": "QC-WEIGHT-001",
"check_type": "measurement",
"category": "Dimensiones",
"description": "Verificación del peso unitario del producto contra los estándares definidos",
"instructions": "Pesar una muestra representativa de 5 unidades y verificar que el peso promedio esté dentro de la tolerancia permitida.",
"parameters": {
"sample_size": 5,
"unit": "grams"
},
"thresholds": {
"min_weight": null,
"max_weight": null,
"tolerance": 5.0
},
"scoring_criteria": {
"weight_accuracy": 5.0,
"consistency": 5.0
},
"is_active": true,
"is_required": true,
"is_critical": false,
"weight": 1.5,
"min_value": null,
"max_value": null,
"target_value": null,
"unit": "g",
"tolerance_percentage": 5.0,
"applicable_stages": ["packaging"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
},
{
"id": "80000000-0000-0000-0000-000000000003",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Control de Temperatura Interna",
"template_code": "QC-TEMP-001",
"check_type": "temperature",
"category": "Cocción",
"description": "Medición de la temperatura interna del producto para verificar cocción completa",
"instructions": "Insertar termómetro en el centro del producto y verificar que alcance la temperatura mínima de seguridad alimentaria.",
"parameters": {
"measurement_location": "center",
"thermometer_type": "digital"
},
"thresholds": {
"min_temp": 88.0,
"max_temp": 98.0,
"critical_min": 75.0
},
"scoring_criteria": {
"temperature_range": 10.0
},
"is_active": true,
"is_required": true,
"is_critical": true,
"weight": 2.0,
"min_value": 88.0,
"max_value": 98.0,
"target_value": 93.0,
"unit": "°C",
"tolerance_percentage": 5.0,
"applicable_stages": ["baking"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
},
{
"id": "80000000-0000-0000-0000-000000000004",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Evaluación de Textura y Estructura",
"template_code": "QC-TEXTURE-001",
"check_type": "visual",
"category": "Estructura",
"description": "Evaluación de la textura interna, alveolado y estructura de la miga",
"instructions": "Cortar el producto por la mitad y evaluar: alveolado uniforme, miga suave y húmeda, estructura adecuada sin zonas densas.",
"parameters": {
"crumb_structure": true,
"moisture_level": true,
"alveoli_distribution": true
},
"thresholds": {
"min_score": 7.0,
"critical_defects": ["dense", "dry", "gummy"]
},
"scoring_criteria": {
"crumb_openness": 3.0,
"moisture": 3.0,
"consistency": 2.0,
"mouthfeel": 2.0
},
"is_active": true,
"is_required": false,
"is_critical": false,
"weight": 1.0,
"min_value": null,
"max_value": null,
"target_value": null,
"unit": null,
"tolerance_percentage": null,
"applicable_stages": ["cooling", "packaging"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
},
{
"id": "80000000-0000-0000-0000-000000000005",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Control de Dimensiones",
"template_code": "QC-DIM-001",
"check_type": "measurement",
"category": "Dimensiones",
"description": "Verificación de las dimensiones (largo, ancho, alto) del producto terminado",
"instructions": "Medir una muestra de 5 unidades con calibrador o regla. Verificar que las dimensiones estén dentro del rango especificado.",
"parameters": {
"sample_size": 5,
"dimensions": ["length", "width", "height"]
},
"thresholds": {
"tolerance": 10.0
},
"scoring_criteria": {
"dimensional_accuracy": 5.0,
"uniformity": 5.0
},
"is_active": true,
"is_required": false,
"is_critical": false,
"weight": 0.8,
"min_value": null,
"max_value": null,
"target_value": null,
"unit": "cm",
"tolerance_percentage": 10.0,
"applicable_stages": ["shaping", "packaging"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
},
{
"id": "80000000-0000-0000-0000-000000000006",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Prueba Organoléptica (Sabor y Aroma)",
"template_code": "QC-TASTE-001",
"check_type": "visual",
"category": "Calidad Sensorial",
"description": "Evaluación del sabor, aroma y características organolépticas del producto",
"instructions": "Probar una muestra del producto. Evaluar sabor apropiado, aroma característico, sin sabores extraños u oxidación.",
"parameters": {
"flavor_profile": true,
"aroma": true,
"off_flavors": false,
"freshness": true
},
"thresholds": {
"min_score": 8.0,
"critical_defects": ["off_taste", "rancid", "bitter"]
},
"scoring_criteria": {
"flavor": 4.0,
"aroma": 3.0,
"freshness": 2.0,
"overall_quality": 1.0
},
"is_active": true,
"is_required": false,
"is_critical": true,
"weight": 1.5,
"min_value": null,
"max_value": null,
"target_value": null,
"unit": null,
"tolerance_percentage": null,
"applicable_stages": ["cooling", "packaging"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
},
{
"id": "80000000-0000-0000-0000-000000000007",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Control de Fermentación",
"template_code": "QC-PROOF-001",
"check_type": "visual",
"category": "Fermentación",
"description": "Evaluación del nivel de fermentación adecuado antes del horneado",
"instructions": "Verificar volumen de la masa, elasticidad al tacto y señales de fermentación apropiada. Prueba del dedo para verificar punto óptimo.",
"parameters": {
"volume_increase": true,
"finger_test": true,
"structure": true
},
"thresholds": {
"min_score": 7.0,
"critical_defects": ["underproofed", "overproofed", "collapsed"]
},
"scoring_criteria": {
"volume": 4.0,
"elasticity": 3.0,
"structure": 3.0
},
"is_active": true,
"is_required": true,
"is_critical": false,
"weight": 1.2,
"min_value": null,
"max_value": null,
"target_value": null,
"unit": null,
"tolerance_percentage": null,
"applicable_stages": ["proofing"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
},
{
"id": "80000000-0000-0000-0000-000000000008",
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Control de Laminado (Hojaldre)",
"template_code": "QC-LAMINATE-001",
"check_type": "visual",
"category": "Técnica",
"description": "Evaluación de la calidad del laminado en productos de hojaldre (croissants, napolitanas)",
"instructions": "Verificar número de capas visibles, separación entre capas, sin mantequilla derramada. Cortar producto para inspeccionar estructura interna.",
"parameters": {
"layer_count": true,
"layer_separation": true,
"butter_distribution": true,
"no_leakage": true
},
"thresholds": {
"min_score": 8.0,
"critical_defects": ["butter_leakage", "collapsed_layers", "uneven_distribution"]
},
"scoring_criteria": {
"layer_definition": 4.0,
"butter_incorporation": 3.0,
"structure": 2.0,
"appearance": 1.0
},
"is_active": true,
"is_required": false,
"is_critical": false,
"weight": 1.3,
"min_value": null,
"max_value": null,
"target_value": null,
"unit": null,
"tolerance_percentage": null,
"applicable_stages": ["shaping", "baking"],
"created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"created_at": "BASE_TS",
"updated_at": "BASE_TS"
}
]
}