Procházet zdrojové kódy

modularize code, and fully travel to toteat

Erwin Jacimino před 1 dnem
rodič
revize
c7540d665f
4 změnil soubory, kde provedl 188 přidání a 170 odebrání
  1. 47 169
      routes/orders.py
  2. 1 1
      routes/products.py
  3. 109 0
      services/order_service.py
  4. 31 0
      services/toteat_service.py

+ 47 - 169
routes/orders.py

@@ -1,208 +1,86 @@
 from logging import getLogger
-from typing import List
-from uuid import uuid4
-
-from pydantic import BaseModel
 
 from fastapi import APIRouter, Depends
 
-from toteat import toteat as fudo
-from models.sales import ItemWeb, OrderWeb
-from models.items import Product
+from models.sales import OrderWeb
 from models.user import User
-from services.fudo_service import add_product_to_fudo
-from services.data_service import DataServiceFactory
-from config.messages import ErrorResponse, SuccessResponse
 from auth.security import get_current_user
+from config.messages import ErrorResponse, SuccessResponse
 from utils.responses import error_response, success_response
+from services.order_service import (
+    compare_prices,
+    fetch_sorted_products,
+    push_items_to_toteat,
+    get_active_sale_id,
+    update_reward_progress,
+    create_sale_record,
+)
 
 logger = getLogger(__name__)
 
-# Data services initialization
-user_data_service = DataServiceFactory.get_user_service()
-product_data_service = DataServiceFactory.get_product_service()
-sale_data_service = DataServiceFactory.get_sales_service()
-
 order_router = APIRouter()
-name_promo = "Cervezas"
-
-
-class ComparePricesResponse(BaseModel):
-    product: Product
-    oldPrice: int
-    newPrice: int
-    isAvailable: bool
-
-
-def compare_prices(products: List[Product], items: List[ItemWeb]) -> list:
-    """Compare prices of products and items and return the cheapest product"""
-    # Initialize a dictionary to store the prices
-    prices: List[ComparePricesResponse] = []
-    for product, item in zip(products, items):
-        if product.status == 0:
-            prices.append(
-                ComparePricesResponse(
-                    product=product,
-                    oldPrice=item.price,
-                    newPrice=product.price,
-                    isAvailable=False
-                )
-            )
-            continue
-        if product.price != item.price:
-            prices.append(
-                ComparePricesResponse(
-                    product=product,
-                    oldPrice=item.price,
-                    newPrice=product.price,
-                    isAvailable=True
-                )
-            )
-
-    return list(map(lambda x: x.model_dump(), prices))
 
 
 @order_router.post("/send")
 async def printer_order(order: OrderWeb, current_user: User = Depends(get_current_user)):
-    """Process printer order"""
-    logger.info(f"Printer order received from user {current_user.email} for table {order.table}")
-    
-    # Extract order data
-    items = order.items
-    table = order.table
+    """Procesa un pedido: valida, registra en Toteat POS y persiste en BD."""
+    items, table = order.items, order.table
+    logger.info(f"Order received from {current_user.email} for table {table}")
 
-    # Input validation
+    # 1. Validación de entrada
     if not items or not table:
-        logger.warning(f"Invalid order data from user {current_user.email}: missing items or table")
+        logger.warning(f"Missing fields from {current_user.email}")
         return error_response(message=ErrorResponse.MISSING_FIELDS, status_code=400)
-
     if not isinstance(table, int):
-        logger.warning(f"Invalid table type from user {current_user.email}: {type(table)}")
-       
+        logger.warning(f"Invalid table type from {current_user.email}: {type(table)}")
         return error_response(message=ErrorResponse.INVALID_TABLE_TYPE, status_code=400)
 
-    logger.info(f"Processing order for table {table} with {len(items)} items")
-
-
-
-    # Get products data
+    # 2. Verificar precios actuales en BD
     try:
-        products = await product_data_service.get_products([item.id for item in items])
-        # Me aseguro de que los items y los productos esten en el mismo orden
-        products = list(sorted(products, key=lambda x: x.id))
-        items = list(sorted(items, key=lambda x: x.id))
-        logger.info(f"Retrieved {len(products)} products from database")
-        
-        
+        products = await fetch_sorted_products(items)
+        items = sorted(items, key=lambda x: x.id)
+        logger.info(f"Retrieved {len(products)} products for table {table}")
     except Exception as e:
-        error_msg = f"Error getting products: {e}"
-        logger.error(error_msg)
-        return error_response(message=error_msg, status_code=500)
-    # Comparo los precios de los items con los productos
-    prices = compare_prices(products, items)
-    if prices:
-        return success_response(data=prices, message="El estado de los productos y items coincide con el de la venta", status_code=409)
+        return error_response(message=f"Error getting products: {e}", status_code=500)
 
-    # Add products to Fudo
-    product_errors = []
+    price_changes = compare_prices(products, items)
+    if price_changes:
+        return success_response(
+            data=price_changes,
+            message="El estado de los productos y items coincide con el de la venta",
+            status_code=409,
+        )
 
-    beers_for_promo = 0
-    
+    # 3. Registrar en Toteat POS
     try:
