""" Service layer for Items business logic Handles CRUD operations for Item with complete one-to-one relationships: Item -> LineItem -> LineFinancial -> LineQuantity -> LineCustoms -> LineDescription -> LineReference """ import logging from typing import Optional, List, Tuple from fastapi import HTTPException from sqlalchemy import and_, or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, joinedload from api.v1.modules.a76.items.line_items.schemas import LineItemCreate, LineItemUpdate from .schemas import ItemCreate, ItemUpdate from .line_items.models import LineItem from .line_financials.models import LineFinancial from .line_quantities.models import LineQuantity from .line_customs.models import LineCustom from .line_descriptions.models import LineDescription from .line_references.models import LineReference from .models import Item logger = logging.getLogger(__name__) class ItemService: """ Service for managing Items and related entities with tenant/company isolation """ @staticmethod def get_by_id( db: Session, item_id: int, tenant_id: int, company_id: int ) -> Optional[Item]: """Get an item by ID with tenant/company validation""" return ( db.query(Item) .options( joinedload(Item.lines).joinedload(LineItem.financial), joinedload(Item.lines).joinedload(LineItem.quantity), joinedload(Item.lines).joinedload(LineItem.customs), joinedload(Item.lines).joinedload(LineItem.description), joinedload(Item.lines).joinedload(LineItem.reference), ) .filter( Item.id == item_id, Item.tenant_id == tenant_id, Item.company_id == company_id, ) .first() ) @staticmethod def get_all( db: Session, tenant_id: int, company_id: int, skip: int = 0, limit: int = 100, filters: Optional[dict] = None, ) -> Tuple[List[Item], int]: """Get all items for a tenant/company with pagination and optional filters""" query = ( db.query(Item) .options( joinedload(Item.lines).joinedload(LineItem.financial), joinedload(Item.lines).joinedload(LineItem.quantity), joinedload(Item.lines).joinedload(LineItem.customs), joinedload(Item.lines).joinedload(LineItem.description), joinedload(Item.lines).joinedload(LineItem.reference), ) .filter( Item.tenant_id == tenant_id, Item.company_id == company_id, ) ) # Apply filters if provided if filters: if filters.get("invoice_id"): query = query.filter(Item.invoice_id == filters["invoice_id"]) if filters.get("item_type"): query = query.filter(Item.item_type == filters["item_type"]) if filters.get("system_origin"): query = query.filter(Item.system_origin == filters["system_origin"]) if filters.get("search"): search_term = f"%{filters['search']}%" query = query.filter( or_( Item.invoice_number.ilike(search_term), Item.reference_number.ilike(search_term), Item.order.ilike(search_term), Item.guide_number.ilike(search_term), ) ) total = query.count() items = query.offset(skip).limit(limit).all() return items, total @staticmethod def get_by_invoice( db: Session, invoice_id: int, tenant_id: int, company_id: int, skip: int = 0, limit: int = 100, ) -> Tuple[List[Item], int]: """Get all items for a specific invoice""" query = ( db.query(Item) .options( joinedload(Item.lines).joinedload(LineItem.financial), joinedload(Item.lines).joinedload(LineItem.quantity), joinedload(Item.lines).joinedload(LineItem.customs), joinedload(Item.lines).joinedload(LineItem.description), joinedload(Item.lines).joinedload(LineItem.reference), ) .filter( Item.invoice_id == invoice_id, Item.tenant_id == tenant_id, Item.company_id == company_id, ) ) total = query.count() items = query.offset(skip).limit(limit).all() return items, total @staticmethod def create( db: Session, item_data: ItemCreate, tenant_id: int, company_id: int, ) -> Item: """Create a new item with all related nested data (multiple lines)""" try: # Extract lines data lines_data = item_data.lines or [] item_dict = item_data.model_dump(exclude={"lines"}) # Add tenant and company item_dict["tenant_id"] = tenant_id item_dict["company_id"] = company_id # Create the item db_item = Item(**item_dict) db.add(db_item) db.flush() # Get the item ID # Create line items if provided for line_data in lines_data: # Extract nested data from line financial_data = line_data.financial quantity_data = line_data.quantity customs_data = line_data.customs description_data = line_data.description reference_data = line_data.reference line_dict = line_data.model_dump( exclude={"financial", "quantity", "customs", "description", "reference"} ) line_dict["item_id"] = db_item.id # Create line item db_line = LineItem(**line_dict) db.add(db_line) db.flush() # Get the line ID # Create financial data if provided if financial_data: financial_dict = financial_data.model_dump() financial_dict["item_line_id"] = db_line.id db_financial = LineFinancial(**financial_dict) db.add(db_financial) # Create quantity data if provided if quantity_data: quantity_dict = quantity_data.model_dump() quantity_dict["item_line_id"] = db_line.id db_quantity = LineQuantity(**quantity_dict) db.add(db_quantity) # Create customs data if provided if customs_data: customs_dict = customs_data.model_dump() customs_dict["item_line_id"] = db_line.id db_customs = LineCustom(**customs_dict) db.add(db_customs) # Create description data if provided if description_data: description_dict = description_data.model_dump() description_dict["item_line_id"] = db_line.id db_description = LineDescription(**description_dict) db.add(db_description) # Create reference data if provided if reference_data: reference_dict = reference_data.model_dump() reference_dict["item_line_id"] = db_line.id db_reference = LineReference(**reference_dict) db.add(db_reference) db.commit() db.refresh(db_item) return db_item except IntegrityError as e: db.rollback() logger.error(f"Error creating item: {e}") raise HTTPException( status_code=400, detail="Item creation failed - integrity constraint violated", ) except Exception as e: db.rollback() logger.error(f"Unexpected error creating item: {e}") raise HTTPException(status_code=500, detail="Error creating item") @staticmethod def update( db: Session, item_id: int, item_data: ItemUpdate, tenant_id: int, company_id: int, ) -> Item: """Update an item and optionally its nested data (multiple lines)""" try: # Get existing item db_item = ItemService.get_by_id(db, item_id, tenant_id, company_id) if not db_item: raise HTTPException(status_code=404, detail="Item not found") # Extract lines data lines_data = item_data.lines item_dict = item_data.model_dump( exclude={"lines"}, exclude_unset=True) # Update item fields for key, value in item_dict.items(): setattr(db_item, key, value) # Update lines if provided (replace all lines) if lines_data is not None: # Delete existing lines (cascade will handle nested data) for existing_line in db_item.lines: db.delete(existing_line) db.flush() # Create new lines for line_data in lines_data: # Extract nested data from line financial_data = line_data.financial quantity_data = line_data.quantity customs_data = line_data.customs description_data = line_data.description reference_data = line_data.reference line_dict = line_data.model_dump( exclude={"financial", "quantity", "customs", "description", "reference"}, exclude_unset=True ) line_dict["item_id"] = db_item.id db_line = LineItem(**line_dict) db.add(db_line) db.flush() # Create nested data if provided if financial_data is not None: financial_dict = financial_data.model_dump( exclude_unset=True) financial_dict["item_line_id"] = db_line.id db.add(LineFinancial(**financial_dict)) if quantity_data is not None: quantity_dict = quantity_data.model_dump( exclude_unset=True) quantity_dict["item_line_id"] = db_line.id db.add(LineQuantity(**quantity_dict)) if customs_data is not None: customs_dict = customs_data.model_dump( exclude_unset=True) customs_dict["item_line_id"] = db_line.id db.add(LineCustom(**customs_dict)) if description_data is not None: description_dict = description_data.model_dump( exclude_unset=True) description_dict["item_line_id"] = db_line.id db.add(LineDescription(**description_dict)) if reference_data is not None: reference_dict = reference_data.model_dump( exclude_unset=True) reference_dict["item_line_id"] = db_line.id db.add(LineReference(**reference_dict)) db.commit() db.refresh(db_item) return db_item except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Unexpected error updating item: {e}") raise HTTPException(status_code=500, detail="Error updating item") @staticmethod def delete( db: Session, item_id: int, tenant_id: int, company_id: int, ) -> bool: """Delete an item and all its related data (cascade delete)""" try: db_item = ItemService.get_by_id(db, item_id, tenant_id, company_id) if not db_item: return False db.delete(db_item) db.commit() return True except Exception as e: db.rollback() logger.error(f"Error deleting item: {e}") raise HTTPException(status_code=500, detail="Error deleting item")