#!/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)