""" Address Service (P0 Refactoring). Provides helper functions for creating, updating, and resolving Address records in the system.addresses table. Usage: from app.services.address_service import create_or_update_address """ import uuid import logging from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.identity.address import Address, GeoPostalCode from app.schemas.address import AddressIn logger = logging.getLogger(__name__) async def _resolve_postal_code(db: AsyncSession, zip_code: str, city: str) -> Optional[int]: """Find or create a GeoPostalCode record and return its ID. Looks up by zip_code first; if not found, creates a new record. """ stmt = select(GeoPostalCode).where(GeoPostalCode.zip_code == zip_code) result = await db.execute(stmt) postal = result.scalar_one_or_none() if not postal: postal = GeoPostalCode( zip_code=zip_code, city=city, country_code="HU", ) db.add(postal) await db.flush() logger.info(f"Created new GeoPostalCode: {zip_code} - {city} (id={postal.id})") return postal.id async def create_address(db: AsyncSession, addr_in: AddressIn) -> Address: """Create a new Address record from AddressIn data. If zip and city are provided, resolves/creates a GeoPostalCode record and links it via postal_code_id. """ address = Address( id=uuid.uuid4(), street_name=addr_in.street_name, street_type=addr_in.street_type, house_number=addr_in.house_number, stairwell=addr_in.stairwell, floor=addr_in.floor, door=addr_in.door, parcel_id=addr_in.parcel_id, full_address_text=addr_in.full_address_text, latitude=addr_in.latitude, longitude=addr_in.longitude, ) # Resolve postal code if zip is provided if addr_in.zip and addr_in.city: postal_code_id = await _resolve_postal_code(db, addr_in.zip, addr_in.city) address.postal_code_id = postal_code_id db.add(address) await db.flush() logger.info(f"Created Address {address.id}") return address async def update_address(db: AsyncSession, address: Address, addr_in: AddressIn) -> Address: """Update an existing Address record from AddressIn data. Only updates fields that are explicitly set (not None) in addr_in. If zip/city changes, resolves the new GeoPostalCode. """ update_fields = addr_in.model_dump(exclude_unset=True) # Handle zip/city -> postal_code_id resolution if "zip" in update_fields or "city" in update_fields: new_zip = update_fields.get("zip") or address.zip new_city = update_fields.get("city") or address.city if new_zip and new_city: postal_code_id = await _resolve_postal_code(db, new_zip, new_city) address.postal_code_id = postal_code_id # Map AddressIn field names to Address model attributes field_mapping = { "zip": None, # handled above "city": None, # handled above "street_name": "street_name", "street_type": "street_type", "house_number": "house_number", "stairwell": "stairwell", "floor": "floor", "door": "door", "parcel_id": "parcel_id", "full_address_text": "full_address_text", "latitude": "latitude", "longitude": "longitude", } for addr_field, model_attr in field_mapping.items(): if model_attr and addr_field in update_fields: setattr(address, model_attr, update_fields[addr_field]) await db.flush() logger.info(f"Updated Address {address.id}") return address async def create_or_update_address( db: AsyncSession, addr_in: Optional[AddressIn], existing_address: Optional[Address] = None, ) -> Optional[Address]: """Create or update an Address record based on whether one already exists. This is the main helper used by PATCH endpoints. Args: db: Database session. addr_in: The incoming address data (or None to skip). existing_address: The existing Address record (or None to create new). Returns: The Address record, or None if addr_in was None. """ if addr_in is None: return existing_address # Keep existing if no new data if existing_address: return await update_address(db, existing_address, addr_in) else: return await create_address(db, addr_in)