# app/services/inventory_transaction_service.py """ Inventory Transaction Service. Provides query operations for inventory transaction history. All transaction WRITES are handled by OrderInventoryService. This service handles transaction READS for reporting and auditing. """ import logging from sqlalchemy import func from sqlalchemy.orm import Session from app.exceptions import OrderNotFoundException, ProductNotFoundException from models.database.inventory import Inventory from models.database.inventory_transaction import InventoryTransaction from models.database.order import Order from models.database.product import Product logger = logging.getLogger(__name__) class InventoryTransactionService: """Service for querying inventory transaction history.""" def get_vendor_transactions( self, db: Session, vendor_id: int, skip: int = 0, limit: int = 50, product_id: int | None = None, transaction_type: str | None = None, ) -> tuple[list[dict], int]: """ Get inventory transactions for a vendor with optional filters. Args: db: Database session vendor_id: Vendor ID skip: Pagination offset limit: Pagination limit product_id: Optional product filter transaction_type: Optional transaction type filter Returns: Tuple of (transactions with product details, total count) """ # Build query query = db.query(InventoryTransaction).filter( InventoryTransaction.vendor_id == vendor_id ) # Apply filters if product_id: query = query.filter(InventoryTransaction.product_id == product_id) if transaction_type: query = query.filter( InventoryTransaction.transaction_type == transaction_type ) # Get total count total = query.count() # Get transactions with pagination (newest first) transactions = ( query.order_by(InventoryTransaction.created_at.desc()) .offset(skip) .limit(limit) .all() ) # Build result with product details result = [] for tx in transactions: product = db.query(Product).filter(Product.id == tx.product_id).first() product_title = None product_sku = None if product: product_sku = product.vendor_sku if product.marketplace_product: product_title = product.marketplace_product.get_title() result.append( { "id": tx.id, "vendor_id": tx.vendor_id, "product_id": tx.product_id, "inventory_id": tx.inventory_id, "transaction_type": ( tx.transaction_type.value if tx.transaction_type else None ), "quantity_change": tx.quantity_change, "quantity_after": tx.quantity_after, "reserved_after": tx.reserved_after, "location": tx.location, "warehouse": tx.warehouse, "order_id": tx.order_id, "order_number": tx.order_number, "reason": tx.reason, "created_by": tx.created_by, "created_at": tx.created_at, "product_title": product_title, "product_sku": product_sku, } ) return result, total def get_product_history( self, db: Session, vendor_id: int, product_id: int, limit: int = 50, ) -> dict: """ Get transaction history for a specific product. Args: db: Database session vendor_id: Vendor ID product_id: Product ID limit: Max transactions to return Returns: Dict with product info, current inventory, and transactions Raises: ProductNotFoundException: If product not found or doesn't belong to vendor """ # Get product details product = ( db.query(Product) .filter(Product.id == product_id, Product.vendor_id == vendor_id) .first() ) if not product: raise ProductNotFoundException( f"Product {product_id} not found in vendor catalog" ) product_title = None product_sku = product.vendor_sku if product.marketplace_product: product_title = product.marketplace_product.get_title() # Get current inventory inventory = ( db.query(Inventory) .filter(Inventory.product_id == product_id, Inventory.vendor_id == vendor_id) .first() ) current_quantity = inventory.quantity if inventory else 0 current_reserved = inventory.reserved_quantity if inventory else 0 # Get transactions transactions = ( db.query(InventoryTransaction) .filter( InventoryTransaction.vendor_id == vendor_id, InventoryTransaction.product_id == product_id, ) .order_by(InventoryTransaction.created_at.desc()) .limit(limit) .all() ) total = ( db.query(func.count(InventoryTransaction.id)) .filter( InventoryTransaction.vendor_id == vendor_id, InventoryTransaction.product_id == product_id, ) .scalar() or 0 ) return { "product_id": product_id, "product_title": product_title, "product_sku": product_sku, "current_quantity": current_quantity, "current_reserved": current_reserved, "transactions": [ { "id": tx.id, "vendor_id": tx.vendor_id, "product_id": tx.product_id, "inventory_id": tx.inventory_id, "transaction_type": ( tx.transaction_type.value if tx.transaction_type else None ), "quantity_change": tx.quantity_change, "quantity_after": tx.quantity_after, "reserved_after": tx.reserved_after, "location": tx.location, "warehouse": tx.warehouse, "order_id": tx.order_id, "order_number": tx.order_number, "reason": tx.reason, "created_by": tx.created_by, "created_at": tx.created_at, } for tx in transactions ], "total": total, } def get_order_history( self, db: Session, vendor_id: int, order_id: int, ) -> dict: """ Get all inventory transactions for a specific order. Args: db: Database session vendor_id: Vendor ID order_id: Order ID Returns: Dict with order info and transactions Raises: OrderNotFoundException: If order not found or doesn't belong to vendor """ # Verify order belongs to vendor order = ( db.query(Order) .filter(Order.id == order_id, Order.vendor_id == vendor_id) .first() ) if not order: raise OrderNotFoundException(f"Order {order_id} not found") # Get transactions for this order transactions = ( db.query(InventoryTransaction) .filter(InventoryTransaction.order_id == order_id) .order_by(InventoryTransaction.created_at.asc()) .all() ) # Build result with product details result = [] for tx in transactions: product = db.query(Product).filter(Product.id == tx.product_id).first() product_title = None product_sku = None if product: product_sku = product.vendor_sku if product.marketplace_product: product_title = product.marketplace_product.get_title() result.append( { "id": tx.id, "vendor_id": tx.vendor_id, "product_id": tx.product_id, "inventory_id": tx.inventory_id, "transaction_type": ( tx.transaction_type.value if tx.transaction_type else None ), "quantity_change": tx.quantity_change, "quantity_after": tx.quantity_after, "reserved_after": tx.reserved_after, "location": tx.location, "warehouse": tx.warehouse, "order_id": tx.order_id, "order_number": tx.order_number, "reason": tx.reason, "created_by": tx.created_by, "created_at": tx.created_at, "product_title": product_title, "product_sku": product_sku, } ) return { "order_id": order_id, "order_number": order.order_number, "transactions": result, } # ========================================================================= # Admin Methods (cross-vendor operations) # ========================================================================= def get_all_transactions_admin( self, db: Session, skip: int = 0, limit: int = 50, vendor_id: int | None = None, product_id: int | None = None, transaction_type: str | None = None, order_id: int | None = None, ) -> tuple[list[dict], int]: """ Get inventory transactions across all vendors (admin only). Args: db: Database session skip: Pagination offset limit: Pagination limit vendor_id: Optional vendor filter product_id: Optional product filter transaction_type: Optional transaction type filter order_id: Optional order filter Returns: Tuple of (transactions with details, total count) """ from models.database.vendor import Vendor # Build query query = db.query(InventoryTransaction) # Apply filters if vendor_id: query = query.filter(InventoryTransaction.vendor_id == vendor_id) if product_id: query = query.filter(InventoryTransaction.product_id == product_id) if transaction_type: query = query.filter( InventoryTransaction.transaction_type == transaction_type ) if order_id: query = query.filter(InventoryTransaction.order_id == order_id) # Get total count total = query.count() # Get transactions with pagination (newest first) transactions = ( query.order_by(InventoryTransaction.created_at.desc()) .offset(skip) .limit(limit) .all() ) # Build result with vendor and product details result = [] for tx in transactions: vendor = db.query(Vendor).filter(Vendor.id == tx.vendor_id).first() product = db.query(Product).filter(Product.id == tx.product_id).first() product_title = None product_sku = None if product: product_sku = product.vendor_sku if product.marketplace_product: product_title = product.marketplace_product.get_title() result.append( { "id": tx.id, "vendor_id": tx.vendor_id, "vendor_name": vendor.name if vendor else None, "vendor_code": vendor.vendor_code if vendor else None, "product_id": tx.product_id, "inventory_id": tx.inventory_id, "transaction_type": ( tx.transaction_type.value if tx.transaction_type else None ), "quantity_change": tx.quantity_change, "quantity_after": tx.quantity_after, "reserved_after": tx.reserved_after, "location": tx.location, "warehouse": tx.warehouse, "order_id": tx.order_id, "order_number": tx.order_number, "reason": tx.reason, "created_by": tx.created_by, "created_at": tx.created_at, "product_title": product_title, "product_sku": product_sku, } ) return result, total def get_transaction_stats_admin(self, db: Session) -> dict: """ Get transaction statistics across the platform (admin only). Returns: Dict with transaction counts by type """ from sqlalchemy import func as sql_func # Count by transaction type type_counts = ( db.query( InventoryTransaction.transaction_type, sql_func.count(InventoryTransaction.id).label("count"), ) .group_by(InventoryTransaction.transaction_type) .all() ) # Total transactions total = db.query(sql_func.count(InventoryTransaction.id)).scalar() or 0 # Transactions today from datetime import UTC, datetime, timedelta today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) today_count = ( db.query(sql_func.count(InventoryTransaction.id)) .filter(InventoryTransaction.created_at >= today_start) .scalar() or 0 ) return { "total_transactions": total, "transactions_today": today_count, "by_type": {tc.transaction_type.value: tc.count for tc in type_counts}, } # Create service instance inventory_transaction_service = InventoryTransactionService()