-        fudo.get_token()
-        logger.info("Fudo token obtained successfully")
-        
-        for item, product in zip(items, products):
-            try:
-               
-                if product.type == name_promo and current_user.name != "Guest":
-                    beers_for_promo += item.quantity
-                    logger.debug(f"Added {item.quantity} beers for promotion calculation")
-                
-                fudo_product = add_product_to_fudo(item.id, item.quantity, table)
-                
-                logger.info(f"Added product {item.id} to table {table} with quantity {item.quantity} in fudo")
-            
-                if not fudo_product: 
-                    error_msg = f"Error adding product {item.id} to table {table}."
-                    product_errors.append(error_msg)
-                    logger.error(error_msg)
-                   
-                    
-            except Exception as e:
-                error_msg = f"Error processing product {item.id}: {e}"
-                logger.error(error_msg)
-                product_errors.append(error_msg)
-                
-                
+        product_errors, beers = push_items_to_toteat(items, products, table, current_user.name)
     except Exception as e:
-        error_msg = f"Error with Fudo integration: {e}"
-        logger.error(error_msg)
-        
-        return error_response(message=error_msg, status_code=500)
+        return error_response(message=f"Error with Toteat integration: {e}", status_code=500)
 
     if product_errors:
-        logger.error(f"Product errors occurred: {product_errors}")
-        
-        return error_response(error={"errors": product_errors}, message=ErrorResponse.PRODUCT_ADD_ERROR, status_code=424)
+        logger.error(f"Product errors for table {table}: {product_errors}")
+        return error_response(error={"errors": product_errors},
+                              message=ErrorResponse.PRODUCT_ADD_ERROR, status_code=424)
 
-
-    
-    # Get active sale
+    # 4. Venta activa de la mesa
     try:
-        active_sale_id = fudo.get_active_sale(fudo.get_table(table))
+        active_sale_id = get_active_sale_id(table)
         if not active_sale_id:
-            error_msg = f"No active sale found for table {table}"
-            logger.error(error_msg)
-            
-            return error_response(message=error_msg, status_code=404)
-            
-        active_sale_id = active_sale_id['id']
-        logger.info(f"Active sale found for table {table}: {active_sale_id}")
+            return error_response(message=f"No active sale found for table {table}", status_code=404)
+        logger.info(f"Active sale for table {table}: {active_sale_id}")
     except Exception as e:
-        error_msg = f"Error retrieving active sale for table {table}: {e}"
-        logger.error(error_msg)
-        
-        return error_response(message=error_msg, status_code=500)
+        return error_response(message=f"Error retrieving active sale: {e}", status_code=500)
 
-    # Update user reward progress
-    try:
-        new_progress = current_user.reward_progress + beers_for_promo * 10
-        user_data_service.set_reward_progress(current_user.id, new_progress)
-        
-        logger.info(f"Updated reward progress for user {current_user.email}: {current_user.reward_progress} -> {new_progress}")
-        
-        
-    except Exception as e:
-        error_msg = f"Error updating reward progress for user {current_user.id}: {e}"
-        logger.error(error_msg)
-        # Don't fail the order for this, just log it
-        new_progress = current_user.reward_progress
+    # 5. Puntos de fidelización (no bloquea el pedido si falla)
+    new_progress = update_reward_progress(current_user, beers)
 
-    # Create sale record
+    # 6. Persistir en BD
     try:
-        sale = sale_data_service.create(
-            order.customerId,
-            active_sale_id or uuid4().hex,
-            order.totalAmount,
-            order.table,
-            [item.id for item in items],
-            [item.quantity for item in items]
-        )   
-        
-        if sale > 0:
-            logger.info(f"Sale created successfully: ID {sale}")
-        else:
-            error_msg = "Failed to create sale record"
-            logger.error(error_msg)
-            return error_response(message=error_msg, status_code=500)
-            
+        sale_id = create_sale_record(order, active_sale_id, items)
+        if sale_id <= 0:
+            return error_response(message="Failed to create sale record", status_code=500)
     except Exception as e:
-        error_msg = f"Error creating sale record: {e}"
-        logger.error(error_msg)
-        
-        return error_response(message=error_msg, status_code=500)
+        return error_response(message=f"Error creating sale record: {e}", status_code=500)
 
-    logger.info(f"Order processing completed successfully for table {table}, sale ID: {sale}")
+    logger.info(f"Order completed for table {table}, sale ID: {sale_id}")
     return success_response(data={"new_progress": new_progress}, message=SuccessResponse.ORDER_SUCCESS)
-

+ 1 - 1
routes/products.py

@@ -29,7 +29,7 @@ from models.items import Product, ProductCreateRequest, ProductEditRequest
 from services.data_service import DataServiceFactory
 from config.messages import ErrorResponse, SuccessResponse, UserResponse
 from utils.responses import error_response, success_response
-from services.fudo_service import get_products_by_table
+from services.toteat_service import get_products_by_table
 
 # Initialize logger for this module
 logger = getLogger(__name__)

+ 109 - 0
services/order_service.py

