diff --git a/services/orchestrator/app/api/internal_demo.py b/services/orchestrator/app/api/internal_demo.py index c6f62472..8dda43e1 100644 --- a/services/orchestrator/app/api/internal_demo.py +++ b/services/orchestrator/app/api/internal_demo.py @@ -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 } diff --git a/services/orders/app/api/internal_demo.py b/services/orders/app/api/internal_demo.py index 870c505d..912c1e96 100644 --- a/services/orders/app/api/internal_demo.py +++ b/services/orders/app/api/internal_demo.py @@ -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: diff --git a/services/production/app/api/internal_demo.py b/services/production/app/api/internal_demo.py index dfc55408..0e4a5704 100644 --- a/services/production/app/api/internal_demo.py +++ b/services/production/app/api/internal_demo.py @@ -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 diff --git a/shared/demo/fixtures/professional/06-production.json b/shared/demo/fixtures/professional/06-production.json index 9ca59f26..e2fba3e2 100644 --- a/shared/demo/fixtures/professional/06-production.json +++ b/shared/demo/fixtures/professional/06-production.json @@ -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" + } ] } \ No newline at end of file