# app/services/customer_address_service.py """ Customer Address Service Business logic for managing customer addresses with vendor isolation. """ import logging from sqlalchemy.orm import Session from app.exceptions import ( AddressLimitExceededException, AddressNotFoundException, ) from models.database.customer import CustomerAddress from models.schema.customer import CustomerAddressCreate, CustomerAddressUpdate logger = logging.getLogger(__name__) class CustomerAddressService: """Service for managing customer addresses with vendor isolation.""" MAX_ADDRESSES_PER_CUSTOMER = 10 def list_addresses( self, db: Session, vendor_id: int, customer_id: int ) -> list[CustomerAddress]: """ Get all addresses for a customer. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID Returns: List of customer addresses """ return ( db.query(CustomerAddress) .filter( CustomerAddress.vendor_id == vendor_id, CustomerAddress.customer_id == customer_id, ) .order_by(CustomerAddress.is_default.desc(), CustomerAddress.created_at.desc()) .all() ) def get_address( self, db: Session, vendor_id: int, customer_id: int, address_id: int ) -> CustomerAddress: """ Get a specific address with ownership validation. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID address_id: Address ID Returns: Customer address Raises: AddressNotFoundException: If address not found or doesn't belong to customer """ address = ( db.query(CustomerAddress) .filter( CustomerAddress.id == address_id, CustomerAddress.vendor_id == vendor_id, CustomerAddress.customer_id == customer_id, ) .first() ) if not address: raise AddressNotFoundException(address_id) return address def get_default_address( self, db: Session, vendor_id: int, customer_id: int, address_type: str ) -> CustomerAddress | None: """ Get the default address for a specific type. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID address_type: 'shipping' or 'billing' Returns: Default address or None if not set """ return ( db.query(CustomerAddress) .filter( CustomerAddress.vendor_id == vendor_id, CustomerAddress.customer_id == customer_id, CustomerAddress.address_type == address_type, CustomerAddress.is_default == True, # noqa: E712 ) .first() ) def create_address( self, db: Session, vendor_id: int, customer_id: int, address_data: CustomerAddressCreate, ) -> CustomerAddress: """ Create a new address for a customer. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID address_data: Address creation data Returns: Created customer address Raises: AddressLimitExceededException: If max addresses reached """ # Check address limit current_count = ( db.query(CustomerAddress) .filter( CustomerAddress.vendor_id == vendor_id, CustomerAddress.customer_id == customer_id, ) .count() ) if current_count >= self.MAX_ADDRESSES_PER_CUSTOMER: raise AddressLimitExceededException(self.MAX_ADDRESSES_PER_CUSTOMER) # If setting as default, clear other defaults of same type if address_data.is_default: self._clear_other_defaults( db, vendor_id, customer_id, address_data.address_type ) # Create the address address = CustomerAddress( vendor_id=vendor_id, customer_id=customer_id, address_type=address_data.address_type, first_name=address_data.first_name, last_name=address_data.last_name, company=address_data.company, address_line_1=address_data.address_line_1, address_line_2=address_data.address_line_2, city=address_data.city, postal_code=address_data.postal_code, country_name=address_data.country_name, country_iso=address_data.country_iso, is_default=address_data.is_default, ) db.add(address) db.flush() logger.info( f"Created address {address.id} for customer {customer_id} " f"(type={address_data.address_type}, default={address_data.is_default})" ) return address def update_address( self, db: Session, vendor_id: int, customer_id: int, address_id: int, address_data: CustomerAddressUpdate, ) -> CustomerAddress: """ Update an existing address. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID address_id: Address ID address_data: Address update data Returns: Updated customer address Raises: AddressNotFoundException: If address not found """ address = self.get_address(db, vendor_id, customer_id, address_id) # Update only provided fields update_data = address_data.model_dump(exclude_unset=True) # Handle default flag - clear others if setting to default if update_data.get("is_default") is True: # Use updated type if provided, otherwise current type address_type = update_data.get("address_type", address.address_type) self._clear_other_defaults( db, vendor_id, customer_id, address_type, exclude_id=address_id ) for field, value in update_data.items(): setattr(address, field, value) db.flush() logger.info(f"Updated address {address_id} for customer {customer_id}") return address def delete_address( self, db: Session, vendor_id: int, customer_id: int, address_id: int ) -> None: """ Delete an address. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID address_id: Address ID Raises: AddressNotFoundException: If address not found """ address = self.get_address(db, vendor_id, customer_id, address_id) db.delete(address) db.flush() logger.info(f"Deleted address {address_id} for customer {customer_id}") def set_default( self, db: Session, vendor_id: int, customer_id: int, address_id: int ) -> CustomerAddress: """ Set an address as the default for its type. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID address_id: Address ID Returns: Updated customer address Raises: AddressNotFoundException: If address not found """ address = self.get_address(db, vendor_id, customer_id, address_id) # Clear other defaults of same type self._clear_other_defaults( db, vendor_id, customer_id, address.address_type, exclude_id=address_id ) # Set this one as default address.is_default = True db.flush() logger.info( f"Set address {address_id} as default {address.address_type} " f"for customer {customer_id}" ) return address def _clear_other_defaults( self, db: Session, vendor_id: int, customer_id: int, address_type: str, exclude_id: int | None = None, ) -> None: """ Clear the default flag on other addresses of the same type. Args: db: Database session vendor_id: Vendor ID for isolation customer_id: Customer ID address_type: 'shipping' or 'billing' exclude_id: Address ID to exclude from clearing """ query = db.query(CustomerAddress).filter( CustomerAddress.vendor_id == vendor_id, CustomerAddress.customer_id == customer_id, CustomerAddress.address_type == address_type, CustomerAddress.is_default == True, # noqa: E712 ) if exclude_id: query = query.filter(CustomerAddress.id != exclude_id) query.update({"is_default": False}, synchronize_session=False) # Singleton instance customer_address_service = CustomerAddressService()