287 lines
12 KiB
Python
287 lines
12 KiB
Python
"""
|
|
Internal Demo API Endpoints for POS Service
|
|
Used by demo_session service to clone data for virtual demo tenants
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Header
|
|
from typing import Dict, Any
|
|
from uuid import UUID
|
|
import structlog
|
|
import os
|
|
|
|
from app.core.database import get_db
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, delete, func
|
|
from app.models.pos_config import POSConfiguration
|
|
from app.models.pos_transaction import POSTransaction, POSTransactionItem
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
router = APIRouter()
|
|
logger = structlog.get_logger()
|
|
|
|
# Internal API key for service-to-service communication
|
|
INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production")
|
|
|
|
|
|
def verify_internal_api_key(x_internal_api_key: str = Header(...)):
|
|
"""Verify internal API key for service-to-service communication"""
|
|
if x_internal_api_key != INTERNAL_API_KEY:
|
|
raise HTTPException(status_code=403, detail="Invalid internal API key")
|
|
return True
|
|
|
|
|
|
@router.post("/internal/demo/clone")
|
|
async def clone_demo_data(
|
|
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),
|
|
_: bool = Depends(verify_internal_api_key)
|
|
):
|
|
"""
|
|
Clone POS demo data from base tenant to virtual tenant
|
|
|
|
This endpoint is called by the demo_session service during session initialization.
|
|
It clones POS configurations and recent transactions.
|
|
"""
|
|
|
|
start_time = datetime.now(timezone.utc)
|
|
|
|
# Parse session_created_at or fallback to now
|
|
if session_created_at:
|
|
try:
|
|
session_time = datetime.fromisoformat(session_created_at.replace('Z', '+00:00'))
|
|
except (ValueError, AttributeError) as e:
|
|
logger.warning(
|
|
"Invalid session_created_at format, using current time",
|
|
session_created_at=session_created_at,
|
|
error=str(e)
|
|
)
|
|
session_time = datetime.now(timezone.utc)
|
|
else:
|
|
logger.warning("session_created_at not provided, using current time")
|
|
session_time = datetime.now(timezone.utc)
|
|
|
|
logger.info(
|
|
"Starting POS 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_time.isoformat()
|
|
)
|
|
|
|
try:
|
|
base_uuid = uuid.UUID(base_tenant_id)
|
|
virtual_uuid = uuid.UUID(virtual_tenant_id)
|
|
|
|
# Fetch base tenant POS configurations
|
|
result = await db.execute(
|
|
select(POSConfiguration).where(POSConfiguration.tenant_id == base_uuid)
|
|
)
|
|
base_configs = list(result.scalars().all())
|
|
|
|
configs_cloned = 0
|
|
transactions_cloned = 0
|
|
|
|
# Clone each configuration
|
|
for base_config in base_configs:
|
|
# Create new config for virtual tenant
|
|
new_config = POSConfiguration(
|
|
id=uuid.uuid4(),
|
|
tenant_id=virtual_uuid,
|
|
pos_system=base_config.pos_system,
|
|
provider_name=f"{base_config.provider_name} (Demo Session)",
|
|
is_active=base_config.is_active,
|
|
is_connected=base_config.is_connected,
|
|
encrypted_credentials=base_config.encrypted_credentials,
|
|
webhook_url=base_config.webhook_url,
|
|
webhook_secret=base_config.webhook_secret,
|
|
environment=base_config.environment,
|
|
location_id=base_config.location_id,
|
|
merchant_id=base_config.merchant_id,
|
|
sync_enabled=base_config.sync_enabled,
|
|
sync_interval_minutes=base_config.sync_interval_minutes,
|
|
auto_sync_products=base_config.auto_sync_products,
|
|
auto_sync_transactions=base_config.auto_sync_transactions,
|
|
last_sync_at=base_config.last_sync_at,
|
|
last_successful_sync_at=base_config.last_successful_sync_at,
|
|
last_sync_status=base_config.last_sync_status,
|
|
last_sync_message=base_config.last_sync_message,
|
|
provider_settings=base_config.provider_settings,
|
|
last_health_check_at=base_config.last_health_check_at,
|
|
health_status=base_config.health_status,
|
|
health_message=base_config.health_message,
|
|
created_at=session_time,
|
|
updated_at=session_time,
|
|
notes=f"Cloned from base config {base_config.id} for demo session {session_id}"
|
|
)
|
|
|
|
db.add(new_config)
|
|
await db.flush()
|
|
configs_cloned += 1
|
|
|
|
# Clone recent transactions for this config
|
|
tx_result = await db.execute(
|
|
select(POSTransaction)
|
|
.where(POSTransaction.pos_config_id == base_config.id)
|
|
.order_by(POSTransaction.transaction_date.desc())
|
|
.limit(10) # Clone last 10 transactions
|
|
)
|
|
base_transactions = list(tx_result.scalars().all())
|
|
|
|
# Clone each transaction
|
|
for base_tx in base_transactions:
|
|
new_tx = POSTransaction(
|
|
id=uuid.uuid4(),
|
|
tenant_id=virtual_uuid,
|
|
pos_config_id=new_config.id,
|
|
pos_system=base_tx.pos_system,
|
|
external_transaction_id=base_tx.external_transaction_id,
|
|
external_order_id=base_tx.external_order_id,
|
|
transaction_type=base_tx.transaction_type,
|
|
status=base_tx.status,
|
|
subtotal=base_tx.subtotal,
|
|
tax_amount=base_tx.tax_amount,
|
|
tip_amount=base_tx.tip_amount,
|
|
discount_amount=base_tx.discount_amount,
|
|
total_amount=base_tx.total_amount,
|
|
currency=base_tx.currency,
|
|
payment_method=base_tx.payment_method,
|
|
payment_status=base_tx.payment_status,
|
|
transaction_date=base_tx.transaction_date,
|
|
pos_created_at=base_tx.pos_created_at,
|
|
pos_updated_at=base_tx.pos_updated_at,
|
|
location_id=base_tx.location_id,
|
|
location_name=base_tx.location_name,
|
|
staff_id=base_tx.staff_id,
|
|
staff_name=base_tx.staff_name,
|
|
customer_id=base_tx.customer_id,
|
|
customer_email=base_tx.customer_email,
|
|
customer_phone=base_tx.customer_phone,
|
|
order_type=base_tx.order_type,
|
|
table_number=base_tx.table_number,
|
|
receipt_number=base_tx.receipt_number,
|
|
is_synced_to_sales=base_tx.is_synced_to_sales,
|
|
sales_record_id=base_tx.sales_record_id,
|
|
sync_attempted_at=base_tx.sync_attempted_at,
|
|
sync_completed_at=base_tx.sync_completed_at,
|
|
sync_error=base_tx.sync_error,
|
|
sync_retry_count=base_tx.sync_retry_count,
|
|
raw_data=base_tx.raw_data,
|
|
is_processed=base_tx.is_processed,
|
|
processing_error=base_tx.processing_error,
|
|
is_duplicate=base_tx.is_duplicate,
|
|
duplicate_of=base_tx.duplicate_of,
|
|
created_at=session_time,
|
|
updated_at=session_time
|
|
)
|
|
|
|
db.add(new_tx)
|
|
await db.flush()
|
|
transactions_cloned += 1
|
|
|
|
# Clone transaction items
|
|
item_result = await db.execute(
|
|
select(POSTransactionItem).where(POSTransactionItem.transaction_id == base_tx.id)
|
|
)
|
|
base_items = list(item_result.scalars().all())
|
|
|
|
for base_item in base_items:
|
|
new_item = POSTransactionItem(
|
|
id=uuid.uuid4(),
|
|
transaction_id=new_tx.id,
|
|
tenant_id=virtual_uuid,
|
|
external_item_id=base_item.external_item_id,
|
|
sku=base_item.sku,
|
|
product_name=base_item.product_name,
|
|
product_category=base_item.product_category,
|
|
product_subcategory=base_item.product_subcategory,
|
|
quantity=base_item.quantity,
|
|
unit_price=base_item.unit_price,
|
|
total_price=base_item.total_price,
|
|
discount_amount=base_item.discount_amount,
|
|
tax_amount=base_item.tax_amount,
|
|
modifiers=base_item.modifiers,
|
|
inventory_product_id=base_item.inventory_product_id,
|
|
is_mapped_to_inventory=base_item.is_mapped_to_inventory,
|
|
is_synced_to_sales=base_item.is_synced_to_sales,
|
|
sync_error=base_item.sync_error,
|
|
raw_data=base_item.raw_data,
|
|
created_at=session_time,
|
|
updated_at=session_time
|
|
)
|
|
|
|
db.add(new_item)
|
|
|
|
await db.commit()
|
|
|
|
logger.info(
|
|
"POS demo data cloned successfully",
|
|
virtual_tenant_id=str(virtual_tenant_id),
|
|
configs_cloned=configs_cloned,
|
|
transactions_cloned=transactions_cloned
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"records_cloned": configs_cloned + transactions_cloned,
|
|
"configs_cloned": configs_cloned,
|
|
"transactions_cloned": transactions_cloned,
|
|
"service": "pos"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to clone POS demo data", error=str(e), exc_info=True)
|
|
await db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to clone POS demo data: {str(e)}")
|
|
|
|
|
|
@router.delete("/internal/demo/tenant/{virtual_tenant_id}")
|
|
async def delete_demo_data(
|
|
virtual_tenant_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: bool = Depends(verify_internal_api_key)
|
|
):
|
|
"""Delete all POS data for a virtual demo tenant"""
|
|
logger.info("Deleting POS data for virtual tenant", virtual_tenant_id=virtual_tenant_id)
|
|
start_time = datetime.now(timezone.utc)
|
|
|
|
try:
|
|
virtual_uuid = uuid.UUID(virtual_tenant_id)
|
|
|
|
# Count records
|
|
config_count = await db.scalar(select(func.count(POSConfiguration.id)).where(POSConfiguration.tenant_id == virtual_uuid))
|
|
transaction_count = await db.scalar(select(func.count(POSTransaction.id)).where(POSTransaction.tenant_id == virtual_uuid))
|
|
item_count = await db.scalar(select(func.count(POSTransactionItem.id)).where(POSTransactionItem.tenant_id == virtual_uuid))
|
|
|
|
# Delete in order (items -> transactions -> configs)
|
|
await db.execute(delete(POSTransactionItem).where(POSTransactionItem.tenant_id == virtual_uuid))
|
|
await db.execute(delete(POSTransaction).where(POSTransaction.tenant_id == virtual_uuid))
|
|
await db.execute(delete(POSConfiguration).where(POSConfiguration.tenant_id == virtual_uuid))
|
|
await db.commit()
|
|
|
|
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
|
logger.info("POS data deleted successfully", virtual_tenant_id=virtual_tenant_id, duration_ms=duration_ms)
|
|
|
|
return {
|
|
"service": "pos",
|
|
"status": "deleted",
|
|
"virtual_tenant_id": virtual_tenant_id,
|
|
"records_deleted": {
|
|
"configurations": config_count,
|
|
"transactions": transaction_count,
|
|
"items": item_count,
|
|
"total": config_count + transaction_count + item_count
|
|
},
|
|
"duration_ms": duration_ms
|
|
}
|
|
except Exception as e:
|
|
logger.error("Failed to delete POS data", error=str(e), exc_info=True)
|
|
await db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|