Files
service-finder/backend/app/services/address_service.py
2026-07-01 19:49:58 +00:00

141 lines
4.4 KiB
Python

"""
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)