# app/services/cart_service.py """ Shopping cart service. This module provides: - Session-based cart management - Cart item operations (add, update, remove) - Cart total calculations """ import logging from typing import Dict, List, Optional from datetime import datetime, timezone from sqlalchemy.orm import Session from sqlalchemy import and_ from models.database.product import Product from models.database.vendor import Vendor from models.database.cart import CartItem from app.exceptions import ( ProductNotFoundException, CartItemNotFoundException, CartValidationException, InsufficientInventoryForCartException, InvalidCartQuantityException, ProductNotAvailableForCartException, ) logger = logging.getLogger(__name__) class CartService: """Service for managing shopping carts.""" def get_cart( self, db: Session, vendor_id: int, session_id: str ) -> Dict: """ Get cart contents for a session. Args: db: Database session vendor_id: Vendor ID session_id: Session ID Returns: Cart data with items and totals """ logger.info( f"[CART_SERVICE] get_cart called", extra={ "vendor_id": vendor_id, "session_id": session_id, } ) # Fetch cart items from database cart_items = db.query(CartItem).filter( and_( CartItem.vendor_id == vendor_id, CartItem.session_id == session_id ) ).all() logger.info( f"[CART_SERVICE] Found {len(cart_items)} items in database", extra={"item_count": len(cart_items)} ) # Build response items = [] subtotal = 0.0 for cart_item in cart_items: product = cart_item.product line_total = cart_item.line_total items.append({ "product_id": product.id, "product_name": product.marketplace_product.title, "quantity": cart_item.quantity, "price": cart_item.price_at_add, "line_total": line_total, "image_url": product.marketplace_product.image_link if product.marketplace_product else None, }) subtotal += line_total cart_data = { "vendor_id": vendor_id, "session_id": session_id, "items": items, "subtotal": subtotal, "total": subtotal # Could add tax/shipping later } logger.info( f"[CART_SERVICE] get_cart returning: {len(cart_data['items'])} items, total: {cart_data['total']}", extra={"cart": cart_data} ) return cart_data def add_to_cart( self, db: Session, vendor_id: int, session_id: str, product_id: int, quantity: int = 1 ) -> Dict: """ Add product to cart. Args: db: Database session vendor_id: Vendor ID session_id: Session ID product_id: Product ID quantity: Quantity to add Returns: Updated cart Raises: ProductNotFoundException: If product not found InsufficientInventoryException: If not enough inventory """ logger.info( f"[CART_SERVICE] add_to_cart called", extra={ "vendor_id": vendor_id, "session_id": session_id, "product_id": product_id, "quantity": quantity } ) # Verify product exists and belongs to vendor product = db.query(Product).filter( and_( Product.id == product_id, Product.vendor_id == vendor_id, Product.is_active == True ) ).first() if not product: logger.error( f"[CART_SERVICE] Product not found", extra={"product_id": product_id, "vendor_id": vendor_id} ) raise ProductNotFoundException(product_id=product_id, vendor_id=vendor_id) logger.info( f"[CART_SERVICE] Product found: {product.marketplace_product.title}", extra={ "product_id": product_id, "product_name": product.marketplace_product.title, "available_inventory": product.available_inventory } ) # Get current price (use sale_price if available, otherwise regular price) current_price = product.sale_price if product.sale_price else product.price # Check if item already exists in cart existing_item = db.query(CartItem).filter( and_( CartItem.vendor_id == vendor_id, CartItem.session_id == session_id, CartItem.product_id == product_id ) ).first() if existing_item: # Update quantity new_quantity = existing_item.quantity + quantity # Check inventory for new total quantity if product.available_inventory < new_quantity: logger.warning( f"[CART_SERVICE] Insufficient inventory for update", extra={ "product_id": product_id, "current_in_cart": existing_item.quantity, "adding": quantity, "requested_total": new_quantity, "available": product.available_inventory } ) raise InsufficientInventoryForCartException( product_id=product_id, product_name=product.marketplace_product.title, requested=new_quantity, available=product.available_inventory ) existing_item.quantity = new_quantity db.commit() db.refresh(existing_item) logger.info( f"[CART_SERVICE] Updated existing cart item", extra={ "cart_item_id": existing_item.id, "new_quantity": new_quantity } ) return { "message": "Product quantity updated in cart", "product_id": product_id, "quantity": new_quantity } else: # Check inventory for new item if product.available_inventory < quantity: logger.warning( f"[CART_SERVICE] Insufficient inventory", extra={ "product_id": product_id, "requested": quantity, "available": product.available_inventory } ) raise InsufficientInventoryForCartException( product_id=product_id, product_name=product.marketplace_product.title, requested=quantity, available=product.available_inventory ) # Create new cart item cart_item = CartItem( vendor_id=vendor_id, session_id=session_id, product_id=product_id, quantity=quantity, price_at_add=current_price ) db.add(cart_item) db.commit() db.refresh(cart_item) logger.info( f"[CART_SERVICE] Created new cart item", extra={ "cart_item_id": cart_item.id, "quantity": quantity, "price": current_price } ) return { "message": "Product added to cart", "product_id": product_id, "quantity": quantity } def update_cart_item( self, db: Session, vendor_id: int, session_id: str, product_id: int, quantity: int ) -> Dict: """ Update quantity of item in cart. Args: db: Database session vendor_id: Vendor ID session_id: Session ID product_id: Product ID quantity: New quantity (must be >= 1) Returns: Success message Raises: ValidationException: If quantity < 1 ProductNotFoundException: If product not found InsufficientInventoryException: If not enough inventory """ if quantity < 1: raise InvalidCartQuantityException(quantity=quantity, min_quantity=1) # Find cart item cart_item = db.query(CartItem).filter( and_( CartItem.vendor_id == vendor_id, CartItem.session_id == session_id, CartItem.product_id == product_id ) ).first() if not cart_item: raise CartItemNotFoundException(product_id=product_id, session_id=session_id) # Verify product still exists and is active product = db.query(Product).filter( and_( Product.id == product_id, Product.vendor_id == vendor_id, Product.is_active == True ) ).first() if not product: raise ProductNotFoundException(str(product_id)) # Check inventory if product.available_inventory < quantity: raise InsufficientInventoryForCartException( product_id=product_id, product_name=product.marketplace_product.title, requested=quantity, available=product.available_inventory ) # Update quantity cart_item.quantity = quantity db.commit() db.refresh(cart_item) logger.info( f"[CART_SERVICE] Updated cart item quantity", extra={ "cart_item_id": cart_item.id, "product_id": product_id, "new_quantity": quantity } ) return { "message": "Cart updated", "product_id": product_id, "quantity": quantity } def remove_from_cart( self, db: Session, vendor_id: int, session_id: str, product_id: int ) -> Dict: """ Remove item from cart. Args: db: Database session vendor_id: Vendor ID session_id: Session ID product_id: Product ID Returns: Success message Raises: ProductNotFoundException: If product not in cart """ # Find and delete cart item cart_item = db.query(CartItem).filter( and_( CartItem.vendor_id == vendor_id, CartItem.session_id == session_id, CartItem.product_id == product_id ) ).first() if not cart_item: raise CartItemNotFoundException(product_id=product_id, session_id=session_id) db.delete(cart_item) db.commit() logger.info( f"[CART_SERVICE] Removed item from cart", extra={ "cart_item_id": cart_item.id, "product_id": product_id, "session_id": session_id } ) return { "message": "Item removed from cart", "product_id": product_id } def clear_cart( self, db: Session, vendor_id: int, session_id: str ) -> Dict: """ Clear all items from cart. Args: db: Database session vendor_id: Vendor ID session_id: Session ID Returns: Success message with count of items removed """ # Delete all cart items for this session deleted_count = db.query(CartItem).filter( and_( CartItem.vendor_id == vendor_id, CartItem.session_id == session_id ) ).delete() db.commit() logger.info( f"[CART_SERVICE] Cleared cart", extra={ "session_id": session_id, "vendor_id": vendor_id, "items_removed": deleted_count } ) return { "message": "Cart cleared", "items_removed": deleted_count } # Create service instance cart_service = CartService()