""" POS Transaction Service - Business Logic Layer """ from typing import List, Optional, Dict, Any from uuid import UUID from datetime import datetime from decimal import Decimal import structlog from app.repositories.pos_transaction_repository import POSTransactionRepository from app.repositories.pos_transaction_item_repository import POSTransactionItemRepository from app.schemas.pos_transaction import ( POSTransactionResponse, POSTransactionDashboardSummary ) from app.core.database import get_db_transaction logger = structlog.get_logger() class POSTransactionService: """Service layer for POS transaction operations""" def __init__(self): pass async def get_transactions_by_tenant( self, tenant_id: UUID, pos_system: Optional[str] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, status: Optional[str] = None, is_synced: Optional[bool] = None, skip: int = 0, limit: int = 50 ) -> List[POSTransactionResponse]: """Get POS transactions for a tenant with filtering""" try: async with get_db_transaction() as db: repository = POSTransactionRepository(db) transactions = await repository.get_transactions_by_tenant( tenant_id=tenant_id, pos_system=pos_system, start_date=start_date, end_date=end_date, status=status, is_synced=is_synced, skip=skip, limit=limit ) # Convert to response schemas responses = [] for transaction in transactions: response = POSTransactionResponse.from_orm(transaction) responses.append(response) return responses except Exception as e: logger.error("Failed to get transactions by tenant", error=str(e), tenant_id=tenant_id) raise async def count_transactions_by_tenant( self, tenant_id: UUID, pos_system: Optional[str] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, status: Optional[str] = None, is_synced: Optional[bool] = None ) -> int: """Count POS transactions for a tenant with filtering""" try: async with get_db_transaction() as db: repository = POSTransactionRepository(db) count = await repository.count_transactions_by_tenant( tenant_id=tenant_id, pos_system=pos_system, start_date=start_date, end_date=end_date, status=status, is_synced=is_synced ) return count except Exception as e: logger.error("Failed to count transactions by tenant", error=str(e), tenant_id=tenant_id) raise async def get_transaction_with_items( self, transaction_id: UUID, tenant_id: UUID ) -> Optional[POSTransactionResponse]: """Get transaction with all its items""" try: async with get_db_transaction() as db: repository = POSTransactionRepository(db) transaction = await repository.get_transaction_with_items( transaction_id=transaction_id, tenant_id=tenant_id ) if not transaction: return None return POSTransactionResponse.from_orm(transaction) except Exception as e: logger.error("Failed to get transaction with items", transaction_id=str(transaction_id), error=str(e)) raise async def get_dashboard_summary( self, tenant_id: UUID ) -> POSTransactionDashboardSummary: """Get dashboard summary for POS transactions""" try: async with get_db_transaction() as db: repository = POSTransactionRepository(db) # Get metrics from repository metrics = await repository.get_dashboard_metrics(tenant_id) # Get sync status sync_status = await repository.get_sync_status_summary(tenant_id) # Construct dashboard summary return POSTransactionDashboardSummary( total_transactions_today=metrics["total_transactions_today"], total_transactions_this_week=metrics["total_transactions_this_week"], total_transactions_this_month=metrics["total_transactions_this_month"], revenue_today=Decimal(str(metrics["revenue_today"])), revenue_this_week=Decimal(str(metrics["revenue_this_week"])), revenue_this_month=Decimal(str(metrics["revenue_this_month"])), average_transaction_value=Decimal(str(metrics["average_transaction_value"])), status_breakdown=metrics["status_breakdown"], payment_method_breakdown=metrics["payment_method_breakdown"], sync_status=sync_status ) except Exception as e: logger.error("Failed to get dashboard summary", error=str(e), tenant_id=tenant_id) raise async def get_sync_metrics( self, tenant_id: UUID ) -> Dict[str, Any]: """Get sync metrics for transactions""" try: async with get_db_transaction() as db: repository = POSTransactionRepository(db) sync_status = await repository.get_sync_status_summary(tenant_id) # Calculate sync rate total = sync_status["synced"] + sync_status["pending"] + sync_status["failed"] sync_rate = (sync_status["synced"] / total * 100) if total > 0 else 0 return { "sync_status": sync_status, "sync_rate_percentage": round(sync_rate, 2), "total_transactions": total } except Exception as e: logger.error("Failed to get sync metrics", error=str(e), tenant_id=tenant_id) raise async def calculate_transaction_analytics( self, tenant_id: UUID, start_date: datetime, end_date: datetime ) -> Dict[str, Any]: """Calculate analytics for transactions within a date range""" try: async with get_db_transaction() as db: repository = POSTransactionRepository(db) transactions = await repository.get_transactions_by_date_range( tenant_id=tenant_id, start_date=start_date.date(), end_date=end_date.date(), skip=0, limit=10000 # Large limit for analytics ) # Calculate analytics total_revenue = Decimal("0") total_transactions = len(transactions) payment_methods = {} order_types = {} hourly_distribution = {} for transaction in transactions: if transaction.status == "completed": total_revenue += transaction.total_amount # Payment method breakdown pm = transaction.payment_method or "unknown" payment_methods[pm] = payment_methods.get(pm, 0) + 1 # Order type breakdown ot = transaction.order_type or "unknown" order_types[ot] = order_types.get(ot, 0) + 1 # Hourly distribution hour = transaction.transaction_date.hour hourly_distribution[hour] = hourly_distribution.get(hour, 0) + 1 avg_transaction_value = (total_revenue / total_transactions) if total_transactions > 0 else Decimal("0") return { "period": { "start_date": start_date.isoformat(), "end_date": end_date.isoformat() }, "total_revenue": float(total_revenue), "total_transactions": total_transactions, "average_transaction_value": float(avg_transaction_value), "payment_methods": payment_methods, "order_types": order_types, "hourly_distribution": hourly_distribution } except Exception as e: logger.error("Failed to calculate transaction analytics", error=str(e), tenant_id=tenant_id) raise async def sync_transaction_to_sales( self, transaction_id: UUID, tenant_id: UUID ) -> Dict[str, Any]: """ Sync a single POS transaction to the sales service Args: transaction_id: Transaction UUID tenant_id: Tenant UUID Returns: Dict with sync status and details """ try: from shared.clients.sales_client import SalesServiceClient from app.core.config import settings async with get_db_transaction() as db: transaction_repo = POSTransactionRepository(db) items_repo = POSTransactionItemRepository(db) # Get transaction transaction = await transaction_repo.get_by_id(transaction_id) if not transaction or transaction.tenant_id != tenant_id: return { "success": False, "error": "Transaction not found or unauthorized" } # Check if already synced if transaction.is_synced_to_sales: logger.info("Transaction already synced to sales", transaction_id=transaction_id, sales_record_id=transaction.sales_record_id) return { "success": True, "already_synced": True, "sales_record_id": str(transaction.sales_record_id) } # Get transaction items items = await items_repo.get_by_transaction_id(transaction_id) # Initialize sales client sales_client = SalesServiceClient(settings, calling_service_name="pos") # Create sales records for each item sales_record_ids = [] failed_items = [] for item in items: try: sales_data = { "inventory_product_id": str(item.product_id) if item.product_id else None, "product_name": item.product_name, "product_category": "finished_product", "quantity_sold": float(item.quantity), "unit_price": float(item.unit_price), "total_amount": float(item.subtotal), "sale_date": transaction.transaction_date.strftime("%Y-%m-%d"), "sales_channel": "pos", "source": f"pos_sync_{transaction.pos_system}", "payment_method": transaction.payment_method or "unknown", "notes": f"POS Transaction: {transaction.external_transaction_id or transaction_id}" } result = await sales_client.create_sales_record( tenant_id=str(tenant_id), sales_data=sales_data ) if result and result.get("id"): sales_record_ids.append(result["id"]) logger.info("Synced item to sales", transaction_id=transaction_id, item_id=item.id, sales_record_id=result["id"]) else: failed_items.append({ "item_id": str(item.id), "product_name": item.product_name, "error": "No sales record ID returned" }) except Exception as item_error: logger.error("Failed to sync item to sales", error=str(item_error), transaction_id=transaction_id, item_id=item.id) failed_items.append({ "item_id": str(item.id), "product_name": item.product_name, "error": str(item_error) }) # Update transaction sync status if sales_record_ids and len(failed_items) == 0: # Full success transaction.is_synced_to_sales = True transaction.sales_record_id = UUID(sales_record_ids[0]) # Store first record ID transaction.sync_completed_at = datetime.utcnow() await db.commit() logger.info("Transaction fully synced to sales", transaction_id=transaction_id, items_synced=len(sales_record_ids)) return { "success": True, "items_synced": len(sales_record_ids), "sales_record_ids": sales_record_ids, "failed_items": [] } elif sales_record_ids and len(failed_items) > 0: # Partial success transaction.sync_attempted_at = datetime.utcnow() transaction.sync_error = f"Partial sync: {len(failed_items)} items failed" transaction.sync_retry_count = (transaction.sync_retry_count or 0) + 1 await db.commit() logger.warning("Transaction partially synced to sales", transaction_id=transaction_id, items_synced=len(sales_record_ids), items_failed=len(failed_items)) return { "success": False, "partial_success": True, "items_synced": len(sales_record_ids), "sales_record_ids": sales_record_ids, "failed_items": failed_items } else: # Complete failure transaction.sync_attempted_at = datetime.utcnow() transaction.sync_error = "All items failed to sync" transaction.sync_retry_count = (transaction.sync_retry_count or 0) + 1 await db.commit() logger.error("Transaction sync failed completely", transaction_id=transaction_id, items_failed=len(failed_items)) return { "success": False, "items_synced": 0, "failed_items": failed_items } except Exception as e: logger.error("Failed to sync transaction to sales", error=str(e), transaction_id=transaction_id, tenant_id=tenant_id) return { "success": False, "error": str(e) } async def sync_unsynced_transactions( self, tenant_id: UUID, limit: int = 50 ) -> Dict[str, Any]: """ Sync all unsynced transactions to the sales service Args: tenant_id: Tenant UUID limit: Maximum number of transactions to sync in one batch Returns: Dict with sync summary """ try: async with get_db_transaction() as db: repository = POSTransactionRepository(db) # Get unsynced transactions unsynced_transactions = await repository.get_transactions_by_tenant( tenant_id=tenant_id, is_synced=False, status="completed", # Only sync completed transactions limit=limit ) if not unsynced_transactions: logger.info("No unsynced transactions found", tenant_id=tenant_id) return { "success": True, "total_transactions": 0, "synced": 0, "failed": 0 } synced_count = 0 failed_count = 0 results = [] for transaction in unsynced_transactions: result = await self.sync_transaction_to_sales( transaction.id, tenant_id ) if result.get("success"): synced_count += 1 else: failed_count += 1 results.append({ "transaction_id": str(transaction.id), "external_id": transaction.external_transaction_id, "result": result }) logger.info("Batch sync completed", tenant_id=tenant_id, total=len(unsynced_transactions), synced=synced_count, failed=failed_count) return { "success": True, "total_transactions": len(unsynced_transactions), "synced": synced_count, "failed": failed_count, "results": results } except Exception as e: logger.error("Failed to batch sync transactions", error=str(e), tenant_id=tenant_id) return { "success": False, "error": str(e) }