Initial commit - production deployment
This commit is contained in:
602
services/inventory/app/api/internal_demo.py
Normal file
602
services/inventory/app/api/internal_demo.py
Normal file
@@ -0,0 +1,602 @@
|
||||
"""
|
||||
Internal Demo Cloning API for Inventory Service
|
||||
Handles internal demo data cloning operations
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from typing import Optional
|
||||
import structlog
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.config import settings
|
||||
from app.models import Ingredient, Stock, ProductType
|
||||
from shared.utils.demo_dates import adjust_date_for_demo, resolve_time_marker, calculate_edge_case_times
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(prefix="/internal/demo", tags=["internal"])
|
||||
|
||||
|
||||
def parse_date_field(date_value, session_time: datetime, field_name: str = "date") -> Optional[datetime]:
|
||||
"""
|
||||
Parse date field, handling both ISO strings and BASE_TS markers.
|
||||
|
||||
Supports:
|
||||
- BASE_TS markers: "BASE_TS + 1h30m", "BASE_TS - 2d"
|
||||
- ISO 8601 strings: "2025-01-15T06:00:00Z"
|
||||
- None values (returns None)
|
||||
|
||||
Returns timezone-aware datetime or None.
|
||||
"""
|
||||
if not date_value:
|
||||
return None
|
||||
|
||||
# Check if it's a BASE_TS marker
|
||||
if isinstance(date_value, str) and date_value.startswith("BASE_TS"):
|
||||
try:
|
||||
return resolve_time_marker(date_value, session_time)
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
f"Invalid BASE_TS marker in {field_name}",
|
||||
marker=date_value,
|
||||
error=str(e)
|
||||
)
|
||||
return None
|
||||
|
||||
# Handle regular ISO date strings
|
||||
try:
|
||||
if isinstance(date_value, str):
|
||||
original_date = datetime.fromisoformat(date_value.replace('Z', '+00:00'))
|
||||
elif hasattr(date_value, 'isoformat'):
|
||||
original_date = date_value
|
||||
else:
|
||||
logger.warning(f"Unsupported date format in {field_name}", date_value=date_value)
|
||||
return None
|
||||
|
||||
return adjust_date_for_demo(original_date, session_time)
|
||||
except (ValueError, AttributeError) as e:
|
||||
logger.warning(
|
||||
f"Invalid date format in {field_name}",
|
||||
date_value=date_value,
|
||||
error=str(e)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/clone")
|
||||
async def clone_demo_data_internal(
|
||||
base_tenant_id: str,
|
||||
virtual_tenant_id: str,
|
||||
demo_account_type: str,
|
||||
session_id: Optional[str] = None,
|
||||
session_created_at: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Clone inventory service data for a virtual demo tenant
|
||||
|
||||
This endpoint creates fresh demo data by:
|
||||
1. Loading seed data from JSON files
|
||||
2. Applying XOR-based ID transformation
|
||||
3. Adjusting dates relative to session creation time
|
||||
4. Creating records in the virtual tenant
|
||||
|
||||
Args:
|
||||
base_tenant_id: Template tenant UUID (for reference)
|
||||
virtual_tenant_id: Target virtual tenant UUID
|
||||
demo_account_type: Type of demo account
|
||||
session_id: Originating session ID for tracing
|
||||
session_created_at: Session creation timestamp for date adjustment
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Dictionary with cloning results
|
||||
|
||||
Raises:
|
||||
HTTPException: On validation or cloning errors
|
||||
"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
# Validate UUIDs
|
||||
virtual_uuid = UUID(virtual_tenant_id)
|
||||
|
||||
# Parse session creation time for date adjustment
|
||||
if session_created_at:
|
||||
try:
|
||||
session_time = datetime.fromisoformat(session_created_at.replace('Z', '+00:00'))
|
||||
except (ValueError, AttributeError):
|
||||
session_time = start_time
|
||||
else:
|
||||
session_time = start_time
|
||||
|
||||
# Debug logging for UUID values
|
||||
logger.debug("Received UUID values", base_tenant_id=base_tenant_id, virtual_tenant_id=virtual_tenant_id)
|
||||
|
||||
if not all([base_tenant_id, virtual_tenant_id, session_id]):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Missing required parameters: base_tenant_id, virtual_tenant_id, session_id"
|
||||
)
|
||||
|
||||
# Validate UUID format before processing
|
||||
try:
|
||||
UUID(base_tenant_id)
|
||||
UUID(virtual_tenant_id)
|
||||
except ValueError as e:
|
||||
logger.error("Invalid UUID format in request",
|
||||
base_tenant_id=base_tenant_id,
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid UUID format: {str(e)}"
|
||||
)
|
||||
|
||||
# Parse session creation time
|
||||
if session_created_at:
|
||||
try:
|
||||
session_created_at_parsed = datetime.fromisoformat(session_created_at.replace('Z', '+00:00'))
|
||||
except (ValueError, AttributeError):
|
||||
session_created_at_parsed = datetime.now(timezone.utc)
|
||||
else:
|
||||
session_created_at_parsed = datetime.now(timezone.utc)
|
||||
|
||||
logger.info(
|
||||
"Starting inventory data cloning with date adjustment",
|
||||
base_tenant_id=base_tenant_id,
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
demo_account_type=demo_account_type,
|
||||
session_id=session_id,
|
||||
session_time=session_created_at_parsed.isoformat()
|
||||
)
|
||||
|
||||
# Load seed data from JSON files
|
||||
from shared.utils.seed_data_paths import get_seed_data_path
|
||||
|
||||
if demo_account_type == "professional":
|
||||
json_file = get_seed_data_path("professional", "03-inventory.json")
|
||||
elif demo_account_type == "enterprise":
|
||||
json_file = get_seed_data_path("enterprise", "03-inventory.json")
|
||||
elif demo_account_type == "enterprise_child":
|
||||
json_file = get_seed_data_path("enterprise", "03-inventory.json", child_id=base_tenant_id)
|
||||
else:
|
||||
raise ValueError(f"Invalid demo account type: {demo_account_type}")
|
||||
|
||||
# Load JSON data
|
||||
with open(json_file, 'r', encoding='utf-8') as f:
|
||||
seed_data = json.load(f)
|
||||
|
||||
# Check if data already exists for this virtual tenant (idempotency)
|
||||
existing_check = await db.execute(
|
||||
select(Ingredient).where(Ingredient.tenant_id == virtual_tenant_id).limit(1)
|
||||
)
|
||||
existing_ingredient = existing_check.scalar_one_or_none()
|
||||
|
||||
if existing_ingredient:
|
||||
logger.warning(
|
||||
"Demo data already exists, skipping clone",
|
||||
virtual_tenant_id=virtual_tenant_id
|
||||
)
|
||||
return {
|
||||
"status": "skipped",
|
||||
"reason": "Data already exists",
|
||||
"records_cloned": 0
|
||||
}
|
||||
|
||||
# Transform and insert data
|
||||
records_cloned = 0
|
||||
|
||||
# Clone ingredients
|
||||
for ingredient_data in seed_data.get('ingredients', []):
|
||||
# Transform ID
|
||||
from shared.utils.demo_id_transformer import transform_id
|
||||
try:
|
||||
ingredient_uuid = UUID(ingredient_data['id'])
|
||||
tenant_uuid = UUID(virtual_tenant_id)
|
||||
transformed_id = transform_id(ingredient_data['id'], tenant_uuid)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to parse UUIDs for ID transformation",
|
||||
ingredient_id=ingredient_data['id'],
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid UUID format in ingredient data: {str(e)}"
|
||||
)
|
||||
|
||||
# Transform dates using standardized helper
|
||||
ingredient_data['created_at'] = parse_date_field(
|
||||
ingredient_data.get('created_at'), session_time, 'created_at'
|
||||
) or session_time
|
||||
ingredient_data['updated_at'] = parse_date_field(
|
||||
ingredient_data.get('updated_at'), session_time, 'updated_at'
|
||||
) or session_time
|
||||
|
||||
# Map category field to ingredient_category enum
|
||||
if 'category' in ingredient_data:
|
||||
category_value = ingredient_data.pop('category')
|
||||
# Convert category string to IngredientCategory enum
|
||||
from app.models.inventory import IngredientCategory
|
||||
try:
|
||||
ingredient_data['ingredient_category'] = IngredientCategory[category_value.upper()]
|
||||
except KeyError:
|
||||
# If category not found in enum, use OTHER
|
||||
ingredient_data['ingredient_category'] = IngredientCategory.OTHER
|
||||
|
||||
# Map unit_of_measure string to enum
|
||||
if 'unit_of_measure' in ingredient_data:
|
||||
from app.models.inventory import UnitOfMeasure
|
||||
unit_mapping = {
|
||||
'kilograms': UnitOfMeasure.KILOGRAMS,
|
||||
'grams': UnitOfMeasure.GRAMS,
|
||||
'liters': UnitOfMeasure.LITERS,
|
||||
'milliliters': UnitOfMeasure.MILLILITERS,
|
||||
'units': UnitOfMeasure.UNITS,
|
||||
'pieces': UnitOfMeasure.PIECES,
|
||||
'packages': UnitOfMeasure.PACKAGES,
|
||||
'bags': UnitOfMeasure.BAGS,
|
||||
'boxes': UnitOfMeasure.BOXES
|
||||
}
|
||||
|
||||
# Also support uppercase versions
|
||||
unit_mapping.update({
|
||||
'KILOGRAMS': UnitOfMeasure.KILOGRAMS,
|
||||
'GRAMS': UnitOfMeasure.GRAMS,
|
||||
'LITERS': UnitOfMeasure.LITERS,
|
||||
'MILLILITERS': UnitOfMeasure.MILLILITERS,
|
||||
'UNITS': UnitOfMeasure.UNITS,
|
||||
'PIECES': UnitOfMeasure.PIECES,
|
||||
'PACKAGES': UnitOfMeasure.PACKAGES,
|
||||
'BAGS': UnitOfMeasure.BAGS,
|
||||
'BOXES': UnitOfMeasure.BOXES
|
||||
})
|
||||
|
||||
unit_str = ingredient_data['unit_of_measure']
|
||||
if unit_str in unit_mapping:
|
||||
ingredient_data['unit_of_measure'] = unit_mapping[unit_str]
|
||||
else:
|
||||
# Default to units if not found
|
||||
ingredient_data['unit_of_measure'] = UnitOfMeasure.UNITS
|
||||
logger.warning("Unknown unit_of_measure, defaulting to UNITS",
|
||||
original_unit=unit_str)
|
||||
|
||||
# Note: All seed data fields now match the model schema exactly
|
||||
# No field filtering needed
|
||||
|
||||
# Remove original id and tenant_id from ingredient_data to avoid conflict
|
||||
ingredient_data.pop('id', None)
|
||||
ingredient_data.pop('tenant_id', None)
|
||||
|
||||
# Create ingredient
|
||||
ingredient = Ingredient(
|
||||
id=str(transformed_id),
|
||||
tenant_id=str(virtual_tenant_id),
|
||||
**ingredient_data
|
||||
)
|
||||
db.add(ingredient)
|
||||
records_cloned += 1
|
||||
|
||||
# Commit ingredients before creating stock to ensure foreign key references exist
|
||||
await db.flush() # Use flush instead of commit to maintain transaction while continuing
|
||||
|
||||
# Clone stock batches
|
||||
for stock_data in seed_data.get('stock', []):
|
||||
# Transform ID - handle both UUID and string IDs
|
||||
from shared.utils.demo_id_transformer import transform_id
|
||||
try:
|
||||
# Try to parse as UUID first
|
||||
stock_uuid = UUID(stock_data['id'])
|
||||
tenant_uuid = UUID(virtual_tenant_id)
|
||||
transformed_id = transform_id(stock_data['id'], tenant_uuid)
|
||||
except ValueError:
|
||||
# If not a UUID, generate a deterministic UUID from the string ID
|
||||
import hashlib
|
||||
stock_id_string = stock_data['id']
|
||||
tenant_uuid = UUID(virtual_tenant_id)
|
||||
|
||||
# Create a deterministic UUID from the string ID and tenant ID
|
||||
combined = f"{stock_id_string}-{tenant_uuid}"
|
||||
hash_obj = hashlib.sha256(combined.encode('utf-8'))
|
||||
transformed_id = UUID(hash_obj.hexdigest()[:32])
|
||||
|
||||
logger.info("Generated UUID for non-UUID stock ID",
|
||||
original_id=stock_id_string,
|
||||
generated_id=str(transformed_id))
|
||||
|
||||
# Transform dates using standardized helper
|
||||
stock_data['received_date'] = parse_date_field(
|
||||
stock_data.get('received_date'), session_time, 'received_date'
|
||||
)
|
||||
stock_data['expiration_date'] = parse_date_field(
|
||||
stock_data.get('expiration_date'), session_time, 'expiration_date'
|
||||
)
|
||||
stock_data['best_before_date'] = parse_date_field(
|
||||
stock_data.get('best_before_date'), session_time, 'best_before_date'
|
||||
)
|
||||
stock_data['created_at'] = parse_date_field(
|
||||
stock_data.get('created_at'), session_time, 'created_at'
|
||||
) or session_time
|
||||
stock_data['updated_at'] = parse_date_field(
|
||||
stock_data.get('updated_at'), session_time, 'updated_at'
|
||||
) or session_time
|
||||
|
||||
# Remove original id and tenant_id from stock_data to avoid conflict
|
||||
stock_data.pop('id', None)
|
||||
stock_data.pop('tenant_id', None)
|
||||
# Remove notes field as it doesn't exist in the Stock model
|
||||
stock_data.pop('notes', None)
|
||||
|
||||
# Transform ingredient_id to match transformed ingredient IDs
|
||||
if 'ingredient_id' in stock_data:
|
||||
ingredient_id_str = stock_data['ingredient_id']
|
||||
try:
|
||||
ingredient_uuid = UUID(ingredient_id_str)
|
||||
transformed_ingredient_id = transform_id(ingredient_id_str, tenant_uuid)
|
||||
stock_data['ingredient_id'] = str(transformed_ingredient_id)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to transform ingredient_id",
|
||||
original_ingredient_id=ingredient_id_str,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid ingredient_id format: {str(e)}"
|
||||
)
|
||||
|
||||
# Transform supplier_id if present
|
||||
if 'supplier_id' in stock_data:
|
||||
supplier_id_str = stock_data['supplier_id']
|
||||
try:
|
||||
supplier_uuid = UUID(supplier_id_str)
|
||||
transformed_supplier_id = transform_id(supplier_id_str, tenant_uuid)
|
||||
stock_data['supplier_id'] = str(transformed_supplier_id)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to transform supplier_id",
|
||||
original_supplier_id=supplier_id_str,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid supplier_id format: {str(e)}"
|
||||
)
|
||||
|
||||
# Create stock batch
|
||||
stock = Stock(
|
||||
id=str(transformed_id),
|
||||
tenant_id=str(virtual_tenant_id),
|
||||
**stock_data
|
||||
)
|
||||
db.add(stock)
|
||||
records_cloned += 1
|
||||
|
||||
# Clone stock movements (for waste tracking and sustainability metrics)
|
||||
from app.models.inventory import StockMovement, StockMovementType
|
||||
|
||||
for movement_data in seed_data.get('stock_movements', []):
|
||||
# Transform ID
|
||||
from shared.utils.demo_id_transformer import transform_id
|
||||
try:
|
||||
movement_uuid = UUID(movement_data['id'])
|
||||
tenant_uuid = UUID(virtual_tenant_id)
|
||||
transformed_id = transform_id(movement_data['id'], tenant_uuid)
|
||||
except ValueError:
|
||||
import hashlib
|
||||
movement_id_string = movement_data['id']
|
||||
tenant_uuid = UUID(virtual_tenant_id)
|
||||
combined = f"{movement_id_string}-{tenant_uuid}"
|
||||
hash_obj = hashlib.sha256(combined.encode('utf-8'))
|
||||
transformed_id = UUID(hash_obj.hexdigest()[:32])
|
||||
|
||||
# Transform dates
|
||||
movement_data['movement_date'] = parse_date_field(
|
||||
movement_data.get('movement_date'), session_time, 'movement_date'
|
||||
) or session_time
|
||||
movement_data['created_at'] = parse_date_field(
|
||||
movement_data.get('created_at'), session_time, 'created_at'
|
||||
) or session_time
|
||||
|
||||
# Transform related IDs
|
||||
if 'ingredient_id' in movement_data:
|
||||
ingredient_id_str = movement_data['ingredient_id']
|
||||
try:
|
||||
transformed_ingredient_id = transform_id(ingredient_id_str, tenant_uuid)
|
||||
movement_data['ingredient_id'] = str(transformed_ingredient_id)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to transform ingredient_id in movement",
|
||||
original_id=ingredient_id_str, error=str(e))
|
||||
raise HTTPException(status_code=400, detail=f"Invalid ingredient_id: {str(e)}")
|
||||
|
||||
if 'stock_id' in movement_data and movement_data['stock_id']:
|
||||
stock_id_str = movement_data['stock_id']
|
||||
try:
|
||||
transformed_stock_id = transform_id(stock_id_str, tenant_uuid)
|
||||
movement_data['stock_id'] = str(transformed_stock_id)
|
||||
except ValueError:
|
||||
# If stock_id doesn't exist or can't be transformed, set to None
|
||||
movement_data['stock_id'] = None
|
||||
|
||||
if 'supplier_id' in movement_data and movement_data['supplier_id']:
|
||||
supplier_id_str = movement_data['supplier_id']
|
||||
try:
|
||||
transformed_supplier_id = transform_id(supplier_id_str, tenant_uuid)
|
||||
movement_data['supplier_id'] = str(transformed_supplier_id)
|
||||
except ValueError:
|
||||
movement_data['supplier_id'] = None
|
||||
|
||||
if 'created_by' in movement_data and movement_data['created_by']:
|
||||
created_by_str = movement_data['created_by']
|
||||
try:
|
||||
transformed_created_by = transform_id(created_by_str, tenant_uuid)
|
||||
movement_data['created_by'] = str(transformed_created_by)
|
||||
except ValueError:
|
||||
movement_data['created_by'] = None
|
||||
|
||||
# Remove original id and tenant_id
|
||||
movement_data.pop('id', None)
|
||||
movement_data.pop('tenant_id', None)
|
||||
|
||||
# Create stock movement
|
||||
stock_movement = StockMovement(
|
||||
id=str(transformed_id),
|
||||
tenant_id=str(virtual_tenant_id),
|
||||
**movement_data
|
||||
)
|
||||
db.add(stock_movement)
|
||||
records_cloned += 1
|
||||
|
||||
# Note: Edge cases are now handled exclusively through JSON seed data
|
||||
# The seed data files already contain comprehensive edge cases including:
|
||||
# - Low stock items below reorder points
|
||||
# - Items expiring soon
|
||||
# - Freshly received stock
|
||||
# - Waste movements for sustainability tracking
|
||||
# This ensures standardization and single source of truth for demo data
|
||||
|
||||
logger.info(
|
||||
"Edge cases handled by JSON seed data - no manual creation needed",
|
||||
seed_data_edge_cases="low_stock, expiring_soon, fresh_stock, waste_movements"
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
|
||||
logger.info(
|
||||
"Inventory data cloned successfully",
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
records_cloned=records_cloned,
|
||||
duration_ms=duration_ms,
|
||||
ingredients_cloned=len(seed_data.get('ingredients', [])),
|
||||
stock_batches_cloned=len(seed_data.get('stock', [])),
|
||||
stock_movements_cloned=len(seed_data.get('stock_movements', []))
|
||||
)
|
||||
|
||||
return {
|
||||
"service": "inventory",
|
||||
"status": "completed",
|
||||
"records_cloned": records_cloned,
|
||||
"duration_ms": duration_ms,
|
||||
"details": {
|
||||
"ingredients": len(seed_data.get('ingredients', [])),
|
||||
"stock": len(seed_data.get('stock', [])),
|
||||
"virtual_tenant_id": str(virtual_tenant_id)
|
||||
}
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
logger.error("Invalid UUID format", error=str(e), virtual_tenant_id=virtual_tenant_id)
|
||||
raise HTTPException(status_code=400, detail=f"Invalid UUID: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to clone inventory data",
|
||||
error=str(e),
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Rollback on error
|
||||
await db.rollback()
|
||||
|
||||
return {
|
||||
"service": "inventory",
|
||||
"status": "failed",
|
||||
"records_cloned": 0,
|
||||
"duration_ms": int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000),
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/clone/health")
|
||||
async def clone_health_check():
|
||||
"""
|
||||
Health check for internal cloning endpoint
|
||||
Used by orchestrator to verify service availability
|
||||
"""
|
||||
return {
|
||||
"service": "inventory",
|
||||
"clone_endpoint": "available",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/tenant/{virtual_tenant_id}")
|
||||
async def delete_demo_tenant_data(
|
||||
virtual_tenant_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Delete all demo data for a virtual tenant.
|
||||
This endpoint is idempotent - safe to call multiple times.
|
||||
"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
|
||||
from app.models.inventory import StockMovement
|
||||
|
||||
records_deleted = {
|
||||
"stock_movements": 0,
|
||||
"stock": 0,
|
||||
"ingredients": 0,
|
||||
"total": 0
|
||||
}
|
||||
|
||||
try:
|
||||
# Delete in reverse dependency order
|
||||
|
||||
# 1. Delete stock movements (depends on stock and ingredients)
|
||||
result = await db.execute(
|
||||
delete(StockMovement)
|
||||
.where(StockMovement.tenant_id == virtual_tenant_id)
|
||||
)
|
||||
records_deleted["stock_movements"] = result.rowcount
|
||||
|
||||
# 2. Delete stock batches (depends on ingredients)
|
||||
result = await db.execute(
|
||||
delete(Stock)
|
||||
.where(Stock.tenant_id == virtual_tenant_id)
|
||||
)
|
||||
records_deleted["stock"] = result.rowcount
|
||||
|
||||
# 3. Delete ingredients
|
||||
result = await db.execute(
|
||||
delete(Ingredient)
|
||||
.where(Ingredient.tenant_id == virtual_tenant_id)
|
||||
)
|
||||
records_deleted["ingredients"] = result.rowcount
|
||||
|
||||
records_deleted["total"] = records_deleted["stock_movements"] + records_deleted["stock"] + records_deleted["ingredients"]
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"demo_data_deleted",
|
||||
service="inventory",
|
||||
virtual_tenant_id=str(virtual_tenant_id),
|
||||
records_deleted=records_deleted
|
||||
)
|
||||
|
||||
return {
|
||||
"service": "inventory",
|
||||
"status": "deleted",
|
||||
"virtual_tenant_id": str(virtual_tenant_id),
|
||||
"records_deleted": records_deleted,
|
||||
"duration_ms": int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(
|
||||
"demo_data_deletion_failed",
|
||||
service="inventory",
|
||||
virtual_tenant_id=str(virtual_tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to delete demo data: {str(e)}"
|
||||
)
|
||||
Reference in New Issue
Block a user