demo seed change

This commit is contained in:
Urtzi Alfaro
2025-12-13 23:57:54 +01:00
parent f3688dfb04
commit ff830a3415
299 changed files with 20328 additions and 19485 deletions

View File

@@ -1,285 +0,0 @@
"""
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
from app.core.config import settings
router = APIRouter()
logger = structlog.get_logger()
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 != settings.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))

View File

@@ -11,8 +11,8 @@ from app.api.configurations import router as configurations_router
from app.api.transactions import router as transactions_router
from app.api.pos_operations import router as pos_operations_router
from app.api.analytics import router as analytics_router
from app.api.internal_demo import router as internal_demo_router
from app.api.audit import router as audit_router
# from app.api.internal_demo import router as internal_demo_router # REMOVED: Replaced by script-based seed data loading
from app.core.database import database_manager
from shared.service_base import StandardFastAPIService
@@ -194,7 +194,7 @@ service.add_router(configurations_router, tags=["pos-configurations"])
service.add_router(transactions_router, tags=["pos-transactions"])
service.add_router(pos_operations_router, tags=["pos-operations"])
service.add_router(analytics_router, tags=["pos-analytics"])
service.add_router(internal_demo_router, tags=["internal-demo"])
# service.add_router(internal_demo_router, tags=["internal-demo"]) # REMOVED: Replaced by script-based seed data loading
if __name__ == "__main__":

View File

@@ -1,308 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demo POS Configurations Seeding Script for POS Service
Creates realistic POS configurations and transactions for demo template tenants
This script runs as a Kubernetes init job inside the pos-service container.
"""
import asyncio
import uuid
import sys
import os
from datetime import datetime, timezone, timedelta
from pathlib import Path
# Add app to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select
import structlog
from app.models.pos_config import POSConfiguration
from app.models.pos_transaction import POSTransaction, POSTransactionItem
# Add shared path for demo utilities
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from shared.utils.demo_dates import BASE_REFERENCE_DATE
# Configure logging
logger = structlog.get_logger()
# Base demo tenant IDs
DEMO_TENANT_PROFESSIONAL = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
async def generate_pos_config_for_tenant(
db: AsyncSession,
tenant_id: uuid.UUID,
tenant_name: str,
pos_system: str,
provider_name: str
):
"""Generate a demo POS configuration for a tenant"""
logger.info(f"Generating POS config for: {tenant_name}", tenant_id=str(tenant_id), pos_system=pos_system)
# Check if config already exists
result = await db.execute(
select(POSConfiguration).where(
POSConfiguration.tenant_id == tenant_id,
POSConfiguration.pos_system == pos_system
).limit(1)
)
existing = result.scalar_one_or_none()
if existing:
logger.info(f"POS config already exists for {tenant_name}, skipping")
return {"tenant_id": str(tenant_id), "configs_created": 0, "skipped": True}
# Create demo POS configuration
config = POSConfiguration(
id=uuid.uuid4(),
tenant_id=tenant_id,
pos_system=pos_system,
provider_name=provider_name,
is_active=True,
is_connected=True,
encrypted_credentials="demo_credentials_encrypted", # In real scenario, this would be encrypted
environment="sandbox",
location_id=f"LOC-{tenant_name.replace(' ', '-').upper()}-001",
merchant_id=f"MERCH-{tenant_name.replace(' ', '-').upper()}",
sync_enabled=True,
sync_interval_minutes="5",
auto_sync_products=True,
auto_sync_transactions=True,
last_sync_at=BASE_REFERENCE_DATE - timedelta(hours=1),
last_successful_sync_at=BASE_REFERENCE_DATE - timedelta(hours=1),
last_sync_status="success",
last_sync_message="Sincronización completada exitosamente",
provider_settings={
"api_key": "demo_api_key_***",
"location_id": f"LOC-{tenant_name.replace(' ', '-').upper()}-001",
"environment": "sandbox"
},
last_health_check_at=BASE_REFERENCE_DATE - timedelta(minutes=30),
health_status="healthy",
health_message="Conexión saludable - todas las operaciones funcionando correctamente",
created_at=BASE_REFERENCE_DATE - timedelta(days=30),
updated_at=BASE_REFERENCE_DATE - timedelta(hours=1),
notes=f"Configuración demo para {tenant_name}"
)
db.add(config)
await db.flush()
logger.info(f"Created POS config for {tenant_name}", config_id=str(config.id))
# Generate demo transactions
transactions_created = await generate_demo_transactions(db, tenant_id, config.id, pos_system)
return {
"tenant_id": str(tenant_id),
"configs_created": 1,
"transactions_created": transactions_created,
"skipped": False
}
async def generate_demo_transactions(
db: AsyncSession,
tenant_id: uuid.UUID,
pos_config_id: uuid.UUID,
pos_system: str
):
"""Generate demo POS transactions"""
transactions_to_create = 10 # Create 10 demo transactions
transactions_created = 0
for i in range(transactions_to_create):
# Calculate transaction date (spread over last 7 days)
days_ago = i % 7
transaction_date = BASE_REFERENCE_DATE - timedelta(days=days_ago, hours=i % 12)
# Generate realistic transaction amounts
base_amounts = [12.50, 25.00, 45.75, 18.20, 32.00, 60.50, 15.80, 28.90, 55.00, 40.25]
subtotal = base_amounts[i % len(base_amounts)]
tax_amount = round(subtotal * 0.10, 2) # 10% tax
total_amount = subtotal + tax_amount
# Create transaction
transaction = POSTransaction(
id=uuid.uuid4(),
tenant_id=tenant_id,
pos_config_id=pos_config_id,
pos_system=pos_system,
external_transaction_id=f"{pos_system.upper()}-TXN-{i+1:05d}",
external_order_id=f"{pos_system.upper()}-ORD-{i+1:05d}",
transaction_type="sale",
status="completed",
subtotal=subtotal,
tax_amount=tax_amount,
tip_amount=0.00,
discount_amount=0.00,
total_amount=total_amount,
currency="EUR",
payment_method="card" if i % 2 == 0 else "cash",
payment_status="paid",
transaction_date=transaction_date,
pos_created_at=transaction_date,
pos_updated_at=transaction_date,
location_id=f"LOC-001",
location_name="Tienda Principal",
order_type="takeout" if i % 3 == 0 else "dine_in",
receipt_number=f"RCP-{i+1:06d}",
is_synced_to_sales=True,
sync_completed_at=transaction_date + timedelta(minutes=5),
sync_retry_count=0,
is_processed=True,
is_duplicate=False,
created_at=transaction_date,
updated_at=transaction_date
)
db.add(transaction)
await db.flush()
# Add transaction items
num_items = (i % 3) + 1 # 1-3 items per transaction
for item_idx in range(num_items):
product_names = [
"Pan de masa madre", "Croissant de mantequilla", "Pastel de chocolate",
"Baguette artesanal", "Tarta de manzana", "Bollería variada",
"Pan integral", "Galletas artesanales", "Café con leche"
]
product_name = product_names[(i + item_idx) % len(product_names)]
item_price = round(subtotal / num_items, 2)
item = POSTransactionItem(
id=uuid.uuid4(),
transaction_id=transaction.id,
tenant_id=tenant_id,
external_item_id=f"ITEM-{i+1:05d}-{item_idx+1}",
sku=f"SKU-{(i + item_idx) % len(product_names):03d}",
product_name=product_name,
product_category="bakery",
quantity=1,
unit_price=item_price,
total_price=item_price,
discount_amount=0.00,
tax_amount=round(item_price * 0.10, 2),
is_mapped_to_inventory=False,
is_synced_to_sales=True,
created_at=transaction_date,
updated_at=transaction_date
)
db.add(item)
transactions_created += 1
logger.info(f"Created {transactions_created} demo transactions for tenant {tenant_id}")
return transactions_created
async def seed_all(db: AsyncSession):
"""Seed all demo tenants with POS configurations"""
logger.info("Starting demo POS configurations seed process")
results = []
# Seed Professional Bakery with Square POS (merged from San Pablo + La Espiga)
result_professional = await generate_pos_config_for_tenant(
db,
DEMO_TENANT_PROFESSIONAL,
"Professional Bakery",
"square",
"Square POS - Professional Bakery"
)
results.append(result_professional)
await db.commit()
total_configs = sum(r["configs_created"] for r in results)
total_transactions = sum(r.get("transactions_created", 0) for r in results)
return {
"results": results,
"total_configs_created": total_configs,
"total_transactions_created": total_transactions,
"status": "completed"
}
def validate_base_reference_date():
"""Ensure BASE_REFERENCE_DATE hasn't changed since last seed"""
expected_date = datetime(2025, 1, 8, 6, 0, 0, tzinfo=timezone.utc)
if BASE_REFERENCE_DATE != expected_date:
logger.warning(
"BASE_REFERENCE_DATE has changed! This may cause date inconsistencies.",
current=BASE_REFERENCE_DATE.isoformat(),
expected=expected_date.isoformat()
)
# Don't fail - just warn. Allow intentional changes.
logger.info("BASE_REFERENCE_DATE validation", date=BASE_REFERENCE_DATE.isoformat())
async def main():
"""Main execution function"""
validate_base_reference_date() # Add this line
# Get database URL from environment
database_url = os.getenv("POS_DATABASE_URL")
if not database_url:
logger.error("POS_DATABASE_URL environment variable must be set")
return 1
# Ensure asyncpg driver
if database_url.startswith("postgresql://"):
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
# Create async engine
engine = create_async_engine(database_url, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
async with async_session() as session:
result = await seed_all(session)
logger.info(
"POS configurations seed completed successfully!",
total_configs=result["total_configs_created"],
total_transactions=result["total_transactions_created"],
status=result["status"]
)
# Print summary
print("\n" + "="*60)
print("DEMO POS CONFIGURATIONS SEED SUMMARY")
print("="*60)
for tenant_result in result["results"]:
tenant_id = tenant_result["tenant_id"]
configs = tenant_result["configs_created"]
transactions = tenant_result.get("transactions_created", 0)
skipped = tenant_result.get("skipped", False)
status = "SKIPPED (already exists)" if skipped else f"CREATED {configs} config(s), {transactions} transaction(s)"
print(f"Tenant {tenant_id}: {status}")
print(f"\nTotal Configs: {result['total_configs_created']}")
print(f"Total Transactions: {result['total_transactions_created']}")
print("="*60 + "\n")
return 0
except Exception as e:
logger.error(f"POS configurations seed failed: {str(e)}", exc_info=True)
return 1
finally:
await engine.dispose()
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View File

@@ -1,285 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demo Retail POS Configurations Seeding Script for POS Service
Creates realistic POS configurations for child retail outlets
This script runs as a Kubernetes init job inside the pos-service container.
It populates child retail tenants with POS system configurations.
Usage:
python /app/scripts/demo/seed_demo_pos_retail.py
Environment Variables Required:
POS_DATABASE_URL - PostgreSQL connection string for POS database
DEMO_MODE - Set to 'production' for production seeding
LOG_LEVEL - Logging level (default: INFO)
"""
import asyncio
import uuid
import sys
import os
from datetime import datetime, timezone, timedelta
from pathlib import Path
# Add app to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
# Add shared to path for demo utilities
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent))
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select
import structlog
from shared.utils.demo_dates import BASE_REFERENCE_DATE
from app.models.pos_config import POSConfiguration
# Configure logging
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer()
]
)
logger = structlog.get_logger()
# Fixed Demo Tenant IDs (must match tenant service)
DEMO_TENANT_CHILD_1 = uuid.UUID("d4e5f6a7-b8c9-40d1-e2f3-a4b5c6d7e8f9") # Madrid Centro
DEMO_TENANT_CHILD_2 = uuid.UUID("e5f6a7b8-c9d0-41e2-f3a4-b5c6d7e8f9a0") # Barcelona Gràcia
DEMO_TENANT_CHILD_3 = uuid.UUID("f6a7b8c9-d0e1-42f3-a4b5-c6d7e8f9a0b1") # Valencia Ruzafa
# POS system configurations for retail outlets
RETAIL_POS_CONFIGS = [
(DEMO_TENANT_CHILD_1, "Madrid Centro", "square", "Square"),
(DEMO_TENANT_CHILD_2, "Barcelona Gràcia", "square", "Square"),
(DEMO_TENANT_CHILD_3, "Valencia Ruzafa", "sumup", "SumUp") # Different POS system for variety
]
async def seed_retail_pos_for_tenant(
db: AsyncSession,
tenant_id: uuid.UUID,
tenant_name: str,
pos_system: str,
provider_name: str
) -> dict:
"""
Generate a demo POS configuration for a retail tenant
Args:
db: Database session
tenant_id: UUID of the child tenant
tenant_name: Name of the tenant (for logging)
pos_system: POS system type (square, sumup, etc.)
provider_name: Provider name for display
Returns:
Dict with seeding statistics
"""
logger.info("" * 80)
logger.info(f"Generating POS config for: {tenant_name}")
logger.info(f"Tenant ID: {tenant_id}")
logger.info(f"POS System: {pos_system}")
logger.info("" * 80)
# Check if config already exists
result = await db.execute(
select(POSConfiguration).where(
POSConfiguration.tenant_id == tenant_id,
POSConfiguration.pos_system == pos_system
).limit(1)
)
existing = result.scalar_one_or_none()
if existing:
logger.info(f"POS config already exists for {tenant_name}, skipping")
return {"tenant_id": str(tenant_id), "configs_created": 0, "skipped": True}
# Create demo POS configuration for retail outlet
config = POSConfiguration(
id=uuid.uuid4(),
tenant_id=tenant_id,
pos_system=pos_system,
provider_name=provider_name,
is_active=True,
is_connected=True,
encrypted_credentials="demo_retail_credentials_encrypted",
environment="sandbox",
location_id=f"LOC-{tenant_name.replace(' ', '-').upper()}-001",
merchant_id=f"MERCH-RETAIL-{tenant_name.replace(' ', '-').upper()}",
sync_enabled=True,
sync_interval_minutes="5", # Sync every 5 minutes for retail
auto_sync_products=True,
auto_sync_transactions=True,
last_sync_at=BASE_REFERENCE_DATE - timedelta(minutes=5),
last_successful_sync_at=BASE_REFERENCE_DATE - timedelta(minutes=5),
last_sync_status="success",
last_sync_message="Retail POS sync completed successfully",
provider_settings={
"api_key": f"demo_retail_{pos_system}_api_key_***",
"location_id": f"LOC-{tenant_name.replace(' ', '-').upper()}-001",
"environment": "sandbox",
"device_id": f"DEVICE-RETAIL-{str(tenant_id).split('-')[0].upper()}",
"receipt_footer": f"¡Gracias por visitar {tenant_name}!",
"tax_enabled": True,
"tax_rate": 10.0, # 10% IVA
"currency": "EUR"
},
last_health_check_at=BASE_REFERENCE_DATE - timedelta(minutes=1),
health_status="healthy",
health_message="Retail POS system operational - all services running",
created_at=BASE_REFERENCE_DATE - timedelta(days=60), # Configured 60 days ago
updated_at=BASE_REFERENCE_DATE - timedelta(minutes=5),
notes=f"Demo POS configuration for {tenant_name} retail outlet"
)
db.add(config)
await db.commit()
logger.info(f" ✅ Created POS config: {pos_system}")
logger.info("")
return {
"tenant_id": str(tenant_id),
"tenant_name": tenant_name,
"configs_created": 1,
"pos_system": pos_system,
"skipped": False
}
async def seed_retail_pos(db: AsyncSession):
"""
Seed retail POS configurations for all child tenant templates
Args:
db: Database session
Returns:
Dict with overall seeding statistics
"""
logger.info("=" * 80)
logger.info("💳 Starting Demo Retail POS Seeding")
logger.info("=" * 80)
logger.info("Creating POS system configurations for retail outlets")
logger.info("")
results = []
# Seed POS configs for each retail outlet
for tenant_id, tenant_name, pos_system, provider_name in RETAIL_POS_CONFIGS:
logger.info("")
result = await seed_retail_pos_for_tenant(
db,
tenant_id,
f"{tenant_name} (Retail Outlet)",
pos_system,
provider_name
)
results.append(result)
# Calculate totals
total_configs = sum(r["configs_created"] for r in results if not r["skipped"])
logger.info("=" * 80)
logger.info("✅ Demo Retail POS Seeding Completed")
logger.info("=" * 80)
return {
"service": "pos_retail",
"tenants_seeded": len(results),
"total_configs_created": total_configs,
"results": results
}
async def main():
"""Main execution function"""
logger.info("Demo Retail POS Seeding Script Starting")
logger.info("Mode: %s", os.getenv("DEMO_MODE", "development"))
logger.info("Log Level: %s", os.getenv("LOG_LEVEL", "INFO"))
# Get database URL from environment
database_url = os.getenv("POS_DATABASE_URL") or os.getenv("DATABASE_URL")
if not database_url:
logger.error("❌ POS_DATABASE_URL or DATABASE_URL environment variable must be set")
return 1
# Convert to async URL if needed
if database_url.startswith("postgresql://"):
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
logger.info("Connecting to POS database")
# Create engine and session
engine = create_async_engine(
database_url,
echo=False,
pool_pre_ping=True,
pool_size=5,
max_overflow=10
)
async_session = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)
try:
async with async_session() as session:
result = await seed_retail_pos(session)
logger.info("")
logger.info("📊 Retail POS Seeding Summary:")
logger.info(f" ✅ Retail outlets configured: {result['tenants_seeded']}")
logger.info(f" ✅ Total POS configs: {result['total_configs_created']}")
logger.info("")
# Print per-tenant details
for tenant_result in result['results']:
if not tenant_result['skipped']:
logger.info(
f" {tenant_result['tenant_name']}: "
f"{tenant_result['pos_system']} configured"
)
logger.info("")
logger.info("🎉 Success! Retail POS systems are ready for cloning.")
logger.info("")
logger.info("POS configuration details:")
logger.info(" ✓ Auto-sync enabled (5-minute intervals)")
logger.info(" ✓ Product and transaction sync configured")
logger.info(" ✓ Tax settings: 10% IVA (Spain)")
logger.info(" ✓ Multiple POS providers (Square, SumUp)")
logger.info(" ✓ Sandbox environment for testing")
logger.info("")
logger.info("Next steps:")
logger.info(" 1. Seed retail forecasting models")
logger.info(" 2. Seed retail alerts")
logger.info(" 3. Test POS transaction integration")
logger.info("")
return 0
except Exception as e:
logger.error("=" * 80)
logger.error("❌ Demo Retail POS Seeding Failed")
logger.error("=" * 80)
logger.error("Error: %s", str(e))
logger.error("", exc_info=True)
return 1
finally:
await engine.dispose()
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)