@@ -0,0 +1,109 @@
+from typing import List, Optional, Tuple
+from uuid import uuid4
+from logging import getLogger
+from pydantic import BaseModel
+
+import toteat.toteat as toteat
+from models.sales import ItemWeb, OrderWeb
+from models.items import Product
+from models.user import User
+from services.toteat_service import add_order_item
+from services.data_service import DataServiceFactory
+
+logger = getLogger(__name__)
+
+_user_svc = DataServiceFactory.get_user_service()
+_product_svc = DataServiceFactory.get_product_service()
+_sale_svc = DataServiceFactory.get_sales_service()
+
+PROMO_PRODUCT_TYPE = "Cervezas"
+REWARD_POINTS_PER_BEER = 10
+
+
+class ComparePricesResponse(BaseModel):
+    product: Product
+    oldPrice: int
+    newPrice: int
+    isAvailable: bool
+
+
+def compare_prices(products: List[Product], items: List[ItemWeb]) -> list:
+    """Detecta diferencias de precio o productos inactivos entre el carrito y la BD."""
+    result = []
+    for product, item in zip(products, items):
+        if product.status == 0:
+            result.append(ComparePricesResponse(
+                product=product, oldPrice=item.price,
+                newPrice=product.price, isAvailable=False
+            ))
+        elif product.price != item.price:
+            result.append(ComparePricesResponse(
+                product=product, oldPrice=item.price,
+                newPrice=product.price, isAvailable=True
+            ))
+    return [r.model_dump() for r in result]
+
+
+async def fetch_sorted_products(items: List[ItemWeb]) -> List[Product]:
+    """Obtiene productos de la BD ordenados igual que los items del carrito."""
+    products = await _product_svc.get_products([item.id for item in items])
+    return sorted(products, key=lambda x: x.id)
+
+
+def push_items_to_toteat(
+    items: List[ItemWeb],
+    products: List[Product],
+    table: int,
+    user_name: str,
+) -> Tuple[List[str], int]:
+    """
+    Registra cada item en Toteat POS.
+    Retorna (errores, cervezas_para_promo).
+    Lanza excepción si toteat.get_token() falla — el handler la captura como error fatal.
+    """
+    toteat.get_token()
+    errors: List[str] = []
+    beers = 0
+    for item, product in zip(items, products):
+        try:
+            if product.type == PROMO_PRODUCT_TYPE and user_name != "Guest":
+                beers += item.quantity
+            result = add_order_item(item.id, item.quantity, table)
+            if not result:
+                errors.append(f"Error adding product {item.id} to table {table}.")
+        except Exception as e:
+            errors.append(f"Error processing product {item.id}: {e}")
+    return errors, beers
+
+
+def get_active_sale_id(table: int) -> Optional[str]:
+    """Devuelve el ID de la venta activa de la mesa en Toteat, o None si no hay."""
+    sale = toteat.get_active_sale(toteat.get_table(table))
+    return sale['id'] if sale else None
+
+
+def update_reward_progress(user: User, beers: int) -> int:
+    """
+    Actualiza puntos de fidelización del usuario.
+    No lanza excepción — un fallo aquí no bloquea el pedido.
+    """
+    new_progress = user.reward_progress + beers * REWARD_POINTS_PER_BEER
+    try:
+        _user_svc.set_reward_progress(user.id, new_progress)
+        logger.info(f"Reward progress updated for user {user.email}: {user.reward_progress} -> {new_progress}")
+    except Exception as e:
+        logger.error(f"Error updating reward progress for user {user.id}: {e}")
+        new_progress = user.reward_progress
+    return new_progress
+
+
+def create_sale_record(order: OrderWeb, active_sale_id: str, items: List[ItemWeb]) -> int:
+    """Persiste la venta en PostgreSQL. Retorna el ID o -1 si falló."""
+    return _sale_svc.create(
+        order.customerId,
+        active_sale_id or uuid4().hex,
+        order.totalAmount,
+        order.table,
+        [item.id for item in items],
+        [item.quantity for item in items],
+    )

+ 31 - 0
services/toteat_service.py

@@ -0,0 +1,31 @@
+import toteat.toteat as toteat
+from logging import getLogger
+
+logger = getLogger(__name__)
+
+
+def add_order_item(product_id: str, quantity: int, table_number: int, comment=None):
+    """Agrega un item a la venta activa de la mesa en Toteat."""
+    table = toteat.get_table(table_number)
+    if not table:
+        logger.error(f"Error: Table {table_number} not found.")
+        return None
+
+    active_sale = toteat.get_active_sale(table)
+    if not active_sale:
+        active_sale = toteat.create_sale(table['id'])
+        if not active_sale:
+            logger.error(f"Error: Could not create sale for table {table_number}.")
+            return None
+
+    item = toteat.create_item(product_id, quantity, active_sale['id'], comment)
+    if not item:
+        logger.error(f"Error: Could not create item for product {product_id}.")
+        return None
+
+    return item
+
+
+def get_products_by_table(table_number: int):
+    """Retorna los productos activos de una mesa desde Toteat."""
+    return toteat.get_table_items(table_number)